From 25bdfca13dadc3478ff9d5ac5f23964fe2965dc4 Mon Sep 17 00:00:00 2001 From: yibo Date: Sun, 16 Aug 2026 11:53:29 +0800 Subject: [PATCH] docs(p3): R-16 失败归因定案与 base agent 四段式现状核实落入 v0.3 --- docs/todo/system-design/27-p3-p4-work-graph-console.md | 780 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/todo/system-design/27-requirement.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 698 insertions(+), 218 deletions(-) diff --git a/docs/todo/system-design/27-p3-p4-work-graph-console.md b/docs/todo/system-design/27-p3-p4-work-graph-console.md index d05b8b7..136245b 100644 --- a/docs/todo/system-design/27-p3-p4-work-graph-console.md +++ b/docs/todo/system-design/27-p3-p4-work-graph-console.md @@ -1,15 +1,28 @@ -# 27-P3/P4. Work Graph 执行控制台 — 需求与设计文档 +# 27-P3/P4. 图执行引擎与执行控制台 — 需求与设计文档 -> **所属**: [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)下钻。 -> **版本**: v0.1(设计阶段,未实现) +> **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase **P3(最小闭环)+ P4(回退 + 熔断)**,外加 27 号文档原始路线图里没有单独立项的两块新增交付物——**一个执行控制台页面**,以及 **`workgraph-planning` agent + 用户→公司的任务入口**。 +> **版本**: v0.3(**按 [27-REQ](./27-requirement.md) R-15 六条定案重写**;v0.1 正文与 v0.2 修正块已合并入正文,不再单列) > **日期**: 2026-08-15 -> **前置**: [27-P2 Org Graph 落地](./27-p2-org-graph.md)(`org_graph_nodes` 是本设计里"每个 org node"的数据来源,本文档假定 P2 已落地)。P1(虚拟引用寻址)**已实现并提交**(commit `226489d`),与本设计无强依赖,见 §九。 -> -> ⚠️ **v0.2 修正(2026-08-15)**:本文档 v0.1 有三处基于过期或不完整调研的陈述,端到端流程梳理时被推翻,**动工前必读** [27-E2E 端到端任务执行流程](./27-e2e-task-execution-flow.md) §六与§九: -> 1. §三.1 说 `runProject` 是 501 未实现 —— **它已完整实现**(`loop-controller.js:576`),含 Kahn 拓扑分波 `_groupUnitsByWave()`、L2 闸门、Integrator、BugTriager 四分支调度、预算熔断、全套 SSE 埋点。 -> 2. §四.B.2 / §六 T3 把"手写分层布局算法"当成新工作量 —— **`web/src/components/devloop/UnitsDAG.vue` 已经实现了** SVG + Kahn 分波布局 + 节点状态着色 + 点击下钻(107 行),应改为扩展既有组件。同目录下 `LiveAgentStream.vue`(带 Last-Event-ID 断线重连的 SSE 消费)、`UnitDrawer.vue`(节点详情抽屉)同样可复用。 -> 3. 本文档整体把"回退/循环/预算"当作全新设计 —— **`docs/prd/newfunction/_spec/loops-{overview,l1,l2,l3}.md` + `sdd-process.md` P-1/P-2/P-7 已经有一套完整的三层循环规格**(含四分类失败归因、差异化预算消耗、`contract_hash` 闸门、增量 unchanged 免重跑),本文档 §四.D 的设计应当从它抽象而来,而不是平行发明。 -> **设计方法论**:按用户要求,正文用 **Model / View / Control / Process(MVCP)** 四段组织核心设计(§四),前置背景与走查、后置任务划分与验收标准按仓库既有文档惯例编排。 +> **前置**: [27-P2 Org Graph 落地](./27-p2-org-graph.md) 阶段 A/B。P0/P1 均已实现并提交(P1 = commit `226489d`)。 +> **命名**: 全文遵循 [27-NAMING](./27-naming-conventions.md),含本文档 §五 T-P2 要求新增的 validator 命名轴。 +> **配套**: [27-E2E 端到端流程](./27-e2e-task-execution-flow.md)、[27-EXAMPLE 参考场景](./27-example-dev-company-walkthrough.md) +> **设计方法论**: 核心设计(§四)用 **Model / View / Control / Process(MVCP)** 四段组织。 + +--- + +## 〇、v0.3 重写说明 + +v0.1/v0.2 的正文有三处**结构性**问题,不是补丁能修的: + +| # | v0.1/v0.2 的写法 | 为什么不成立 | 本版怎么改 | +|---|---|---|---| +| 1 | 四张 `work_graph_*` 表,「定义即实例」不拆表 | 该辩护的前提是 work graph「规划完即执行、执行完即归档」。**org graph 也要被同一套引擎执行**(R-15.4),而它是活整个公司生命周期的长期定义,一份定义要被跑很多次 | 定义层与运行时层分离,五张表,§四.A | +| 2 | `advanceOne()` 只有一种节点执行语义(调 `AgentManager`) | org node 的「执行」是起一张子图并等它收敛,不是一次 agent 调用 | 两种 node executor,§四.D.0 | +| 3 | §一非目标排除 `workgraph-planning` 与任务入口 | 代价是 v0.1 §八.5 自己承认「只能手写测试数据构造图,不能端到端验证」 | 两条非目标作废(R-15.5),收进阶段 D | + +另有四处 v0.2 已在头部修正、本版直接写进正文的过期陈述:`runProject` 已实现(不是 501)、`UnitsDAG.vue` 已实现分波布局(不用从零写)、回退/预算应从 `_spec/loops-*.md` 抽象而来(不是平行发明)、P1 已实现。 + +以及两个已被关掉的 open question:v0.1 §八.3「重试要不要级联下游」由 `loops-l3.md` 的 `contract_hash` unchanged 四分类判据关闭;v0.1 §八.4「子图不继承父图暂停」被 [27-E2E](./27-e2e-task-execution-flow.md) §7.5 推翻,改为向下级联。 --- @@ -17,69 +30,80 @@ ### 目标 -1. **一个新页面**:给定一家公司,左侧展示它的 Org Graph(复用 [27-P2 §4.7](./27-p2-org-graph.md) 的分层列表设计),点击任意一个 org node,右侧展示这个节点当前正在跑的 / 历史跑过的 Work Graph。 -2. **执行控制**:启动、下一步、暂停、停止、重试五个操作,且要同时支持**手动**(每次点"下一步"才推进一个节点)和**自动**(一键授权后从头跑到尾,一个节点成功自动跳转执行下一个节点)两种模式——这是本文档动笔前用户明确的一条硬需求,直接决定了 §四.C 的状态机设计不能走 DevLoop 现有 `runUnit()` 那种"一次函数调用跑到底、外部无法中途插手"的模型(见 §三.1 走查结论)。 -3. **节点运行日志**:每个 Work Graph 节点对应一次 agent 调用,要能看到这次调用的完整过程(不只是成功/失败两个字)。 -4. **边的输入输出展示**:一条边连接的两个节点之间,上游节点产出了什么、下游节点实际收到了什么,要可视化出来。 -5. **循环回退连线**:Loop-Validator 相关的回退边(27 号文档 §3.5/§3.4 第 4/5 点)要在图上可视化成一条区别于正常前进边的连线,并显示当前是第几次循环、硬顶还剩几次。 -6. **主流程 / 异常流程处理**:节点成功后按 `success` 边前进是主流程;节点失败后按 `fail` 边路由(可能进 Loop-Validator 回退,也可能是死路进入需要人工介入的状态)是异常流程,两者在引擎状态机和 UI 上都要有明确区分。 -7. **子流程下钻**:点开一个节点,能看到两类下钻内容——① 这次 agent 调用内部的对话/工具调用记录(类似现有 `AgentChat.vue` 的 sub-agent 面板);② 如果这个节点本身触发了一张更小的嵌套 Work Graph,能在同一个页面里逐层下钻查看子图的执行状态(用户已确认两种下钻语义都要)。 +1. **一套图执行引擎**,同时驱动 org graph(company 级 workflow)和 work graph(org node 级 workflow)—— 两者是同一种东西在不同层级的递归(R-9.1),引擎不为它们各写一套。 +2. **执行控制**:启动、下一步、暂停、停止、重试五个操作,同时支持**手动**(点一次推进一个节点)和**自动**(一键授权跑到底)两种模式,且在**每一层语义完全一致**。 +3. **回退与熔断**:`edge_kind='backward'` 的回退边带循环计数与硬顶,两层通用;跨所有层级共享一个**任务级总预算**。 +4. **一个执行控制台页面**:左栏 org graph,中间选中 org node 的 work graph 画布,右栏节点详情;回退边可视化带 `N/3` 徽标;主流程 / 异常流程视觉区分;逐层下钻(agent 对话记录 + 嵌套子图)。 +5. **验收契约**:先定义 `goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一格式并建执行器,再动引擎(R-15.6)。 +6. **规划闭环**:`workgraph-planning` agent + 用户→公司的任务入口,让「一句自然语言 → 一张可执行的图 → 跑完」端到端成立(R-15.5)。 ### 非目标(本阶段明确不做) -- **不做** `workgraph-planning` agent 本身的详细设计("一张 Work Graph 的节点/边最初是怎么被规划出来的")——27 号文档 §六已经把这个交付物列在 P3 里,本文档假定图已经规划好、满足 27 号文档 §3.4 第 5 点的执行前结构校验,只负责"图规划完之后,怎么跑、怎么控制、怎么看"。这不是回避,是刻意收窄范围:规划("这张图长什么样")和执行控制("这张图怎么被跑起来、怎么被人盯着")是两个复杂度都不低的问题,硬塞进一份文档会让两边都设计得不扎实。 -- **不做**对 DevLoop 现有 `loop-controller.js`/`unit-registry.js` 的收编或改造——27 号文档 §五把这个列为 P6(可选),本文档的执行引擎是全新的、只服务 Work Graph,不改 DevLoop 一行代码。§三.1 会说明为什么 DevLoop 现有引擎的执行模型不能直接套用到本文档的需求上。 -- **不做**多人协同编辑同一张 Work Graph(比如两个人同时点"下一步")的并发控制之外的复杂协作特性(评论、@提醒等)——并发控制本身要做(§四.A 幂等设计),但"协作"停留在"不冲突",不做"多人分工"这类产品特性。 -- **不做**跨公司/跨 Org Graph 的全局视图——页面粒度是"一家公司的一个 org node 的一张 Work Graph",不做"看所有公司所有 Work Graph"的总览大屏。 -- **不做**节点执行中途的"硬中断后从中间状态精确恢复"(比如 Coder 写到一半被强杀,下次重试不是从头写而是接着上次的文件状态续写)——重试语义是"整个节点重新执行一次",不是"断点续传",见 §四.C 重试小节的说明。 +- **不做 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 才需要。 +- **不做 L4**(system agent 自我迭代)。见 [27-E2E](./27-e2e-task-execution-flow.md) §八,且它是三级恢复叠加里最贵、爆炸半径最大的一级。 +- **不做**对 DevLoop 现有 `loop-controller.js` / `unit-registry.js` 的收编。它是本架构的参考实现([27-E2E](./27-e2e-task-execution-flow.md) §二结论 2),本引擎从它**抽象**,但不改它一行;收编是 P6。 +- **不做**多人协同编辑同一张图的协作特性(评论、@提醒)。并发**冲突控制**要做(§四.A.4 幂等),「协作」停留在「不冲突」。 +- **不做**节点执行中途的断点续传。重试语义是「整个节点重新执行一次」。 +- **不做**跨公司的全局大屏。页面粒度是「一家公司」。 --- ## 二、背景 -27 号文档 §六路线图把 P3 定义为"Work Graph 最小闭环:单向执行,先不做回退"(顺序执行器,`success → next,fail → 停止/标记 blocked`),P4 才引入回退和 Loop-Validator。本文档没有严格切两份文档去对应这两个 phase,原因是:**执行引擎的状态机一旦要支持"暂停/单步/停止/重试"这种可以被外部随时打断和摆布的控制语义,"先做单向执行、再加回退"这种分阶段方式反而会让第一版的状态机设计得过窄,P4 加回退时必须回来重新改数据模型和引擎主循环**。所以本文档一次性把 P3(正向执行)+ P4(回退 + Loop-Validator)的数据模型和引擎设计完,落地时仍然可以分两个迭代交付(任务划分 §六 里 T1-T6 是 P3 范围、T7-T9 是 P4 范围),但底层 schema 和状态机不需要中途推倒重来。 +27 号总纲 §六把 P3 定义为「Work Graph 最小闭环:单向执行,先不做回退」,P4 才引入回退和 Loop-Validator。本文档不切两份,原因:**执行引擎的状态机一旦要支持「暂停/单步/停止/重试」这种可被外部随时打断和摆布的控制语义,「先做单向、再加回退」会让第一版状态机设计得过窄,P4 加回退时必须回来重改数据模型和主循环**。所以一次性把两个 phase 的数据模型和引擎设计完,落地仍分阶段交付(§五)。 + +v0.3 在此之上还合并了两块 27 号路线图里没单独立项的东西——控制台页面(R-4)和规划闭环(R-15.5)——理由是没有它们,P3 验收只能靠手写测试数据,验不到「这套东西真的能跑起来」。 --- ## 三、现状代码走查(决定设计边界的关键证据) -调研了仓库里两处最相关的既有实现,用于确定新引擎"该抄什么、不该抄什么"。 +### 3.1 DevLoop `runUnit()` / `runProject()`:状态机可抄,驱动方式不可抄 -### 3.1 DevLoop `runUnit()`:一个反面参照——展示了"为什么不能照抄" +`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 请求-响应周期里,外部在它跑完前完全没有插手机会**。 -> ⚠️ **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 同步快,不需并行)`),分波结构在、并行没做。 +> 补充观察:`runProject` 的波内**实际是串行**的(行 609 注释「串行跑(mock 同步快,不需并行)」),分波结构在、并行没做。这也印证了 §一非目标里「fan-out 并行是独立工作量」。 -`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=` 直接 `await loopController.runUnit(...)` 再吐响应——**这意味着整个 retry 循环(最多 10 次 agent 调用)全部塞在一次 HTTP 请求-响应周期里,外部在它跑完之前完全没有插手的机会**。这套模型没有任何"暂停"/"单步"/"停止"的概念,`runUnit()` 一旦被调用,只能等它跑完或者进程崩溃。 +配套的 `unit-registry.js` 状态机(`TRANSITIONS`,行 30-37)设计得很干净:显式转换表 + 非法转换报错 + 每次合法转换 emit 事件。**这部分直接借鉴**。 -配套的 `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()` 的"一次函数调用跑到底"模型**。 +**结论**:状态机照抄 `unit-registry.js` 的模式,主循环**绝不能**照抄「一次函数调用跑到底」。 ### 3.2 Pipeline Service:正确的参照模型 -`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[]`。 +`pipeline-service.js` 的 `startRun()`(144-181 行)只做状态机初始化,**不同步执行任何 agent**。推进靠外部显式调用 `advanceStep(runId, stepIndex, {output, status})`。`rejectStep()` 是现成的「退回重做」语义。 + +**这正是本文档需要的执行模型**:状态是持久化数据,「推进一步」是一次显式、幂等、外部可随时调用的动作,不是占着调用栈不放的循环。手动模式下「下一步」= 用户点一次调一次;自动模式下 = 引擎自己在上一次返回后立刻再调一次。**手动和自动共用同一个原语,区别只在谁触发下一次调用**。 + +**两处不能照抄的坑**: -**这正是本文档需要的执行模型**:状态是持久化数据,"推进一步"是一次显式、幂等、外部可随时调用的动作,不是一个占着调用栈不放的循环。手动模式下"下一步"按钮 = 用户点一次调一次 `advance`;自动模式下,引擎自己在每次 `advance` 成功返回后,检查"还要不要继续",是就立刻自己再调一次 `advance`——**手动和自动共用同一个 `advance` 原语,区别只在于"谁来触发下一次调用"**(§四.C 详细展开)。 +1. `CompanyDashboard.vue:1019` 的 `loadPipelineDetail()` 请求的是流水线**定义**,但模板里 `:class="{'step-current': selectedPipeline.currentStep === idx}"`(行 258)引用的 `currentStep` 只存在于 run 实例上——这个高亮效果**从未生效过**。→ 本设计的定义层与运行时层必须是两个各自完整、明确分开的对象(§四.A)。 +2. `retryPipelineRun()`(行 1034)调 `POST .../advance` 但没传 `stepIndex`,而路由端有 `if (stepIndex === undefined) return 400`——这个「重试」按钮点了必定 400。→ 「重试」必须是独立、参数自解释的端点(§四.C)。 -**两处不能照抄的坑**(写新代码时要主动避开): +### 3.3 SSE 基础设施:可直接复用 -1. `CompanyDashboard.vue:1019-1024` 的 `loadPipelineDetail()` 请求的是流水线**定义**,不是某次 run 的**实例**状态,但模板里 `:class="{'step-current': selectedPipeline.currentStep === idx}"`(行 258)引用的 `currentStep` 只存在于 run 实例对象上,定义对象里根本没有——这个"当前步骤高亮"视觉效果实际上从未生效过。新页面的数据契约必须让"图的静态定义"和"某次执行的运行时状态"是两个明确分开、各自完整的对象,不能像这样把运行时字段错绑到定义对象上。 -2. `retryPipelineRun()`(CompanyDashboard.vue:1034-1040)调用 `POST .../advance` 但完全没传 `stepIndex`,而路由端有 `if (stepIndex === undefined) return res.status(400)` 的校验——现状这个"重试"按钮点了必定 400。新设计的"重试"必须是一个**独立、参数自解释的端点**(§四.C),不能依赖调用方正确拼出一个复用端点的隐藏参数组合。 +`server/services/devloop/sse-broker.js`:每项目一个广播通道,`subscribe(pid, res, {replayFromOffset})` 支持 `Last-Event-ID` 断线重放,`emit()` 同时落盘 + 广播,25s 心跳。按 `run_id` 复用同一套实现。 -### 3.3 SSE 基础设施:可以直接复用 +**既有缺口不要重复**:DevLoop 里 `timeline-logger.js` 的审计事件和 `sse-broker.js` 的实时推送是两条独立的轨,没有自动桥接。本引擎要求**状态转换、timeline 写入、SSE 广播三者在同一次原子操作里一起发生**。 -`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),不重复这个缺口。 +### 3.4 子流程展示:`AgentChat.vue` 的双轨折叠卡片,直接复用 -### 3.4 子流程展示:`AgentChat.vue` 的双轨折叠卡片模式,直接复用 +第四列 "ACP Events" 面板已实现:`subAgents`(以 `callId` 为 key 的响应式 map)承载**进行中**的子调用,靠流式事件持续追加;`subLogList` 是**已完成**的历史日志卡片列表,外观一致但无 `LIVE` 徽标。这套「live map + history list」双轨模型正是节点运行日志需要的东西。 -`web/src/views/AgentChat.vue` 第四列 "ACP Events" 面板已经实现了本文档 §四.B 需要的下钻 UI 模式: +### 3.5 前端 DAG:`UnitsDAG.vue` 已实现,扩展而非重写 -- `subAgents`(以 `callId` 为 key 的响应式 map)承载**正在跑**的子调用,每项 `{agent, status:'running'|'success'|'error', content, collapsed}`,靠流式事件(`chunk`/`start`/`done`)持续追加内容、更新终态;卡片 header 是状态 emoji + agent 名 + `LIVE` 徽标 + 折叠箭头,展开后是等宽字体滚动日志区。 -- `subLogList` 是**已完成/历史**的日志卡片列表,外观一致但没有 `LIVE` 徽标。 +`web/src/components/devloop/UnitsDAG.vue`(107 行)已实现 SVG + `` 箭头 + **Kahn 分波拓扑布局**(`waves` computed,带 safety 上限防死循环)+ 按 status 着色 + 点击 emit 下钻。同目录 `LiveAgentStream.vue`(带 Last-Event-ID 重连的 SSE 消费)、`UnitDrawer.vue`(节点详情抽屉)同样可复用。**真正的新增工作量只剩回退边的曲线路由**。 -这套"live map(进行中)+ history list(已完成)"的双轨模型,跟本文档"节点运行日志"需要的东西完全对应,直接搬过来用,不重新发明。 +### 3.6 进程级取消:机制存在,调用链需实现前二次确认 -### 3.5 进程级取消能力:已经存在,但调用链需要在实现时二次确认 +`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 的实现前置核实项,不在设计阶段假设死。 -`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 的实现前置核实项,不在设计阶段假设死。 +### 3.7 P2 已落地的图校验,直接复用 + +`server/services/org-graph-service.js`(469 行)已实现四项写入前校验:`acyclic`(三色 DFS,仅针对 forward 边)、`single-entry`(forward 意义下入度为 0 的节点恰好一个)、`reachable`(孤立节点拒绝)、`backward-labeled`(构成回路的边必须标 `backward`,且不构成回路的边不许标 `backward`——**双向复核,不信任标注**)。 + +**work graph 的写入校验直接复用这一套**,不另写。这意味着 work graph 也**强制单入口**(R-13.4 原本只对 org graph 定案,本文档统一到两层)。 + +**需新增第五项 `fail-fallback-edge`**(R-16,见 §四.D.2.2):任何有 `fail` 出边的节点,必须至少留一条 `fail_reason IS NULL` 的兜底边。这一项 P2 现有实现里没有,两层都要加。 --- @@ -87,103 +111,214 @@ ### A. Model(数据模型) -#### A.1 表结构(在 P0/P2 的 `graph.sqlite` 上追加,复用同一个 DB 文件) +#### A.1 分层:定义层 ≠ 运行时层 + +这是 v0.3 相对 v0.1 最大的改动,直接来自 R-15.4。 + +| | org graph | work graph | +|---|---|---| +| 生成者 | `orggraph-planning` | `workgraph-planning` | +| 生成时机 | 建公司时,一次 | 每个 org node 接到任务时 | +| 生命周期 | **长期**,活整个公司生命周期 | **一次性**,规划完即执行 | +| 一份定义被跑几次 | **多次**(每来一个任务跑一遍) | 一次 | + +因为 org graph 是「一份定义、多次执行」,运行时状态**不能**存在定义表上(否则一家公司永远只能跑一个任务)。所以: + +``` +定义层(两套,各自保留,形态同构) + org_graph_nodes / org_graph_edges ← P2 已落地,本文档只读不改(除 T-P3 加一列) + work_graph_nodes / work_graph_edges ← 本文档新增 + +运行时层(一套,两层共用,引擎只认这个) + graph_runs ← 一次执行实例 + graph_run_nodes ← 该次执行里每个节点的状态 + graph_run_loop_records ← 回退边的循环计数与判词 +``` + +`work_graphs` 表在 v0.1 里混装了图的定义元数据和运行时状态(`run_status` / `pause_requested` / `execution_mode`),本版把运行时那半拆去 `graph_runs`。 + +#### A.2 表结构(追加在 P0/P2 的 `graph.sqlite` 上,复用同一 DB 文件) ```sql --- 一次具体的 Work Graph 执行实例(图的定义和运行时状态在同一行—— --- 与 §3.2 指出的"定义 vs 实例分离"原则不矛盾:Work Graph 本来就是"规划完即执行、 --- 执行完即归档"的一次性产物(27 号文档 §二术语定义),不像 Pipeline 那样"定义可以脱离某次 --- run 单独被复用",所以这里定义即实例,没有必要拆两张表) +-- ── 定义层:work graph(org graph 的两张表 P2 已建) ────────────── + CREATE TABLE IF NOT EXISTS work_graphs ( - id TEXT PRIMARY KEY, -- wg_ - company_id TEXT NOT NULL, - triggered_by_org_node_id INTEGER REFERENCES org_graph_nodes(id), -- 对应"每个 org node 运行的 work graph" - parent_node_id TEXT REFERENCES work_graph_nodes(id), -- 非空 = 这是另一个节点下钻出来的嵌套子图(§四.D.3) - goal_id TEXT, -- 一次人类需求描述(唯一,验收边界) - okr_node_id TEXT, -- 形如 O-001.1.1 - task_id TEXT, -- 形如 T-xxx - execution_mode TEXT NOT NULL DEFAULT 'manual' CHECK (execution_mode IN ('manual','auto')), - run_status TEXT NOT NULL DEFAULT 'planning', - -- planning | ready | running | paused | blocked | completed | failed | stopped - pause_requested INTEGER NOT NULL DEFAULT 0, -- 0/1,auto 模式下"暂停"先置位,下一个节点边界生效 - stop_requested INTEGER NOT NULL DEFAULT 0, -- 0/1,同上,"停止"的软信号 - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, -- wg_ + company_id TEXT NOT NULL, + owner_org_node_id INTEGER REFERENCES org_graph_nodes(id), -- 这张图属于哪个 org node + planned_by TEXT NOT NULL DEFAULT 'workgraph-planning', + goal_id TEXT, -- 一次人类需求描述(唯一,验收边界) + task_id TEXT, -- 执行单元,一个 goal 多个 + okr_node_id TEXT, -- 形如 O-001.1.1,长周期度量归属 + created_at TEXT NOT NULL ); +-- goal_id / task_id / okr_node_id 三者基数不同、不可混用,见 27-NAMING §四。 +-- v0.1 的单字段 goal_task_id 不够用,已拆开。 CREATE TABLE IF NOT EXISTS work_graph_nodes ( - id TEXT PRIMARY KEY, -- wgn_ - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), - system_agent_id TEXT NOT NULL REFERENCES system_agents(id), - node_kind TEXT NOT NULL DEFAULT 'task' CHECK (node_kind IN ('task','loop_validator')), - status TEXT NOT NULL DEFAULT 'pending', - -- pending | ready | running | success | fail | blocked | skipped - attempt INTEGER NOT NULL DEFAULT 0, - agent_run_id TEXT, -- 本次 attempt 对应的 RunRegistry runId,用于"停止"时定位要 cancel 哪个子进程(§三.5) - has_subgraph INTEGER NOT NULL DEFAULT 0, -- 0/1,非 0 时应存在一行 work_graphs.parent_node_id = 本节点 id - output_ref TEXT, -- 产出物相对路径,见 A.2 路径约定,不是 DB 存内容本身 - started_at TEXT, - completed_at TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, -- wgn_ + work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), + node_name TEXT NOT NULL, -- 图内唯一,如 org-coder-validator + system_agent_id TEXT NOT NULL REFERENCES system_agents(id), -- 引用的本体,如 coder-validator + node_kind TEXT NOT NULL DEFAULT 'agent_call' + CHECK (node_kind IN ('agent_call','subgraph')), + created_at TEXT NOT NULL ); +-- node_name ≠ system_agent_id:节点名带领域前缀、本体名不带(R-15.3)。 +-- 同一本体可被多个节点、多家公司引用,与 P1 虚拟引用寻址一致。 CREATE TABLE IF NOT EXISTS work_graph_edges ( + id TEXT PRIMARY KEY, + work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), + from_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), + to_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), + condition TEXT NOT NULL CHECK (condition IN ('success','fail')), + fail_reason TEXT, -- 可空;condition='fail' 时的细分路由键,见 §四.D.2.2 + edge_kind TEXT NOT NULL DEFAULT 'forward' + CHECK (edge_kind IN ('forward','backward')), + created_at TEXT NOT NULL +); + +-- ── 运行时层:两层共用 ────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS graph_runs ( + id TEXT PRIMARY KEY, -- run_ + graph_kind TEXT NOT NULL CHECK (graph_kind IN ('org','work')), + graph_id TEXT NOT NULL, -- kind='org' → company_id;kind='work' → work_graphs.id + company_id TEXT NOT NULL, + root_run_id TEXT NOT NULL, -- 自身为 root 时 = 自己的 id;冗余存储,避免每次上溯 + parent_run_id TEXT REFERENCES graph_runs(id), + parent_node_run_id TEXT REFERENCES graph_run_nodes(id), -- 是哪个节点下钻出来的 + goal_id TEXT, + task_id TEXT, + okr_node_id TEXT, + execution_mode TEXT NOT NULL DEFAULT 'manual' + CHECK (execution_mode IN ('manual','auto')), + run_status TEXT NOT NULL DEFAULT 'planning', + -- planning | ready | running | paused | blocked | completed | failed | stopped + pause_requested INTEGER NOT NULL DEFAULT 0, -- 软信号,下一个节点边界生效 + stop_requested INTEGER NOT NULL DEFAULT 0, + backward_count INTEGER NOT NULL DEFAULT 0, -- run 级总回退次数,双层熔断的外层,见 §四.D.2.1 + budget_json TEXT, -- 仅 root run 非空,见 §四.A.3 + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +-- v1 约束:同一 company_id 至多一个 run_status ∈ {ready,running,paused} 的 root run(§一非目标) +CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_runs_one_active_root + ON graph_runs(company_id) + WHERE parent_run_id IS NULL AND run_status IN ('ready','running','paused'); + +CREATE TABLE IF NOT EXISTS graph_run_nodes ( + id TEXT PRIMARY KEY, -- rn_ + run_id TEXT NOT NULL REFERENCES graph_runs(id), + def_node_id TEXT NOT NULL, -- 按所属 run 的 graph_kind 解释: + -- 'org' → org_graph_nodes.id(整数转字符串) + -- 'work' → work_graph_nodes.id + status TEXT NOT NULL DEFAULT 'pending', + -- pending | ready | running | success | fail | blocked | skipped + attempt INTEGER NOT NULL DEFAULT 0, + agent_run_id TEXT, -- node_kind='agent_call' 时的 RunRegistry runId + child_run_id TEXT REFERENCES graph_runs(id), -- node_kind='subgraph' 时本次 attempt 起的子 run + output_ref TEXT, -- 产出物相对路径,见 §四.A.5 + fail_reason TEXT, -- 本次失败的细分原因,供边路由,见 §四.D.2.2 + started_at TEXT, + completed_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_run_nodes_unique + ON graph_run_nodes(run_id, def_node_id); + +CREATE TABLE IF NOT EXISTS graph_run_loop_records ( id TEXT PRIMARY KEY, - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), - from_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), - to_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), - condition TEXT NOT NULL CHECK (condition IN ('success','fail')), - edge_kind TEXT NOT NULL DEFAULT 'forward' CHECK (edge_kind IN ('forward','backward')), + run_id TEXT NOT NULL REFERENCES graph_runs(id), + edge_id TEXT NOT NULL, -- 触发回退的那条 backward 边(org/work 边表按 kind 解释) + from_node_run_id TEXT NOT NULL REFERENCES graph_run_nodes(id), + to_node_run_id TEXT NOT NULL REFERENCES graph_run_nodes(id), + loop_count INTEGER NOT NULL, -- 这是第几次沿这条边回退(1-based) + verdict TEXT NOT NULL + CHECK (verdict IN ('registered','fixed','not_fixed','force_terminated')), + remediation_note TEXT, -- 「应该如何修复」,供下一轮读取 created_at TEXT NOT NULL ); +``` --- Loop-Validator 每次"备案-审计-修复方案传递校验"的记录(27 号文档 §3.5 三步), --- 是"循环划线"上显示"第 N/3 次、上次判断"这类信息的数据来源,不是从 work_graph_nodes.attempt 反推 -CREATE TABLE IF NOT EXISTS work_graph_loop_records ( - id TEXT PRIMARY KEY, - work_graph_id TEXT NOT NULL REFERENCES work_graphs(id), - loop_validator_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), - target_node_id TEXT NOT NULL REFERENCES work_graph_nodes(id), -- 回退目标节点 - loop_count INTEGER NOT NULL, -- 这是第几次回退(1-based) - verdict TEXT NOT NULL CHECK (verdict IN ('registered','fixed','not_fixed','force_terminated')), - remediation_note TEXT, -- "应该如何修复"的方案文本,供下一轮读取 - created_at TEXT NOT NULL -); +> ⚠️ **一处已知的类型不整齐**:`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 主键。 + +#### A.3 任务级总预算:挂 root run + +[27-E2E](./27-e2e-task-execution-flow.md) §7.1 指出的最严重问题:三级恢复(跳回节点 → 重新 planning → 迭代 agent)如果每级都重置下一级的计数器,整个循环没有终止条件。分层预算(`retry_budget` / `l2_budget`)只是**局部快速失败**信号,不是兜底。 + +有了 run 层,兜底的挂载点很自然——**`budget_json` 只在 root run 上非空,所有子 run 通过 `root_run_id` 扣同一个池**: + +```jsonc +{ + "total_agent_calls": 100, // 所有层级所有 agent 调用的总次数 + "total_tokens": 5000000, + "wallclock_ms": 7200000, // 2h + "spent": { "agent_calls": 0, "tokens": 0, "started_at": "..." } +} ``` -节点/边的**内容**(任务描述、日志、上下文)依旧不进 DB——沿用 27 号文档 §四已经定下的原则:`output_ref` 只存相对路径,指向 System Agent 目录下按 §3.2 已定义的物理位置:`system/agents/{agentId}/companies/{companyId}/history/{workGraphId}-{nodeId}-{attempt}.md`,本文档不新造一套路径约定,直接复用。 +**只增不减**。任何一层发起 agent 调用前先按 `root_run_id` 检查余额;扣完不是失败,是**整棵 run 树挂起等人工**(root run → `blocked`,向下级联 `pause_requested=1`)。 -#### A.2 状态机(照抄 `unit-registry.js` 的转换表模式,不照抄它的驱动方式) +按 `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)。 + +#### A.4 状态机 ``` -work_graph_nodes.status 转换表: - pending → ready (上游全部满足 / 无上游依赖,进入可执行队列) - ready → running (被 advance 拾取,开始调用 agent) - running → success | fail (agent 调用结束) - fail → ready (loop_validator 回退命中) | blocked (无回退边/回退耗尽) | ready (人工点"重试") - success → (终态,除非人工"重试"强制打回 ready) - blocked → ready (人工点"重试",唯一走出 blocked 的路) - * → skipped (run 被"停止"时,所有仍处于 pending/ready 的节点) +graph_run_nodes.status: + pending → ready (上游全部满足 / 无上游依赖) + ready → running (被 advanceOne 拾取) + running → success | fail (agent 调用结束 / 子 run 收敛) + fail → ready (命中一条 backward 边且未超硬顶;或人工「重试」) + → blocked (无可走的 fail 边 / 回退硬顶耗尽) + success → (终态,除非人工「重试」强制打回 ready) + blocked → ready (人工「重试」,唯一走出 blocked 的路) + * → skipped (run 被「停止」时,所有仍处 pending/ready 的节点) + +graph_runs.run_status: + planning → ready (图规划完成 + 通过 §三.7 四项校验) + ready → running (「启动」) + running → paused (「暂停」软信号,下一个节点边界生效) + paused → running (「启动」或「下一步」恢复) + running → blocked (某节点 blocked 且无其他节点在 ready/running) + blocked → running (对 blocked 节点「重试」后) + running/paused/blocked → stopped (「停止」) + running → completed (所有可达节点到达终态,且无 pending/ready 悬空) ``` -非法转换(比如对一个 `success` 节点直接调"下一步"执行它)在服务层直接拒绝并返回明确错误,模式与 `unit-registry.js` 的 `InvalidTransitionError` 一致。 +非法转换(如对 `success` 节点直接「下一步」)在服务层拒绝并返回明确错误,模式与 `unit-registry.js` 的 `InvalidTransitionError` 一致。 + +#### A.5 产物布局与跨节点传递 + +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**。 ``` -work_graphs.run_status 转换表: - planning → ready (图规划完成 + 通过 27 号文档 §3.4 第 5 点的结构校验) - ready → running (点"启动") - running → paused (auto 模式下点"暂停",软信号,下一个节点边界生效) - paused → running (点"启动"或"下一步"恢复) - running → blocked (某节点进入 blocked 且不是被 stop 打断) - blocked → running (对 blocked 节点点"重试"后) - running/paused/blocked → stopped (点"停止") - running → completed (所有可达节点都到达终态,且没有 pending/ready 的悬空节点) +data/companies/{cid}/runs/{rootRunId}/ +├── artifacts/ # 任务级共享产物区(root run 级,跨 org node 可读) +│ ├── goal-benchmark.json # 验收契约,见阶段 0 +│ ├── goal-test-data/ +│ └── goal-test-script/ +└── nodes/{nodeRunId}/ # 节点私有产出,output_ref 指向这里 + └── attempt-{n}.md ``` -#### A.3 幂等与并发 +**读写规则(照搬 `CONVENTIONS.md` §12 的隔离思路)**: + +| | 读 | 写 | +|---|---|---| +| 自己的 `nodes/{自己}/` | ✅ | ✅ | +| 别的节点的 `nodes/{其他}/` | ❌ 禁止 | ❌ | +| `artifacts/`(按 `root_run_id` 上溯) | ✅ 任意层级只读 | ⚠️ 需节点在定义里显式声明产出权 | + +「下游只能看到上游的契约,看不到实现」这条 DevLoop 已验证的原则原样继承。写 `artifacts/` 是特权动作——只有被 `workgraph-planning` 标记为验收契约产出者的节点才有,防止任意节点往共享区乱写。 + +节点私有产出的物理位置仍沿用 27 号文档 §四已定的 System Agent 目录约定,本文档不新造路径。 + +#### A.6 幂等与并发 -`advance` 类操作(§四.C)必须幂等:同一节点被并发触发两次"下一步"(比如自动驱动器和人工同时点),第二次调用检测到节点 `status` 已经不是 `ready` 就直接返回当前状态而不是报错——这是 §一非目标里"不做复杂协作、但要做到不冲突"的具体实现方式,不需要引入分布式锁,SQLite 的行级事务 + `status` 字段的 `WHERE status='ready'` 条件更新(受影响行数=0 即视为"已经被别人拿走了")就够。 +`advanceOne` 必须幂等:同一节点被并发触发两次(自动驱动器和人工同时点),第二次检测到 `status` 已不是 `ready` 就返回当前状态而不报错。不引入分布式锁——SQLite 行级事务 + `UPDATE ... WHERE status='ready'` 条件更新(受影响行数 = 0 即「已被别人拿走」)就够。 --- @@ -191,139 +326,289 @@ work_graphs.run_status 转换表: #### B.1 页面结构 -新页面(新路由,不挂在 `CompanyDashboard.vue` 已有的 tab 体系下——这个页面信息密度和交互复杂度明显超过一个 tab 该承载的量,参照仓库里 `PipelineEditor.vue` 独立成页的先例): +新页面、新路由,不挂在 `CompanyDashboard.vue` 已有的 tab 体系下(信息密度和交互复杂度明显超过一个 tab 该承载的量,参照 `PipelineEditor.vue` 独立成页的先例): ``` -┌─────────────────────────────────────────────────────────────────┐ -│ 组织与执行 │ -├───────────────┬─────────────────────────────────┬─────────────────┤ -│ Org Graph │ Work Graph 画布 │ 节点详情面板 │ -│ (左栏,复用 │ (选中的 org node 当前/历史 │ (点某个 work │ -│ 27-P2 §4.7 │ Work Graph 的 DAG 可视化, │ graph node 后 │ -│ 分层列表) │ 见 B.2) │ 展开,见 B.3) │ -│ │ │ │ -│ [CTO] │ 控制栏: ▶启动 ⏭下一步 ⏸暂停 │ 状态/耗时/attempt │ -│ └ [HR] │ ⏹停止 🔁重试 │ [概览|输入输出| │ -│ └ [PM] │ │ 运行日志|子图] │ -│ │ ●pending ●running ●success │ │ -│ │ ●fail ●blocked ●skipped │ │ -│ │ ┄┄▶ 循环回退边 │ │ -└───────────────┴─────────────────────────────────┴─────────────────┘ +┌──────────────────────────────────────────────────────────────────┐ +│ 组织与执行 预算 42/100 calls · $6.2 · 38min │ +├───────────────┬──────────────────────────────┬───────────────────┤ +│ Org Graph │ Work Graph 画布 │ 节点详情面板 │ +│ (同一个画布 │ (选中 org node 的 work │ (点某个 work │ +│ 组件,只是 │ graph,见 B.2) │ graph node 后 │ +│ graph_kind= │ │ 展开,见 B.3) │ +│ 'org') │ 控制栏: ▶启动 ⏭下一步 │ │ +│ │ ⏸暂停 ⏹停止 🔁重试 │ 状态/耗时/attempt │ +│ org-planner │ │ [概览|输入输出| │ +│ ↓ │ ●pending ●running ●success │ 运行日志|子图] │ +│ org-coder │ ●fail ●blocked ●skipped │ │ +│ ↓ │ ┄┄▶ 回退边 2/3 │ │ +│ org-validator │ │ │ +└───────────────┴──────────────────────────────┴───────────────────┘ + 面包屑:公司 › org-coder › (子图) ``` -左栏点击某个 org node:右侧主画布加载该节点关联的 Work Graph 列表(一个 org node 生命周期里可能触发过多张 Work Graph,每张对应一次 goal/task,默认显示最新一张,提供下拉切换历史)。 +左栏和中栏是**同一个画布组件的两个实例**——因为两层图同构(R-9.1),这是 R-15.4 引擎泛化在前端的对应收益。左栏点击某个 org node → 中栏加载该节点的 work graph(一个 org node 生命周期里可能跑过多张,默认显示最新,下拉切换历史)。 -#### B.2 Work Graph 画布 +#### B.2 画布 -- **节点**:矩形卡片,标题是 `system_agent_id`(如 `coder`),右上角小圆点按 `status` 着色(沿用仓库既有色彩语义:绿=success、蓝=running、灰=pending/ready、红=fail、橙=blocked、浅灰斜纹=skipped)。`loop_validator` 节点用菱形(区别于普通 `task` 节点的矩形),视觉上直接点名"这是一个决策节点,不是执行节点"。 -- **边**:`edge_kind='forward'` 是实线箭头;`edge_kind='backward'` 是**虚线曲线箭头**,颜色区别于前进边(比如统一用琥珀色),且在曲线中点位置渲染一个小徽标,内容取自 `work_graph_loop_records` 里该 `(loop_validator_node_id, target_node_id)` 组合最新一条记录的 `loop_count`,格式 `2/3`——这直接对应用户提出的"loop 的划线"需求:不是随便画一条回退线,是带着"当前循环到第几次、硬顶是几"这个上下文的线。 -- **布局**:⚠️ **v0.2 修正——不要从零写**。`web/src/components/devloop/UnitsDAG.vue`(107 行)已经实现了完全符合这里需求的方案:SVG + `` 箭头 + **Kahn 分波拓扑布局**(`waves` computed,按 `depends_on` 逐波剥离,带 safety 上限防死循环)+ 按 `status` 着色的节点矩形 + 点击 emit 下钻。P3/P4 应当扩展它(加 `loop_validator` 菱形节点、`edge_kind='backward'` 的虚线曲线、循环徽标),而不是平行实现一个。仍然不引入图形库依赖——既有实现已经证明这个规模的图手写 SVG 足够。真正的新增工作量只剩**回退边的曲线路由**(避免与其他节点重叠),这一块是新的。 -- **主流程 vs 异常流程的视觉区分**:正常的 `success` 前进边和节点是画布里的"主干";任何和 `fail` 条件相关的边、`blocked` 状态的节点、`loop_validator` 节点及其回退边,整体降低主干部分的视觉权重(比如统一给异常相关元素加一层浅色背景框),让人一眼能分清"这张图正常应该怎么走"和"这次实际上因为哪里失败绕了远路"。 +- **节点**:矩形卡片,标题是 `node_name`,右上角小圆点按 `status` 着色(沿用仓库既有色彩语义:绿=success、蓝=running、灰=pending/ready、红=fail、橙=blocked、浅灰斜纹=skipped)。`node_kind='subgraph'` 的节点右下角带一个「⊞」角标,提示可下钻。 +- **边**:`forward` 是实线箭头;`backward` 是**虚线曲线箭头**,琥珀色,曲线中点渲染徽标,内容取自 `graph_run_loop_records` 里该 `edge_id` 最新一条记录的 `loop_count`,格式 `2/3`——不是随便画一条回退线,是带着「当前第几次、硬顶是几」上下文的线。 +- **布局**:**扩展 `UnitsDAG.vue`**(§3.5),不从零写。新增工作量只有三块:`node_kind='subgraph'` 角标、`backward` 边的虚线曲线路由(避开其他节点,这块是真新的)、循环徽标。不引入图形库——既有实现已证明这个规模手写 SVG 足够。 +- **主流程 vs 异常流程**:`success` 前进边和节点是画布主干;所有和 `fail` 相关的边、`blocked` 节点、回退边整体**降低视觉权重**(统一加一层浅色背景框),让人一眼分清「这张图正常该怎么走」和「这次因为哪里失败绕了远路」。 #### B.3 节点详情面板(四个 tab) -1. **概览**:`status`、`attempt`/最大重试预算、`started_at`/`completed_at`/耗时、如果是 `loop_validator` 触发的回退,展示 `work_graph_loop_records` 里这次相关的完整记录(`verdict`/`remediation_note`)。 -2. **输入输出**:读 `output_ref` 指向的 markdown/JSON 文件渲染展示("边的输入输出"需求的落地点——一条边的"输出"就是它 `from_node` 的 `output_ref` 内容,"输入"就是 `to_node` 实际执行时读取的上下文,两者在**大多数正向边**上是同一份内容,只有经过 Loop-Validator 回退、`_buildRetryContext()` 类似逻辑拼接了"上次失败摘要"的场景下才会不同——这个 tab 需要能同时展示"原始产出"和"实际被下游消费时是否被加工过"两种视图,不能假设两者总是相同)。 -3. **运行日志**:复用 §3.4 的 `AgentChat.vue` 双轨折叠卡片模式——节点正在 `running` 时走 live 面板(流式追加),`success`/`fail` 之后切换成历史日志视图。这一层展示的是**这次 agent 调用内部**的对话/工具调用记录(用户确认的下钻语义①)。 -4. **子图**:仅当 `has_subgraph=1` 时出现这个 tab,内容是**递归复用同一个 Work Graph 画布组件**,指向 `work_graphs.parent_node_id = 本节点id` 的那张子图,画布上方有面包屑(`公司 › CTO 组织 › PM 的任务 › 子任务`)支持逐层下钻和返回(用户确认的下钻语义②)。 +1. **概览**:`status`、`attempt`、`started_at`/`completed_at`/耗时、`fail_reason`;如果这个节点参与过回退,展示 `graph_run_loop_records` 里相关的完整记录(`loop_count` / `verdict` / `remediation_note`)。 +2. **输入输出**:渲染 `output_ref` 指向的文件。**「原始产出」和「实际被下游消费时的形态」要能分别展示**——大多数正向边上两者相同,但经过回退、拼接了「上一轮 remediation_note」之后就不同了,不能假设总是相同。若该节点读过 `artifacts/`,一并列出读了哪些。 +3. **运行日志**:复用 §3.4 的双轨折叠卡片——`running` 时走 live 面板流式追加,`success`/`fail` 后切换成历史视图。展示的是**这次 agent 调用内部**的对话/工具调用记录。 +4. **子图**:仅当 `node_kind='subgraph'` 且 `child_run_id` 非空时出现,内容是**递归复用同一个画布组件**指向 `child_run_id`,上方面包屑支持逐层下钻和返回。 -#### B.4 存量/降级展示 +#### B.4 预算条 -一个 org node 从未触发过 Work Graph(P3 上线初期,或者这个角色本身不承担会触发 Work Graph 的任务):右侧主画布显示空态提示,不报错、不留白页面。 +页头常驻显示 root run 的预算消耗(calls / tokens / 墙钟)。**自动模式点「启动」前弹一次预估成本确认**——按 §四.A.3 的量级,这不是可选的礼貌,是必要的。 + +#### B.5 空态 + +一个 org node 从未跑过 work graph:中栏显示空态提示,不报错、不留白页面。 --- ### C. Control(控制操作) -| 操作 | 可用条件(`run_status` / `execution_mode`) | 引擎侧动作 | 对"正在跑的节点"的影响 | +| 操作 | 可用条件 | 引擎侧动作 | 对正在跑的节点的影响 | |---|---|---|---| -| **启动** | `run_status ∈ {ready, paused, blocked}` | `ready→running`;若 `execution_mode='auto'` 立即调用一次内部 `advance()` 并让其在每次成功后自我调度下一次(见下);若 `manual`,只是把 run 标成 running,停在原地等"下一步" | 无(这个操作只在没有节点在跑的时候可点) | -| **下一步** | `run_status='running'` 且 `execution_mode='manual'`(或 `auto` 但已 `paused`——暂停后允许人工手动单步推进) | 拾取一个当前 `status='ready'` 的节点(如果有多个并行 ready 的节点,一次"下一步"只推进**一个**,需要在 UI 上明确标出"接下来要跑哪个",避免用户以为点一下会推进所有分支),执行完(`success`/`fail`)后评估出边,把满足条件的下游节点置 `ready`,**不继续往下跑**,停在这里等下一次点击 | 会真正发起一次 agent 调用,不可中途取消(§三.5 的取消能力用在"停止"上,不是给"下一步"配的) | -| **暂停** | `run_status='running'` 且 `execution_mode='auto'` | 置 `pause_requested=1`。**不打断当前正在 `running` 的节点**(agent 调用一旦发起就让它跑完,理由见下方说明),当前节点完成、准备自动调度下一个节点之前检查这个标志位,若为真则停止自我调度,`run_status→paused` | 无——这是"软暂停",生效点是下一个节点的调度前,不是立即打断 | -| **停止** | `run_status ∈ {running, paused, blocked}` | 置 `stop_requested=1`;所有仍处于 `pending`/`ready` 的节点批量置 `skipped`;若此刻有节点正 `running`,尝试通过 §三.5 的机制硬取消对应子进程(**取消能力需要 §六 T5 在实现前二次确认调用链,设计上按"能取消"设计,实现时如果确认做不到就退化成"等它跑完、记录结果、但不再调度后续节点"**);`run_status→stopped` | 尝试硬取消,退化路径是"软停止" | -| **重试** | 节点 `status ∈ {fail, blocked}` | 把该节点 `status` 打回 `ready`,`attempt+1`;**默认只重置这一个节点,不级联重置下游**——因为下游节点在这次重试产生新结果之前根本还没执行到(`fail`/`blocked` 节点的下游按定义还是 `pending`),除非是"节点曾经 success 过、后来因为上游数据变化想强制重跑"这种场景才需要级联,这种场景本版本不支持(见 §八.3) | 无(`fail`/`blocked` 节点本身已经不在 `running`) | +| **启动** | `run_status ∈ {ready, paused, blocked}` | `→running`;`auto` 模式立即调一次 `advanceOne()` 并让它在每次成功后自我调度下一次;`manual` 模式只标 running,停在原地等「下一步」 | 无 | +| **下一步** | `run_status='running'` 且(`manual` 或 `auto` 已 `paused`) | 拾取**一个** `status='ready'` 的节点执行,完成后评估出边把满足条件的下游置 `ready`,**不继续往下跑**。多个并行 ready 时 UI 必须明确标出「接下来要跑哪个」,避免用户以为点一下推进所有分支 | 会真正发起一次 agent 调用,不可中途取消 | +| **暂停** | `run_status='running'` | 置 `pause_requested=1`,**并沿 `parent_run_id` 向下级联到所有 active 子 run**。不打断当前正在 `running` 的节点,在下一个节点的调度前检查标志位 | 软暂停,生效点是下一个节点边界 | +| **停止** | `run_status ∈ {running, paused, blocked}` | 置 `stop_requested=1` 并**向下级联**;所有仍 `pending`/`ready` 的节点批量 `skipped`;若有节点正 `running`,尝试通过 §3.6 的机制硬取消子进程;`→stopped` | 尝试硬取消,退化路径见下 | +| **重试** | 节点 `status ∈ {fail, blocked}` | 该节点 `→ready`,`attempt+1`。**默认不级联重置下游** | 无 | + +**三层级联**(R-15.4 / [27-E2E](./27-e2e-task-execution-flow.md) §7.5):v0.1 §八.4 把「子图不继承父图暂停状态」列为刻意简化,现在层级从两层变三层(root → org node → 嵌套子图),「点了暂停但子图还在偷偷跑」会成为真实困惑点。**定案:暂停/停止向下级联;「下一步」只作用于当前查看的层级**(符合直觉——暂停是全局意图,单步是局部操作)。`root_run_id` 冗余列让级联是一次索引查询,不用递归遍历。 + +**「重试」不级联下游的判据**(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 位),直接复用。 -**手动/自动的统一原语**:无论手动点"下一步"还是自动模式下引擎自己驱动,落到服务层都是同一个 `advanceOne(workGraphId)` 函数调用——区别只在于**谁在调用它、调用节奏由谁决定**:手动模式下调用方是收到用户点击的路由 handler,每次一下;自动模式下调用方是引擎自己的一个"节点完成回调",只要 `pause_requested`/`stop_requested` 都是假,就在上一次 `advanceOne` 的 promise resolve 之后立刻发起下一次。这个设计直接对应 §三.2 里"手动和自动共用同一个 advance 原语"的结论,也是本设计避免"两套并行的执行逻辑互相踩"的关键——**只有一套推进逻辑,自动模式只是一个不停调用它的循环,不是另一套实现**。 +**手动/自动的统一原语**:两种模式落到服务层都是同一个 `advanceOne(runId)`,区别只在**谁调用它、调用节奏由谁决定**。手动 = 收到用户点击的路由 handler,每次一下;自动 = 引擎自己的「节点完成回调」,只要 `pause_requested`/`stop_requested` 都是假就在上一次 resolve 后立刻发起下一次。**只有一套推进逻辑,自动模式只是一个不停调用它的循环,不是另一套实现**——这是避免两套并行执行逻辑互相踩的关键。 + +**「停止」的降级路径**:§3.6 的取消能力需要 T9 在实现前二次确认调用链。设计上按「能取消」写;若确认做不到干净取消,退化为「等它跑完、记录结果、但不再调度后续节点」。这不会导致设计推倒重来,只是用户点了停止还要再等一会。 --- -### D. Process(主流程 / 异常流程 / 子流程处理) +### D. Process(执行过程) + +#### D.0 两种 node executor + +v0.1 只设计了一种执行语义。org graph 加入后必须分派(R-15.4): + +| | `agent_call` | `subgraph` | +|---|---|---| +| 执行 = | 调 `AgentManager` 跑 `system_agent_id`,写 `agent_run_id` | 起一张子图并等它收敛 | +| success/fail 来源 | agent 调用结果 | 子 run `completed` → success;`blocked`/`failed` → fail | +| 耗时量级 | 秒~分钟 | 分钟~小时 | +| 谁用 | work graph 节点(默认) | **org graph 节点(全部)**;work graph 里 agent 自行判断需要拆子图的节点 | + +`subgraph` 节点的完整动作: + +``` +节点 → running + → 调 workgraph-planning(全局通用本体,R-15.2),输入 = 该 org node 的角色定义 + + 任务描述 + 可读的 artifacts/ + 上一轮 remediation_note(若有) + → 产出图 → 过 §三.7 四项校验 → 写 work_graphs / work_graph_nodes / work_graph_edges + → 建子 graph_run(parent_run_id / parent_node_run_id / root_run_id 三者填好, + execution_mode 默认继承父 run)→ 回填 child_run_id + → 递归执行子 run(同一个 advanceOne) + → 子 run 收敛 → 回写本节点 success/fail + fail_reason + remediation_note +``` + +父节点在子 run 跑完之前一直停在 `running`。这也让 §四.B.3 的「子图」tab 有了明确的数据来源。 + +**⚠️ 收敛时必须透传三个值,不只是 success/fail**(R-16)。原因是决定「失败后走哪条边」的**不是失败节点自己,而是它子图里的归因节点**(§四.D.2.2):`org-validator` 这个 org node 判定任务未达成时,是它 work graph 里的 `validator-reflection` 节点算出了根因,这个结论必须能穿透子 run 边界回到 org 层,否则 org 层选边时无据可依。 + +| 回写字段 | 来源 | 用途 | +|---|---|---| +| `success` / `fail` | 子 run 的 `run_status` | 父节点状态转换 | +| `fail_reason` | 子图归因节点的产出 | 父层 §四.D.2.2 选边 | +| `remediation_note` | 同上 | 随回退传给目标节点,成为它下一次 `workgraph-planning` 的输入 | #### D.1 主流程 ``` 节点进入 ready - → advanceOne 拾取 → running → 调 AgentManager 执行对应 system_agent - → 调用结束,产出写入 output_ref - → success:遍历该节点的出边中 condition='success' 的边,目标节点若所有前置条件(可能有多条入边, - 需要"任意一条满足即可"还是"全部满足才能进入 ready"——采用"全部满足"语义,与 27 号文档 §3.4 的 - 图规划约定一致,多入边节点代表"汇合点")满足,置 ready - → 所有节点都到达终态(success/fail 被消化成 blocked 或被路由掉/skipped)且没有 ready/pending 悬空 - → work_graphs.run_status = completed + → advanceOne 拾取 → running → 按 node_kind 分派执行(D.0) + → 产出写入 output_ref(特权节点可另写 artifacts/) + → success:遍历出边中 condition='success' 的边,目标节点若**所有**入边条件都满足则置 ready + (多入边 = 汇合点,采用「全部满足」语义,与 27 号文档 §3.4 的图规划约定一致) + → 所有节点到达终态且无 ready/pending 悬空 → run_status = completed + → 若本 run 有 parent_node_run_id,回写父节点 success ``` #### D.2 异常流程 ``` -节点 running → fail - → 遍历出边中 condition='fail' 的边 - ├─ 存在一条 → to_node: - │ ├─ to_node.node_kind='loop_validator'(27 号文档 §3.4 第 4 点的结构要求,非法图在规划阶段已被 - │ │ §3.4 第 5 点的执行前校验拦截,本引擎不需要再兜底校验合法性,只需要按图执行) - │ │ → 进入 Loop-Validator 流程(D.2.1) - │ └─ to_node 是普通 task 节点(无回退语义的显式失败分支,比如"失败就转人工审核节点") - │ → 正常按主流程把 to_node 置 ready - └─ 不存在 fail 边(死路)→ 该节点 status='blocked',run_status 视其余节点情况可能同步变 - 'blocked'(如果没有其他节点还在 ready/running)——这是需要人工点"重试"或人工介入的终止态, - 不是系统自动判定"整个 run 失败"(区别于"停止"是人工主动终止,"blocked" 是系统探测到走不下去) +节点 running → fail(附带 fail_reason) + → 遍历出边中 condition='fail' 的边,按 D.2.2 选一条 + ├─ 选中边 edge_kind='forward' → 普通失败分支(如「失败转人工审核节点」) + │ → 按主流程把 to_node 置 ready + ├─ 选中边 edge_kind='backward' → 回退,进 D.2.1 熔断流程 + └─ 无可走的 fail 边(死路)→ 该节点 blocked + → 若无其他节点在 ready/running,run_status 同步 blocked + → 这是「需要人工介入」的终止态,区别于「停止」(人工主动终止) ``` -**D.2.1 Loop-Validator 流程**(落实 27 号文档 §3.5 的三步 + 硬顶): +**D.2.1 回退熔断(两层通用,取代 v0.1 的 `loop_validator` 强制节点)** -1. **备案**:进入 `loop_validator` 节点时,插入一条 `work_graph_loop_records`(`verdict='registered'`),`loop_count` = 该 `(validator, target)` 组合已有记录数 + 1。 -2. **硬顶检查**:`loop_count > 3` → 不再调用 LLM 判断,直接 `verdict='force_terminated'`,`loop_validator` 节点本身 `status='fail'`(且它没有 `fail` 出边或出边同样是死路),整条链路收敛到 `blocked`,等待人工介入。这一条判断是纯代码逻辑(比较整数),不依赖 agent 调用,保证"熔断"不会被 LLM 的自由发挥绕过。 -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 给出的修复建议")。 +引擎准备沿一条 `backward` 边推进时,无条件执行: -#### D.3 子流程(下钻) +1. **备案**:按 `(run_id, edge_id)` 查 `graph_run_loop_records` 已有记录数 n,`loop_count = n + 1`,插入一条 `verdict='registered'`;同时 `graph_runs.backward_count += 1`。 +2. **双层硬顶检查**:`loop_count > 3`(单边)**或** `backward_count > 5`(run 级总数)→ 不再调用任何 LLM,直接改 `verdict='force_terminated'`,源节点 `blocked`,链路收敛,等人工介入。**这一条是纯整数比较,不依赖 agent 调用**——保证熔断不会被 LLM 的自由发挥绕过。 +3. **回退**:两个计数都未撞顶时把 `to_node` 置 `ready`、`attempt+1`,把上一轮的 `remediation_note` 作为上下文补充带给它。 -两类下钻,数据结构上分别对应: +> **为什么单边计数不够**(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「每一级都重置下一级的计数器则整个循环没有终止条件」的同型变体:不是重置计数器,但效果一样。 +> +> 所以**两个计数都要**:单边计数防同一条路径上的死循环,run 级 `backward_count` 防「换个借口继续退」。它与 §四.A.3 的任务级总预算也是两回事——**预算管花了多少钱,`backward_count` 管绕了多少圈**,两者互不替代。 + +**`infra_flake` 不烧任何计数**:进程超时 / 被杀这类基础设施抖动,归因为 `infra_flake` 后直接原地重跑,**既不写 `graph_run_loop_records`,也不加 `backward_count`,也不扣 `retry` 预算**——沿用 DevLoop 的差异化预算消耗(`_spec/sdd-process.md` P-7)。不设这一类的话,超时会被误判成执行问题白烧一轮。 + +> **为什么不强制插 `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 就停」是代码的活。两者分工清楚,不互相依赖。 + +**D.2.2 一个节点有多条 fail 边时走哪条(R-16 定案)** + +这是 org 层引入后冒出来的新问题。`org-validator` 判定任务未达成时,退回 `org-coder`(实现有问题)还是 `org-planner`(规划有问题)?两条都是从同一节点出去的 `fail` 边,`condition` 单靠 `success`/`fail` 二值区分不了。 + +**定案:归因是一次显式的 agent 调用,由专职节点做,不是失败节点顺手吐一个值。** + +`org-validator` 自己的 work graph 里,**第一个节点是 `validator-reflection`**,它的唯一职责是判断这次失败的根因:是哪个 agent 的哪个 skill 有问题、是哪个 agent 拿到的输入约束不够、还是哪个 agent 的执行出了问题。归因完成才知道该先改 skill、还是直接让某个 agent 带 bug context 重做。 + +这与 DevLoop 的既有分工同构——Integrator 判定集成失败、BugTriager 单独归因,是**两个不同的 agent**。理由是同一个 agent 既当裁判又当法医,容易把「我判失败」和「我认为该怪谁」混成一次输出。拆开的额外好处:归因有独立的落盘产物、可审计、可在控制台单独下钻。 + +**归因取值域与 DevLoop 四分类的对照**: + +| `fail_reason` | DevLoop 对应 | 回退动作 | 烧计数? | +|---|---|---|---| +| `impl_bug` | `impl_bug` | 该 agent 带 bug context 重做 | 烧单边 + `backward_count` | +| `input_underspecified` | `contract_mismatch` / `spec_ambiguous` | 退回上游产出约束的那个节点 | 同上 | +| `skill_defect` | **无对应** | 见下方 ⚠️ | 同上 | +| `infra_flake` | `infra_flake` | 原地重跑 | **不烧任何计数**(D.2.1) | + +⚠️ **`skill_defect` 这一支即 L4-company,而 L4 在 §一是非目标**。**v1 处理方式:保留这个归因取值让 `validator-reflection` 照常判定**(数据先攒着,归因体系一次成型,不用等 L4 做完再回来改分类),**但动作降级为「挂起等人工」,不自动触发 L4**。理由见 [27-E2E](./27-e2e-task-execution-flow.md) §八——L4 是三级恢复里最贵、爆炸半径最大的一级,§十 建议最后做。 + +**引擎侧的选边规则**(引擎本身不理解上述语义,只做匹配): + +- 边上有可空的 `fail_reason` 列;节点失败时把 `fail_reason` 写进 `graph_run_nodes`(子图节点的这个值由 §四.D.0 从子 run 透传上来); +- **先找 `fail_reason` 精确匹配的边 → 无匹配则退到 `fail_reason IS NULL` 的兜底边 → 再无则视为死路**; +- 取值域**不在引擎里硬编码**,由图的规划者(`orggraph-planning` / `workgraph-planning`)与对应 validator 约定。上表是推荐取值,不是引擎约束。 + +**因此写入前校验要加第五项**:任何有 `fail` 出边的节点,**必须至少留一条 `fail_reason IS NULL` 的兜底边**。否则归因产出一个图上没有对应边的取值时会直接落到死路,表现为「莫名其妙 blocked」。这一项要加进 §三.7 复用的那套校验里。 -- **Agent 对话记录**(B.3 第 3 个 tab):不建新表,直接关联现有会话持久化(`server/conversations/`,CLAUDE.md 已列出的既有目录)——`work_graph_nodes` 不需要冗余存对话内容,`agent_run_id`/节点执行时机足以反查到对应的会话记录,实现时确认现有会话持久化的查询接口能否直接按"这次特定 attempt 的调用"过滤(如果不能精确过滤到单次 attempt,退化到按时间窗口近似匹配,列入 §六 T4 的实现细节)。 -- **嵌套子 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)。 +这对 P2 已落地的 `org_graph_edges` 是**加一列**(T-P3),不动现有唯一索引——两条 fail 边指向不同 `to_node_id`,本来就不冲突。 + +#### D.3 两级验收的引擎语义(R-15.3) + +| | work 层 `{orgNode}-validator` | org 层 `org-validator` | +|---|---|---| +| 在图上是 | work graph 里一个普通 `agent_call` 节点 | org graph 的末节点(`subgraph` 节点,它自己也有 work graph) | +| 验什么 | 这个 org node 自己的产出合格吗 | 整个任务达成了吗 | +| 引用本体 | `planner-validator` / `coder-validator` | `org-validator` | +| fail 触发 | 本张 work graph 内的 backward 边 | **org graph 的** backward 边 → 退回某个 org node → 该 org node 整张 work graph 重跑(起新子 run,`attempt+1`) | + +`org-validator` 内部调 `validator-reflection` / `benchmark-validator` / `testdata-validator` 等 sub agent **不需要新机制**——它是一个 org node,按定义有自己的 work graph,这些 sub agent 就是那张图的节点: + +``` +org graph: org-planner → org-coder → org-validator + ↓(它自己的 work graph) + validator-reflection → benchmark-validator → testdata-validator → … + ↑ 排第一位:先归因,再决定后面怎么验 / 怎么退 +``` + +`validator-reflection` 的产出(`fail_reason` + `remediation_note`)通过 §四.D.0 的三值透传回到 org 层,成为 §四.D.2.2 选边的依据。 + +两级验收的失败都从**同一个 root run 预算池**扣(§四.A.3),这正是 [27-E2E](./27-e2e-task-execution-flow.md) §7.1「三级恢复叠加会让熔断失效」的解法。 + +**谁验收「验收标准」本身**:按 R-9.3「planner 生成、validator 执行」,产出者和被验收者天然分离,不存在「自己给自己出考题」。但「标准写错了」这一层仍未解决,两条缓解都要:① `planner-validator` 明确承担一项职责——审「这套验收标准是否可度量、是否覆盖原始需求、是否存在平凡通过的漏洞」,不只审 PRD/SDD 本身;② 保留人工 sign-off 作为最终关卡,v1 阶段不做全自动闭环。 --- -## 五、任务划分(Task Breakdown) +## 五、任务划分 + +### 五个阶段(每个可独立验收、独立上线) + +切法与 [27-P2](./27-p2-org-graph.md) §五「三个可独立验收的阶段」一致:**不起 LLM 的先做完、能单独验收的先上线**。 + +| 阶段 | 含 | 交付什么 | 怎么验收 | 起 LLM | +|---|---|---|---|---| +| **0 · 验收契约** | T0 | 三种验收产物的统一契约格式 + 执行器 | 手写一份契约 + 一个被测产物 → 执行器跑出通过/不通过,判决可持久化 | ❌ | +| **A · 引擎地基** | T1→T2→T5、T6、T9、T10 | 五张表、`advanceOne` 幂等原语、两种 node executor、SSE 三者原子化、五个控制端点 | 手写图 JSON → 手动模式逐节点推进到 `completed`;切自动模式一键跑完;暂停后能手动单步、能恢复自动 | ❌ mock | +| **B · 回退熔断** | T7、T8 | backward 边计数 + 硬顶、`fail_reason` 路由、任务级预算、三层级联 | 总是失败的节点第 4 次 `force_terminated` 收敛到 `blocked`;预算扣完整棵 run 树挂起;暂停 root 后子 run 也停 | ❌ | +| **C · 控制台** | T3、T4 | 扩展 `UnitsDAG.vue`、四 tab 详情、`parent_run_id` 面包屑下钻、预算条 | 回退边虚线 + `N/3` 徽标随流程递增;运行日志 live↔history 切换不丢内容;子图 tab 能逐层进出 | ❌ | +| **D · 规划闭环** | T11、T12 | `workgraph-planning` agent + 用户→公司任务入口 | 一句自然语言任务 → org graph 自动逐节点起 work graph → 两层跑完 | ✅ | +| **P · P2 连带** | T-P1、T-P2、T-P3 | R-15.3 的改名 + `fail_reason` 加列 | 三个模板末节点为 `org-validator`;NAMING 补 validator 命名轴;`org_graph_edges` 多一列且旧数据不受影响 | ❌ | + +**阶段间可以停**:只做完 0+A,系统就有了一个「能手工建图、能校验、能按五个操作跑」的通用图引擎;B 让它在失败时不失控;C 让人看得见;D 才让它自己规划。**最快看到端到端效果是 0→A→B→D,C 可后延**。 + +### 任务表 -| # | 任务 | 涉及文件/模块 | 依赖 | 对应 Phase | +| # | 任务 | 涉及文件 | 依赖 | 阶段 | |---|---|---|---|---| -| **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 | -| **T2** | `work-graph-engine.js`:`advanceOne()` 核心原语(拾取 ready 节点→执行→按边推进)+ 幂等更新(A.3) | `server/services/work-graph-engine.js`(新增) | T1 | P3 | -| **T3** | Work Graph 画布前端组件:**扩展既有 `UnitsDAG.vue`**(Kahn 分波布局 + SVG 渲染已具备),新增 `loop_validator` 菱形节点、回退边虚线曲线路由、循环徽标 | `web/src/views/WorkGraphConsole.vue`(新增)、`web/src/components/devloop/UnitsDAG.vue`(抽取复用或拷贝改造) | T5(先有数据能画) | P3 | -| **T4** | 节点详情面板四个 tab(概览/输入输出/运行日志/子图),运行日志 tab 复用 `AgentChat.vue` 的双轨折叠卡片模式 | 同上 + 会话记录查询接口确认 | T2, T6 | P3 | -| **T5** | SSE:按 `work_graph_id` 复用 `sse-broker.js` 模式,状态转换 + timeline 写入 + SSE 广播三者原子化 | `server/services/work-graph-sse.js`(新增,参照 `devloop/sse-broker.js`) | T2 | P3 | -| **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 | -| **T7** | Loop-Validator 流程:D.2.1 三步 + 硬顶,`work_graph_loop_records` 读写 | `server/services/work-graph-engine.js` 扩展 | T2 | P4 | -| **T8** | 回退边可视化:循环徽标(`N/3`)、异常流程视觉降权(B.2 最后一条) | T3 组件扩展 | T7 | P4 | -| **T9** | §三.5 取消能力的实现前核实 + 落地:确认 `RunRegistry`/`ProcessGuard` 的调用链,"停止"操作接入硬取消(或确认做不到、落地降级方案并更新本文档 §四.C 的表述) | `server/src/engine/`(只读研究 + 必要时补一个查询接口,不改现有取消逻辑本身) | 无(可以提前做,独立于 T1-T8) | P3(阻塞"停止"的完整语义,但不阻塞其余功能先上线) | -| **T10** | 测试:`work-graph-engine.test.js`(状态机非法转换、幂等 advance、Loop-Validator 硬顶);一条端到端脚本(建图→启动→若干次 advance→触发一次回退→硬顶终止→查最终状态) | `server/tests/unit/work-graph-engine.test.js`(新增) | T2, T7 | P3+P4 | - -**顺序建议**:T1→T2 是地基。T9(取消能力核实)可以最早独立开工,不阻塞其他任务,但要在 T6 的"停止"端点定稿前给出结论。T5/T6 依赖 T2,可并行。T3 依赖有真实数据可画,建议先用 T1 的 schema 手写几行测试数据跑通画布,再接 T6 的真实端点。T7/T8(P4)在 T2/T6 稳定后开始,不阻塞 P3 先上线一个"只有正向执行、没有回退"的初版(这也是为什么 §二解释了"一次设计、分两次交付")。 +| **T0** | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一 schema)+ 执行器 + 判决持久化 | `server/services/goal-contract.js`(新增)、契约 schema 文档 | 无 | 0 | +| **T1** | schema:五张表 + 状态转换表实现(照抄 `unit-registry.js` 模式)+ work graph 写入校验(复用 `org-graph-service.js` 四项) | `server/services/work-graph-db.js`(新增) | 无 | A | +| **T2** | `graph-engine.js`:`advanceOne(runId)` 核心原语、两种 node executor、幂等条件更新、run 树级联工具函数 | `server/services/graph-engine.js`(新增) | T1 | A | +| **T3** | 画布组件:**扩展 `UnitsDAG.vue`**,加 `subgraph` 角标、backward 虚线曲线路由、循环徽标;org/work 两层共用同一实例 | `web/src/components/WorkGraphCanvas.vue`(从 `UnitsDAG.vue` 抽取)、`web/src/views/GraphConsole.vue`(新增) | T6 | C | +| **T4** | 节点详情四 tab + 面包屑下钻 + 预算条;运行日志复用 `AgentChat.vue` 双轨模式 | 同上 + 会话记录查询接口确认 | T2, T6 | C | +| **T5** | SSE:按 `run_id` 复用 `sse-broker.js` 模式;**状态转换 + timeline 写入 + SSE 广播三者原子化**(不重复 DevLoop 的两轨缺口) | `server/services/graph-run-sse.js`(新增,参照 `devloop/sse-broker.js`) | T2 | A | +| **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 | +| **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 | +| **T8** | 任务级总预算(root run 扣减 + 熔断)+ 三层暂停/停止级联 + 「重试」的 `contract_hash` unchanged 判据 | `graph-engine.js` 扩展 | T2 | B | +| **T9** | §3.6 取消能力的**实现前核实** + 落地:确认 `RunRegistry`/`ProcessGuard` 调用链,「停止」接入硬取消;或确认做不到、落地降级并回改本文档 §四.C | `server/src/engine/`(只读研究为主,必要时补一个查询接口) | 无,可最早独立开工 | A | +| **T10** | 测试:状态机非法转换、幂等 `advanceOne`、回退硬顶、预算熔断、级联;一条端到端脚本(建两层图 → 启动 → 若干次 advance → 触发回退 → 硬顶终止 → 查最终状态) | `server/tests/unit/graph-engine.test.js`(新增) | T2, T7, T8 | A/B | +| **T11** | `workgraph-planning` agent 本体(全局通用,R-15.2):读 org node 角色定义 + 任务 + `artifacts/` + 上一轮 `remediation_note` → 产出图 → 过校验 → 落库。生成的图**不得在节点内部再套四段式流转**(§九bis) | `system/templates/workgraph-planning/`(新增) | T1, T6 | D | +| **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 | +| **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 | +| **T-P2** | NAMING §三 删掉 `org-validator` 行;**新增 validator 命名轴一节**(节点名带领域前缀、本体名不带,R-15.3 的表) | `docs/todo/system-design/27-naming-conventions.md` | 无 | P | +| **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 | +| **T13** | `validator-reflection` agent 本体(R-16):归因产出 `fail_reason` + `remediation_note`;`org-validator` 的 work graph 骨架把它排在第一位 | `system/templates/validator-reflection/`(新增) | T-P3 | B | +| **T14** | 写入前校验新增第五项 `fail-fallback-edge`(§三.7):有 `fail` 出边的节点必须留一条 `fail_reason IS NULL` 兜底边;org / work 两层都加 | `server/services/org-graph-service.js`、`work-graph-db.js` | T1 | B | + +> ⚠️ **T-P1 / T-P2 / T-P3 是 P2 侧文件的连带修改,但归 P3 执行**。P2 正由另一条线推进中,这批改动写在本文档而非直接改 P2 的文档/模板,是为了避免两边同时写同一批文件产生冲突。P2 那条线**不要动**这三项。 + +**顺序建议**:T0 和 T9 可最早独立开工,互不阻塞。T-P3 要在 T7 之前(`fail_reason` 是 D.2.2 的地基)。T1→T2 是硬串行地基。T5/T6 依赖 T2,可并行。T3 建议先用 T1 的 schema 手写几行数据跑通画布,再接 T6 的真实端点。 --- ## 六、验收标准 -1. 对一张只有正向边、3-4 个节点的 Work Graph:手动模式下连续点"下一步"能逐节点推进到 `completed`,每次点击只推进一个节点(不会因为下游 ready 了就自动继续)。 -2. 同一张图切到自动模式点"启动":不再需要任何人工点击,自动跑到 `completed`。 -3. 自动模式运行中点"暂停":当前正在 `running` 的节点跑完后,`run_status` 变成 `paused`,不再自动调度下一个节点;此时点"下一步"能手动继续单步推进;点"启动"能恢复自动模式。 -4. 构造一个"总是失败"的节点、其 `fail` 边指向一个 `loop_validator` 节点、`loop_validator` 的 `fail` 边指回该节点自身的上游:验证第 1-3 次回退能正常触发(`work_graph_loop_records` 各有一行 `verdict∈{fixed,not_fixed}`),第 4 次触发时不再调用 agent、直接 `force_terminated`,图收敛到 `blocked`。 -5. 对一个 `blocked` 节点点"重试":节点回到 `ready`,后续能继续正常推进(不需要重建整张图)。 -6. 点"停止":所有 `pending`/`ready` 节点变 `skipped`,`run_status` 变 `stopped`;如果 T9 确认取消能力可用,正在 `running` 的节点被中断且状态被正确记录为某种可区分于正常 `fail` 的终止态(本设计的 `skipped`/`stopped` 组合已经覆盖,具体是否需要给"被强制取消"单独一个 status 值,留给 T9 落地时决定)。 -7. Work Graph 画布:回退边渲染为虚线曲线且显示 `N/3` 循环徽标,随着 D.2.1 流程推进徽标数字正确递增。 -8. 点开一个节点的"运行日志" tab:`running` 状态下看到实时追加的内容(对应 `AgentChat.vue` 的 live 面板行为),`success`/`fail` 后切换成历史视图,内容不丢失。 -9. 对一个 `has_subgraph=1` 的节点:出现"子图" tab,点进去是完整可交互(能点"下一步"等)的嵌套 Work Graph 画布,面包屑能返回上一层。 -10. Org Graph 左栏点击一个从未触发过 Work Graph 的 org node:右侧显示空态提示,不报错。 +**阶段 0** + +1. 手写一份 `goal-benchmark` + `goal-test-data` + `goal-test-script`,执行器能对一个被测产物跑出明确的通过/不通过判决,判决落库可查。 + +**阶段 A** + +2. 一张只有正向边、3-4 节点的图:手动模式连续点「下一步」逐节点推进到 `completed`,**每次点击只推进一个节点**(不因为下游 ready 了就自动继续)。 +3. 同一张图切自动模式点「启动」:不再需要任何人工点击,自动跑到 `completed`。 +4. 自动模式运行中点「暂停」:当前 `running` 节点跑完后 `run_status → paused`,不再自动调度;此时点「下一步」能手动单步;点「启动」能恢复自动。 +5. 点「停止」:所有 `pending`/`ready` 节点变 `skipped`,`run_status → stopped`。若 T9 确认取消可用,正在 `running` 的节点被中断且状态可区分于正常 `fail`。 +6. 一张 org graph(`graph_kind='org'`)用**同一套端点**跑通上述 2-5,代码路径与 work graph 完全一致——这是 R-15.4 引擎泛化的核心验收点。 +7. 一个 `subgraph` 节点执行时正确起子 run、`child_run_id` 回填、子 run 收敛后父节点拿到正确的 success/fail。 + +**阶段 B** + +8. 构造一个总是失败的节点,其 `fail` 边是指回上游的 `backward` 边:第 1-3 次回退正常触发(`graph_run_loop_records` 各一行),**第 4 次不再调用任何 agent、直接 `force_terminated`**,图收敛到 `blocked`。 +9. 一个节点有两条 `fail` 边(`fail_reason` 分别为 A / 空):节点带 `fail_reason=A` 失败时走第一条,带其他值失败时走兜底边。写入一张有 `fail` 出边却无兜底边的图 → 被 `fail-fallback-edge` 校验拒绝,返回 400。 +9b. **多边不架空熔断**:构造一个有 3 条回退边的节点,让归因每次归到不同的边——单边计数各自都没到 3,但 run 级 `backward_count` 到 5 时仍 `force_terminated`。 +9c. `validator-reflection` 归因为 `infra_flake` 时原地重跑,`graph_run_loop_records` 不增行、`backward_count` 不递增。 +9d. 一个 `subgraph` 节点的子 run 因内部归因失败而收敛:父节点同时拿到 `fail`、`fail_reason`、`remediation_note` 三个值,并据此在父层正确选边。 +10. root run 预算扣完:整棵 run 树挂起,不是某一层单独失败。 +11. 对 root run 点「暂停」:正在跑的子 run、孙 run 全部在下一个节点边界停下。 +12. 对一个 `blocked` 节点点「重试」:节点回 `ready` 并能继续推进,不需要重建整张图;下游 `contract_hash` 未变的节点保持原状不重跑。 + +**阶段 C** + +13. 画布:回退边渲染为虚线曲线且显示 `N/3` 徽标,随 D.2.1 推进数字正确递增。 +14. 「运行日志」tab:`running` 时实时追加,`success`/`fail` 后切历史视图,内容不丢。 +15. `subgraph` 节点的「子图」tab 点进去是完整可交互(能点「下一步」)的嵌套画布,面包屑能逐层返回。 +16. 左栏点一个从未跑过 work graph 的 org node:中栏空态提示,不报错。 + +**阶段 D** + +17. 对一家 org graph 已就绪的公司下达一句自然语言任务:自动建 root run → 第一个 org node 起 work graph → 两层跑到 `completed`,全程无需手工构造任何图数据。 --- @@ -331,31 +616,90 @@ work_graphs.run_status 转换表: | 文件 | 改动类型 | |---|---| -| `server/services/graph-db.js`(或新 `work-graph-db.js`) | 新增四张表 | -| `server/services/work-graph-engine.js` | 新增:状态机 + `advanceOne` + Loop-Validator 流程 | -| `server/services/work-graph-sse.js` | 新增:参照 `sse-broker.js` | -| `server/routes/work-graph.js` | 新增:控制类 + 只读查询类端点 | -| `web/src/views/WorkGraphConsole.vue` | 新增:主页面 | -| `web/src/components/WorkGraphCanvas.vue`(视实现拆分情况) | 新增:可复用的画布组件(父图/子图共用,见 §四.D.3) | -| `server/src/engine/`(`run-registry.js` 相关) | 只读研究为主,T9 视核实结果决定是否需要补一个小的查询接口,不改现有取消逻辑 | - -**明确不改**:DevLoop 全部现有代码、`27-P2` 已落地的 `org_graph_nodes`/`edges`(本设计只读取,通过 `triggered_by_org_node_id` 引用,不写入)、`CompanyDashboard.vue`(新页面独立存在,不改造现有 tab,只是从 Dashboard 某处加一个跳转入口)。 +| `server/services/work-graph-db.js` | 新增:五张表 + 状态转换表 + work graph 写入校验 | +| `server/services/graph-engine.js` | 新增:`advanceOne` + 两种 executor + 回退熔断 + 预算 + 级联 | +| `server/services/graph-run-sse.js` | 新增:参照 `sse-broker.js` | +| `server/services/goal-contract.js` | 新增:验收契约 schema + 执行器 | +| `server/routes/graph-run.js` | 新增:控制类 + 只读查询类端点 | +| `server/routes/company.js` | 追加:任务入口端点(T12) | +| `web/src/views/GraphConsole.vue` | 新增:主页面 | +| `web/src/components/WorkGraphCanvas.vue` | 新增:从 `UnitsDAG.vue` 抽取,org/work 两层共用 | +| `system/templates/workgraph-planning/` 等四个 | 新增:规划 agent + 三个 validator 本体 | +| `server/services/graph-db.js` | 追加:`org_graph_edges` 加 `fail_reason` 列(T-P3) | +| `system/org-graph-templates/*.json` | 修改:末节点改名 + 回退边重画(T-P1) | +| `docs/todo/system-design/27-naming-conventions.md` | 修改:§三 删行 + 新增 validator 命名轴(T-P2) | +| `server/src/engine/run-registry.js` | 只读研究为主,T9 视核实结果决定是否补一个查询接口 | + +**明确不改**:DevLoop 全部现有代码(`loop-controller.js` / `unit-registry.js` / `doc-store.js` 只被**读取和借鉴**,收编是 P6)、P2 的 `org-graph-service.js` 四项校验(只复用)、`CompanyDashboard.vue`(新页面独立存在,只从 Dashboard 加一个跳转入口)。 --- ## 八、风险 / Open Questions -1. **`RunRegistry`/`ProcessGuard` 的取消调用链没有 100% 核实**(§三.5)——"停止"操作里"硬取消正在跑的节点"这条能力,设计上按能做出发,但 T9 是这个功能能不能达到设计预期的真正决定因素。如果核实后发现做不到干净取消,退化方案("停止"只影响未开始的节点,正在跑的等它自然结束)已经在 §四.C 写明,不会导致整个设计推倒重来,但用户体验会打折扣(点了停止还要再等一会)。 -2. **Work Graph 画布的分层布局 + 回退边曲线路由是一块不小的手写前端工程量**,没有引入图形库(符合仓库一贯风格),但意味着这部分工时不能按"随便拿个组件库拼一下"估计。如果实现过程中发现手写布局的边界情况(比如多条回退边交叉)处理成本远超预期,值得回来重新评估是否值得为此引入一个轻量布局库(如 dagre 的纯布局计算部分,不需要它的渲染层)——本文档不预先下结论,留给实现阶段视实际复杂度决定。 -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` 节点也允许点重试",当前设计没开放这个入口)。如果后续需要支持,涉及"下游要不要级联失效"的产品决策,留待有真实需求时再设计。 -4. **子图默认不继承父图的暂停/停止状态**(§四.D.3)——"暂停父图"不会暂停正在跑的子图,是刻意简化,避免一次"暂停"要级联遍历整棵下钻树。如果实际使用中这个不一致体验造成困惑(用户以为暂停了父图,子图还在偷偷跑),需要重新评估是否要做级联传播,工作量不小(需要引擎知道"我有没有子图、子图现在什么状态",目前的 `parent_node_id` 只是单向引用,没有双向感知机制)。 -5. **本文档假定"图已经规划好"**,不覆盖 `workgraph-planning` agent 本身怎么工作(§一非目标已声明)——这意味着在 planning agent 真正设计出来之前,T1-T10 的验收(§六)只能用手写测试数据构造图,不能端到端验证"从一句自然语言目标到一张可执行的图"这条链路。这是刻意的范围切分,不是遗漏,但要求 27 号文档后续单独出一份"`workgraph-planning` agent"设计文档,两者共用本文档定义的 schema(§四.A)。 +1. **归因能精确到哪一层,尚未定案**(R-16 遗留)。原话说「直接让某个 agent 直接 +bug context 描述直接重做」,但若 `validator-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 内部做,不上升到 org 层;坏处是多一跳。 + - **允许跨层直接寻址目标节点**:更快,但破坏封装,且控制台上「暂停 / 单步」作用于哪一层会变得含糊。 +2. **`validator-reflection` 的命名尚未定案**(R-16 遗留)。按 [27-NAMING](./27-naming-conventions.md) §三 现行规则(work graph 节点 = `{父 org 节点职能}-{职能}`),它作为 `org-validator` 的 work graph 节点应为 `validator-reflection`,与原话一致。但它做的是**归因**不是**判定**,是否改叫 `validator-triager`(对齐 DevLoop 的 BugTriager)待定。 +3. **`fail_reason` 的取值域不由引擎约束**(§四.D.2.2)——引擎只做字符串匹配,取值由图的规划者和 validator 自行约定。好处是不把 DevLoop 的四分类硬编码进通用层;风险是归因产出一个图上没有对应边的取值。**缓解已定案**:写入前校验新增第五项 `fail-fallback-edge`(§三.7),强制每个有 `fail` 出边的节点留一条兜底边。 +4. **`org_graph_nodes.id` 是整数、`work_graph_nodes.id` 是文本**,`graph_run_nodes.def_node_id` 只能取 TEXT 且无法建外键(§四.A.2 的注)。引用完整性靠服务层保证。 +5. **回退边曲线路由是真实的手写前端工程量**(§四.B.2)。没引入图形库符合仓库一贯风格,但这块工时不能按「拿组件库拼一下」估。若实现中发现多条回退边交叉的边界情况成本远超预期,值得重新评估引入 dagre 的纯布局计算部分(不要它的渲染层)——本文档不预设结论。 +6. **`RunRegistry`/`ProcessGuard` 的取消调用链没有 100% 核实**(§3.6)。T9 是「停止」能否达到设计预期的真正决定因素。降级方案已在 §四.C 写明,不会导致设计推倒重来。 +7. **v1 的「一家公司同时只有一个 active root run」是一条真实的产品限制**,不只是技术妥协。它由 `CONVENTIONS.md` §12 的不变量和 SessionPool key 复用共同决定(§一非目标)。如果用户很快就要「一家公司并行接多个任务」,实例隔离要提前排期。 +8. **`workgraph-planning` 的规划质量没有硬校验**。§三.7 只保证图**结构合法**,不保证它**适合这个任务**。缓解手段与 P2 同款:规划记录落盘供人工复核,发现不合理走增量修正。 +9. **阶段 0 的契约格式一旦定错,阶段 A 的 schema 可能要返工**。这也正是 R-15.6 把它提到最前面的原因——但「提前」只降低概率,不消除风险。建议 T0 先把契约的**存储位置和与 `output_ref` / `artifacts/` 的关联方式**定死,格式内部细节可以后续演进。 --- ## 九、与其他阶段的关系 -- **与 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 上线后再接上真实数据——两者顺序不是互相阻塞的硬依赖,只是"最终要接起来"。 -- **与 P1(虚拟引用寻址)**:无直接依赖,且 P1 **已实现并提交**(commit `226489d`)。Work Graph 节点执行时调用 `AgentManager` 的方式不受影响(P1 改的是 `prompt` 字段从哪来,不改调用接口)。 -- **与端到端流程**:本文档设计的是"一张 Work Graph 怎么被执行和控制",但一个真实任务会有**三层图**(任务级 → org node 级 → 并行子图级)。第三层之上的两层、任务入口、验收收敛条件,见 [27-E2E 端到端任务执行流程](./27-e2e-task-execution-flow.md)。该文档同时指出本文档需要补的四项:`node_kind='org_node'`(让 work graph 节点能指向 org node)、fan-out 并行 + 同 agent 多实例隔离、跨层级共享的任务级总预算、三层暂停/停止的级联语义。 -- **与 27 号文档 P3/P4 原始路线图**:本文档实质上把这两个 phase 的交付物做了细化和合并(理由见 §二),且额外交付了 27 号文档路线图里没有单独列出的"执行控制台页面"。建议 27 号文档 §六路线图表的 P3/P4 两行补充链接指向本文档,与 [27-P2](./27-p2-org-graph.md) 现有的链接方式一致。 +- **与 P2**:强依赖。P2 产出 org graph 定义,本文档执行它。**两者共用同一套 schema 形态、执行引擎、控制台画布**——这是 R-9.1 新模型最大的收益。本文档只读 `org_graph_nodes/edges`(唯一例外是 T-P3 加一列 `fail_reason`)。P2 侧的三项连带修改(T-P1/T-P2/T-P3)归本阶段执行,见 §五的警告框。 +- **与 P1**:无直接依赖,已实现(commit `226489d`)。P1 改的是 prompt 从哪来,不改调用接口。但 R-15.3 的「本体名不带领域前缀」正是 P1「同一份本体、多处引用、各自私有语境」的直接推论。 +- **与 P5(自我进化)/ L4**:本文档不含。任务级预算里预留了 `l4_iterations` 的位置但不实现。 +- **与 P6(DevLoop 收编)**:本引擎从 DevLoop **抽象**而来(§三),但不改它一行。收编的验收标准是「DevLoop 现有测试套件在新引擎上全部通过」,属 P6。 +- **与 base agent 的四段式**:见下方 §九bis——原表述「已强制一套四段式生命周期」不准确,实测有一半没落地。 +- **与端到端流程**:本文档设计的是「一张图怎么被执行和控制」,完整的七阶段链路见 [27-E2E](./27-e2e-task-execution-flow.md) §三,跑一遍具体的数见 [27-EXAMPLE](./27-example-dev-company-walkthrough.md)。 + +--- + +## 九bis、base agent 四段式的现状核实 + +[27-E2E](./27-e2e-task-execution-flow.md) §八bis 说 `system/templates/base/` 「已强制一套四段式生命周期」,并把它当作「每个 node agent 内部也是一张图」的现成雏形。**核实后这个说法只对一半**,本节记录实测结论——它直接决定了 P3 之后那项「统一两套状态机」的待办到底要做什么。 + +### 设计意图(`system/templates/base/agent.md`,28 行) + +``` +RESEARCH ──→ PLAN ──→ EXECUTE ──→ VERIFY + ↑ │ + └───────── 失败写 _lessons.md 打回 ┘ +``` + +agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 `node bin/lemon.js task transition --agentId --phase --artifact ` 才能换脑子换技能包;每次流转**清空上下文**(原文叫「斩断记忆 / Context Flush」)防长对话累积幻觉;EXECUTE 阶段才拿到「修改核心文件的破坏性权限」;VERIFY 通过才可 `report_task({status:"PASS"})`。 + +**这确实就是一张最小 work graph**:四个节点、线性前进、末端一条回退边,且已具备 P3 引擎需要的三样东西——显式的阶段状态、阶段间的产物传递(`--artifact`)、失败回退。 + +### 实测:上下文隔离是真的,权限/技能隔离是假的 + +`POST /api/agent/:agentId/task/transition`(`server/routes/agent-chat.js:1832`)已实现,且比 prompt 里写的还多: + +| 机制 | 状态 | 位置 | +|---|---|---| +| Context Flush(断 ACP 进程 + `history.json` 清空 + artifact 注入新起步指令 + 重新唤醒) | ✅ 真的在跑 | `agent-chat.js:1916-1960` | +| **P0 事前阻断阀**——P0 任务 `plan → execute` 拦截挂起、通知 Master 人工审批才放行 | ✅ 已实现(prompt 里没写这条) | `agent-chat.js:1854-1891` | +| OKR 联动——顺带把节点状态更新为 `researching`/`planning`/`executing`/`verifying` | ✅ | `agent-chat.js:1894-1905` | +| **Hot Swap 换技能包** | ❌ **静默失效** | `agent-chat.js:1908` | + +失效原因:代码是 `const sourceSkill = .../system/skills/meta-${phase}/SKILL.md` 后跟 `if (fs.existsSync(sourceSkill))` 才拷贝,而 **`system/skills/` 这个目录在仓库里根本不存在**,四个 `meta-*` 技能一个都没有。所以每次流转的「动态换脑」是空转,那句 `🧠 动态技能树已切换` 的日志压根不会打印,**也没有任何报错**。 + +**后果**:「EXECUTE 阶段才拿到破坏性权限」这个设计意图目前**只靠 prompt 话术约束,没有任何机制保障**——agent 在 RESEARCH 阶段照样能写文件。 + +另两处顺带发现:`phase` 参数**没有任何校验**,既不校验流转顺序(可以 research 直接跳 verify、跳过中间两段),也不校验取值本身,而它被直接拼进文件路径。 + +### 对 P3 的影响 + +原来那条待办(「保证两套状态机语义不冲突」)应当升级为一个更具体、收益也更明确的目标:**把四段式改写成引擎上的一张标准图模板**。这样做同时解决三件事: + +1. 消除「引擎层的图」和「agent 内部的四段式」两套并行状态机; +2. `meta-*` 技能的挂载正好对应 work graph 节点的 `system_agent_id` 引用,**顺手把 `system/skills/` 缺失这个洞补上**——现在这批技能没有读取方,改造后引擎就是读取方; +3. 阶段顺序校验由引擎的边定义天然提供,不需要另写。 + +**仍列为 P3 之后的待办**,不进本阶段范围。但 §五 的 T11(`workgraph-planning`)在生成图时应当**避免与四段式语义打架**——具体是别让 work graph 节点内部再套一层四段式流转,否则同一次执行会有两层 Context Flush 互相清对方的上下文。 diff --git a/docs/todo/system-design/27-requirement.md b/docs/todo/system-design/27-requirement.md index 15d0d31..4c31d8c 100644 --- a/docs/todo/system-design/27-requirement.md +++ b/docs/todo/system-design/27-requirement.md @@ -218,6 +218,135 @@ --- +## R-15 · P3 需求规划讨论中的六个定案 + +针对「27 系列 P3 该做成什么样」的一轮讨论产出。 + +⚠️ **本节两种来源混排,逐条标注**:R-15.1 / R-15.2 / R-15.3 有用户原话(引用块内一字不改,遵循本页照搬原则);R-15.4 / R-15.5 / R-15.6 是**选项式确认**——选项文本由我拟、用户勾选,不是用户自己的措辞,因此只记为定案,不冒充原话。 + +> 📌 **恢复记录(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 引用的原话,故保留新版。 + +### R-15.1 · 讨论对象是 org graph 这条线的 P3 + +> 有一个进程正在开发p2 我们来看看 p3 的需求的规划 + +> 对 我们要讨论 org graph 的 p3 + +**澄清了什么**:仓库里有**两条互不相干的 P3**,容易混。 + +| | DevLoop 的 P3 | 27 系列的 P3 | +|---|---|---| +| 出处 | `docs/prd/newfunction/_spec/plan-migration.md` | [27 总纲](./27-graph-agent-architecture.md) §六 | +| 内容 | L1+L2 联动,跨 Unit `runProject` | Work Graph 最小闭环 | +| 状态 | `runProject` 已实现(`loop-controller.js:576`) | 零代码 | + +本条确认讨论的是**后者**。下文 R-15.2 起全部属于 27 系列。 + +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) 全文。 + +### R-15.2 · `workgraph-planning` 是全局通用 agent + +> ① 用全局通用 workgraph-planning + +**澄清了什么**:每个 org node 接到任务时不是各自带一份规划能力,而是**都调同一个全局 `workgraph-planning` 本体**,靠记忆分区区分它在不同 org node / 不同公司下的语境。与 R-9.2 元 agent 继承、[27-P1](./27-p1-virtual-reference.md) 虚拟引用寻址一致——agent 池不随 org node 数量膨胀。 + +**落点**:[27-NAMING](./27-naming-conventions.md) §二(该条目已在表内)、[27-P3/P4](./27-p3-p4-work-graph-console.md) 阶段 D。 + +### R-15.3 · validator 分两个层级,各有全局本体 + +> ② 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 来完成任务。 + +**两级验收的分工**: + +| | 节点名 | 验什么 | 决定什么 | 失败退到哪 | +|---|---|---|---|---| +| **org 层** | `org-validator` | 整个任务达成了吗([27-E2E](./27-e2e-task-execution-flow.md) §三 阶段 7 的全局 validator) | 整个 org graph run 的收敛 | 退回某个 org node,**整张 work graph 重跑** | +| **work 层** | `{orgNode}-validator` | 这个 org node 自己的产出合格吗 | 该 org node 的 success/fail | 退回本张 work graph 内的某个节点 | + +**节点名 vs 本体名**(本条推导出的一条新规则): + +| | 例 | 带前缀? | +|---|---|---| +| system agent 本体 | `planner-validator` / `coder-validator` / `org-validator` | ❌ 不带领域前缀 | +| org graph 节点 | `org-planner` / `org-coder` / `org-validator` | ✅ 带 `org-` | +| work graph 节点 | `{父 org 节点职能}-{职能}` | ✅ 带父节点名 | + +禁止裸用 `validator` 的理由是「一家公司内两个节点不能重名」——该理由针对的是**节点**,不针对**本体**。本体若被迫带领域前缀,`coder-validator` 会复制成多份,与 P1「同一份本体、多处引用、各自私有语境」直接冲突。 + +**三个 validator 是三个独立本体**:它们验的东西本质不同——规划产物 / 代码 / 整个任务达成,是不同能力,不是同一能力的不同语境。[27-E2E](./27-e2e-task-execution-flow.md) §八bis 的「不特化」原则针对的是同一能力在不同 org node 下,与本条不冲突。 + +**`org-validator` 调 sub agent 不需要新机制**:`org-validator` 本身是一个 org node,按定义有自己的 work graph,`benchmark-validator` / `testdata-validator` 就是这张 work graph 的节点,落在已有的递归模型里,引擎不加分支。 + +**连带影响**: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`(节点内产物指针)不够。 + +**落点**:[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)。 + +### R-15.4 · 引擎泛化,定义层与运行时层分离 + +**选项确认**,非原话。给出的三个选项是:引擎泛化接受表名参数(选中)/ org graph 执行时物化成 work_graph 实例 / 先只做 work graph 把 org graph 执行推到 P5。 + +**推翻了什么**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A 的注释专门为「不拆定义表和实例表」辩护,理由是 work graph「规划完即执行、执行完即归档」,定义即实例。**该前提在 org graph 也要被执行之后失效**——org graph 是建公司时生成一次、活整个公司生命周期的长期定义,一份定义要被跑很多次(每来一个任务跑一遍)。 + +**定案**:定义层保留两套(`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)。 + +**连带定案**:org node 与 work node 的执行语义不同——前者是「起一张子图并等它收敛」(子 run `completed` → success,`blocked` → fail),后者是「一次 agent 调用」。引擎需要 `agent_call` / `subgraph` 两种 node executor。org 层回退**不**强制插 `loop_validator` 节点(会污染「这家公司有哪些角色」的语义),改由引擎对 `edge_kind='backward'` 的边统一计数 + 硬顶熔断。 + +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A / §四.D。 + +### R-15.5 · P3 范围含 `workgraph-planning` + 任务入口 + +**选项确认**,非原话。 + +**推翻了什么**:[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 标注的「完全空白」,没有它图没有来源。 + +**定案**:两者都收进 P3,作为最后一个阶段(前面的阶段全部不起 LLM、可独立验收,与 [27-P2](./27-p2-org-graph.md) §五「三个可独立验收的阶段」同一套切法)。 + +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §一非目标(两条作废)、§五任务划分。 + +### R-15.6 · 验收契约提到最前面 + +**选项确认**,非原话。 + +**依据**:[27-E2E](./27-e2e-task-execution-flow.md) §十「整条流程的终止条件(验收)是实现程度最低的一环……**先建验收,再建循环**——否则会得到一个跑得很顺畅但永远不知道该不该停的系统」。按该节 v0.2 调整,「验收先行」的含义**不是**建 benchmark 基础设施(那是误解),而是**定义验收契约格式 + 建执行器**。 + +**定案**:P3 新增一个前置阶段,先定 `goal-benchmark` / `goal-test-data` / `goal-test-script` 三种产物的统一契约格式并建执行器,再动引擎。 + +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §五任务划分、[27-E2E](./27-e2e-task-execution-flow.md) §十一。 + +--- + +## R-16 · 失败归因由专职 subagent 做,回退边是多条 + +> org-validator 判定任务未达成时,退回 code-coder(实现有问题)还是 code-planner(规划有问题)? 这个org-validator 会有自己的 work graph 里的 subagent 决定,它首先需要有一个 validator-reflection subagent 来判断当前的 bug 的源头是哪个 agent 的哪个 skill 的问题,还是哪个 agent 的输入约束不够的问题,还是哪个 agent 的执行问题,判断完原因,才知道是先改 skill,还是直接让某个 agent 直接+bug context 描述直接重做。 所以回退的边是多个的,不是一个的,在回放的时候才知道用哪个边实现回退。你觉得如何? + +**落点**:[27-P3/P4](./27-p3-p4-work-graph-console.md) §四.D.2.2(选边)、§四.D.0(子 run 回写透传)、§四.D.2.1(熔断) + +**这条回答了什么**:[27-P3/P4](./27-p3-p4-work-graph-console.md) v0.3 §八.1 把「一个节点有多条 fail 边时走哪条」列为本版最需要确认的 open question,本条关闭它。 + +**关键结构**:归因**不是**失败节点自己顺手吐一个 reason,而是 `org-validator` 自己 work graph 里排第一位的一个专职节点 `validator-reflection`。这与 DevLoop 的既有分工同构——Integrator 判定集成失败、BugTriager 单独归因,是两个不同 agent,理由是同一个 agent 既当裁判又当法医容易把「我判失败」和「我认为该怪谁」混成一次输出。 + +**三分类归因与 DevLoop 四分类的对照**: + +| 原话的归因 | DevLoop 对应 | 回退动作 | +|---|---|---| +| 哪个 agent 的执行问题 | `impl_bug` | 该 agent 带 bug context 重做 | +| 哪个 agent 的输入约束不够 | `contract_mismatch` / `spec_ambiguous` | 退回上游产出约束的那个节点 | +| **哪个 agent 的哪个 skill 的问题** | **无对应** | 先改 skill —— 这一支即 **L4-company** | +| (原话未提)进程超时 / 被杀 | `infra_flake` | 直接重跑,不烧预算、不动节点 | + +⚠️ **「skill 问题」这一支落在 P3 的非目标里**(L4 是三级恢复中最贵、爆炸半径最大的一级,[27-E2E](./27-e2e-task-execution-flow.md) §十 建议最后做)。**建议**:v1 保留 `skill_defect` 这个归因取值让 reflection 照常判定(数据先攒着,归因体系一次成型),但动作降级为「挂起等人工」,不自动触发 L4。`infra_flake` 建议补上——纯引擎逻辑、近乎零成本,不补则超时会被误判成执行问题、白烧一轮预算。 + +**对 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」,缺这条透传。 + +**本条带出的一个熔断漏洞**: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`。这与任务级总预算是两回事——预算管花了多少钱,这个管绕了多少圈,两者都要。 + +**两处待确认**(尚未定案): + +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 内部判断。另一条路是允许跨层直接寻址目标节点,更快但破坏封装,且控制台「暂停/单步」的层级语义会变含糊。 +2. **`validator-reflection` 的命名**。按 [27-NAMING](./27-naming-conventions.md) §三 现行规则(work graph 节点 = `{父 org 节点职能}-{职能}`),它作为 `org-validator` 的 work graph 节点应为 `validator-reflection`,与原话一致。但它做的是归因不是判定,是否改叫 `validator-triager`(对齐 DevLoop 的 BugTriager)待定。 + +--- + ## 附 · 需求 → 设计落点 速查 | 需求 | 一句话 | 主要落点 | @@ -236,3 +365,10 @@ | R-12 | **验收产物定名 `goal-`**;goal 唯一、task 多个 | NAMING §四 | | R-13 | goal 三层;类型由 agent 推断;新 agent 从 base 继承;强制单入口 | NAMING §四 / P2 §3.3 §4.1 §五 | | R-14 | 命名轴统一为作用域:`subagent-goal` → **`subtask-goal`** | NAMING §四 | +| R-15.1 | 讨论的是 27 系列的 P3,不是 DevLoop 的 P3 | P3/P4 全文 | +| R-15.2 | `workgraph-planning` 是全局通用 agent,不按 org node 特化 | NAMING §二 / P3/P4 阶段 D | +| R-15.3 | **validator 分 org / work 两层**;本体名不带领域前缀 | NAMING §三 / P2 §4.6 / P3/P4 | +| R-15.4 | **引擎泛化**,定义层(org/work 两套)与运行时层(`graph_runs` 一套)分离 | P3/P4 §四.A / §四.D | +| R-15.5 | P3 范围含 `workgraph-planning` + 任务入口,两条非目标作废 | P3/P4 §一 / §五 | +| R-15.6 | 验收契约格式 + 执行器提到最前面,作为 P3 的前置阶段 | P3/P4 §五 / E2E §十一 | +| R-16 | **归因由 `validator-reflection` 专职节点做**;回退边多条,运行时选边;熔断需双层计数 | P3/P4 §四.D.0 / §四.D.2.1 / §四.D.2.2 | -- libgit2 0.21.4