Commit 25bdfca13dadc3478ff9d5ac5f23964fe2965dc4
1 parent
d3417679
docs(p3): R-16 失败归因定案与 base agent 四段式现状核实落入 v0.3
R-16(归因由专职 subagent 做,回退边多条): - §四.D.2.2 归因改为 org-validator work graph 内的 validator-reflection 专职节点,与 DevLoop 的 Integrator/BugTriager 分工同构;给出四类 fail_reason 取值与 DevLoop 四分类的对照 - §四.D.0 子 run 收敛改为透传三值(success/fail + fail_reason + remediation_note),归因结论才能穿透子 run 边界供父层选边 - §四.D.2.1 熔断改为双层计数:单边 loop_count 防同路径死循环,新增 run 级 backward_count 防"换个借口继续退"——多条回退边各自计数会把 硬顶架空成 3xN;infra_flake 不烧任何计数 - §三.7 写入前校验新增第五项 fail-fallback-edge(强制兜底边) - schema:graph_runs 加 backward_count - 任务表加 T13/T14,验收标准加 9b/9c/9d - skill_defect 属 L4,v1 保留归因取值但动作降级为挂起等人工 base agent 四段式(新增 §九bis): - 核实后原表述"已强制一套四段式生命周期"只对一半 - Context Flush / P0 事前阻断阀 / OKR 联动确实在跑 - Hot Swap 换技能包静默失效:system/skills/ 目录不存在, agent-chat.js:1908 的 existsSync 守卫使其空转且不报错 - 后果:EXECUTE 才拿破坏性权限只剩 prompt 话术约束,无机制保障 - phase 参数无取值/顺序校验,且被直接拼进文件路径 需求文档:补回 R-15.1 全文、R-15.4 后半、R-15.5、R-15.6 及速查表 对应行(此前被误回滚丢失),新增 R-16 条目。R-15.3 的命名表保留 NAMING §三 重写后的版本,未还原旧版。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
2 changed files
with
698 additions
and
218 deletions
docs/todo/system-design/27-p3-p4-work-graph-console.md
| 1 | -# 27-P3/P4. Work Graph 执行控制台 — 需求与设计文档 | |
| 1 | +# 27-P3/P4. 图执行引擎与执行控制台 — 需求与设计文档 | |
| 2 | 2 | |
| 3 | -> **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase **P3(Work Graph 最小闭环)+ P4(回退 + Loop-Validator)**,外加一块 27 号文档原始路线图里没有单独立项的新增交付物——**一个新页面**:展示公司 Org Graph、以及每个 org node 当前/历史跑过的 Work Graph,并提供启动/下一步/暂停/停止/重试的执行控制、节点日志、边的输入输出、循环回退连线、主流程/异常流程可视化、子流程(agent 对话记录 + 嵌套子 Work Graph)下钻。 | |
| 4 | -> **版本**: v0.1(设计阶段,未实现) | |
| 3 | +> **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase **P3(最小闭环)+ P4(回退 + 熔断)**,外加 27 号文档原始路线图里没有单独立项的两块新增交付物——**一个执行控制台页面**,以及 **`workgraph-planning` agent + 用户→公司的任务入口**。 | |
| 4 | +> **版本**: v0.3(**按 [27-REQ](./27-requirement.md) R-15 六条定案重写**;v0.1 正文与 v0.2 修正块已合并入正文,不再单列) | |
| 5 | 5 | > **日期**: 2026-08-15 |
| 6 | -> **前置**: [27-P2 Org Graph 落地](./27-p2-org-graph.md)(`org_graph_nodes` 是本设计里"每个 org node"的数据来源,本文档假定 P2 已落地)。P1(虚拟引用寻址)**已实现并提交**(commit `226489d`),与本设计无强依赖,见 §九。 | |
| 7 | -> | |
| 8 | -> ⚠️ **v0.2 修正(2026-08-15)**:本文档 v0.1 有三处基于过期或不完整调研的陈述,端到端流程梳理时被推翻,**动工前必读** [27-E2E 端到端任务执行流程](./27-e2e-task-execution-flow.md) §六与§九: | |
| 9 | -> 1. §三.1 说 `runProject` 是 501 未实现 —— **它已完整实现**(`loop-controller.js:576`),含 Kahn 拓扑分波 `_groupUnitsByWave()`、L2 闸门、Integrator、BugTriager 四分支调度、预算熔断、全套 SSE 埋点。 | |
| 10 | -> 2. §四.B.2 / §六 T3 把"手写分层布局算法"当成新工作量 —— **`web/src/components/devloop/UnitsDAG.vue` 已经实现了** SVG + Kahn 分波布局 + 节点状态着色 + 点击下钻(107 行),应改为扩展既有组件。同目录下 `LiveAgentStream.vue`(带 Last-Event-ID 断线重连的 SSE 消费)、`UnitDrawer.vue`(节点详情抽屉)同样可复用。 | |
| 11 | -> 3. 本文档整体把"回退/循环/预算"当作全新设计 —— **`docs/prd/newfunction/_spec/loops-{overview,l1,l2,l3}.md` + `sdd-process.md` P-1/P-2/P-7 已经有一套完整的三层循环规格**(含四分类失败归因、差异化预算消耗、`contract_hash` 闸门、增量 unchanged 免重跑),本文档 §四.D 的设计应当从它抽象而来,而不是平行发明。 | |
| 12 | -> **设计方法论**:按用户要求,正文用 **Model / View / Control / Process(MVCP)** 四段组织核心设计(§四),前置背景与走查、后置任务划分与验收标准按仓库既有文档惯例编排。 | |
| 6 | +> **前置**: [27-P2 Org Graph 落地](./27-p2-org-graph.md) 阶段 A/B。P0/P1 均已实现并提交(P1 = commit `226489d`)。 | |
| 7 | +> **命名**: 全文遵循 [27-NAMING](./27-naming-conventions.md),含本文档 §五 T-P2 要求新增的 validator 命名轴。 | |
| 8 | +> **配套**: [27-E2E 端到端流程](./27-e2e-task-execution-flow.md)、[27-EXAMPLE 参考场景](./27-example-dev-company-walkthrough.md) | |
| 9 | +> **设计方法论**: 核心设计(§四)用 **Model / View / Control / Process(MVCP)** 四段组织。 | |
| 10 | + | |
| 11 | +--- | |
| 12 | + | |
| 13 | +## 〇、v0.3 重写说明 | |
| 14 | + | |
| 15 | +v0.1/v0.2 的正文有三处**结构性**问题,不是补丁能修的: | |
| 16 | + | |
| 17 | +| # | v0.1/v0.2 的写法 | 为什么不成立 | 本版怎么改 | | |
| 18 | +|---|---|---|---| | |
| 19 | +| 1 | 四张 `work_graph_*` 表,「定义即实例」不拆表 | 该辩护的前提是 work graph「规划完即执行、执行完即归档」。**org graph 也要被同一套引擎执行**(R-15.4),而它是活整个公司生命周期的长期定义,一份定义要被跑很多次 | 定义层与运行时层分离,五张表,§四.A | | |
| 20 | +| 2 | `advanceOne()` 只有一种节点执行语义(调 `AgentManager`) | org node 的「执行」是起一张子图并等它收敛,不是一次 agent 调用 | 两种 node executor,§四.D.0 | | |
| 21 | +| 3 | §一非目标排除 `workgraph-planning` 与任务入口 | 代价是 v0.1 §八.5 自己承认「只能手写测试数据构造图,不能端到端验证」 | 两条非目标作废(R-15.5),收进阶段 D | | |
| 22 | + | |
| 23 | +另有四处 v0.2 已在头部修正、本版直接写进正文的过期陈述:`runProject` 已实现(不是 501)、`UnitsDAG.vue` 已实现分波布局(不用从零写)、回退/预算应从 `_spec/loops-*.md` 抽象而来(不是平行发明)、P1 已实现。 | |
| 24 | + | |
| 25 | +以及两个已被关掉的 open question:v0.1 §八.3「重试要不要级联下游」由 `loops-l3.md` 的 `contract_hash` unchanged 四分类判据关闭;v0.1 §八.4「子图不继承父图暂停」被 [27-E2E](./27-e2e-task-execution-flow.md) §7.5 推翻,改为向下级联。 | |
| 13 | 26 | |
| 14 | 27 | --- |
| 15 | 28 | |
| ... | ... | @@ -17,69 +30,80 @@ |
| 17 | 30 | |
| 18 | 31 | ### 目标 |
| 19 | 32 | |
| 20 | -1. **一个新页面**:给定一家公司,左侧展示它的 Org Graph(复用 [27-P2 §4.7](./27-p2-org-graph.md) 的分层列表设计),点击任意一个 org node,右侧展示这个节点当前正在跑的 / 历史跑过的 Work Graph。 | |
| 21 | -2. **执行控制**:启动、下一步、暂停、停止、重试五个操作,且要同时支持**手动**(每次点"下一步"才推进一个节点)和**自动**(一键授权后从头跑到尾,一个节点成功自动跳转执行下一个节点)两种模式——这是本文档动笔前用户明确的一条硬需求,直接决定了 §四.C 的状态机设计不能走 DevLoop 现有 `runUnit()` 那种"一次函数调用跑到底、外部无法中途插手"的模型(见 §三.1 走查结论)。 | |
| 22 | -3. **节点运行日志**:每个 Work Graph 节点对应一次 agent 调用,要能看到这次调用的完整过程(不只是成功/失败两个字)。 | |
| 23 | -4. **边的输入输出展示**:一条边连接的两个节点之间,上游节点产出了什么、下游节点实际收到了什么,要可视化出来。 | |
| 24 | -5. **循环回退连线**:Loop-Validator 相关的回退边(27 号文档 §3.5/§3.4 第 4/5 点)要在图上可视化成一条区别于正常前进边的连线,并显示当前是第几次循环、硬顶还剩几次。 | |
| 25 | -6. **主流程 / 异常流程处理**:节点成功后按 `success` 边前进是主流程;节点失败后按 `fail` 边路由(可能进 Loop-Validator 回退,也可能是死路进入需要人工介入的状态)是异常流程,两者在引擎状态机和 UI 上都要有明确区分。 | |
| 26 | -7. **子流程下钻**:点开一个节点,能看到两类下钻内容——① 这次 agent 调用内部的对话/工具调用记录(类似现有 `AgentChat.vue` 的 sub-agent 面板);② 如果这个节点本身触发了一张更小的嵌套 Work Graph,能在同一个页面里逐层下钻查看子图的执行状态(用户已确认两种下钻语义都要)。 | |
| 33 | +1. **一套图执行引擎**,同时驱动 org graph(company 级 workflow)和 work graph(org node 级 workflow)—— 两者是同一种东西在不同层级的递归(R-9.1),引擎不为它们各写一套。 | |
| 34 | +2. **执行控制**:启动、下一步、暂停、停止、重试五个操作,同时支持**手动**(点一次推进一个节点)和**自动**(一键授权跑到底)两种模式,且在**每一层语义完全一致**。 | |
| 35 | +3. **回退与熔断**:`edge_kind='backward'` 的回退边带循环计数与硬顶,两层通用;跨所有层级共享一个**任务级总预算**。 | |
| 36 | +4. **一个执行控制台页面**:左栏 org graph,中间选中 org node 的 work graph 画布,右栏节点详情;回退边可视化带 `N/3` 徽标;主流程 / 异常流程视觉区分;逐层下钻(agent 对话记录 + 嵌套子图)。 | |
| 37 | +5. **验收契约**:先定义 `goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一格式并建执行器,再动引擎(R-15.6)。 | |
| 38 | +6. **规划闭环**:`workgraph-planning` agent + 用户→公司的任务入口,让「一句自然语言 → 一张可执行的图 → 跑完」端到端成立(R-15.5)。 | |
| 27 | 39 | |
| 28 | 40 | ### 非目标(本阶段明确不做) |
| 29 | 41 | |
| 30 | -- **不做** `workgraph-planning` agent 本身的详细设计("一张 Work Graph 的节点/边最初是怎么被规划出来的")——27 号文档 §六已经把这个交付物列在 P3 里,本文档假定图已经规划好、满足 27 号文档 §3.4 第 5 点的执行前结构校验,只负责"图规划完之后,怎么跑、怎么控制、怎么看"。这不是回避,是刻意收窄范围:规划("这张图长什么样")和执行控制("这张图怎么被跑起来、怎么被人盯着")是两个复杂度都不低的问题,硬塞进一份文档会让两边都设计得不扎实。 | |
| 31 | -- **不做**对 DevLoop 现有 `loop-controller.js`/`unit-registry.js` 的收编或改造——27 号文档 §五把这个列为 P6(可选),本文档的执行引擎是全新的、只服务 Work Graph,不改 DevLoop 一行代码。§三.1 会说明为什么 DevLoop 现有引擎的执行模型不能直接套用到本文档的需求上。 | |
| 32 | -- **不做**多人协同编辑同一张 Work Graph(比如两个人同时点"下一步")的并发控制之外的复杂协作特性(评论、@提醒等)——并发控制本身要做(§四.A 幂等设计),但"协作"停留在"不冲突",不做"多人分工"这类产品特性。 | |
| 33 | -- **不做**跨公司/跨 Org Graph 的全局视图——页面粒度是"一家公司的一个 org node 的一张 Work Graph",不做"看所有公司所有 Work Graph"的总览大屏。 | |
| 34 | -- **不做**节点执行中途的"硬中断后从中间状态精确恢复"(比如 Coder 写到一半被强杀,下次重试不是从头写而是接着上次的文件状态续写)——重试语义是"整个节点重新执行一次",不是"断点续传",见 §四.C 重试小节的说明。 | |
| 42 | +- **不做 fan-out 并行**。它卡在两个既有不变量上:`CONVENTIONS.md` §12「同 `(pid, agent)` 只允许 1 个 `status=running`」和 SessionPool 的 key 复用([27-E2E](./27-e2e-task-execution-flow.md) §7.4)。必须先做同一 system agent 的多实例隔离,是独立工作量。**v1 直接约束「一家公司同时只有一个 active root run」**,新任务排队——注意这条约束在 org 层就需要,不是等到 work 层 fan-out 才需要。 | |
| 43 | +- **不做 L4**(system agent 自我迭代)。见 [27-E2E](./27-e2e-task-execution-flow.md) §八,且它是三级恢复叠加里最贵、爆炸半径最大的一级。 | |
| 44 | +- **不做**对 DevLoop 现有 `loop-controller.js` / `unit-registry.js` 的收编。它是本架构的参考实现([27-E2E](./27-e2e-task-execution-flow.md) §二结论 2),本引擎从它**抽象**,但不改它一行;收编是 P6。 | |
| 45 | +- **不做**多人协同编辑同一张图的协作特性(评论、@提醒)。并发**冲突控制**要做(§四.A.4 幂等),「协作」停留在「不冲突」。 | |
| 46 | +- **不做**节点执行中途的断点续传。重试语义是「整个节点重新执行一次」。 | |
| 47 | +- **不做**跨公司的全局大屏。页面粒度是「一家公司」。 | |
| 35 | 48 | |
| 36 | 49 | --- |
| 37 | 50 | |
| 38 | 51 | ## 二、背景 |
| 39 | 52 | |
| 40 | -27 号文档 §六路线图把 P3 定义为"Work Graph 最小闭环:单向执行,先不做回退"(顺序执行器,`success → next,fail → 停止/标记 blocked`),P4 才引入回退和 Loop-Validator。本文档没有严格切两份文档去对应这两个 phase,原因是:**执行引擎的状态机一旦要支持"暂停/单步/停止/重试"这种可以被外部随时打断和摆布的控制语义,"先做单向执行、再加回退"这种分阶段方式反而会让第一版的状态机设计得过窄,P4 加回退时必须回来重新改数据模型和引擎主循环**。所以本文档一次性把 P3(正向执行)+ P4(回退 + Loop-Validator)的数据模型和引擎设计完,落地时仍然可以分两个迭代交付(任务划分 §六 里 T1-T6 是 P3 范围、T7-T9 是 P4 范围),但底层 schema 和状态机不需要中途推倒重来。 | |
| 53 | +27 号总纲 §六把 P3 定义为「Work Graph 最小闭环:单向执行,先不做回退」,P4 才引入回退和 Loop-Validator。本文档不切两份,原因:**执行引擎的状态机一旦要支持「暂停/单步/停止/重试」这种可被外部随时打断和摆布的控制语义,「先做单向、再加回退」会让第一版状态机设计得过窄,P4 加回退时必须回来重改数据模型和主循环**。所以一次性把两个 phase 的数据模型和引擎设计完,落地仍分阶段交付(§五)。 | |
| 54 | + | |
| 55 | +v0.3 在此之上还合并了两块 27 号路线图里没单独立项的东西——控制台页面(R-4)和规划闭环(R-15.5)——理由是没有它们,P3 验收只能靠手写测试数据,验不到「这套东西真的能跑起来」。 | |
| 41 | 56 | |
| 42 | 57 | --- |
| 43 | 58 | |
| 44 | 59 | ## 三、现状代码走查(决定设计边界的关键证据) |
| 45 | 60 | |
| 46 | -调研了仓库里两处最相关的既有实现,用于确定新引擎"该抄什么、不该抄什么"。 | |
| 61 | +### 3.1 DevLoop `runUnit()` / `runProject()`:状态机可抄,驱动方式不可抄 | |
| 47 | 62 | |
| 48 | -### 3.1 DevLoop `runUnit()`:一个反面参照——展示了"为什么不能照抄" | |
| 63 | +`loop-controller.js:345` 的 `runUnit()` 本质是一个同步的 `while (attempt < retry_budget)` 循环:一次函数调用里 Coder → Tester → 失败则 `_buildRetryContext()` 拼上次失败摘要 → 重跑。`runProject()`(行 576,**已完整实现**,含 `_groupUnitsByWave()` Kahn 分波、L2 闸门、Integrator、BugTriager 四分支、预算熔断)同样是一次调用跑到底。`server/routes/devloop.js` 直接 `await loopController.runUnit(...)` 再吐响应——**整个 retry 循环最多 10 次 agent 调用全部塞在一次 HTTP 请求-响应周期里,外部在它跑完前完全没有插手机会**。 | |
| 49 | 64 | |
| 50 | -> ⚠️ **v0.2 修正**:本节原文说"`runProject` 目前直接 501,注释写明 P3 才补全"——**这条已过期**。`runProject` 已完整实现(`loop-controller.js:576`),且它内部就是一个跨 unit 的图执行器:`_groupUnitsByWave()`(行 523)做 Kahn 拓扑分波、L2 闸门判定、Integrator、BugTriager 四分支调度、`l2_budget` 熔断。下面对 `runUnit()`「不可中断」的分析仍然成立(`runProject` 同样是一次调用跑到底),但"跨 unit 执行器不存在"这个判断是错的。另注:`runProject` 的波内**实际是串行**执行的(行 609 注释 `// 串行跑(mock 同步快,不需并行)`),分波结构在、并行没做。 | |
| 65 | +> 补充观察:`runProject` 的波内**实际是串行**的(行 609 注释「串行跑(mock 同步快,不需并行)」),分波结构在、并行没做。这也印证了 §一非目标里「fan-out 并行是独立工作量」。 | |
| 51 | 66 | |
| 52 | -`server/src/core/devloop/loop-controller.js:345-431` 的 `runUnit()`,本质是一个**同步的 `while (attempt < retry_budget)` 循环**:一次函数调用里,Coder → Tester → 失败则 `_buildRetryContext()` 拼装上次失败摘要 → 重跑,直到测试通过或耗尽预算(默认 5 次)。`server/routes/devloop.js` 里 `POST /projects/:pid/run?only=<unitId>` 直接 `await loopController.runUnit(...)` 再吐响应——**这意味着整个 retry 循环(最多 10 次 agent 调用)全部塞在一次 HTTP 请求-响应周期里,外部在它跑完之前完全没有插手的机会**。这套模型没有任何"暂停"/"单步"/"停止"的概念,`runUnit()` 一旦被调用,只能等它跑完或者进程崩溃。 | |
| 67 | +配套的 `unit-registry.js` 状态机(`TRANSITIONS`,行 30-37)设计得很干净:显式转换表 + 非法转换报错 + 每次合法转换 emit 事件。**这部分直接借鉴**。 | |
| 53 | 68 | |
| 54 | -配套的 `unit-registry.js` 状态机(`TRANSITIONS = {pending:[coding], coding:[testing,failed], testing:[passed,failed], failed:[coding,blocked], passed:[pending], blocked:[pending]}`,行 30-37)设计得很干净,**这部分值得直接借鉴**——校验非法转换、每次合法转换 emit SSE 事件(见 3.3)——但状态机是"谁在跑循环"这件事的记录者,不是驱动者,驱动者是那个不可中断的 `while` 循环本身。**结论:新引擎的状态机设计可以照抄 `unit-registry.js` 的模式(显式转换表 + 非法转换报错 + emit 事件),但主循环绝不能照抄 `runUnit()` 的"一次函数调用跑到底"模型**。 | |
| 69 | +**结论**:状态机照抄 `unit-registry.js` 的模式,主循环**绝不能**照抄「一次函数调用跑到底」。 | |
| 55 | 70 | |
| 56 | 71 | ### 3.2 Pipeline Service:正确的参照模型 |
| 57 | 72 | |
| 58 | -`server/services/pipeline-service.js` 的 `startRun()`(144-181 行)只做状态机初始化(第一步 `status='ready'`,其余 `'waiting'`),**不会同步执行任何 agent**。真正推进流程靠外部显式调用 `advanceStep(runId, stepIndex, {output, status})`:`status:'failed'` 直接把整个 run 标 `failed` 终止;否则把 `currentStep` 前移、下一步置 `ready`;全部完成则 `status:'completed'`。`rejectStep()` 是现成的"退回重做"语义——把某一步打回 `ready`,历史记入 `step.rejections[]`。 | |
| 73 | +`pipeline-service.js` 的 `startRun()`(144-181 行)只做状态机初始化,**不同步执行任何 agent**。推进靠外部显式调用 `advanceStep(runId, stepIndex, {output, status})`。`rejectStep()` 是现成的「退回重做」语义。 | |
| 74 | + | |
| 75 | +**这正是本文档需要的执行模型**:状态是持久化数据,「推进一步」是一次显式、幂等、外部可随时调用的动作,不是占着调用栈不放的循环。手动模式下「下一步」= 用户点一次调一次;自动模式下 = 引擎自己在上一次返回后立刻再调一次。**手动和自动共用同一个原语,区别只在谁触发下一次调用**。 | |
| 76 | + | |
| 77 | +**两处不能照抄的坑**: | |
| 59 | 78 | |
| 60 | -**这正是本文档需要的执行模型**:状态是持久化数据,"推进一步"是一次显式、幂等、外部可随时调用的动作,不是一个占着调用栈不放的循环。手动模式下"下一步"按钮 = 用户点一次调一次 `advance`;自动模式下,引擎自己在每次 `advance` 成功返回后,检查"还要不要继续",是就立刻自己再调一次 `advance`——**手动和自动共用同一个 `advance` 原语,区别只在于"谁来触发下一次调用"**(§四.C 详细展开)。 | |
| 79 | +1. `CompanyDashboard.vue:1019` 的 `loadPipelineDetail()` 请求的是流水线**定义**,但模板里 `:class="{'step-current': selectedPipeline.currentStep === idx}"`(行 258)引用的 `currentStep` 只存在于 run 实例上——这个高亮效果**从未生效过**。→ 本设计的定义层与运行时层必须是两个各自完整、明确分开的对象(§四.A)。 | |
| 80 | +2. `retryPipelineRun()`(行 1034)调 `POST .../advance` 但没传 `stepIndex`,而路由端有 `if (stepIndex === undefined) return 400`——这个「重试」按钮点了必定 400。→ 「重试」必须是独立、参数自解释的端点(§四.C)。 | |
| 61 | 81 | |
| 62 | -**两处不能照抄的坑**(写新代码时要主动避开): | |
| 82 | +### 3.3 SSE 基础设施:可直接复用 | |
| 63 | 83 | |
| 64 | -1. `CompanyDashboard.vue:1019-1024` 的 `loadPipelineDetail()` 请求的是流水线**定义**,不是某次 run 的**实例**状态,但模板里 `:class="{'step-current': selectedPipeline.currentStep === idx}"`(行 258)引用的 `currentStep` 只存在于 run 实例对象上,定义对象里根本没有——这个"当前步骤高亮"视觉效果实际上从未生效过。新页面的数据契约必须让"图的静态定义"和"某次执行的运行时状态"是两个明确分开、各自完整的对象,不能像这样把运行时字段错绑到定义对象上。 | |
| 65 | -2. `retryPipelineRun()`(CompanyDashboard.vue:1034-1040)调用 `POST .../advance` 但完全没传 `stepIndex`,而路由端有 `if (stepIndex === undefined) return res.status(400)` 的校验——现状这个"重试"按钮点了必定 400。新设计的"重试"必须是一个**独立、参数自解释的端点**(§四.C),不能依赖调用方正确拼出一个复用端点的隐藏参数组合。 | |
| 84 | +`server/services/devloop/sse-broker.js`:每项目一个广播通道,`subscribe(pid, res, {replayFromOffset})` 支持 `Last-Event-ID` 断线重放,`emit()` 同时落盘 + 广播,25s 心跳。按 `run_id` 复用同一套实现。 | |
| 66 | 85 | |
| 67 | -### 3.3 SSE 基础设施:可以直接复用 | |
| 86 | +**既有缺口不要重复**:DevLoop 里 `timeline-logger.js` 的审计事件和 `sse-broker.js` 的实时推送是两条独立的轨,没有自动桥接。本引擎要求**状态转换、timeline 写入、SSE 广播三者在同一次原子操作里一起发生**。 | |
| 68 | 87 | |
| 69 | -`server/services/devloop/sse-broker.js`:每个项目一个广播通道,`subscribe(pid, res, {replayFromOffset})` 支持基于 `Last-Event-ID` 的断线重放(从磁盘 `stream.log` 按序号过滤),`emit(pid, event, data)` 同时落盘 + 广播,25s 心跳防连接被中间层判死。这是一套成熟、可直接复用的多订阅者 SSE 基础设施,本文档按 `work_graph_id` 复用同一套实现(§四.A 的 SSE 小节)。**发现的既有缺口**:DevLoop 里 `timeline-logger.js` 的审计事件和 `sse-broker.js` 的实时推送是两条独立的轨,没有自动桥接——`loop-controller.js` 往 timeline 里追加一行,不会顺带 emit 一个 SSE 事件。本文档的引擎明确要求**状态转换、timeline 写入、SSE 广播三者在同一次原子操作里一起发生**(§四.A),不重复这个缺口。 | |
| 88 | +### 3.4 子流程展示:`AgentChat.vue` 的双轨折叠卡片,直接复用 | |
| 70 | 89 | |
| 71 | -### 3.4 子流程展示:`AgentChat.vue` 的双轨折叠卡片模式,直接复用 | |
| 90 | +第四列 "ACP Events" 面板已实现:`subAgents`(以 `callId` 为 key 的响应式 map)承载**进行中**的子调用,靠流式事件持续追加;`subLogList` 是**已完成**的历史日志卡片列表,外观一致但无 `LIVE` 徽标。这套「live map + history list」双轨模型正是节点运行日志需要的东西。 | |
| 72 | 91 | |
| 73 | -`web/src/views/AgentChat.vue` 第四列 "ACP Events" 面板已经实现了本文档 §四.B 需要的下钻 UI 模式: | |
| 92 | +### 3.5 前端 DAG:`UnitsDAG.vue` 已实现,扩展而非重写 | |
| 74 | 93 | |
| 75 | -- `subAgents`(以 `callId` 为 key 的响应式 map)承载**正在跑**的子调用,每项 `{agent, status:'running'|'success'|'error', content, collapsed}`,靠流式事件(`chunk`/`start`/`done`)持续追加内容、更新终态;卡片 header 是状态 emoji + agent 名 + `LIVE` 徽标 + 折叠箭头,展开后是等宽字体滚动日志区。 | |
| 76 | -- `subLogList` 是**已完成/历史**的日志卡片列表,外观一致但没有 `LIVE` 徽标。 | |
| 94 | +`web/src/components/devloop/UnitsDAG.vue`(107 行)已实现 SVG + `<marker>` 箭头 + **Kahn 分波拓扑布局**(`waves` computed,带 safety 上限防死循环)+ 按 status 着色 + 点击 emit 下钻。同目录 `LiveAgentStream.vue`(带 Last-Event-ID 重连的 SSE 消费)、`UnitDrawer.vue`(节点详情抽屉)同样可复用。**真正的新增工作量只剩回退边的曲线路由**。 | |
| 77 | 95 | |
| 78 | -这套"live map(进行中)+ history list(已完成)"的双轨模型,跟本文档"节点运行日志"需要的东西完全对应,直接搬过来用,不重新发明。 | |
| 96 | +### 3.6 进程级取消:机制存在,调用链需实现前二次确认 | |
| 79 | 97 | |
| 80 | -### 3.5 进程级取消能力:已经存在,但调用链需要在实现时二次确认 | |
| 98 | +`server/src/engine/process-guard.js` 的 `ProcessGuard.cancel()`(82-93 行)对底层 Claude CLI 子进程先 `SIGTERM`、超时后 `SIGKILL`。`run-registry.js` 的 `RunRegistry` 按 `runId` 登记每次调用,但其公开方法(`add/get/markState/touchOutput/listActive/listByBackend/listByAgent/list/remove/stats/clear`)里**没有 `cancel`**——取消动作大概率是调用方自己持有 `ProcessGuard` 引用后直接调,`RunRegistry` 只做登记和查询。列入 §五 T9 的实现前置核实项,不在设计阶段假设死。 | |
| 81 | 99 | |
| 82 | -`server/src/engine/process-guard.js` 的 `ProcessGuard.cancel()`(82-93 行)会对底层 Claude CLI 子进程先发 `SIGTERM`、超时后 `SIGKILL`,`server/src/engine/run-registry.js` 的 `RunRegistry` 按 `runId` 登记每次调用(`get(runId)`/`listByAgent(agentName)`),`AgentManager` 是这套机制的调用方之一。这意味着"停止一个正在执行的节点"理论上可以做到**真正打断子进程**,不是只能等它自然跑完。但本文档写作时**没有逐行确认** `RunRegistry` 的记录里是否直接暴露了对应 `ProcessGuard` 实例的引用(`run-registry.js` 目前看到的公开方法是 `add/get/markState/touchOutput/listActive/listByBackend/listByAgent/list/remove/stats/clear`,没看到 `cancel`——取消动作大概率是调用方自己持有 `ProcessGuard` 引用后直接调用,`RunRegistry` 只做登记和查询)。这一点列入 §六 T5 的实现前置核实项,不在设计阶段假设死。 | |
| 100 | +### 3.7 P2 已落地的图校验,直接复用 | |
| 101 | + | |
| 102 | +`server/services/org-graph-service.js`(469 行)已实现四项写入前校验:`acyclic`(三色 DFS,仅针对 forward 边)、`single-entry`(forward 意义下入度为 0 的节点恰好一个)、`reachable`(孤立节点拒绝)、`backward-labeled`(构成回路的边必须标 `backward`,且不构成回路的边不许标 `backward`——**双向复核,不信任标注**)。 | |
| 103 | + | |
| 104 | +**work graph 的写入校验直接复用这一套**,不另写。这意味着 work graph 也**强制单入口**(R-13.4 原本只对 org graph 定案,本文档统一到两层)。 | |
| 105 | + | |
| 106 | +**需新增第五项 `fail-fallback-edge`**(R-16,见 §四.D.2.2):任何有 `fail` 出边的节点,必须至少留一条 `fail_reason IS NULL` 的兜底边。这一项 P2 现有实现里没有,两层都要加。 | |
| 83 | 107 | |
| 84 | 108 | --- |
| 85 | 109 | |
| ... | ... | @@ -87,103 +111,214 @@ |
| 87 | 111 | |
| 88 | 112 | ### A. Model(数据模型) |
| 89 | 113 | |
| 90 | -#### A.1 表结构(在 P0/P2 的 `graph.sqlite` 上追加,复用同一个 DB 文件) | |
| 114 | +#### A.1 分层:定义层 ≠ 运行时层 | |
| 115 | + | |
| 116 | +这是 v0.3 相对 v0.1 最大的改动,直接来自 R-15.4。 | |
| 117 | + | |
| 118 | +| | org graph | work graph | | |
| 119 | +|---|---|---| | |
| 120 | +| 生成者 | `orggraph-planning` | `workgraph-planning` | | |
| 121 | +| 生成时机 | 建公司时,一次 | 每个 org node 接到任务时 | | |
| 122 | +| 生命周期 | **长期**,活整个公司生命周期 | **一次性**,规划完即执行 | | |
| 123 | +| 一份定义被跑几次 | **多次**(每来一个任务跑一遍) | 一次 | | |
| 124 | + | |
| 125 | +因为 org graph 是「一份定义、多次执行」,运行时状态**不能**存在定义表上(否则一家公司永远只能跑一个任务)。所以: | |
| 126 | + | |
| 127 | +``` | |
| 128 | +定义层(两套,各自保留,形态同构) | |
| 129 | + org_graph_nodes / org_graph_edges ← P2 已落地,本文档只读不改(除 T-P3 加一列) | |
| 130 | + work_graph_nodes / work_graph_edges ← 本文档新增 | |
| 131 | + | |
| 132 | +运行时层(一套,两层共用,引擎只认这个) | |
| 133 | + graph_runs ← 一次执行实例 | |
| 134 | + graph_run_nodes ← 该次执行里每个节点的状态 | |
| 135 | + graph_run_loop_records ← 回退边的循环计数与判词 | |
| 136 | +``` | |
| 137 | + | |
| 138 | +`work_graphs` 表在 v0.1 里混装了图的定义元数据和运行时状态(`run_status` / `pause_requested` / `execution_mode`),本版把运行时那半拆去 `graph_runs`。 | |
| 139 | + | |
| 140 | +#### A.2 表结构(追加在 P0/P2 的 `graph.sqlite` 上,复用同一 DB 文件) | |
| 91 | 141 | |
| 92 | 142 | ```sql |
| 93 | --- 一次具体的 Work Graph 执行实例(图的定义和运行时状态在同一行—— | |
| 94 | --- 与 §3.2 指出的"定义 vs 实例分离"原则不矛盾:Work Graph 本来就是"规划完即执行、 | |
| 95 | --- 执行完即归档"的一次性产物(27 号文档 §二术语定义),不像 Pipeline 那样"定义可以脱离某次 | |
| 96 | --- run 单独被复用",所以这里定义即实例,没有必要拆两张表) | |
| 143 | +-- ── 定义层:work graph(org graph 的两张表 P2 已建) ────────────── | |
| 144 | + | |
| 97 | 145 | CREATE TABLE IF NOT EXISTS work_graphs ( |
| 98 | - id TEXT PRIMARY KEY, -- wg_<nanoid> | |
| 99 | - company_id TEXT NOT NULL, | |
| 100 | - triggered_by_org_node_id INTEGER REFERENCES org_graph_nodes(id), -- 对应"每个 org node 运行的 work graph" | |
| 101 | - parent_node_id TEXT REFERENCES work_graph_nodes(id), -- 非空 = 这是另一个节点下钻出来的嵌套子图(§四.D.3) | |
| 102 | - goal_id TEXT, -- 一次人类需求描述(唯一,验收边界) | |
| 103 | - okr_node_id TEXT, -- 形如 O-001.1.1 | |
| 104 | - task_id TEXT, -- 形如 T-xxx | |
| 105 | - execution_mode TEXT NOT NULL DEFAULT 'manual' CHECK (execution_mode IN ('manual','auto')), | |
| 106 | - run_status TEXT NOT NULL DEFAULT 'planning', | |
| 107 | - -- planning | ready | running | paused | blocked | completed | failed | stopped | |
| 108 | - pause_requested INTEGER NOT NULL DEFAULT 0, -- 0/1,auto 模式下"暂停"先置位,下一个节点边界生效 | |
| 109 | - stop_requested INTEGER NOT NULL DEFAULT 0, -- 0/1,同上,"停止"的软信号 | |
| 110 | - created_at TEXT NOT NULL, | |
| 111 | - updated_at TEXT NOT NULL | |
| 146 | + id TEXT PRIMARY KEY, -- wg_<nanoid> | |
| 147 | + company_id TEXT NOT NULL, | |
| 148 | + owner_org_node_id INTEGER REFERENCES org_graph_nodes(id), -- 这张图属于哪个 org node | |
| 149 | + planned_by TEXT NOT NULL DEFAULT 'workgraph-planning', | |
| 150 | + goal_id TEXT, -- 一次人类需求描述(唯一,验收边界) | |
| 151 | + task_id TEXT, -- 执行单元,一个 goal 多个 | |
| 152 | + okr_node_id TEXT, -- 形如 O-001.1.1,长周期度量归属 | |
| 153 | + created_at TEXT NOT NULL | |
| 112 | 154 | ); |
| 155 | +-- goal_id / task_id / okr_node_id 三者基数不同、不可混用,见 27-NAMING §四。 | |
| 156 | +-- v0.1 的单字段 goal_task_id 不够用,已拆开。 | |
| 113 | 157 | |
| 114 | 158 | CREATE TABLE IF NOT EXISTS work_graph_nodes ( |
| 115 | - id TEXT PRIMARY KEY, -- wgn_<nanoid> | |
| 116 | - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), | |
| 117 | - system_agent_id TEXT NOT NULL REFERENCES system_agents(id), | |
| 118 | - node_kind TEXT NOT NULL DEFAULT 'task' CHECK (node_kind IN ('task','loop_validator')), | |
| 119 | - status TEXT NOT NULL DEFAULT 'pending', | |
| 120 | - -- pending | ready | running | success | fail | blocked | skipped | |
| 121 | - attempt INTEGER NOT NULL DEFAULT 0, | |
| 122 | - agent_run_id TEXT, -- 本次 attempt 对应的 RunRegistry runId,用于"停止"时定位要 cancel 哪个子进程(§三.5) | |
| 123 | - has_subgraph INTEGER NOT NULL DEFAULT 0, -- 0/1,非 0 时应存在一行 work_graphs.parent_node_id = 本节点 id | |
| 124 | - output_ref TEXT, -- 产出物相对路径,见 A.2 路径约定,不是 DB 存内容本身 | |
| 125 | - started_at TEXT, | |
| 126 | - completed_at TEXT, | |
| 127 | - created_at TEXT NOT NULL, | |
| 128 | - updated_at TEXT NOT NULL | |
| 159 | + id TEXT PRIMARY KEY, -- wgn_<nanoid> | |
| 160 | + work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), | |
| 161 | + node_name TEXT NOT NULL, -- 图内唯一,如 org-coder-validator | |
| 162 | + system_agent_id TEXT NOT NULL REFERENCES system_agents(id), -- 引用的本体,如 coder-validator | |
| 163 | + node_kind TEXT NOT NULL DEFAULT 'agent_call' | |
| 164 | + CHECK (node_kind IN ('agent_call','subgraph')), | |
| 165 | + created_at TEXT NOT NULL | |
| 129 | 166 | ); |
| 167 | +-- node_name ≠ system_agent_id:节点名带领域前缀、本体名不带(R-15.3)。 | |
| 168 | +-- 同一本体可被多个节点、多家公司引用,与 P1 虚拟引用寻址一致。 | |
| 130 | 169 | |
| 131 | 170 | CREATE TABLE IF NOT EXISTS work_graph_edges ( |
| 171 | + id TEXT PRIMARY KEY, | |
| 172 | + work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), | |
| 173 | + from_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), | |
| 174 | + to_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), | |
| 175 | + condition TEXT NOT NULL CHECK (condition IN ('success','fail')), | |
| 176 | + fail_reason TEXT, -- 可空;condition='fail' 时的细分路由键,见 §四.D.2.2 | |
| 177 | + edge_kind TEXT NOT NULL DEFAULT 'forward' | |
| 178 | + CHECK (edge_kind IN ('forward','backward')), | |
| 179 | + created_at TEXT NOT NULL | |
| 180 | +); | |
| 181 | + | |
| 182 | +-- ── 运行时层:两层共用 ────────────────────────────────────────── | |
| 183 | + | |
| 184 | +CREATE TABLE IF NOT EXISTS graph_runs ( | |
| 185 | + id TEXT PRIMARY KEY, -- run_<nanoid> | |
| 186 | + graph_kind TEXT NOT NULL CHECK (graph_kind IN ('org','work')), | |
| 187 | + graph_id TEXT NOT NULL, -- kind='org' → company_id;kind='work' → work_graphs.id | |
| 188 | + company_id TEXT NOT NULL, | |
| 189 | + root_run_id TEXT NOT NULL, -- 自身为 root 时 = 自己的 id;冗余存储,避免每次上溯 | |
| 190 | + parent_run_id TEXT REFERENCES graph_runs(id), | |
| 191 | + parent_node_run_id TEXT REFERENCES graph_run_nodes(id), -- 是哪个节点下钻出来的 | |
| 192 | + goal_id TEXT, | |
| 193 | + task_id TEXT, | |
| 194 | + okr_node_id TEXT, | |
| 195 | + execution_mode TEXT NOT NULL DEFAULT 'manual' | |
| 196 | + CHECK (execution_mode IN ('manual','auto')), | |
| 197 | + run_status TEXT NOT NULL DEFAULT 'planning', | |
| 198 | + -- planning | ready | running | paused | blocked | completed | failed | stopped | |
| 199 | + pause_requested INTEGER NOT NULL DEFAULT 0, -- 软信号,下一个节点边界生效 | |
| 200 | + stop_requested INTEGER NOT NULL DEFAULT 0, | |
| 201 | + backward_count INTEGER NOT NULL DEFAULT 0, -- run 级总回退次数,双层熔断的外层,见 §四.D.2.1 | |
| 202 | + budget_json TEXT, -- 仅 root run 非空,见 §四.A.3 | |
| 203 | + created_at TEXT NOT NULL, | |
| 204 | + updated_at TEXT NOT NULL | |
| 205 | +); | |
| 206 | +-- v1 约束:同一 company_id 至多一个 run_status ∈ {ready,running,paused} 的 root run(§一非目标) | |
| 207 | +CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_runs_one_active_root | |
| 208 | + ON graph_runs(company_id) | |
| 209 | + WHERE parent_run_id IS NULL AND run_status IN ('ready','running','paused'); | |
| 210 | + | |
| 211 | +CREATE TABLE IF NOT EXISTS graph_run_nodes ( | |
| 212 | + id TEXT PRIMARY KEY, -- rn_<nanoid> | |
| 213 | + run_id TEXT NOT NULL REFERENCES graph_runs(id), | |
| 214 | + def_node_id TEXT NOT NULL, -- 按所属 run 的 graph_kind 解释: | |
| 215 | + -- 'org' → org_graph_nodes.id(整数转字符串) | |
| 216 | + -- 'work' → work_graph_nodes.id | |
| 217 | + status TEXT NOT NULL DEFAULT 'pending', | |
| 218 | + -- pending | ready | running | success | fail | blocked | skipped | |
| 219 | + attempt INTEGER NOT NULL DEFAULT 0, | |
| 220 | + agent_run_id TEXT, -- node_kind='agent_call' 时的 RunRegistry runId | |
| 221 | + child_run_id TEXT REFERENCES graph_runs(id), -- node_kind='subgraph' 时本次 attempt 起的子 run | |
| 222 | + output_ref TEXT, -- 产出物相对路径,见 §四.A.5 | |
| 223 | + fail_reason TEXT, -- 本次失败的细分原因,供边路由,见 §四.D.2.2 | |
| 224 | + started_at TEXT, | |
| 225 | + completed_at TEXT, | |
| 226 | + created_at TEXT NOT NULL, | |
| 227 | + updated_at TEXT NOT NULL | |
| 228 | +); | |
| 229 | +CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_run_nodes_unique | |
| 230 | + ON graph_run_nodes(run_id, def_node_id); | |
| 231 | + | |
| 232 | +CREATE TABLE IF NOT EXISTS graph_run_loop_records ( | |
| 132 | 233 | id TEXT PRIMARY KEY, |
| 133 | - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), | |
| 134 | - from_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), | |
| 135 | - to_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), | |
| 136 | - condition TEXT NOT NULL CHECK (condition IN ('success','fail')), | |
| 137 | - edge_kind TEXT NOT NULL DEFAULT 'forward' CHECK (edge_kind IN ('forward','backward')), | |
| 234 | + run_id TEXT NOT NULL REFERENCES graph_runs(id), | |
| 235 | + edge_id TEXT NOT NULL, -- 触发回退的那条 backward 边(org/work 边表按 kind 解释) | |
| 236 | + from_node_run_id TEXT NOT NULL REFERENCES graph_run_nodes(id), | |
| 237 | + to_node_run_id TEXT NOT NULL REFERENCES graph_run_nodes(id), | |
| 238 | + loop_count INTEGER NOT NULL, -- 这是第几次沿这条边回退(1-based) | |
| 239 | + verdict TEXT NOT NULL | |
| 240 | + CHECK (verdict IN ('registered','fixed','not_fixed','force_terminated')), | |
| 241 | + remediation_note TEXT, -- 「应该如何修复」,供下一轮读取 | |
| 138 | 242 | created_at TEXT NOT NULL |
| 139 | 243 | ); |
| 244 | +``` | |
| 140 | 245 | |
| 141 | --- Loop-Validator 每次"备案-审计-修复方案传递校验"的记录(27 号文档 §3.5 三步), | |
| 142 | --- 是"循环划线"上显示"第 N/3 次、上次判断"这类信息的数据来源,不是从 work_graph_nodes.attempt 反推 | |
| 143 | -CREATE TABLE IF NOT EXISTS work_graph_loop_records ( | |
| 144 | - id TEXT PRIMARY KEY, | |
| 145 | - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), | |
| 146 | - loop_validator_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), | |
| 147 | - target_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), -- 回退目标节点 | |
| 148 | - loop_count INTEGER NOT NULL, -- 这是第几次回退(1-based) | |
| 149 | - verdict TEXT NOT NULL CHECK (verdict IN ('registered','fixed','not_fixed','force_terminated')), | |
| 150 | - remediation_note TEXT, -- "应该如何修复"的方案文本,供下一轮读取 | |
| 151 | - created_at TEXT NOT NULL | |
| 152 | -); | |
| 246 | +> ⚠️ **一处已知的类型不整齐**:`org_graph_nodes.id` 是 `INTEGER AUTOINCREMENT`(P0 建表时定的),`work_graph_nodes.id` 是 `TEXT`。`graph_run_nodes.def_node_id` 统一取 `TEXT`,org 侧存整数的字符串形式。代价是这一列不能建外键约束,得靠服务层保证引用完整性。备选是把 `org_graph_nodes` 的主键改成 TEXT——那要动 P2 已落地并已有数据的表,不值得。**上限**:`def_node_id` 无 FK 保护,删除定义节点时需服务层显式级联;升级路径是等 org 侧有独立迁移窗口时统一成 TEXT 主键。 | |
| 247 | + | |
| 248 | +#### A.3 任务级总预算:挂 root run | |
| 249 | + | |
| 250 | +[27-E2E](./27-e2e-task-execution-flow.md) §7.1 指出的最严重问题:三级恢复(跳回节点 → 重新 planning → 迭代 agent)如果每级都重置下一级的计数器,整个循环没有终止条件。分层预算(`retry_budget` / `l2_budget`)只是**局部快速失败**信号,不是兜底。 | |
| 251 | + | |
| 252 | +有了 run 层,兜底的挂载点很自然——**`budget_json` 只在 root run 上非空,所有子 run 通过 `root_run_id` 扣同一个池**: | |
| 253 | + | |
| 254 | +```jsonc | |
| 255 | +{ | |
| 256 | + "total_agent_calls": 100, // 所有层级所有 agent 调用的总次数 | |
| 257 | + "total_tokens": 5000000, | |
| 258 | + "wallclock_ms": 7200000, // 2h | |
| 259 | + "spent": { "agent_calls": 0, "tokens": 0, "started_at": "..." } | |
| 260 | +} | |
| 153 | 261 | ``` |
| 154 | 262 | |
| 155 | -节点/边的**内容**(任务描述、日志、上下文)依旧不进 DB——沿用 27 号文档 §四已经定下的原则:`output_ref` 只存相对路径,指向 System Agent 目录下按 §3.2 已定义的物理位置:`system/agents/{agentId}/companies/{companyId}/history/{workGraphId}-{nodeId}-{attempt}.md`,本文档不新造一套路径约定,直接复用。 | |
| 263 | +**只增不减**。任何一层发起 agent 调用前先按 `root_run_id` 检查余额;扣完不是失败,是**整棵 run 树挂起等人工**(root run → `blocked`,向下级联 `pause_requested=1`)。 | |
| 156 | 264 | |
| 157 | -#### A.2 状态机(照抄 `unit-registry.js` 的转换表模式,不照抄它的驱动方式) | |
| 265 | +按 `plan-cost-concurrency.md` 的实测,DevLoop 一个中等项目 ≈ 39 次 agent 调用 / 410K token / $4.8 / 墙钟 20 分钟;本架构还要叠加两层图的规划,粗估中等任务 **60-100 次调用、$10-20、1-2 小时**([27-E2E](./27-e2e-task-execution-flow.md) §7.6)。这个量级下自动模式**启动前必须把预估成本告诉用户**(§四.C)。 | |
| 266 | + | |
| 267 | +#### A.4 状态机 | |
| 158 | 268 | |
| 159 | 269 | ``` |
| 160 | -work_graph_nodes.status 转换表: | |
| 161 | - pending → ready (上游全部满足 / 无上游依赖,进入可执行队列) | |
| 162 | - ready → running (被 advance 拾取,开始调用 agent) | |
| 163 | - running → success | fail (agent 调用结束) | |
| 164 | - fail → ready (loop_validator 回退命中) | blocked (无回退边/回退耗尽) | ready (人工点"重试") | |
| 165 | - success → (终态,除非人工"重试"强制打回 ready) | |
| 166 | - blocked → ready (人工点"重试",唯一走出 blocked 的路) | |
| 167 | - * → skipped (run 被"停止"时,所有仍处于 pending/ready 的节点) | |
| 270 | +graph_run_nodes.status: | |
| 271 | + pending → ready (上游全部满足 / 无上游依赖) | |
| 272 | + ready → running (被 advanceOne 拾取) | |
| 273 | + running → success | fail (agent 调用结束 / 子 run 收敛) | |
| 274 | + fail → ready (命中一条 backward 边且未超硬顶;或人工「重试」) | |
| 275 | + → blocked (无可走的 fail 边 / 回退硬顶耗尽) | |
| 276 | + success → (终态,除非人工「重试」强制打回 ready) | |
| 277 | + blocked → ready (人工「重试」,唯一走出 blocked 的路) | |
| 278 | + * → skipped (run 被「停止」时,所有仍处 pending/ready 的节点) | |
| 279 | + | |
| 280 | +graph_runs.run_status: | |
| 281 | + planning → ready (图规划完成 + 通过 §三.7 四项校验) | |
| 282 | + ready → running (「启动」) | |
| 283 | + running → paused (「暂停」软信号,下一个节点边界生效) | |
| 284 | + paused → running (「启动」或「下一步」恢复) | |
| 285 | + running → blocked (某节点 blocked 且无其他节点在 ready/running) | |
| 286 | + blocked → running (对 blocked 节点「重试」后) | |
| 287 | + running/paused/blocked → stopped (「停止」) | |
| 288 | + running → completed (所有可达节点到达终态,且无 pending/ready 悬空) | |
| 168 | 289 | ``` |
| 169 | 290 | |
| 170 | -非法转换(比如对一个 `success` 节点直接调"下一步"执行它)在服务层直接拒绝并返回明确错误,模式与 `unit-registry.js` 的 `InvalidTransitionError` 一致。 | |
| 291 | +非法转换(如对 `success` 节点直接「下一步」)在服务层拒绝并返回明确错误,模式与 `unit-registry.js` 的 `InvalidTransitionError` 一致。 | |
| 292 | + | |
| 293 | +#### A.5 产物布局与跨节点传递 | |
| 294 | + | |
| 295 | +v0.1 只有 `output_ref`(节点内产物指针),**不足以支撑 R-15.3 推导出的跨 org node 传递**:`goal-benchmark` / `goal-test-data` / `goal-test-script` 在 `org-planner` 这个 org node 的 work graph 里产出,消费者却是 `org-validator` 这个**另一个 org node**。 | |
| 171 | 296 | |
| 172 | 297 | ``` |
| 173 | -work_graphs.run_status 转换表: | |
| 174 | - planning → ready (图规划完成 + 通过 27 号文档 §3.4 第 5 点的结构校验) | |
| 175 | - ready → running (点"启动") | |
| 176 | - running → paused (auto 模式下点"暂停",软信号,下一个节点边界生效) | |
| 177 | - paused → running (点"启动"或"下一步"恢复) | |
| 178 | - running → blocked (某节点进入 blocked 且不是被 stop 打断) | |
| 179 | - blocked → running (对 blocked 节点点"重试"后) | |
| 180 | - running/paused/blocked → stopped (点"停止") | |
| 181 | - running → completed (所有可达节点都到达终态,且没有 pending/ready 的悬空节点) | |
| 298 | +data/companies/{cid}/runs/{rootRunId}/ | |
| 299 | +├── artifacts/ # 任务级共享产物区(root run 级,跨 org node 可读) | |
| 300 | +│ ├── goal-benchmark.json # 验收契约,见阶段 0 | |
| 301 | +│ ├── goal-test-data/ | |
| 302 | +│ └── goal-test-script/ | |
| 303 | +└── nodes/{nodeRunId}/ # 节点私有产出,output_ref 指向这里 | |
| 304 | + └── attempt-{n}.md | |
| 182 | 305 | ``` |
| 183 | 306 | |
| 184 | -#### A.3 幂等与并发 | |
| 307 | +**读写规则(照搬 `CONVENTIONS.md` §12 的隔离思路)**: | |
| 308 | + | |
| 309 | +| | 读 | 写 | | |
| 310 | +|---|---|---| | |
| 311 | +| 自己的 `nodes/{自己}/` | ✅ | ✅ | | |
| 312 | +| 别的节点的 `nodes/{其他}/` | ❌ 禁止 | ❌ | | |
| 313 | +| `artifacts/`(按 `root_run_id` 上溯) | ✅ 任意层级只读 | ⚠️ 需节点在定义里显式声明产出权 | | |
| 314 | + | |
| 315 | +「下游只能看到上游的契约,看不到实现」这条 DevLoop 已验证的原则原样继承。写 `artifacts/` 是特权动作——只有被 `workgraph-planning` 标记为验收契约产出者的节点才有,防止任意节点往共享区乱写。 | |
| 316 | + | |
| 317 | +节点私有产出的物理位置仍沿用 27 号文档 §四已定的 System Agent 目录约定,本文档不新造路径。 | |
| 318 | + | |
| 319 | +#### A.6 幂等与并发 | |
| 185 | 320 | |
| 186 | -`advance` 类操作(§四.C)必须幂等:同一节点被并发触发两次"下一步"(比如自动驱动器和人工同时点),第二次调用检测到节点 `status` 已经不是 `ready` 就直接返回当前状态而不是报错——这是 §一非目标里"不做复杂协作、但要做到不冲突"的具体实现方式,不需要引入分布式锁,SQLite 的行级事务 + `status` 字段的 `WHERE status='ready'` 条件更新(受影响行数=0 即视为"已经被别人拿走了")就够。 | |
| 321 | +`advanceOne` 必须幂等:同一节点被并发触发两次(自动驱动器和人工同时点),第二次检测到 `status` 已不是 `ready` 就返回当前状态而不报错。不引入分布式锁——SQLite 行级事务 + `UPDATE ... WHERE status='ready'` 条件更新(受影响行数 = 0 即「已被别人拿走」)就够。 | |
| 187 | 322 | |
| 188 | 323 | --- |
| 189 | 324 | |
| ... | ... | @@ -191,139 +326,289 @@ work_graphs.run_status 转换表: |
| 191 | 326 | |
| 192 | 327 | #### B.1 页面结构 |
| 193 | 328 | |
| 194 | -新页面(新路由,不挂在 `CompanyDashboard.vue` 已有的 tab 体系下——这个页面信息密度和交互复杂度明显超过一个 tab 该承载的量,参照仓库里 `PipelineEditor.vue` 独立成页的先例): | |
| 329 | +新页面、新路由,不挂在 `CompanyDashboard.vue` 已有的 tab 体系下(信息密度和交互复杂度明显超过一个 tab 该承载的量,参照 `PipelineEditor.vue` 独立成页的先例): | |
| 195 | 330 | |
| 196 | 331 | ``` |
| 197 | -┌─────────────────────────────────────────────────────────────────┐ | |
| 198 | -│ <company name> 组织与执行 │ | |
| 199 | -├───────────────┬─────────────────────────────────┬─────────────────┤ | |
| 200 | -│ Org Graph │ Work Graph 画布 │ 节点详情面板 │ | |
| 201 | -│ (左栏,复用 │ (选中的 org node 当前/历史 │ (点某个 work │ | |
| 202 | -│ 27-P2 §4.7 │ Work Graph 的 DAG 可视化, │ graph node 后 │ | |
| 203 | -│ 分层列表) │ 见 B.2) │ 展开,见 B.3) │ | |
| 204 | -│ │ │ │ | |
| 205 | -│ [CTO] │ 控制栏: ▶启动 ⏭下一步 ⏸暂停 │ 状态/耗时/attempt │ | |
| 206 | -│ └ [HR] │ ⏹停止 🔁重试 │ [概览|输入输出| │ | |
| 207 | -│ └ [PM] │ │ 运行日志|子图] │ | |
| 208 | -│ │ ●pending ●running ●success │ │ | |
| 209 | -│ │ ●fail ●blocked ●skipped │ │ | |
| 210 | -│ │ ┄┄▶ 循环回退边 │ │ | |
| 211 | -└───────────────┴─────────────────────────────────┴─────────────────┘ | |
| 332 | +┌──────────────────────────────────────────────────────────────────┐ | |
| 333 | +│ <company name> 组织与执行 预算 42/100 calls · $6.2 · 38min │ | |
| 334 | +├───────────────┬──────────────────────────────┬───────────────────┤ | |
| 335 | +│ Org Graph │ Work Graph 画布 │ 节点详情面板 │ | |
| 336 | +│ (同一个画布 │ (选中 org node 的 work │ (点某个 work │ | |
| 337 | +│ 组件,只是 │ graph,见 B.2) │ graph node 后 │ | |
| 338 | +│ graph_kind= │ │ 展开,见 B.3) │ | |
| 339 | +│ 'org') │ 控制栏: ▶启动 ⏭下一步 │ │ | |
| 340 | +│ │ ⏸暂停 ⏹停止 🔁重试 │ 状态/耗时/attempt │ | |
| 341 | +│ org-planner │ │ [概览|输入输出| │ | |
| 342 | +│ ↓ │ ●pending ●running ●success │ 运行日志|子图] │ | |
| 343 | +│ org-coder │ ●fail ●blocked ●skipped │ │ | |
| 344 | +│ ↓ │ ┄┄▶ 回退边 2/3 │ │ | |
| 345 | +│ org-validator │ │ │ | |
| 346 | +└───────────────┴──────────────────────────────┴───────────────────┘ | |
| 347 | + 面包屑:公司 › org-coder › (子图) | |
| 212 | 348 | ``` |
| 213 | 349 | |
| 214 | -左栏点击某个 org node:右侧主画布加载该节点关联的 Work Graph 列表(一个 org node 生命周期里可能触发过多张 Work Graph,每张对应一次 goal/task,默认显示最新一张,提供下拉切换历史)。 | |
| 350 | +左栏和中栏是**同一个画布组件的两个实例**——因为两层图同构(R-9.1),这是 R-15.4 引擎泛化在前端的对应收益。左栏点击某个 org node → 中栏加载该节点的 work graph(一个 org node 生命周期里可能跑过多张,默认显示最新,下拉切换历史)。 | |
| 215 | 351 | |
| 216 | -#### B.2 Work Graph 画布 | |
| 352 | +#### B.2 画布 | |
| 217 | 353 | |
| 218 | -- **节点**:矩形卡片,标题是 `system_agent_id`(如 `coder`),右上角小圆点按 `status` 着色(沿用仓库既有色彩语义:绿=success、蓝=running、灰=pending/ready、红=fail、橙=blocked、浅灰斜纹=skipped)。`loop_validator` 节点用菱形(区别于普通 `task` 节点的矩形),视觉上直接点名"这是一个决策节点,不是执行节点"。 | |
| 219 | -- **边**:`edge_kind='forward'` 是实线箭头;`edge_kind='backward'` 是**虚线曲线箭头**,颜色区别于前进边(比如统一用琥珀色),且在曲线中点位置渲染一个小徽标,内容取自 `work_graph_loop_records` 里该 `(loop_validator_node_id, target_node_id)` 组合最新一条记录的 `loop_count`,格式 `2/3`——这直接对应用户提出的"loop 的划线"需求:不是随便画一条回退线,是带着"当前循环到第几次、硬顶是几"这个上下文的线。 | |
| 220 | -- **布局**:⚠️ **v0.2 修正——不要从零写**。`web/src/components/devloop/UnitsDAG.vue`(107 行)已经实现了完全符合这里需求的方案:SVG + `<marker>` 箭头 + **Kahn 分波拓扑布局**(`waves` computed,按 `depends_on` 逐波剥离,带 safety 上限防死循环)+ 按 `status` 着色的节点矩形 + 点击 emit 下钻。P3/P4 应当扩展它(加 `loop_validator` 菱形节点、`edge_kind='backward'` 的虚线曲线、循环徽标),而不是平行实现一个。仍然不引入图形库依赖——既有实现已经证明这个规模的图手写 SVG 足够。真正的新增工作量只剩**回退边的曲线路由**(避免与其他节点重叠),这一块是新的。 | |
| 221 | -- **主流程 vs 异常流程的视觉区分**:正常的 `success` 前进边和节点是画布里的"主干";任何和 `fail` 条件相关的边、`blocked` 状态的节点、`loop_validator` 节点及其回退边,整体降低主干部分的视觉权重(比如统一给异常相关元素加一层浅色背景框),让人一眼能分清"这张图正常应该怎么走"和"这次实际上因为哪里失败绕了远路"。 | |
| 354 | +- **节点**:矩形卡片,标题是 `node_name`,右上角小圆点按 `status` 着色(沿用仓库既有色彩语义:绿=success、蓝=running、灰=pending/ready、红=fail、橙=blocked、浅灰斜纹=skipped)。`node_kind='subgraph'` 的节点右下角带一个「⊞」角标,提示可下钻。 | |
| 355 | +- **边**:`forward` 是实线箭头;`backward` 是**虚线曲线箭头**,琥珀色,曲线中点渲染徽标,内容取自 `graph_run_loop_records` 里该 `edge_id` 最新一条记录的 `loop_count`,格式 `2/3`——不是随便画一条回退线,是带着「当前第几次、硬顶是几」上下文的线。 | |
| 356 | +- **布局**:**扩展 `UnitsDAG.vue`**(§3.5),不从零写。新增工作量只有三块:`node_kind='subgraph'` 角标、`backward` 边的虚线曲线路由(避开其他节点,这块是真新的)、循环徽标。不引入图形库——既有实现已证明这个规模手写 SVG 足够。 | |
| 357 | +- **主流程 vs 异常流程**:`success` 前进边和节点是画布主干;所有和 `fail` 相关的边、`blocked` 节点、回退边整体**降低视觉权重**(统一加一层浅色背景框),让人一眼分清「这张图正常该怎么走」和「这次因为哪里失败绕了远路」。 | |
| 222 | 358 | |
| 223 | 359 | #### B.3 节点详情面板(四个 tab) |
| 224 | 360 | |
| 225 | -1. **概览**:`status`、`attempt`/最大重试预算、`started_at`/`completed_at`/耗时、如果是 `loop_validator` 触发的回退,展示 `work_graph_loop_records` 里这次相关的完整记录(`verdict`/`remediation_note`)。 | |
| 226 | -2. **输入输出**:读 `output_ref` 指向的 markdown/JSON 文件渲染展示("边的输入输出"需求的落地点——一条边的"输出"就是它 `from_node` 的 `output_ref` 内容,"输入"就是 `to_node` 实际执行时读取的上下文,两者在**大多数正向边**上是同一份内容,只有经过 Loop-Validator 回退、`_buildRetryContext()` 类似逻辑拼接了"上次失败摘要"的场景下才会不同——这个 tab 需要能同时展示"原始产出"和"实际被下游消费时是否被加工过"两种视图,不能假设两者总是相同)。 | |
| 227 | -3. **运行日志**:复用 §3.4 的 `AgentChat.vue` 双轨折叠卡片模式——节点正在 `running` 时走 live 面板(流式追加),`success`/`fail` 之后切换成历史日志视图。这一层展示的是**这次 agent 调用内部**的对话/工具调用记录(用户确认的下钻语义①)。 | |
| 228 | -4. **子图**:仅当 `has_subgraph=1` 时出现这个 tab,内容是**递归复用同一个 Work Graph 画布组件**,指向 `work_graphs.parent_node_id = 本节点id` 的那张子图,画布上方有面包屑(`公司 › CTO 组织 › PM 的任务 › 子任务`)支持逐层下钻和返回(用户确认的下钻语义②)。 | |
| 361 | +1. **概览**:`status`、`attempt`、`started_at`/`completed_at`/耗时、`fail_reason`;如果这个节点参与过回退,展示 `graph_run_loop_records` 里相关的完整记录(`loop_count` / `verdict` / `remediation_note`)。 | |
| 362 | +2. **输入输出**:渲染 `output_ref` 指向的文件。**「原始产出」和「实际被下游消费时的形态」要能分别展示**——大多数正向边上两者相同,但经过回退、拼接了「上一轮 remediation_note」之后就不同了,不能假设总是相同。若该节点读过 `artifacts/`,一并列出读了哪些。 | |
| 363 | +3. **运行日志**:复用 §3.4 的双轨折叠卡片——`running` 时走 live 面板流式追加,`success`/`fail` 后切换成历史视图。展示的是**这次 agent 调用内部**的对话/工具调用记录。 | |
| 364 | +4. **子图**:仅当 `node_kind='subgraph'` 且 `child_run_id` 非空时出现,内容是**递归复用同一个画布组件**指向 `child_run_id`,上方面包屑支持逐层下钻和返回。 | |
| 229 | 365 | |
| 230 | -#### B.4 存量/降级展示 | |
| 366 | +#### B.4 预算条 | |
| 231 | 367 | |
| 232 | -一个 org node 从未触发过 Work Graph(P3 上线初期,或者这个角色本身不承担会触发 Work Graph 的任务):右侧主画布显示空态提示,不报错、不留白页面。 | |
| 368 | +页头常驻显示 root run 的预算消耗(calls / tokens / 墙钟)。**自动模式点「启动」前弹一次预估成本确认**——按 §四.A.3 的量级,这不是可选的礼貌,是必要的。 | |
| 369 | + | |
| 370 | +#### B.5 空态 | |
| 371 | + | |
| 372 | +一个 org node 从未跑过 work graph:中栏显示空态提示,不报错、不留白页面。 | |
| 233 | 373 | |
| 234 | 374 | --- |
| 235 | 375 | |
| 236 | 376 | ### C. Control(控制操作) |
| 237 | 377 | |
| 238 | -| 操作 | 可用条件(`run_status` / `execution_mode`) | 引擎侧动作 | 对"正在跑的节点"的影响 | | |
| 378 | +| 操作 | 可用条件 | 引擎侧动作 | 对正在跑的节点的影响 | | |
| 239 | 379 | |---|---|---|---| |
| 240 | -| **启动** | `run_status ∈ {ready, paused, blocked}` | `ready→running`;若 `execution_mode='auto'` 立即调用一次内部 `advance()` 并让其在每次成功后自我调度下一次(见下);若 `manual`,只是把 run 标成 running,停在原地等"下一步" | 无(这个操作只在没有节点在跑的时候可点) | | |
| 241 | -| **下一步** | `run_status='running'` 且 `execution_mode='manual'`(或 `auto` 但已 `paused`——暂停后允许人工手动单步推进) | 拾取一个当前 `status='ready'` 的节点(如果有多个并行 ready 的节点,一次"下一步"只推进**一个**,需要在 UI 上明确标出"接下来要跑哪个",避免用户以为点一下会推进所有分支),执行完(`success`/`fail`)后评估出边,把满足条件的下游节点置 `ready`,**不继续往下跑**,停在这里等下一次点击 | 会真正发起一次 agent 调用,不可中途取消(§三.5 的取消能力用在"停止"上,不是给"下一步"配的) | | |
| 242 | -| **暂停** | `run_status='running'` 且 `execution_mode='auto'` | 置 `pause_requested=1`。**不打断当前正在 `running` 的节点**(agent 调用一旦发起就让它跑完,理由见下方说明),当前节点完成、准备自动调度下一个节点之前检查这个标志位,若为真则停止自我调度,`run_status→paused` | 无——这是"软暂停",生效点是下一个节点的调度前,不是立即打断 | | |
| 243 | -| **停止** | `run_status ∈ {running, paused, blocked}` | 置 `stop_requested=1`;所有仍处于 `pending`/`ready` 的节点批量置 `skipped`;若此刻有节点正 `running`,尝试通过 §三.5 的机制硬取消对应子进程(**取消能力需要 §六 T5 在实现前二次确认调用链,设计上按"能取消"设计,实现时如果确认做不到就退化成"等它跑完、记录结果、但不再调度后续节点"**);`run_status→stopped` | 尝试硬取消,退化路径是"软停止" | | |
| 244 | -| **重试** | 节点 `status ∈ {fail, blocked}` | 把该节点 `status` 打回 `ready`,`attempt+1`;**默认只重置这一个节点,不级联重置下游**——因为下游节点在这次重试产生新结果之前根本还没执行到(`fail`/`blocked` 节点的下游按定义还是 `pending`),除非是"节点曾经 success 过、后来因为上游数据变化想强制重跑"这种场景才需要级联,这种场景本版本不支持(见 §八.3) | 无(`fail`/`blocked` 节点本身已经不在 `running`) | | |
| 380 | +| **启动** | `run_status ∈ {ready, paused, blocked}` | `→running`;`auto` 模式立即调一次 `advanceOne()` 并让它在每次成功后自我调度下一次;`manual` 模式只标 running,停在原地等「下一步」 | 无 | | |
| 381 | +| **下一步** | `run_status='running'` 且(`manual` 或 `auto` 已 `paused`) | 拾取**一个** `status='ready'` 的节点执行,完成后评估出边把满足条件的下游置 `ready`,**不继续往下跑**。多个并行 ready 时 UI 必须明确标出「接下来要跑哪个」,避免用户以为点一下推进所有分支 | 会真正发起一次 agent 调用,不可中途取消 | | |
| 382 | +| **暂停** | `run_status='running'` | 置 `pause_requested=1`,**并沿 `parent_run_id` 向下级联到所有 active 子 run**。不打断当前正在 `running` 的节点,在下一个节点的调度前检查标志位 | 软暂停,生效点是下一个节点边界 | | |
| 383 | +| **停止** | `run_status ∈ {running, paused, blocked}` | 置 `stop_requested=1` 并**向下级联**;所有仍 `pending`/`ready` 的节点批量 `skipped`;若有节点正 `running`,尝试通过 §3.6 的机制硬取消子进程;`→stopped` | 尝试硬取消,退化路径见下 | | |
| 384 | +| **重试** | 节点 `status ∈ {fail, blocked}` | 该节点 `→ready`,`attempt+1`。**默认不级联重置下游** | 无 | | |
| 385 | + | |
| 386 | +**三层级联**(R-15.4 / [27-E2E](./27-e2e-task-execution-flow.md) §7.5):v0.1 §八.4 把「子图不继承父图暂停状态」列为刻意简化,现在层级从两层变三层(root → org node → 嵌套子图),「点了暂停但子图还在偷偷跑」会成为真实困惑点。**定案:暂停/停止向下级联;「下一步」只作用于当前查看的层级**(符合直觉——暂停是全局意图,单步是局部操作)。`root_run_id` 冗余列让级联是一次索引查询,不用递归遍历。 | |
| 387 | + | |
| 388 | +**「重试」不级联下游的判据**(v0.1 §八.3 的 open question,现已关闭):不由重试动作决定,由**产出契约是否真的变了**决定。采用 `loops-l3.md` 的增量算法——按 `contract_hash` 把下游分成 added / changed / removed / **unchanged** 四类,`unchanged`(id 在且 hash 不变)保持原状**无须重跑**,只有 hash 变了的才回退到 `pending`。`hashContract()` 的实现见 `doc-store.js:382`(对 `contract.exports` 做递归 key 排序的规范化序列化后 sha256 取前 16 位),直接复用。 | |
| 245 | 389 | |
| 246 | -**手动/自动的统一原语**:无论手动点"下一步"还是自动模式下引擎自己驱动,落到服务层都是同一个 `advanceOne(workGraphId)` 函数调用——区别只在于**谁在调用它、调用节奏由谁决定**:手动模式下调用方是收到用户点击的路由 handler,每次一下;自动模式下调用方是引擎自己的一个"节点完成回调",只要 `pause_requested`/`stop_requested` 都是假,就在上一次 `advanceOne` 的 promise resolve 之后立刻发起下一次。这个设计直接对应 §三.2 里"手动和自动共用同一个 advance 原语"的结论,也是本设计避免"两套并行的执行逻辑互相踩"的关键——**只有一套推进逻辑,自动模式只是一个不停调用它的循环,不是另一套实现**。 | |
| 390 | +**手动/自动的统一原语**:两种模式落到服务层都是同一个 `advanceOne(runId)`,区别只在**谁调用它、调用节奏由谁决定**。手动 = 收到用户点击的路由 handler,每次一下;自动 = 引擎自己的「节点完成回调」,只要 `pause_requested`/`stop_requested` 都是假就在上一次 resolve 后立刻发起下一次。**只有一套推进逻辑,自动模式只是一个不停调用它的循环,不是另一套实现**——这是避免两套并行执行逻辑互相踩的关键。 | |
| 391 | + | |
| 392 | +**「停止」的降级路径**:§3.6 的取消能力需要 T9 在实现前二次确认调用链。设计上按「能取消」写;若确认做不到干净取消,退化为「等它跑完、记录结果、但不再调度后续节点」。这不会导致设计推倒重来,只是用户点了停止还要再等一会。 | |
| 247 | 393 | |
| 248 | 394 | --- |
| 249 | 395 | |
| 250 | -### D. Process(主流程 / 异常流程 / 子流程处理) | |
| 396 | +### D. Process(执行过程) | |
| 397 | + | |
| 398 | +#### D.0 两种 node executor | |
| 399 | + | |
| 400 | +v0.1 只设计了一种执行语义。org graph 加入后必须分派(R-15.4): | |
| 401 | + | |
| 402 | +| | `agent_call` | `subgraph` | | |
| 403 | +|---|---|---| | |
| 404 | +| 执行 = | 调 `AgentManager` 跑 `system_agent_id`,写 `agent_run_id` | 起一张子图并等它收敛 | | |
| 405 | +| success/fail 来源 | agent 调用结果 | 子 run `completed` → success;`blocked`/`failed` → fail | | |
| 406 | +| 耗时量级 | 秒~分钟 | 分钟~小时 | | |
| 407 | +| 谁用 | work graph 节点(默认) | **org graph 节点(全部)**;work graph 里 agent 自行判断需要拆子图的节点 | | |
| 408 | + | |
| 409 | +`subgraph` 节点的完整动作: | |
| 410 | + | |
| 411 | +``` | |
| 412 | +节点 → running | |
| 413 | + → 调 workgraph-planning(全局通用本体,R-15.2),输入 = 该 org node 的角色定义 | |
| 414 | + + 任务描述 + 可读的 artifacts/ + 上一轮 remediation_note(若有) | |
| 415 | + → 产出图 → 过 §三.7 四项校验 → 写 work_graphs / work_graph_nodes / work_graph_edges | |
| 416 | + → 建子 graph_run(parent_run_id / parent_node_run_id / root_run_id 三者填好, | |
| 417 | + execution_mode 默认继承父 run)→ 回填 child_run_id | |
| 418 | + → 递归执行子 run(同一个 advanceOne) | |
| 419 | + → 子 run 收敛 → 回写本节点 success/fail + fail_reason + remediation_note | |
| 420 | +``` | |
| 421 | + | |
| 422 | +父节点在子 run 跑完之前一直停在 `running`。这也让 §四.B.3 的「子图」tab 有了明确的数据来源。 | |
| 423 | + | |
| 424 | +**⚠️ 收敛时必须透传三个值,不只是 success/fail**(R-16)。原因是决定「失败后走哪条边」的**不是失败节点自己,而是它子图里的归因节点**(§四.D.2.2):`org-validator` 这个 org node 判定任务未达成时,是它 work graph 里的 `validator-reflection` 节点算出了根因,这个结论必须能穿透子 run 边界回到 org 层,否则 org 层选边时无据可依。 | |
| 425 | + | |
| 426 | +| 回写字段 | 来源 | 用途 | | |
| 427 | +|---|---|---| | |
| 428 | +| `success` / `fail` | 子 run 的 `run_status` | 父节点状态转换 | | |
| 429 | +| `fail_reason` | 子图归因节点的产出 | 父层 §四.D.2.2 选边 | | |
| 430 | +| `remediation_note` | 同上 | 随回退传给目标节点,成为它下一次 `workgraph-planning` 的输入 | | |
| 251 | 431 | |
| 252 | 432 | #### D.1 主流程 |
| 253 | 433 | |
| 254 | 434 | ``` |
| 255 | 435 | 节点进入 ready |
| 256 | - → advanceOne 拾取 → running → 调 AgentManager 执行对应 system_agent | |
| 257 | - → 调用结束,产出写入 output_ref | |
| 258 | - → success:遍历该节点的出边中 condition='success' 的边,目标节点若所有前置条件(可能有多条入边, | |
| 259 | - 需要"任意一条满足即可"还是"全部满足才能进入 ready"——采用"全部满足"语义,与 27 号文档 §3.4 的 | |
| 260 | - 图规划约定一致,多入边节点代表"汇合点")满足,置 ready | |
| 261 | - → 所有节点都到达终态(success/fail 被消化成 blocked 或被路由掉/skipped)且没有 ready/pending 悬空 | |
| 262 | - → work_graphs.run_status = completed | |
| 436 | + → advanceOne 拾取 → running → 按 node_kind 分派执行(D.0) | |
| 437 | + → 产出写入 output_ref(特权节点可另写 artifacts/) | |
| 438 | + → success:遍历出边中 condition='success' 的边,目标节点若**所有**入边条件都满足则置 ready | |
| 439 | + (多入边 = 汇合点,采用「全部满足」语义,与 27 号文档 §3.4 的图规划约定一致) | |
| 440 | + → 所有节点到达终态且无 ready/pending 悬空 → run_status = completed | |
| 441 | + → 若本 run 有 parent_node_run_id,回写父节点 success | |
| 263 | 442 | ``` |
| 264 | 443 | |
| 265 | 444 | #### D.2 异常流程 |
| 266 | 445 | |
| 267 | 446 | ``` |
| 268 | -节点 running → fail | |
| 269 | - → 遍历出边中 condition='fail' 的边 | |
| 270 | - ├─ 存在一条 → to_node: | |
| 271 | - │ ├─ to_node.node_kind='loop_validator'(27 号文档 §3.4 第 4 点的结构要求,非法图在规划阶段已被 | |
| 272 | - │ │ §3.4 第 5 点的执行前校验拦截,本引擎不需要再兜底校验合法性,只需要按图执行) | |
| 273 | - │ │ → 进入 Loop-Validator 流程(D.2.1) | |
| 274 | - │ └─ to_node 是普通 task 节点(无回退语义的显式失败分支,比如"失败就转人工审核节点") | |
| 275 | - │ → 正常按主流程把 to_node 置 ready | |
| 276 | - └─ 不存在 fail 边(死路)→ 该节点 status='blocked',run_status 视其余节点情况可能同步变 | |
| 277 | - 'blocked'(如果没有其他节点还在 ready/running)——这是需要人工点"重试"或人工介入的终止态, | |
| 278 | - 不是系统自动判定"整个 run 失败"(区别于"停止"是人工主动终止,"blocked" 是系统探测到走不下去) | |
| 447 | +节点 running → fail(附带 fail_reason) | |
| 448 | + → 遍历出边中 condition='fail' 的边,按 D.2.2 选一条 | |
| 449 | + ├─ 选中边 edge_kind='forward' → 普通失败分支(如「失败转人工审核节点」) | |
| 450 | + │ → 按主流程把 to_node 置 ready | |
| 451 | + ├─ 选中边 edge_kind='backward' → 回退,进 D.2.1 熔断流程 | |
| 452 | + └─ 无可走的 fail 边(死路)→ 该节点 blocked | |
| 453 | + → 若无其他节点在 ready/running,run_status 同步 blocked | |
| 454 | + → 这是「需要人工介入」的终止态,区别于「停止」(人工主动终止) | |
| 279 | 455 | ``` |
| 280 | 456 | |
| 281 | -**D.2.1 Loop-Validator 流程**(落实 27 号文档 §3.5 的三步 + 硬顶): | |
| 457 | +**D.2.1 回退熔断(两层通用,取代 v0.1 的 `loop_validator` 强制节点)** | |
| 282 | 458 | |
| 283 | -1. **备案**:进入 `loop_validator` 节点时,插入一条 `work_graph_loop_records`(`verdict='registered'`),`loop_count` = 该 `(validator, target)` 组合已有记录数 + 1。 | |
| 284 | -2. **硬顶检查**:`loop_count > 3` → 不再调用 LLM 判断,直接 `verdict='force_terminated'`,`loop_validator` 节点本身 `status='fail'`(且它没有 `fail` 出边或出边同样是死路),整条链路收敛到 `blocked`,等待人工介入。这一条判断是纯代码逻辑(比较整数),不依赖 agent 调用,保证"熔断"不会被 LLM 的自由发挥绕过。 | |
| 285 | -3. **审计 + 修复方案传递校验**(`loop_count ≤ 3` 时才发生):调用 `loop_validator` 对应的 system agent(本身是一次 agent 调用,`node_kind='loop_validator'` 不改变"它也要 running→success/fail"这条状态机路径),产出 `verdict ∈ {fixed, not_fixed}` + `remediation_note`,写回同一条记录。`fixed` → 正常按 `success` 边继续(回到主流程);`not_fixed` → 按该节点的 `fail` 出边指向的**回退目标节点**(`target_node_id`)重新置 `ready`,`remediation_note` 作为这次重跑的上下文补充写入目标节点下一次 attempt 的 `output_ref`(沿用 D.1 里"产出物路径"约定,附加一段"上一轮 Loop-Validator 给出的修复建议")。 | |
| 459 | +引擎准备沿一条 `backward` 边推进时,无条件执行: | |
| 286 | 460 | |
| 287 | -#### D.3 子流程(下钻) | |
| 461 | +1. **备案**:按 `(run_id, edge_id)` 查 `graph_run_loop_records` 已有记录数 n,`loop_count = n + 1`,插入一条 `verdict='registered'`;同时 `graph_runs.backward_count += 1`。 | |
| 462 | +2. **双层硬顶检查**:`loop_count > 3`(单边)**或** `backward_count > 5`(run 级总数)→ 不再调用任何 LLM,直接改 `verdict='force_terminated'`,源节点 `blocked`,链路收敛,等人工介入。**这一条是纯整数比较,不依赖 agent 调用**——保证熔断不会被 LLM 的自由发挥绕过。 | |
| 463 | +3. **回退**:两个计数都未撞顶时把 `to_node` 置 `ready`、`attempt+1`,把上一轮的 `remediation_note` 作为上下文补充带给它。 | |
| 288 | 464 | |
| 289 | -两类下钻,数据结构上分别对应: | |
| 465 | +> **为什么单边计数不够**(R-16):v0.1/v0.2 只按 `(run_id, edge_id)` 计数,前提是一个节点只有一条回退边。D.2.2 引入多条回退边之后,每条边各自计数,**硬顶实际变成 3×N**——归因节点只要每次归因到不同的 agent、走不同的边,就永远撞不到任何一个计数器的顶。这是 [27-E2E](./27-e2e-task-execution-flow.md) §7.1「每一级都重置下一级的计数器则整个循环没有终止条件」的同型变体:不是重置计数器,但效果一样。 | |
| 466 | +> | |
| 467 | +> 所以**两个计数都要**:单边计数防同一条路径上的死循环,run 级 `backward_count` 防「换个借口继续退」。它与 §四.A.3 的任务级总预算也是两回事——**预算管花了多少钱,`backward_count` 管绕了多少圈**,两者互不替代。 | |
| 468 | + | |
| 469 | +**`infra_flake` 不烧任何计数**:进程超时 / 被杀这类基础设施抖动,归因为 `infra_flake` 后直接原地重跑,**既不写 `graph_run_loop_records`,也不加 `backward_count`,也不扣 `retry` 预算**——沿用 DevLoop 的差异化预算消耗(`_spec/sdd-process.md` P-7)。不设这一类的话,超时会被误判成执行问题白烧一轮。 | |
| 470 | + | |
| 471 | +> **为什么不强制插 `loop_validator` 节点**:27 号总纲 §3.4 第 4 点要求 work graph 的回退边必须经过 `loop_validator`。但 org graph 是 `orggraph-planning` 生成的「这家公司有哪些角色」,往里塞一个基础设施节点会污染这个语义。**定案:熔断计数由引擎无条件做(两层都有),`loop_validator` 节点降级为可选增强**——`workgraph-planning` 可以在 work graph 里插它,由它做 27 号 §3.5 的「审计 + 修复方案传递校验」(判断上一轮到底修好没有、产出 `remediation_note`,写回 `verdict ∈ {fixed, not_fixed}`),那是 LLM 的活;「数到 3 就停」是代码的活。两者分工清楚,不互相依赖。 | |
| 472 | + | |
| 473 | +**D.2.2 一个节点有多条 fail 边时走哪条(R-16 定案)** | |
| 474 | + | |
| 475 | +这是 org 层引入后冒出来的新问题。`org-validator` 判定任务未达成时,退回 `org-coder`(实现有问题)还是 `org-planner`(规划有问题)?两条都是从同一节点出去的 `fail` 边,`condition` 单靠 `success`/`fail` 二值区分不了。 | |
| 476 | + | |
| 477 | +**定案:归因是一次显式的 agent 调用,由专职节点做,不是失败节点顺手吐一个值。** | |
| 478 | + | |
| 479 | +`org-validator` 自己的 work graph 里,**第一个节点是 `validator-reflection`**,它的唯一职责是判断这次失败的根因:是哪个 agent 的哪个 skill 有问题、是哪个 agent 拿到的输入约束不够、还是哪个 agent 的执行出了问题。归因完成才知道该先改 skill、还是直接让某个 agent 带 bug context 重做。 | |
| 480 | + | |
| 481 | +这与 DevLoop 的既有分工同构——Integrator 判定集成失败、BugTriager 单独归因,是**两个不同的 agent**。理由是同一个 agent 既当裁判又当法医,容易把「我判失败」和「我认为该怪谁」混成一次输出。拆开的额外好处:归因有独立的落盘产物、可审计、可在控制台单独下钻。 | |
| 482 | + | |
| 483 | +**归因取值域与 DevLoop 四分类的对照**: | |
| 484 | + | |
| 485 | +| `fail_reason` | DevLoop 对应 | 回退动作 | 烧计数? | | |
| 486 | +|---|---|---|---| | |
| 487 | +| `impl_bug` | `impl_bug` | 该 agent 带 bug context 重做 | 烧单边 + `backward_count` | | |
| 488 | +| `input_underspecified` | `contract_mismatch` / `spec_ambiguous` | 退回上游产出约束的那个节点 | 同上 | | |
| 489 | +| `skill_defect` | **无对应** | 见下方 ⚠️ | 同上 | | |
| 490 | +| `infra_flake` | `infra_flake` | 原地重跑 | **不烧任何计数**(D.2.1) | | |
| 491 | + | |
| 492 | +⚠️ **`skill_defect` 这一支即 L4-company,而 L4 在 §一是非目标**。**v1 处理方式:保留这个归因取值让 `validator-reflection` 照常判定**(数据先攒着,归因体系一次成型,不用等 L4 做完再回来改分类),**但动作降级为「挂起等人工」,不自动触发 L4**。理由见 [27-E2E](./27-e2e-task-execution-flow.md) §八——L4 是三级恢复里最贵、爆炸半径最大的一级,§十 建议最后做。 | |
| 493 | + | |
| 494 | +**引擎侧的选边规则**(引擎本身不理解上述语义,只做匹配): | |
| 495 | + | |
| 496 | +- 边上有可空的 `fail_reason` 列;节点失败时把 `fail_reason` 写进 `graph_run_nodes`(子图节点的这个值由 §四.D.0 从子 run 透传上来); | |
| 497 | +- **先找 `fail_reason` 精确匹配的边 → 无匹配则退到 `fail_reason IS NULL` 的兜底边 → 再无则视为死路**; | |
| 498 | +- 取值域**不在引擎里硬编码**,由图的规划者(`orggraph-planning` / `workgraph-planning`)与对应 validator 约定。上表是推荐取值,不是引擎约束。 | |
| 499 | + | |
| 500 | +**因此写入前校验要加第五项**:任何有 `fail` 出边的节点,**必须至少留一条 `fail_reason IS NULL` 的兜底边**。否则归因产出一个图上没有对应边的取值时会直接落到死路,表现为「莫名其妙 blocked」。这一项要加进 §三.7 复用的那套校验里。 | |
| 290 | 501 | |
| 291 | -- **Agent 对话记录**(B.3 第 3 个 tab):不建新表,直接关联现有会话持久化(`server/conversations/`,CLAUDE.md 已列出的既有目录)——`work_graph_nodes` 不需要冗余存对话内容,`agent_run_id`/节点执行时机足以反查到对应的会话记录,实现时确认现有会话持久化的查询接口能否直接按"这次特定 attempt 的调用"过滤(如果不能精确过滤到单次 attempt,退化到按时间窗口近似匹配,列入 §六 T4 的实现细节)。 | |
| 292 | -- **嵌套子 Work Graph**(B.3 第 4 个 tab):`work_graph_nodes.has_subgraph=1` 的节点,触发时机是这个节点对应的 agent 在执行过程中自己判断"这个任务需要再规划一张子图"(比如一个复杂 Coder 任务内部拆成多个子步骤)——**子图的创建和执行,复用本文档设计的同一套 `work_graphs`/`advanceOne`/控制操作**,只是 `parent_node_id` 非空、`triggered_by_org_node_id` 可以为空(子图不是直接被某个 org node 触发的,是被上一层 work graph node 触发的)。子图有自己独立的 `execution_mode`/`run_status`,默认继承父图当前的 `execution_mode`(父图 auto,子图默认也 auto;用户可以在下钻进去之后单独改子图的模式,两者互不影响——即"暂停父图"不会自动暂停正在跑的子图,这是刻意的简化,见 §八.4)。 | |
| 502 | +这对 P2 已落地的 `org_graph_edges` 是**加一列**(T-P3),不动现有唯一索引——两条 fail 边指向不同 `to_node_id`,本来就不冲突。 | |
| 503 | + | |
| 504 | +#### D.3 两级验收的引擎语义(R-15.3) | |
| 505 | + | |
| 506 | +| | work 层 `{orgNode}-validator` | org 层 `org-validator` | | |
| 507 | +|---|---|---| | |
| 508 | +| 在图上是 | work graph 里一个普通 `agent_call` 节点 | org graph 的末节点(`subgraph` 节点,它自己也有 work graph) | | |
| 509 | +| 验什么 | 这个 org node 自己的产出合格吗 | 整个任务达成了吗 | | |
| 510 | +| 引用本体 | `planner-validator` / `coder-validator` | `org-validator` | | |
| 511 | +| fail 触发 | 本张 work graph 内的 backward 边 | **org graph 的** backward 边 → 退回某个 org node → 该 org node 整张 work graph 重跑(起新子 run,`attempt+1`) | | |
| 512 | + | |
| 513 | +`org-validator` 内部调 `validator-reflection` / `benchmark-validator` / `testdata-validator` 等 sub agent **不需要新机制**——它是一个 org node,按定义有自己的 work graph,这些 sub agent 就是那张图的节点: | |
| 514 | + | |
| 515 | +``` | |
| 516 | +org graph: org-planner → org-coder → org-validator | |
| 517 | + ↓(它自己的 work graph) | |
| 518 | + validator-reflection → benchmark-validator → testdata-validator → … | |
| 519 | + ↑ 排第一位:先归因,再决定后面怎么验 / 怎么退 | |
| 520 | +``` | |
| 521 | + | |
| 522 | +`validator-reflection` 的产出(`fail_reason` + `remediation_note`)通过 §四.D.0 的三值透传回到 org 层,成为 §四.D.2.2 选边的依据。 | |
| 523 | + | |
| 524 | +两级验收的失败都从**同一个 root run 预算池**扣(§四.A.3),这正是 [27-E2E](./27-e2e-task-execution-flow.md) §7.1「三级恢复叠加会让熔断失效」的解法。 | |
| 525 | + | |
| 526 | +**谁验收「验收标准」本身**:按 R-9.3「planner 生成、validator 执行」,产出者和被验收者天然分离,不存在「自己给自己出考题」。但「标准写错了」这一层仍未解决,两条缓解都要:① `planner-validator` 明确承担一项职责——审「这套验收标准是否可度量、是否覆盖原始需求、是否存在平凡通过的漏洞」,不只审 PRD/SDD 本身;② 保留人工 sign-off 作为最终关卡,v1 阶段不做全自动闭环。 | |
| 293 | 527 | |
| 294 | 528 | --- |
| 295 | 529 | |
| 296 | -## 五、任务划分(Task Breakdown) | |
| 530 | +## 五、任务划分 | |
| 531 | + | |
| 532 | +### 五个阶段(每个可独立验收、独立上线) | |
| 533 | + | |
| 534 | +切法与 [27-P2](./27-p2-org-graph.md) §五「三个可独立验收的阶段」一致:**不起 LLM 的先做完、能单独验收的先上线**。 | |
| 535 | + | |
| 536 | +| 阶段 | 含 | 交付什么 | 怎么验收 | 起 LLM | | |
| 537 | +|---|---|---|---|---| | |
| 538 | +| **0 · 验收契约** | T0 | 三种验收产物的统一契约格式 + 执行器 | 手写一份契约 + 一个被测产物 → 执行器跑出通过/不通过,判决可持久化 | ❌ | | |
| 539 | +| **A · 引擎地基** | T1→T2→T5、T6、T9、T10 | 五张表、`advanceOne` 幂等原语、两种 node executor、SSE 三者原子化、五个控制端点 | 手写图 JSON → 手动模式逐节点推进到 `completed`;切自动模式一键跑完;暂停后能手动单步、能恢复自动 | ❌ mock | | |
| 540 | +| **B · 回退熔断** | T7、T8 | backward 边计数 + 硬顶、`fail_reason` 路由、任务级预算、三层级联 | 总是失败的节点第 4 次 `force_terminated` 收敛到 `blocked`;预算扣完整棵 run 树挂起;暂停 root 后子 run 也停 | ❌ | | |
| 541 | +| **C · 控制台** | T3、T4 | 扩展 `UnitsDAG.vue`、四 tab 详情、`parent_run_id` 面包屑下钻、预算条 | 回退边虚线 + `N/3` 徽标随流程递增;运行日志 live↔history 切换不丢内容;子图 tab 能逐层进出 | ❌ | | |
| 542 | +| **D · 规划闭环** | T11、T12 | `workgraph-planning` agent + 用户→公司任务入口 | 一句自然语言任务 → org graph 自动逐节点起 work graph → 两层跑完 | ✅ | | |
| 543 | +| **P · P2 连带** | T-P1、T-P2、T-P3 | R-15.3 的改名 + `fail_reason` 加列 | 三个模板末节点为 `org-validator`;NAMING 补 validator 命名轴;`org_graph_edges` 多一列且旧数据不受影响 | ❌ | | |
| 544 | + | |
| 545 | +**阶段间可以停**:只做完 0+A,系统就有了一个「能手工建图、能校验、能按五个操作跑」的通用图引擎;B 让它在失败时不失控;C 让人看得见;D 才让它自己规划。**最快看到端到端效果是 0→A→B→D,C 可后延**。 | |
| 546 | + | |
| 547 | +### 任务表 | |
| 297 | 548 | |
| 298 | -| # | 任务 | 涉及文件/模块 | 依赖 | 对应 Phase | | |
| 549 | +| # | 任务 | 涉及文件 | 依赖 | 阶段 | | |
| 299 | 550 | |---|---|---|---|---| |
| 300 | -| **T1** | schema:`work_graphs`/`work_graph_nodes`/`work_graph_edges`/`work_graph_loop_records` 四张表,状态转换表实现(照抄 `unit-registry.js` 模式) | `server/services/graph-db.js`(追加)或新文件 `server/services/work-graph-db.js` | 27-P2 已落地(`org_graph_nodes` 存在) | P3 | | |
| 301 | -| **T2** | `work-graph-engine.js`:`advanceOne()` 核心原语(拾取 ready 节点→执行→按边推进)+ 幂等更新(A.3) | `server/services/work-graph-engine.js`(新增) | T1 | P3 | | |
| 302 | -| **T3** | Work Graph 画布前端组件:**扩展既有 `UnitsDAG.vue`**(Kahn 分波布局 + SVG 渲染已具备),新增 `loop_validator` 菱形节点、回退边虚线曲线路由、循环徽标 | `web/src/views/WorkGraphConsole.vue`(新增)、`web/src/components/devloop/UnitsDAG.vue`(抽取复用或拷贝改造) | T5(先有数据能画) | P3 | | |
| 303 | -| **T4** | 节点详情面板四个 tab(概览/输入输出/运行日志/子图),运行日志 tab 复用 `AgentChat.vue` 的双轨折叠卡片模式 | 同上 + 会话记录查询接口确认 | T2, T6 | P3 | | |
| 304 | -| **T5** | SSE:按 `work_graph_id` 复用 `sse-broker.js` 模式,状态转换 + timeline 写入 + SSE 广播三者原子化 | `server/services/work-graph-sse.js`(新增,参照 `devloop/sse-broker.js`) | T2 | P3 | | |
| 305 | -| **T6** | REST API:`POST /work-graphs/:id/start`、`/next`、`/pause`、`/stop`、`/retry`(node 级)、`GET /work-graphs/:id`(含 nodes/edges 全量状态)、`GET /org-graph-nodes/:id/work-graphs`(某 org node 的历史 Work Graph 列表) | `server/routes/work-graph.js`(新增) | T2 | P3 | | |
| 306 | -| **T7** | Loop-Validator 流程:D.2.1 三步 + 硬顶,`work_graph_loop_records` 读写 | `server/services/work-graph-engine.js` 扩展 | T2 | P4 | | |
| 307 | -| **T8** | 回退边可视化:循环徽标(`N/3`)、异常流程视觉降权(B.2 最后一条) | T3 组件扩展 | T7 | P4 | | |
| 308 | -| **T9** | §三.5 取消能力的实现前核实 + 落地:确认 `RunRegistry`/`ProcessGuard` 的调用链,"停止"操作接入硬取消(或确认做不到、落地降级方案并更新本文档 §四.C 的表述) | `server/src/engine/`(只读研究 + 必要时补一个查询接口,不改现有取消逻辑本身) | 无(可以提前做,独立于 T1-T8) | P3(阻塞"停止"的完整语义,但不阻塞其余功能先上线) | | |
| 309 | -| **T10** | 测试:`work-graph-engine.test.js`(状态机非法转换、幂等 advance、Loop-Validator 硬顶);一条端到端脚本(建图→启动→若干次 advance→触发一次回退→硬顶终止→查最终状态) | `server/tests/unit/work-graph-engine.test.js`(新增) | T2, T7 | P3+P4 | | |
| 310 | - | |
| 311 | -**顺序建议**:T1→T2 是地基。T9(取消能力核实)可以最早独立开工,不阻塞其他任务,但要在 T6 的"停止"端点定稿前给出结论。T5/T6 依赖 T2,可并行。T3 依赖有真实数据可画,建议先用 T1 的 schema 手写几行测试数据跑通画布,再接 T6 的真实端点。T7/T8(P4)在 T2/T6 稳定后开始,不阻塞 P3 先上线一个"只有正向执行、没有回退"的初版(这也是为什么 §二解释了"一次设计、分两次交付")。 | |
| 551 | +| **T0** | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一 schema)+ 执行器 + 判决持久化 | `server/services/goal-contract.js`(新增)、契约 schema 文档 | 无 | 0 | | |
| 552 | +| **T1** | schema:五张表 + 状态转换表实现(照抄 `unit-registry.js` 模式)+ work graph 写入校验(复用 `org-graph-service.js` 四项) | `server/services/work-graph-db.js`(新增) | 无 | A | | |
| 553 | +| **T2** | `graph-engine.js`:`advanceOne(runId)` 核心原语、两种 node executor、幂等条件更新、run 树级联工具函数 | `server/services/graph-engine.js`(新增) | T1 | A | | |
| 554 | +| **T3** | 画布组件:**扩展 `UnitsDAG.vue`**,加 `subgraph` 角标、backward 虚线曲线路由、循环徽标;org/work 两层共用同一实例 | `web/src/components/WorkGraphCanvas.vue`(从 `UnitsDAG.vue` 抽取)、`web/src/views/GraphConsole.vue`(新增) | T6 | C | | |
| 555 | +| **T4** | 节点详情四 tab + 面包屑下钻 + 预算条;运行日志复用 `AgentChat.vue` 双轨模式 | 同上 + 会话记录查询接口确认 | T2, T6 | C | | |
| 556 | +| **T5** | SSE:按 `run_id` 复用 `sse-broker.js` 模式;**状态转换 + timeline 写入 + SSE 广播三者原子化**(不重复 DevLoop 的两轨缺口) | `server/services/graph-run-sse.js`(新增,参照 `devloop/sse-broker.js`) | T2 | A | | |
| 557 | +| **T6** | REST:`POST /api/graph-runs/:id/{start,next,pause,stop}`、`POST /api/graph-runs/:id/nodes/:nodeId/retry`、`GET /api/graph-runs/:id`(含 nodes/edges 全量状态)、`GET /api/graph-runs/:id/stream`、`GET /api/company/:cid/graph-runs` | `server/routes/graph-run.js`(新增) | T2 | A | | |
| 558 | +| **T7** | 回退熔断:D.2.1 三步 + **双层硬顶**(单边 `loop_count` + run 级 `backward_count`)+ `graph_run_loop_records` 读写;`infra_flake` 不烧计数;D.2.2 的 `fail_reason` 选边 + 子 run 三值透传 | `graph-engine.js` 扩展 | T2, T-P3 | B | | |
| 559 | +| **T8** | 任务级总预算(root run 扣减 + 熔断)+ 三层暂停/停止级联 + 「重试」的 `contract_hash` unchanged 判据 | `graph-engine.js` 扩展 | T2 | B | | |
| 560 | +| **T9** | §3.6 取消能力的**实现前核实** + 落地:确认 `RunRegistry`/`ProcessGuard` 调用链,「停止」接入硬取消;或确认做不到、落地降级并回改本文档 §四.C | `server/src/engine/`(只读研究为主,必要时补一个查询接口) | 无,可最早独立开工 | A | | |
| 561 | +| **T10** | 测试:状态机非法转换、幂等 `advanceOne`、回退硬顶、预算熔断、级联;一条端到端脚本(建两层图 → 启动 → 若干次 advance → 触发回退 → 硬顶终止 → 查最终状态) | `server/tests/unit/graph-engine.test.js`(新增) | T2, T7, T8 | A/B | | |
| 562 | +| **T11** | `workgraph-planning` agent 本体(全局通用,R-15.2):读 org node 角色定义 + 任务 + `artifacts/` + 上一轮 `remediation_note` → 产出图 → 过校验 → 落库。生成的图**不得在节点内部再套四段式流转**(§九bis) | `system/templates/workgraph-planning/`(新增) | T1, T6 | D | | |
| 563 | +| **T12** | 用户→公司的任务入口:`POST /api/company/:cid/tasks`,建 root run 并起 org graph 执行;复用既有 `task/dispatch` + `task/report` 回调链路 | `server/routes/company.js` 或新 `routes/task-entry.js` | T2, T11 | D | | |
| 564 | +| **T-P1** | 三个 org graph 模板末节点改 `org-validator`;`dev.json` 的回退边目标按 D.2.2 重画(`fail_reason` 区分退 coder 还是退 planner) | `system/org-graph-templates/{dev,content,ops}.json` | T-P3 | P | | |
| 565 | +| **T-P2** | NAMING §三 删掉 `org-validator` 行;**新增 validator 命名轴一节**(节点名带领域前缀、本体名不带,R-15.3 的表) | `docs/todo/system-design/27-naming-conventions.md` | 无 | P | | |
| 566 | +| **T-P3** | `org_graph_edges` 加可空 `fail_reason` 列(D.2.2);`graph_runs` 加 `backward_count`;新建三个 validator 本体的 agent 模板 | `server/services/graph-db.js`、`system/templates/{planner-validator,coder-validator,org-validator}/` | 无 | P | | |
| 567 | +| **T13** | `validator-reflection` agent 本体(R-16):归因产出 `fail_reason` + `remediation_note`;`org-validator` 的 work graph 骨架把它排在第一位 | `system/templates/validator-reflection/`(新增) | T-P3 | B | | |
| 568 | +| **T14** | 写入前校验新增第五项 `fail-fallback-edge`(§三.7):有 `fail` 出边的节点必须留一条 `fail_reason IS NULL` 兜底边;org / work 两层都加 | `server/services/org-graph-service.js`、`work-graph-db.js` | T1 | B | | |
| 569 | + | |
| 570 | +> ⚠️ **T-P1 / T-P2 / T-P3 是 P2 侧文件的连带修改,但归 P3 执行**。P2 正由另一条线推进中,这批改动写在本文档而非直接改 P2 的文档/模板,是为了避免两边同时写同一批文件产生冲突。P2 那条线**不要动**这三项。 | |
| 571 | + | |
| 572 | +**顺序建议**:T0 和 T9 可最早独立开工,互不阻塞。T-P3 要在 T7 之前(`fail_reason` 是 D.2.2 的地基)。T1→T2 是硬串行地基。T5/T6 依赖 T2,可并行。T3 建议先用 T1 的 schema 手写几行数据跑通画布,再接 T6 的真实端点。 | |
| 312 | 573 | |
| 313 | 574 | --- |
| 314 | 575 | |
| 315 | 576 | ## 六、验收标准 |
| 316 | 577 | |
| 317 | -1. 对一张只有正向边、3-4 个节点的 Work Graph:手动模式下连续点"下一步"能逐节点推进到 `completed`,每次点击只推进一个节点(不会因为下游 ready 了就自动继续)。 | |
| 318 | -2. 同一张图切到自动模式点"启动":不再需要任何人工点击,自动跑到 `completed`。 | |
| 319 | -3. 自动模式运行中点"暂停":当前正在 `running` 的节点跑完后,`run_status` 变成 `paused`,不再自动调度下一个节点;此时点"下一步"能手动继续单步推进;点"启动"能恢复自动模式。 | |
| 320 | -4. 构造一个"总是失败"的节点、其 `fail` 边指向一个 `loop_validator` 节点、`loop_validator` 的 `fail` 边指回该节点自身的上游:验证第 1-3 次回退能正常触发(`work_graph_loop_records` 各有一行 `verdict∈{fixed,not_fixed}`),第 4 次触发时不再调用 agent、直接 `force_terminated`,图收敛到 `blocked`。 | |
| 321 | -5. 对一个 `blocked` 节点点"重试":节点回到 `ready`,后续能继续正常推进(不需要重建整张图)。 | |
| 322 | -6. 点"停止":所有 `pending`/`ready` 节点变 `skipped`,`run_status` 变 `stopped`;如果 T9 确认取消能力可用,正在 `running` 的节点被中断且状态被正确记录为某种可区分于正常 `fail` 的终止态(本设计的 `skipped`/`stopped` 组合已经覆盖,具体是否需要给"被强制取消"单独一个 status 值,留给 T9 落地时决定)。 | |
| 323 | -7. Work Graph 画布:回退边渲染为虚线曲线且显示 `N/3` 循环徽标,随着 D.2.1 流程推进徽标数字正确递增。 | |
| 324 | -8. 点开一个节点的"运行日志" tab:`running` 状态下看到实时追加的内容(对应 `AgentChat.vue` 的 live 面板行为),`success`/`fail` 后切换成历史视图,内容不丢失。 | |
| 325 | -9. 对一个 `has_subgraph=1` 的节点:出现"子图" tab,点进去是完整可交互(能点"下一步"等)的嵌套 Work Graph 画布,面包屑能返回上一层。 | |
| 326 | -10. Org Graph 左栏点击一个从未触发过 Work Graph 的 org node:右侧显示空态提示,不报错。 | |
| 578 | +**阶段 0** | |
| 579 | + | |
| 580 | +1. 手写一份 `goal-benchmark` + `goal-test-data` + `goal-test-script`,执行器能对一个被测产物跑出明确的通过/不通过判决,判决落库可查。 | |
| 581 | + | |
| 582 | +**阶段 A** | |
| 583 | + | |
| 584 | +2. 一张只有正向边、3-4 节点的图:手动模式连续点「下一步」逐节点推进到 `completed`,**每次点击只推进一个节点**(不因为下游 ready 了就自动继续)。 | |
| 585 | +3. 同一张图切自动模式点「启动」:不再需要任何人工点击,自动跑到 `completed`。 | |
| 586 | +4. 自动模式运行中点「暂停」:当前 `running` 节点跑完后 `run_status → paused`,不再自动调度;此时点「下一步」能手动单步;点「启动」能恢复自动。 | |
| 587 | +5. 点「停止」:所有 `pending`/`ready` 节点变 `skipped`,`run_status → stopped`。若 T9 确认取消可用,正在 `running` 的节点被中断且状态可区分于正常 `fail`。 | |
| 588 | +6. 一张 org graph(`graph_kind='org'`)用**同一套端点**跑通上述 2-5,代码路径与 work graph 完全一致——这是 R-15.4 引擎泛化的核心验收点。 | |
| 589 | +7. 一个 `subgraph` 节点执行时正确起子 run、`child_run_id` 回填、子 run 收敛后父节点拿到正确的 success/fail。 | |
| 590 | + | |
| 591 | +**阶段 B** | |
| 592 | + | |
| 593 | +8. 构造一个总是失败的节点,其 `fail` 边是指回上游的 `backward` 边:第 1-3 次回退正常触发(`graph_run_loop_records` 各一行),**第 4 次不再调用任何 agent、直接 `force_terminated`**,图收敛到 `blocked`。 | |
| 594 | +9. 一个节点有两条 `fail` 边(`fail_reason` 分别为 A / 空):节点带 `fail_reason=A` 失败时走第一条,带其他值失败时走兜底边。写入一张有 `fail` 出边却无兜底边的图 → 被 `fail-fallback-edge` 校验拒绝,返回 400。 | |
| 595 | +9b. **多边不架空熔断**:构造一个有 3 条回退边的节点,让归因每次归到不同的边——单边计数各自都没到 3,但 run 级 `backward_count` 到 5 时仍 `force_terminated`。 | |
| 596 | +9c. `validator-reflection` 归因为 `infra_flake` 时原地重跑,`graph_run_loop_records` 不增行、`backward_count` 不递增。 | |
| 597 | +9d. 一个 `subgraph` 节点的子 run 因内部归因失败而收敛:父节点同时拿到 `fail`、`fail_reason`、`remediation_note` 三个值,并据此在父层正确选边。 | |
| 598 | +10. root run 预算扣完:整棵 run 树挂起,不是某一层单独失败。 | |
| 599 | +11. 对 root run 点「暂停」:正在跑的子 run、孙 run 全部在下一个节点边界停下。 | |
| 600 | +12. 对一个 `blocked` 节点点「重试」:节点回 `ready` 并能继续推进,不需要重建整张图;下游 `contract_hash` 未变的节点保持原状不重跑。 | |
| 601 | + | |
| 602 | +**阶段 C** | |
| 603 | + | |
| 604 | +13. 画布:回退边渲染为虚线曲线且显示 `N/3` 徽标,随 D.2.1 推进数字正确递增。 | |
| 605 | +14. 「运行日志」tab:`running` 时实时追加,`success`/`fail` 后切历史视图,内容不丢。 | |
| 606 | +15. `subgraph` 节点的「子图」tab 点进去是完整可交互(能点「下一步」)的嵌套画布,面包屑能逐层返回。 | |
| 607 | +16. 左栏点一个从未跑过 work graph 的 org node:中栏空态提示,不报错。 | |
| 608 | + | |
| 609 | +**阶段 D** | |
| 610 | + | |
| 611 | +17. 对一家 org graph 已就绪的公司下达一句自然语言任务:自动建 root run → 第一个 org node 起 work graph → 两层跑到 `completed`,全程无需手工构造任何图数据。 | |
| 327 | 612 | |
| 328 | 613 | --- |
| 329 | 614 | |
| ... | ... | @@ -331,31 +616,90 @@ work_graphs.run_status 转换表: |
| 331 | 616 | |
| 332 | 617 | | 文件 | 改动类型 | |
| 333 | 618 | |---|---| |
| 334 | -| `server/services/graph-db.js`(或新 `work-graph-db.js`) | 新增四张表 | | |
| 335 | -| `server/services/work-graph-engine.js` | 新增:状态机 + `advanceOne` + Loop-Validator 流程 | | |
| 336 | -| `server/services/work-graph-sse.js` | 新增:参照 `sse-broker.js` | | |
| 337 | -| `server/routes/work-graph.js` | 新增:控制类 + 只读查询类端点 | | |
| 338 | -| `web/src/views/WorkGraphConsole.vue` | 新增:主页面 | | |
| 339 | -| `web/src/components/WorkGraphCanvas.vue`(视实现拆分情况) | 新增:可复用的画布组件(父图/子图共用,见 §四.D.3) | | |
| 340 | -| `server/src/engine/`(`run-registry.js` 相关) | 只读研究为主,T9 视核实结果决定是否需要补一个小的查询接口,不改现有取消逻辑 | | |
| 341 | - | |
| 342 | -**明确不改**:DevLoop 全部现有代码、`27-P2` 已落地的 `org_graph_nodes`/`edges`(本设计只读取,通过 `triggered_by_org_node_id` 引用,不写入)、`CompanyDashboard.vue`(新页面独立存在,不改造现有 tab,只是从 Dashboard 某处加一个跳转入口)。 | |
| 619 | +| `server/services/work-graph-db.js` | 新增:五张表 + 状态转换表 + work graph 写入校验 | | |
| 620 | +| `server/services/graph-engine.js` | 新增:`advanceOne` + 两种 executor + 回退熔断 + 预算 + 级联 | | |
| 621 | +| `server/services/graph-run-sse.js` | 新增:参照 `sse-broker.js` | | |
| 622 | +| `server/services/goal-contract.js` | 新增:验收契约 schema + 执行器 | | |
| 623 | +| `server/routes/graph-run.js` | 新增:控制类 + 只读查询类端点 | | |
| 624 | +| `server/routes/company.js` | 追加:任务入口端点(T12) | | |
| 625 | +| `web/src/views/GraphConsole.vue` | 新增:主页面 | | |
| 626 | +| `web/src/components/WorkGraphCanvas.vue` | 新增:从 `UnitsDAG.vue` 抽取,org/work 两层共用 | | |
| 627 | +| `system/templates/workgraph-planning/` 等四个 | 新增:规划 agent + 三个 validator 本体 | | |
| 628 | +| `server/services/graph-db.js` | 追加:`org_graph_edges` 加 `fail_reason` 列(T-P3) | | |
| 629 | +| `system/org-graph-templates/*.json` | 修改:末节点改名 + 回退边重画(T-P1) | | |
| 630 | +| `docs/todo/system-design/27-naming-conventions.md` | 修改:§三 删行 + 新增 validator 命名轴(T-P2) | | |
| 631 | +| `server/src/engine/run-registry.js` | 只读研究为主,T9 视核实结果决定是否补一个查询接口 | | |
| 632 | + | |
| 633 | +**明确不改**:DevLoop 全部现有代码(`loop-controller.js` / `unit-registry.js` / `doc-store.js` 只被**读取和借鉴**,收编是 P6)、P2 的 `org-graph-service.js` 四项校验(只复用)、`CompanyDashboard.vue`(新页面独立存在,只从 Dashboard 加一个跳转入口)。 | |
| 343 | 634 | |
| 344 | 635 | --- |
| 345 | 636 | |
| 346 | 637 | ## 八、风险 / Open Questions |
| 347 | 638 | |
| 348 | -1. **`RunRegistry`/`ProcessGuard` 的取消调用链没有 100% 核实**(§三.5)——"停止"操作里"硬取消正在跑的节点"这条能力,设计上按能做出发,但 T9 是这个功能能不能达到设计预期的真正决定因素。如果核实后发现做不到干净取消,退化方案("停止"只影响未开始的节点,正在跑的等它自然结束)已经在 §四.C 写明,不会导致整个设计推倒重来,但用户体验会打折扣(点了停止还要再等一会)。 | |
| 349 | -2. **Work Graph 画布的分层布局 + 回退边曲线路由是一块不小的手写前端工程量**,没有引入图形库(符合仓库一贯风格),但意味着这部分工时不能按"随便拿个组件库拼一下"估计。如果实现过程中发现手写布局的边界情况(比如多条回退边交叉)处理成本远超预期,值得回来重新评估是否值得为此引入一个轻量布局库(如 dagre 的纯布局计算部分,不需要它的渲染层)——本文档不预先下结论,留给实现阶段视实际复杂度决定。 | |
| 350 | -3. ~~**"重试"默认不级联重置下游**~~ ✅ **v0.2 已由既有规格关闭**:`_spec/loops-l3.md` 的增量算法已经给出标准答案——按 `contract_hash` 把节点分成 added / changed / removed / **unchanged** 四类,`unchanged`(id 在且 hash 不变)**保持 passed、无须重跑**,只有 hash 变了的才回退到 pending。也就是说"要不要级联"不该由重试动作决定,而由**产出契约是否真的变了**决定。P3/P4 实现时直接采用这套判据。下面保留原始论述作为背景:**重试默认不级联重置下游**(§四.C 表格备注)——如果实际使用中发现"节点 A 失败重试后产出变了,但 A 的下游 B 早就用旧产出跑完了"这种场景大量出现(比如 B 是在 A 曾经短暂 success 又被人工强制重跑的情况下执行的),现在的设计没有处理"重跑一个已经有下游消费过它产出的节点"这条路径。P2 阶段(本文档当前范围)刻意不支持这个场景(`fail`/`blocked` 节点的下游按定义还没跑过,不存在这个问题;真正的场景是"对一个 `success` 节点也允许点重试",当前设计没开放这个入口)。如果后续需要支持,涉及"下游要不要级联失效"的产品决策,留待有真实需求时再设计。 | |
| 351 | -4. **子图默认不继承父图的暂停/停止状态**(§四.D.3)——"暂停父图"不会暂停正在跑的子图,是刻意简化,避免一次"暂停"要级联遍历整棵下钻树。如果实际使用中这个不一致体验造成困惑(用户以为暂停了父图,子图还在偷偷跑),需要重新评估是否要做级联传播,工作量不小(需要引擎知道"我有没有子图、子图现在什么状态",目前的 `parent_node_id` 只是单向引用,没有双向感知机制)。 | |
| 352 | -5. **本文档假定"图已经规划好"**,不覆盖 `workgraph-planning` agent 本身怎么工作(§一非目标已声明)——这意味着在 planning agent 真正设计出来之前,T1-T10 的验收(§六)只能用手写测试数据构造图,不能端到端验证"从一句自然语言目标到一张可执行的图"这条链路。这是刻意的范围切分,不是遗漏,但要求 27 号文档后续单独出一份"`workgraph-planning` agent"设计文档,两者共用本文档定义的 schema(§四.A)。 | |
| 639 | +1. **归因能精确到哪一层,尚未定案**(R-16 遗留)。原话说「直接让某个 agent 直接 +bug context 描述直接重做」,但若 `validator-reflection` 判定问题出在某个 org node **内部 work graph 的具体节点**,org 层的边只能退到 **org node 粒度**。两条路: | |
| 640 | + - **(倾向)保持层级封装**:org 层只退到 org node,`remediation_note` 跟着传下去,由该 org node 自己的 `workgraph-planning` 决定是重新规划一张图、还是复用旧图只重跑某个节点。好处是 [27-E2E](./27-e2e-task-execution-flow.md) §四 #4 的「重新 planning vs 跳回节点」这个判断留在 org node 内部做,不上升到 org 层;坏处是多一跳。 | |
| 641 | + - **允许跨层直接寻址目标节点**:更快,但破坏封装,且控制台上「暂停 / 单步」作用于哪一层会变得含糊。 | |
| 642 | +2. **`validator-reflection` 的命名尚未定案**(R-16 遗留)。按 [27-NAMING](./27-naming-conventions.md) §三 现行规则(work graph 节点 = `{父 org 节点职能}-{职能}`),它作为 `org-validator` 的 work graph 节点应为 `validator-reflection`,与原话一致。但它做的是**归因**不是**判定**,是否改叫 `validator-triager`(对齐 DevLoop 的 BugTriager)待定。 | |
| 643 | +3. **`fail_reason` 的取值域不由引擎约束**(§四.D.2.2)——引擎只做字符串匹配,取值由图的规划者和 validator 自行约定。好处是不把 DevLoop 的四分类硬编码进通用层;风险是归因产出一个图上没有对应边的取值。**缓解已定案**:写入前校验新增第五项 `fail-fallback-edge`(§三.7),强制每个有 `fail` 出边的节点留一条兜底边。 | |
| 644 | +4. **`org_graph_nodes.id` 是整数、`work_graph_nodes.id` 是文本**,`graph_run_nodes.def_node_id` 只能取 TEXT 且无法建外键(§四.A.2 的注)。引用完整性靠服务层保证。 | |
| 645 | +5. **回退边曲线路由是真实的手写前端工程量**(§四.B.2)。没引入图形库符合仓库一贯风格,但这块工时不能按「拿组件库拼一下」估。若实现中发现多条回退边交叉的边界情况成本远超预期,值得重新评估引入 dagre 的纯布局计算部分(不要它的渲染层)——本文档不预设结论。 | |
| 646 | +6. **`RunRegistry`/`ProcessGuard` 的取消调用链没有 100% 核实**(§3.6)。T9 是「停止」能否达到设计预期的真正决定因素。降级方案已在 §四.C 写明,不会导致设计推倒重来。 | |
| 647 | +7. **v1 的「一家公司同时只有一个 active root run」是一条真实的产品限制**,不只是技术妥协。它由 `CONVENTIONS.md` §12 的不变量和 SessionPool key 复用共同决定(§一非目标)。如果用户很快就要「一家公司并行接多个任务」,实例隔离要提前排期。 | |
| 648 | +8. **`workgraph-planning` 的规划质量没有硬校验**。§三.7 只保证图**结构合法**,不保证它**适合这个任务**。缓解手段与 P2 同款:规划记录落盘供人工复核,发现不合理走增量修正。 | |
| 649 | +9. **阶段 0 的契约格式一旦定错,阶段 A 的 schema 可能要返工**。这也正是 R-15.6 把它提到最前面的原因——但「提前」只降低概率,不消除风险。建议 T0 先把契约的**存储位置和与 `output_ref` / `artifacts/` 的关联方式**定死,格式内部细节可以后续演进。 | |
| 353 | 650 | |
| 354 | 651 | --- |
| 355 | 652 | |
| 356 | 653 | ## 九、与其他阶段的关系 |
| 357 | 654 | |
| 358 | -- **与 P2(Org Graph)**:强依赖,`triggered_by_org_node_id` 直接引用 `org_graph_nodes(id)`,本文档假定 [27-P2](./27-p2-org-graph.md) 已经落地——如果 P2 还没上线,`GET /org-graph-nodes/:id/work-graphs` 这类按 org node 反查的端点没有数据可读,但 Work Graph 引擎本身(T1-T2/T7 的状态机和 Loop-Validator 逻辑)不依赖 P2 的任何具体实现细节,理论上可以先把引擎和画布用一个假的 `triggered_by_org_node_id` 跑通,P2 上线后再接上真实数据——两者顺序不是互相阻塞的硬依赖,只是"最终要接起来"。 | |
| 359 | -- **与 P1(虚拟引用寻址)**:无直接依赖,且 P1 **已实现并提交**(commit `226489d`)。Work Graph 节点执行时调用 `AgentManager` 的方式不受影响(P1 改的是 `prompt` 字段从哪来,不改调用接口)。 | |
| 360 | -- **与端到端流程**:本文档设计的是"一张 Work Graph 怎么被执行和控制",但一个真实任务会有**三层图**(任务级 → org node 级 → 并行子图级)。第三层之上的两层、任务入口、验收收敛条件,见 [27-E2E 端到端任务执行流程](./27-e2e-task-execution-flow.md)。该文档同时指出本文档需要补的四项:`node_kind='org_node'`(让 work graph 节点能指向 org node)、fan-out 并行 + 同 agent 多实例隔离、跨层级共享的任务级总预算、三层暂停/停止的级联语义。 | |
| 361 | -- **与 27 号文档 P3/P4 原始路线图**:本文档实质上把这两个 phase 的交付物做了细化和合并(理由见 §二),且额外交付了 27 号文档路线图里没有单独列出的"执行控制台页面"。建议 27 号文档 §六路线图表的 P3/P4 两行补充链接指向本文档,与 [27-P2](./27-p2-org-graph.md) 现有的链接方式一致。 | |
| 655 | +- **与 P2**:强依赖。P2 产出 org graph 定义,本文档执行它。**两者共用同一套 schema 形态、执行引擎、控制台画布**——这是 R-9.1 新模型最大的收益。本文档只读 `org_graph_nodes/edges`(唯一例外是 T-P3 加一列 `fail_reason`)。P2 侧的三项连带修改(T-P1/T-P2/T-P3)归本阶段执行,见 §五的警告框。 | |
| 656 | +- **与 P1**:无直接依赖,已实现(commit `226489d`)。P1 改的是 prompt 从哪来,不改调用接口。但 R-15.3 的「本体名不带领域前缀」正是 P1「同一份本体、多处引用、各自私有语境」的直接推论。 | |
| 657 | +- **与 P5(自我进化)/ L4**:本文档不含。任务级预算里预留了 `l4_iterations` 的位置但不实现。 | |
| 658 | +- **与 P6(DevLoop 收编)**:本引擎从 DevLoop **抽象**而来(§三),但不改它一行。收编的验收标准是「DevLoop 现有测试套件在新引擎上全部通过」,属 P6。 | |
| 659 | +- **与 base agent 的四段式**:见下方 §九bis——原表述「已强制一套四段式生命周期」不准确,实测有一半没落地。 | |
| 660 | +- **与端到端流程**:本文档设计的是「一张图怎么被执行和控制」,完整的七阶段链路见 [27-E2E](./27-e2e-task-execution-flow.md) §三,跑一遍具体的数见 [27-EXAMPLE](./27-example-dev-company-walkthrough.md)。 | |
| 661 | + | |
| 662 | +--- | |
| 663 | + | |
| 664 | +## 九bis、base agent 四段式的现状核实 | |
| 665 | + | |
| 666 | +[27-E2E](./27-e2e-task-execution-flow.md) §八bis 说 `system/templates/base/` 「已强制一套四段式生命周期」,并把它当作「每个 node agent 内部也是一张图」的现成雏形。**核实后这个说法只对一半**,本节记录实测结论——它直接决定了 P3 之后那项「统一两套状态机」的待办到底要做什么。 | |
| 667 | + | |
| 668 | +### 设计意图(`system/templates/base/agent.md`,28 行) | |
| 669 | + | |
| 670 | +``` | |
| 671 | +RESEARCH ──→ PLAN ──→ EXECUTE ──→ VERIFY | |
| 672 | + ↑ │ | |
| 673 | + └───────── 失败写 _lessons.md 打回 ┘ | |
| 674 | +``` | |
| 675 | + | |
| 676 | +agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 `node bin/lemon.js task transition --agentId <id> --phase <next> --artifact <path>` 才能换脑子换技能包;每次流转**清空上下文**(原文叫「斩断记忆 / Context Flush」)防长对话累积幻觉;EXECUTE 阶段才拿到「修改核心文件的破坏性权限」;VERIFY 通过才可 `report_task({status:"PASS"})`。 | |
| 677 | + | |
| 678 | +**这确实就是一张最小 work graph**:四个节点、线性前进、末端一条回退边,且已具备 P3 引擎需要的三样东西——显式的阶段状态、阶段间的产物传递(`--artifact`)、失败回退。 | |
| 679 | + | |
| 680 | +### 实测:上下文隔离是真的,权限/技能隔离是假的 | |
| 681 | + | |
| 682 | +`POST /api/agent/:agentId/task/transition`(`server/routes/agent-chat.js:1832`)已实现,且比 prompt 里写的还多: | |
| 683 | + | |
| 684 | +| 机制 | 状态 | 位置 | | |
| 685 | +|---|---|---| | |
| 686 | +| Context Flush(断 ACP 进程 + `history.json` 清空 + artifact 注入新起步指令 + 重新唤醒) | ✅ 真的在跑 | `agent-chat.js:1916-1960` | | |
| 687 | +| **P0 事前阻断阀**——P0 任务 `plan → execute` 拦截挂起、通知 Master 人工审批才放行 | ✅ 已实现(prompt 里没写这条) | `agent-chat.js:1854-1891` | | |
| 688 | +| OKR 联动——顺带把节点状态更新为 `researching`/`planning`/`executing`/`verifying` | ✅ | `agent-chat.js:1894-1905` | | |
| 689 | +| **Hot Swap 换技能包** | ❌ **静默失效** | `agent-chat.js:1908` | | |
| 690 | + | |
| 691 | +失效原因:代码是 `const sourceSkill = .../system/skills/meta-${phase}/SKILL.md` 后跟 `if (fs.existsSync(sourceSkill))` 才拷贝,而 **`system/skills/` 这个目录在仓库里根本不存在**,四个 `meta-*` 技能一个都没有。所以每次流转的「动态换脑」是空转,那句 `🧠 动态技能树已切换` 的日志压根不会打印,**也没有任何报错**。 | |
| 692 | + | |
| 693 | +**后果**:「EXECUTE 阶段才拿到破坏性权限」这个设计意图目前**只靠 prompt 话术约束,没有任何机制保障**——agent 在 RESEARCH 阶段照样能写文件。 | |
| 694 | + | |
| 695 | +另两处顺带发现:`phase` 参数**没有任何校验**,既不校验流转顺序(可以 research 直接跳 verify、跳过中间两段),也不校验取值本身,而它被直接拼进文件路径。 | |
| 696 | + | |
| 697 | +### 对 P3 的影响 | |
| 698 | + | |
| 699 | +原来那条待办(「保证两套状态机语义不冲突」)应当升级为一个更具体、收益也更明确的目标:**把四段式改写成引擎上的一张标准图模板**。这样做同时解决三件事: | |
| 700 | + | |
| 701 | +1. 消除「引擎层的图」和「agent 内部的四段式」两套并行状态机; | |
| 702 | +2. `meta-*` 技能的挂载正好对应 work graph 节点的 `system_agent_id` 引用,**顺手把 `system/skills/` 缺失这个洞补上**——现在这批技能没有读取方,改造后引擎就是读取方; | |
| 703 | +3. 阶段顺序校验由引擎的边定义天然提供,不需要另写。 | |
| 704 | + | |
| 705 | +**仍列为 P3 之后的待办**,不进本阶段范围。但 §五 的 T11(`workgraph-planning`)在生成图时应当**避免与四段式语义打架**——具体是别让 work graph 节点内部再套一层四段式流转,否则同一次执行会有两层 Context Flush 互相清对方的上下文。 | ... | ... |
docs/todo/system-design/27-requirement.md
| ... | ... | @@ -218,6 +218,135 @@ |
| 218 | 218 | |
| 219 | 219 | --- |
| 220 | 220 | |
| 221 | +## R-15 · P3 需求规划讨论中的六个定案 | |
| 222 | + | |
| 223 | +针对「27 系列 P3 该做成什么样」的一轮讨论产出。 | |
| 224 | + | |
| 225 | +⚠️ **本节两种来源混排,逐条标注**:R-15.1 / R-15.2 / R-15.3 有用户原话(引用块内一字不改,遵循本页照搬原则);R-15.4 / R-15.5 / R-15.6 是**选项式确认**——选项文本由我拟、用户勾选,不是用户自己的措辞,因此只记为定案,不冒充原话。 | |
| 226 | + | |
| 227 | +> 📌 **恢复记录(2026-08-15)**:R-15.1 的全文、R-15.4 的后半、R-15.5、R-15.6、以及速查表对应行,曾因一次错误回滚(`git checkout --`)丢失,后据会话上下文补回。R-15.3 的「节点名 vs 本体名」表**未**按原始版本还原——它在丢失期间被 [27-NAMING](./27-naming-conventions.md) §三 的重写取代(命名轴从「业务领域」改为「所在图的层级」),新版更贴合 R-15.3 引用的原话,故保留新版。 | |
| 228 | + | |
| 229 | +### R-15.1 · 讨论对象是 org graph 这条线的 P3 | |
| 230 | + | |
| 231 | +> 有一个进程正在开发p2 我们来看看 p3 的需求的规划 | |
| 232 | + | |
| 233 | +> 对 我们要讨论 org graph 的 p3 | |
| 234 | + | |
| 235 | +**澄清了什么**:仓库里有**两条互不相干的 P3**,容易混。 | |
| 236 | + | |
| 237 | +| | DevLoop 的 P3 | 27 系列的 P3 | | |
| 238 | +|---|---|---| | |
| 239 | +| 出处 | `docs/prd/newfunction/_spec/plan-migration.md` | [27 总纲](./27-graph-agent-architecture.md) §六 | | |
| 240 | +| 内容 | L1+L2 联动,跨 Unit `runProject` | Work Graph 最小闭环 | | |
| 241 | +| 状态 | `runProject` 已实现(`loop-controller.js:576`) | 零代码 | | |
| 242 | + | |
| 243 | +本条确认讨论的是**后者**。下文 R-15.2 起全部属于 27 系列。 | |
| 244 | + | |
| 245 | +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) 全文。 | |
| 246 | + | |
| 247 | +### R-15.2 · `workgraph-planning` 是全局通用 agent | |
| 248 | + | |
| 249 | +> ① 用全局通用 workgraph-planning | |
| 250 | + | |
| 251 | +**澄清了什么**:每个 org node 接到任务时不是各自带一份规划能力,而是**都调同一个全局 `workgraph-planning` 本体**,靠记忆分区区分它在不同 org node / 不同公司下的语境。与 R-9.2 元 agent 继承、[27-P1](./27-p1-virtual-reference.md) 虚拟引用寻址一致——agent 池不随 org node 数量膨胀。 | |
| 252 | + | |
| 253 | +**落点**:[27-NAMING](./27-naming-conventions.md) §二(该条目已在表内)、[27-P3/P4](./27-p3-p4-work-graph-console.md) 阶段 D。 | |
| 254 | + | |
| 255 | +### R-15.3 · validator 分两个层级,各有全局本体 | |
| 256 | + | |
| 257 | +> ② org graph 的 validator 叫做 org-validator work graph 里面生成的 validator 叫做 xx-validator 比如 planner agent 的 validator 叫做 planner-validator ,code agent 内生成的 work graph 的 validator 叫做 code-validator。所以是两个层级的不同的 validator ,每个层级的 validator 在 system agent 里面都有对应的全局 agent ,比如有 planner-validator coder-validator org-validator , org-validator agent 里面也许会调用 benchmark-validator testdata-validator 等等不同的 sub agent 来完成任务。 | |
| 258 | + | |
| 259 | +**两级验收的分工**: | |
| 260 | + | |
| 261 | +| | 节点名 | 验什么 | 决定什么 | 失败退到哪 | | |
| 262 | +|---|---|---|---|---| | |
| 263 | +| **org 层** | `org-validator` | 整个任务达成了吗([27-E2E](./27-e2e-task-execution-flow.md) §三 阶段 7 的全局 validator) | 整个 org graph run 的收敛 | 退回某个 org node,**整张 work graph 重跑** | | |
| 264 | +| **work 层** | `{orgNode}-validator` | 这个 org node 自己的产出合格吗 | 该 org node 的 success/fail | 退回本张 work graph 内的某个节点 | | |
| 265 | + | |
| 266 | +**节点名 vs 本体名**(本条推导出的一条新规则): | |
| 267 | + | |
| 268 | +| | 例 | 带前缀? | | |
| 269 | +|---|---|---| | |
| 270 | +| system agent 本体 | `planner-validator` / `coder-validator` / `org-validator` | ❌ 不带领域前缀 | | |
| 271 | +| org graph 节点 | `org-planner` / `org-coder` / `org-validator` | ✅ 带 `org-` | | |
| 272 | +| work graph 节点 | `{父 org 节点职能}-{职能}` | ✅ 带父节点名 | | |
| 273 | + | |
| 274 | +禁止裸用 `validator` 的理由是「一家公司内两个节点不能重名」——该理由针对的是**节点**,不针对**本体**。本体若被迫带领域前缀,`coder-validator` 会复制成多份,与 P1「同一份本体、多处引用、各自私有语境」直接冲突。 | |
| 275 | + | |
| 276 | +**三个 validator 是三个独立本体**:它们验的东西本质不同——规划产物 / 代码 / 整个任务达成,是不同能力,不是同一能力的不同语境。[27-E2E](./27-e2e-task-execution-flow.md) §八bis 的「不特化」原则针对的是同一能力在不同 org node 下,与本条不冲突。 | |
| 277 | + | |
| 278 | +**`org-validator` 调 sub agent 不需要新机制**:`org-validator` 本身是一个 org node,按定义有自己的 work graph,`benchmark-validator` / `testdata-validator` 就是这张 work graph 的节点,落在已有的递归模型里,引擎不加分支。 | |
| 279 | + | |
| 280 | +**连带影响**:R-9.3「planner 生成、validator 执行」的两个角色现在分属**两个不同的 org node**——`goal-benchmark` / `goal-test-data` / `goal-test-script` 在 `org-planner` 的 work graph 里产出,由 `org-validator` 这个 org node 消费。因此需要**跨 org node 的产物传递机制**,[27-P3/P4](./27-p3-p4-work-graph-console.md) 现有的 `output_ref`(节点内产物指针)不够。 | |
| 281 | + | |
| 282 | +**落点**:[27-NAMING](./27-naming-conventions.md) §三 + 新增 validator 命名轴、[27-P2](./27-p2-org-graph.md) §4.6 三个模板、[27-P3/P4](./27-p3-p4-work-graph-console.md)。 | |
| 283 | + | |
| 284 | +### R-15.4 · 引擎泛化,定义层与运行时层分离 | |
| 285 | + | |
| 286 | +**选项确认**,非原话。给出的三个选项是:引擎泛化接受表名参数(选中)/ org graph 执行时物化成 work_graph 实例 / 先只做 work graph 把 org graph 执行推到 P5。 | |
| 287 | + | |
| 288 | +**推翻了什么**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A 的注释专门为「不拆定义表和实例表」辩护,理由是 work graph「规划完即执行、执行完即归档」,定义即实例。**该前提在 org graph 也要被执行之后失效**——org graph 是建公司时生成一次、活整个公司生命周期的长期定义,一份定义要被跑很多次(每来一个任务跑一遍)。 | |
| 289 | + | |
| 290 | +**定案**:定义层保留两套(`org_graph_*` 长期 / `work_graph_*` 一次性),运行时层统一为一套 `graph_runs` + `graph_run_nodes`,引擎只操作运行时层,定义从哪来由 `graph_runs.graph_kind` 决定。三层下钻改用 `parent_run_id` 一条链(取代 `parent_node_id`);任务级总预算([27-E2E](./27-e2e-task-execution-flow.md) §7.1)挂 root run,子 run 上溯扣同一个池;暂停/停止沿同一条链向下级联(§7.5)。 | |
| 291 | + | |
| 292 | +**连带定案**:org node 与 work node 的执行语义不同——前者是「起一张子图并等它收敛」(子 run `completed` → success,`blocked` → fail),后者是「一次 agent 调用」。引擎需要 `agent_call` / `subgraph` 两种 node executor。org 层回退**不**强制插 `loop_validator` 节点(会污染「这家公司有哪些角色」的语义),改由引擎对 `edge_kind='backward'` 的边统一计数 + 硬顶熔断。 | |
| 293 | + | |
| 294 | +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A / §四.D。 | |
| 295 | + | |
| 296 | +### R-15.5 · P3 范围含 `workgraph-planning` + 任务入口 | |
| 297 | + | |
| 298 | +**选项确认**,非原话。 | |
| 299 | + | |
| 300 | +**推翻了什么**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §一非目标把 `workgraph-planning` 排除在外,代价是该文档 §八.5 自己承认「只能用手写测试数据构造图,不能端到端验证从一句自然语言目标到一张可执行的图这条链路」。且 [27 总纲](./27-graph-agent-architecture.md) §六本来就把 `workgraph-planning` 列为 P3 交付物。任务入口(用户 → 公司)是 [27-E2E](./27-e2e-task-execution-flow.md) §四 #7 标注的「完全空白」,没有它图没有来源。 | |
| 301 | + | |
| 302 | +**定案**:两者都收进 P3,作为最后一个阶段(前面的阶段全部不起 LLM、可独立验收,与 [27-P2](./27-p2-org-graph.md) §五「三个可独立验收的阶段」同一套切法)。 | |
| 303 | + | |
| 304 | +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §一非目标(两条作废)、§五任务划分。 | |
| 305 | + | |
| 306 | +### R-15.6 · 验收契约提到最前面 | |
| 307 | + | |
| 308 | +**选项确认**,非原话。 | |
| 309 | + | |
| 310 | +**依据**:[27-E2E](./27-e2e-task-execution-flow.md) §十「整条流程的终止条件(验收)是实现程度最低的一环……**先建验收,再建循环**——否则会得到一个跑得很顺畅但永远不知道该不该停的系统」。按该节 v0.2 调整,「验收先行」的含义**不是**建 benchmark 基础设施(那是误解),而是**定义验收契约格式 + 建执行器**。 | |
| 311 | + | |
| 312 | +**定案**:P3 新增一个前置阶段,先定 `goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一契约格式并建执行器,再动引擎。 | |
| 313 | + | |
| 314 | +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §五任务划分、[27-E2E](./27-e2e-task-execution-flow.md) §十一。 | |
| 315 | + | |
| 316 | +--- | |
| 317 | + | |
| 318 | +## R-16 · 失败归因由专职 subagent 做,回退边是多条 | |
| 319 | + | |
| 320 | +> org-validator 判定任务未达成时,退回 code-coder(实现有问题)还是 code-planner(规划有问题)? 这个org-validator 会有自己的 work graph 里的 subagent 决定,它首先需要有一个 validator-reflection subagent 来判断当前的 bug 的源头是哪个 agent 的哪个 skill 的问题,还是哪个 agent 的输入约束不够的问题,还是哪个 agent 的执行问题,判断完原因,才知道是先改 skill,还是直接让某个 agent 直接+bug context 描述直接重做。 所以回退的边是多个的,不是一个的,在回放的时候才知道用哪个边实现回退。你觉得如何? | |
| 321 | + | |
| 322 | +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.D.2.2(选边)、§四.D.0(子 run 回写透传)、§四.D.2.1(熔断) | |
| 323 | + | |
| 324 | +**这条回答了什么**:[27-P3/P4](./27-p3-p4-work-graph-console.md) v0.3 §八.1 把「一个节点有多条 fail 边时走哪条」列为本版最需要确认的 open question,本条关闭它。 | |
| 325 | + | |
| 326 | +**关键结构**:归因**不是**失败节点自己顺手吐一个 reason,而是 `org-validator` 自己 work graph 里排第一位的一个专职节点 `validator-reflection`。这与 DevLoop 的既有分工同构——Integrator 判定集成失败、BugTriager 单独归因,是两个不同 agent,理由是同一个 agent 既当裁判又当法医容易把「我判失败」和「我认为该怪谁」混成一次输出。 | |
| 327 | + | |
| 328 | +**三分类归因与 DevLoop 四分类的对照**: | |
| 329 | + | |
| 330 | +| 原话的归因 | DevLoop 对应 | 回退动作 | | |
| 331 | +|---|---|---| | |
| 332 | +| 哪个 agent 的执行问题 | `impl_bug` | 该 agent 带 bug context 重做 | | |
| 333 | +| 哪个 agent 的输入约束不够 | `contract_mismatch` / `spec_ambiguous` | 退回上游产出约束的那个节点 | | |
| 334 | +| **哪个 agent 的哪个 skill 的问题** | **无对应** | 先改 skill —— 这一支即 **L4-company** | | |
| 335 | +| (原话未提)进程超时 / 被杀 | `infra_flake` | 直接重跑,不烧预算、不动节点 | | |
| 336 | + | |
| 337 | +⚠️ **「skill 问题」这一支落在 P3 的非目标里**(L4 是三级恢复中最贵、爆炸半径最大的一级,[27-E2E](./27-e2e-task-execution-flow.md) §十 建议最后做)。**建议**:v1 保留 `skill_defect` 这个归因取值让 reflection 照常判定(数据先攒着,归因体系一次成型),但动作降级为「挂起等人工」,不自动触发 L4。`infra_flake` 建议补上——纯引擎逻辑、近乎零成本,不补则超时会被误判成执行问题、白烧一轮预算。 | |
| 338 | + | |
| 339 | +**对 schema 的要求**:子 run 收敛时不能只回写 success/fail,**还要把 `fail_reason` 和 `remediation_note` 一起透传回父节点**——因为决定走哪条边的是子图里的 reflection 节点,不是失败节点本身。[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.D.0 原表只写了「子 run completed → success;blocked/failed → fail」,缺这条透传。 | |
| 340 | + | |
| 341 | +**本条带出的一个熔断漏洞**:v0.3 §四.D.2.1 按 `(run_id, edge_id)` 计数、硬顶 3,前提是一个节点只有一条回退边。多条回退边各自计数后,**硬顶实际变成 3×N**——reflection 只要每次归因到不同 agent、走不同的边,就永远撞不到任何一个计数器的顶。这是 [27-E2E](./27-e2e-task-execution-flow.md) §7.1「每级都重置下一级计数器则没有终止条件」的同型变体。**修法:双层计数**,单边计数(防同一路径死循环)+ `graph_runs.backward_count` 的 run 级总回退计数(防「换个借口继续退」),任一撞顶即 `force_terminated`。这与任务级总预算是两回事——预算管花了多少钱,这个管绕了多少圈,两者都要。 | |
| 342 | + | |
| 343 | +**两处待确认**(尚未定案): | |
| 344 | + | |
| 345 | +1. **归因能精确到哪一层**。原话说「直接让某个 agent 直接 +bug context 描述直接重做」,若 reflection 判定问题在某个 org node 内部 work graph 的具体节点,org 层的边只能退到 **org node 粒度**。建议保持层级封装:org 层只退到 org node,`remediation_note` 跟着传下去,由该 org node 自己的 `workgraph-planning` 决定是重新规划一张图还是复用旧图只重跑某节点——这让 [27-E2E](./27-e2e-task-execution-flow.md) §四 #4 的「重新 planning vs 跳回节点」留在 org node 内部判断。另一条路是允许跨层直接寻址目标节点,更快但破坏封装,且控制台「暂停/单步」的层级语义会变含糊。 | |
| 346 | +2. **`validator-reflection` 的命名**。按 [27-NAMING](./27-naming-conventions.md) §三 现行规则(work graph 节点 = `{父 org 节点职能}-{职能}`),它作为 `org-validator` 的 work graph 节点应为 `validator-reflection`,与原话一致。但它做的是归因不是判定,是否改叫 `validator-triager`(对齐 DevLoop 的 BugTriager)待定。 | |
| 347 | + | |
| 348 | +--- | |
| 349 | + | |
| 221 | 350 | ## 附 · 需求 → 设计落点 速查 |
| 222 | 351 | |
| 223 | 352 | | 需求 | 一句话 | 主要落点 | |
| ... | ... | @@ -236,3 +365,10 @@ |
| 236 | 365 | | R-12 | **验收产物定名 `goal-`**;goal 唯一、task 多个 | NAMING §四 | |
| 237 | 366 | | R-13 | goal 三层;类型由 agent 推断;新 agent 从 base 继承;强制单入口 | NAMING §四 / P2 §3.3 §4.1 §五 | |
| 238 | 367 | | R-14 | 命名轴统一为作用域:`subagent-goal` → **`subtask-goal`** | NAMING §四 | |
| 368 | +| R-15.1 | 讨论的是 27 系列的 P3,不是 DevLoop 的 P3 | P3/P4 全文 | | |
| 369 | +| R-15.2 | `workgraph-planning` 是全局通用 agent,不按 org node 特化 | NAMING §二 / P3/P4 阶段 D | | |
| 370 | +| R-15.3 | **validator 分 org / work 两层**;本体名不带领域前缀 | NAMING §三 / P2 §4.6 / P3/P4 | | |
| 371 | +| R-15.4 | **引擎泛化**,定义层(org/work 两套)与运行时层(`graph_runs` 一套)分离 | P3/P4 §四.A / §四.D | | |
| 372 | +| R-15.5 | P3 范围含 `workgraph-planning` + 任务入口,两条非目标作废 | P3/P4 §一 / §五 | | |
| 373 | +| R-15.6 | 验收契约格式 + 执行器提到最前面,作为 P3 的前置阶段 | P3/P4 §五 / E2E §十一 | | |
| 374 | +| R-16 | **归因由 `validator-reflection` 专职节点做**;回退边多条,运行时选边;熔断需双层计数 | P3/P4 §四.D.0 / §四.D.2.1 / §四.D.2.2 | | ... | ... |