ThearchyHelios
|
d57cdcaffc
|
驳回出口(后端):审批、补充输入、编辑稿件、动作确认、代码和工具审批、双盲裁决接了「驳回」出口时沿它往下走
以前这六类关卡被驳回一律抛 NodeRejected,运行收成 cancelled + end_reason=rejected,没有「驳回之后走另一条路」
(docs/design/orchestration-round/00-总方案.md §3)。现在画布上多一个可选的「驳回」出口(sourceHandle="rejected",和
分支、审批同一套连线机制):
- engine/schema:GATE_TYPES(人工、调用工具、沙箱代码、双盲复算)、reject_targets(驳回出口连到哪几步)、approve_mode;
画布校验:别的节点接驳回出口报错(graph.reject_exit_invalid,条件分支的 case 标识可以叫 rejected,不算);审批同时设了
「驳回即终止运行」和驳回边的给一条提示(human.stop_on_reject_exit)
- engine/compiler:接了驳回出口的关卡按出口路由(驳回出口 / 其余是普通出口,连线没写 sourceHandle 的照样是普通出口);
NodeRejected 被接住时这一步正常结束:产出 NodeRejected.exit_payload()(rejected、decision、actor、note、kind、text,
审批另带 approved=false、「结果存为变量」照旧写 false),node.finished 带 outcome=rejected、哪种关卡、结论、谁、备注、
转到哪几步(to),trail 记 outcome=rejected;不重试、不被「出错时继续」接住,照旧
- engine/context:NodeRejected 加 stop(审批明说了驳回即终止的,驳回边照旧走不到)和 exit_payload;动作确认抛在发送之前,
走出口时什么都没发
- engine/runner:成功结束的运行途经驳回出口时,run.finished 带 rejected_via(在哪一步、谁、结论、转到哪),照实说「途经
驳回」;没有途经的终态事件一字不变
- engine/governance:发布检查各级都拦「驳回出口那条路走不到成果节点或结束」(reject_exit.no_end);退回上游再问一次的放过
- engine/layout:驳回出口排在普通出口下面
- apps/resolve:实例进度写「被驳回,转到「X」」(动作确认写「未确认(被驳回),转到…」,超时驳回写「超时驳回,转到…」);
实例状态(实例页头、列表、通知同一句)写「「x」被驳回,转到「y」」,走到另一个成果节点的照旧写那个成果节点;退回修改后
又批准了的按批准算;子工作流里的步骤不进实例进度
- 没接出口:行为一字不变(node.failed reason=rejected、运行 cancelled + rejected);审批没设驳回即终止、没接驳回边的老规矩
照旧。接了驳回边的审批,实例进度那一步的说法从「已驳回」改成「被驳回,转到「已退回」」(test_app_instance_page 跟着改)
另:engine/context 的 RunContext 加 prefix / global_id、事件经 stream_writer 发(子工作流把父运行的写入口交下来),
这一步本身不改变最外层的事件,下一个提交接上子工作流
测试:test_reject_exit(五类关卡接了出口各走一遍、批准走普通出口、超时驳回走出口、没接一字不变、驳回即终止优先、
重试和出错时继续不管驳回、退回修改再问一次、校验与发布检查、布局、实例进度);test_process_actions(动作确认走出口
不发送);test_dual_check_node(裁决走出口)
|
2026-10-18 15:56:29 +08:00 |
|
ThearchyHelios
|
66cc96b463
|
汇合节点只跑一次:长短支路不再把它触发两遍,循环的 done 出口也不再被提前放行
多条边汇入同一个节点时,编译器对每条入边各 add_edge 一次。LangGraph 对这种
接法的语义是"任一上游跑完就触发一次":两条支路一样长时,两次触发落在同一步
被合并,看不出问题;一长一短时,汇合节点先拿短支路的结果跑一次,长支路到了
再跑一次——模型调用、工具副作用、下游全部翻倍。循环体里更糟:每轮汇合两次,
循环节点就多推进一格,done 出口在循环跑到一半时就放行了,成果节点跑了 3 次。
README 写的"自动等待汇聚"只在支路等长时成立。
修法是给汇合节点加 defer(等图里其余待执行的任务都跑完再跑一次),而不是
屏障 add_edge([a, b], j):分支之后的二选一汇合,没走的那条永远不会到,屏障
会一直等,而 LangGraph 没有可执行任务时会安静结束——汇合节点和下游就被无声
跳过。代价写在 _join_nodes 的注释和 README 里:它也会等图里无关、恰好在跑的支路。
汇合 = 两个以上前向上游,循环回边不算。回边识别因此从 layout.py 挪到
schema.back_edges,加了 roots 参数:执行语义要从入口起 DFS,否则循环体里的
前向边可能被误判成回边;排版照旧按节点顺序起算,行为不变(check-canvas-layout
全过)。
新增 test_fan_in.py 5 例:长短支路、循环体内汇合(修前分别跑了 2 次和 4 次);
二选一汇合、被分支剪掉一路的汇合(守着别把修法做成屏障);回边不算汇合。
292 → 297 passed。内置模板里 ③④ 的成果节点是二选一汇合,行为不变。
|
2026-09-25 14:33:56 +08:00 |
|
ThearchyHelios
|
8905fe5e60
|
多 agent 节点在画布上展开协作矩阵,运行时的动效统一成一套语汇
多 agent 节点原本是个黑盒:卡片转三十秒圈,然后吐一段文本。"它同时派了三个
人"、"谁还在跑"、"并行到底省了多少"全都要点开右栏才知道,而右栏是线性的
时间线,看不出并发的形状。
现在它跑起来会在卡片里展开一块协作矩阵。三条设计上的选择:
- 花名册固定在原地,不按派单顺序重排——几轮下来眼睛知道谁在哪一行
- 条形长度按**这一轮里最慢的那个人**归一化。三个人并排三条长短不一的条,
墙钟为什么等于最长那条、而不是三条之和,不用解释就看懂了;页脚给出
并行一共省下多少
- 本轮没被派到的人不显示成"待命",而是留着上一轮的成绩(压暗)——它已经
交回来了,说它待命是错的,看的人会以为它还没干活
矩阵和右栏泳道读的是同一份翻译:把 decodeRun 里那段攒泳道的逻辑抽成
reduceTeam(不可变,每次返回新对象,画布那份挂在 zustand 上需要新引用)。
两边各写一遍的话,同一次运行在右栏和画布上会给出不同的并行度——而"几个人
同时在跑、省了多少"正是这个节点唯一值得看的东西。抽完之后 check-decode
一条没红。
其余节点按同一套语汇动:边框上转一圈光弧 + 扫描线 = 正在跑,绿色扫一遍 =
刚跑完(只扫一次——常驻动画会让整张画布一直在动),短促抖动 = 刚失败,
琥珀呼吸 = 在等人。循环卡片显示"第 1/5 轮";分支跑完点亮命中的出口、压暗
其余,实际走过的边留在画布上,那就是这次的执行路径。连线上的虚线换成顺着
路径跑的光点:虚线只让人觉得"在动",光点能看出往哪走,速度按路径长度归一化
(长边上的光点走得慢会像卡住了)。
原来那个往外扩的声纳圈(pulse-ring)删掉了。同一个意思说两遍只会更吵,而且
声纳扩出去会和邻居卡片的边框糊在一起。
一个前提先验证过:offset-path 在 SVG 子元素上能不能对齐路径。测下来和
getPointAtLength 的位置差 0px,才敢用它。
排版要按**展开后**的高度给多 agent 节点留位置(layout.py 的 _height 按名册
人数算)。不留的话它一跑起来就盖住下面那张卡,而且运行结束后矩阵不会收起来
(那正是要回看的东西),重叠是永久的。
前庭敏感的人由 prefers-reduced-motion 兜住:光弧、扫描线、流光条、光点整体
不绘制,而不是把时长压到 0.01ms——那样它们会停在动画的最后一帧,光弧卡在
角落、光点全堆在起点,看着像渲染坏了。静态下"谁在跑"由实心圆点和"进行中"
三个字承担。
新增 scripts/check-canvas-fx.mjs:把脚本化的事件灌进 store(走的是页面上那一份
applyEvent / NodeCard / FlowEdge,只有事件来源是脚本),断言协作矩阵真的长出来、
并行度算对、条形长度和耗时成正比、分支命中的出口被点亮、关掉动效时光效确实不
绘制。真跑一张多 agent 的图要连模型、要等十几秒,而且撞不上"三个人同时还在跑"
那一瞬间——那样的检查会变成偶发失败。另外用仓库里 fixtures.json 那份真实导出
的事件验过一遍,含老运行那种只有中文消息串的 log(走正则兜底)。
顺手修掉检查脚本互相下绊子:check-canvas-fx 原来会留下一张没坐标的工作流
(节点全叠在原点),而 check-ui 打开 /studio 会落到"最近更新"的那张上,于是
点节点被挡住。现在落库前先让后端排好版。
292 passed(新增 2 条:展开的多 agent 卡片必须在排版里留出位置)。
|
2026-09-25 12:47:07 +08:00 |
|
ThearchyHelios
|
75be5f485e
|
画布上的线不再叠在一起:排版按车道留走廊,走线不再交给 smoothstep
原来所有边都交给 React Flow 的 smoothstep,它的竖直段一律落在源和目标的
正中间。同一列的边于是全挤在同一个 x 上:一张"入口扇出三路 → 归并 → 分支
→ 出具/驳回"的图上,跨层的 a3→join 和 m2→join 的横段**完全压在同一条线
上**,长边还直接从"归并B"卡片中间穿过去。排版那边同样糙:同层按 id 字典序
竖排,所以 out_no 排在 out_yes 上面("n" < "y"),分支的 yes/no 是反的;
层内没有任何减少交叉的排序;层间距写死 300。
后端 app/engine/layout.py 重写成分层布局:先剔回边再按最长路径分层(loop
的回边不参与分层,否则循环体会被压到循环节点左边);层内中位数启发式上下扫
六轮取交叉最少的一版,分支/循环的出口再按声明顺序占位重排;y 先均匀堆叠再按
邻居重心迭代松弛,同列节点因此和上下游对齐,长边也走得直;最后按"穿过这条
走廊的连线在竖直方向最多重叠几条"给走廊留宽——区间图上的贪心染色,和前端
用的是同一套,走廊留窄了前端再聪明也只能把线叠起来。
前端 canvas/routing.ts 自己算走线:出边沿右边缘错开、入边沿左边缘错开(入边
按上游纵向位置排序),竖直段按区间染色分配车道——真叠在一起的才各占一条,
首尾相接的(同一个扇出点)共用一条,看起来就是一条总线;跨层的长边和循环的
回边在中间各列的纵向占用里找空档绕过去,不再压过卡片。边换成自定义组件后
React Flow 不再自动加动画类,实测确认 .edge-active 仍然生效。
顺带修掉"排完版看不见图":整张图换过坐标后(自动排版 / Copilot 生成完 /
切图)自动重新取景。坐标变了视角没变,人盯着的是一片空白。
新增 scripts/check-canvas-layout.mjs:把夹具图交给真后端 /api/copilot/layout
排版,再直接 import('/src/canvas/routing.ts') 用页面上跑的那份代码算路径,
按几何断言没有两条线压在同一段上、没有线穿过别人的卡片、两端都接在节点边缘,
另有 1325 条随机坐标的 fuzz 防手拖之后算出 NaN 路径(NaN 的 path 什么都不
画,界面上就是"线没了",一声不吭)。这条检查做过变异验证——把 laneX 改成
永远返回走廊中点,它立刻报 a3→join × m2→join,正是原图上那处重叠。
285 passed(新增 12 条排版不变量)。check-ui 69 项通过,唯一失败项"本地哈希
要标出没有语义能力"已 git stash 到基线复现,与本次无关。
|
2026-09-24 22:58:54 +08:00 |
|