README.md 17.3 KB

system/work-graph-templates/

Work Graph(org node 级 workflow)的骨架模板库。与 system/org-graph-templates/(公司级)同构, 只是低一层:那边一家公司一张图、活整个公司生命周期;这边一个 org node 接到一次任务规划一张图,用完即归档。

每个 *.json 是一种 org node 职能的执行流图起点。org graph 上的节点被执行到时,引擎调 workgraph-planning(全局通用本体,27-REQ R-15.2),它会 ls 本目录、逐个读取、对照各自的 whenToUse 选型,再结合宿主 org node 的角色定义、本次任务描述、可读的 artifacts/、上一轮 remediation_note, 裁剪 / 替换 / 组合出这次真正要跑的 work graph。

设计依据:27-P3/P4 §四.D.0 / §四.D.2.2 / §四.D.3 / §九bis 命名规范:27-NAMING §二 / §三

这是起点集合,不是穷举枚举。 加一种起手式只需往本目录扔一个 JSON 文件,不改任何代码。 模板里的 systemAgentId 对 workgraph-planning 是建议——它可以替换成更合适的本体, 但替换后的那个也必须真实存在:work_graph_nodes.system_agent_id 有外键,引用不存在的本体 落库时直接 FOREIGN KEY constraint failed。模板里写着的那些则不是建议而是承诺, 由下面那道 CI 防线保证它们始终指得到真东西。

当前模板

文件 id 宿主 org node 节点链
planner.json planner 规划类(org-planner) brd → prd → sdd → benchmark → validator
executor.json executor 执行类(org-coder / org-writer / org-monitor),单交付物 impl → test → validator
executor-fanout.json executor-fanout 执行类,可并行拆分 split → (impl/test)×N → merge → integration → validator
validator.json validator org-validator 专用 validator-triager → benchmark-validator → testdata-validator

四张图共享一个形状:做 → 自检 → 本层验收,验收失败沿 backward 边退回重做。差异只在中段。

🛡️ 本目录的 systemAgentId 有一道 CI 防线

server/tests/unit/template-refs.test.js 对本目录每个 JSON 做三件事:

  1. 每个非占位符 systemAgentId 在 system/agents/ 下必须有目录;
  2. 同一个 id 在 system/agents/_registry.json 里必须有条目;
  3. 真的建一次库、把不含占位符的骨架全部 createWorkGraph 落一遍。

为什么第 1 条查的是目录:system_agents 的数据源头是目录扫描 (graph-seed-service.seedAgents() 读 system/agents/*),_registry.json 只用来补 benchmarkScore。而 work_graph_nodes.system_agent_id 有外键指向 system_agents(id)—— 目录不在 = 落库时 FOREIGN KEY constraint failed,createWorkGraphSafely 会把它翻成 rule=reference 打回 agent,连打三轮后抛错,节点按无 fail_reason 的 fail 走兜底边。 这正是 R6-F1 实际踩到的形态(当时 validator.json + planner.json 一共 6 个空引用)。

改本目录的任何 JSON 后跑一次:

npx vitest run server/tests/unit/template-refs.test.js

{{占位符}} 必须替换

executor*.json 里的 {{orgNode}} / {{implAgent}} / {{testAgent}} / {{validatorAgent}} / {{splitAgent}} / {{mergeAgent}} / {{integrationAgent}} 是占位符,不是合法值, 提交前必须全部替换成真实的节点名与本体名,否则落库出来的是一张引用不存在 agent 的废图。

  • {{orgNode}} ← 宿主 org node 的全名(如 org-coder)。
  • {{xxxAgent}} ← 一个真实存在的 system agent 本体 id。核实方式见下一节。

planner.json / validator.json 没有占位符——因为 org-planner 与 org-validator 在三个 org graph 模板里首尾同名(27-NAMING §3.1),它们的 work graph 节点名可以写死。

节点名 ≠ 本体名(27-NAMING §3.2,最容易搞错的一条)

规则 例
nodeName 图内唯一,带宿主 org 节点前缀 org-coder-validator / org-planner-benchmark
systemAgentId 跨图复用的本体,不带领域前缀 coder-validator / goal-benchmark

本体若被迫带前缀,coder-validator 会复制成多份,与 P1「同一份本体、多处引用、各自私有语境」直接冲突。

⚠️ 唯一的例外是 validator.json 里的三个节点:validator-triager / benchmark-validator / testdata-validator 按 §3.1 字面规则本应写成 org-validator-triager 等,但 27-P3/P4 全文用的是 父节点职能的简写。前缀取全名还是取职能这一处仍未定案(27-NAMING §3.5 第 1 行), 定案前一律按简写书写,不要以任一方去改另一方。 至于职能词,reflection → triager 已由 27-REQ R-17.2 定案,不再是待定项。

validator.json 为什么归因排第一位

org-validator 判定任务未达成时该退回 org-planner(规划有问题)还是 org-coder(实现有问题)? 这个判断不由失败节点顺手吐出,而是由 org-validator 自己 work graph 里排第一位的专职归因节点 validator-triager 做出(27-REQ R-16)。它的产出 fail_reason + remediation_note 随子 run 收敛 透传回 org 层(§四.D.0 三值透传),成为 org graph 选边的唯一依据。

三个 fail 出边的走向由此确定:

  • benchmark-validator / testdata-validator 验收不过 → 兜底 backward 边退回 validator-triager 归因;
  • validator-triager 故意不留 fail 出边——它归因完发现「这层修不了」时以 fail 收尾, 引擎判为死路 → 节点 blocked → 子 run blocked → 父节点拿到 fail + fail_reason + remediation_note, 回到 org 层按 §四.D.2.2 选边。这是设计好的收敛出口,不是漏画的边。

反复归因不会失控:backward 边有单边 loop_count ≤ 3 与 run 级 backward_count ≤ 5 的双层熔断 (§四.D.2.1),撞顶直接 force_terminated,纯整数比较,不依赖任何 LLM 判断。

后两个节点各自只认一格(两个都是 goal-contract.js 的生产消费方)

节点 它做的 server/services/goal-contract.js 做的
benchmark-validator 逐维度找证据给分 → 写 attempt-{n}.scores.json 加权求和 + 阈值比较 + 判决落盘
testdata-validator 逐 case 真的喂一遍 → 如实记实际输出到 attempt-{n}.results.json deep-equal / subset 逐条比对 + 判决落盘

切法是「LLM 只给分 / 只记实际输出,算术与阈值交给不起 LLM 的执行器」—— 同一个 agent 既给分又判过没过,很容易把想要的结论倒推成分数。

两个文件名后缀是刻意选的:引擎认判词只认严格的 attempt-{n}.json(graph-engine.js 的 ATTEMPT_FILE_RE),不会误认这两份;而 goal-contract.js 的 deriveTargetIds 又能从同样的 文件名推出 nodeRunId 与 attempt,判决四元组不缺环。

⚠️ 两处已知边界,不是漏画:

  1. test_data 段常常根本不存在。三件套(goal-benchmark / goal-test-data / goal-test-script) 按 27-E2E §十一 是动态选入的,本目录只建了 goal-benchmark 一个产出本体; 另两段由图上任何带 writesArtifacts: true 的节点用 goal-contract.js put --section … 写, 不另设专用本体。契约里没有 test_data 段时 testdata-validator 走 verdict: "skipped" 放行—— 这是常态,不是它没干活。
  2. 两个节点都只读自己那一格(sections.benchmark / sections.test_data), 不看 run 返回的顶层 ok。run 会把契约里所有非空 section 一起跑,顶层是三段的合取; 拿它当自己的结论 = 同一个失败被两个节点各算一遍 = 回退次数翻倍、熔断提前撞顶。

文件格式

{
  "id": "planner",                  // 必填,与文件名同名(不含 .json)
  "label": "规划型 org node 的 work graph",  // 必填,给人看的中文名
  "whenToUse": "……",                // 必填,LLM 选型的唯一依据:适用场景 + 典型信号词 + 与其他模板的区别
  "ownerOrgNode": "org-planner",    // 必填,这张图挂在哪类 org node 下;占位符写 {{orgNode}}
  "nodes": [
    { "nodeName": "org-planner-brd", "systemAgentId": "brd-planning" },
    { "nodeName": "org-planner-benchmark", "systemAgentId": "goal-benchmark",
      "writesArtifacts": true },     // 可选,声明它有权写 root run 的共享 artifacts/
    { "nodeName": "benchmark-validator", "systemAgentId": "benchmark-validator",
      "readsArtifacts": ["goal-benchmark.json"] }  // 可选,声明它读哪些共享产物
  ],
  "dataContractNotes": "……",        // 可选,给规划 agent 看的取舍说明:这张图为什么这么声明数据契约
  "edges": [
    { "from": "org-planner-brd", "to": "org-planner-prd", "condition": "success",
      "produces": ["brd"] },         // 数据契约:上游产出什么,交给下游
    { "from": "org-planner-sdd", "to": "org-planner-benchmark", "condition": "success",
      "produces": ["sdd"], "needs": ["prd"] },  // needs = 下游要吃、但产出方不是紧邻上游
    { "from": "org-planner-validator", "to": "org-planner-prd", "condition": "fail",
      "failReason": "input_underspecified", "edgeKind": "backward" },
    { "from": "org-planner-validator", "to": "org-planner-sdd", "condition": "fail",
      "edgeKind": "backward" }       // 无 failReason = 兜底边;backward 边一律不声明契约
  ]
}
  • condition:"success" | "fail",边的语义是执行流(上游出这个结果时流向下游)。
  • edgeKind:"forward"(默认,可省略)| "backward"。指向拓扑序更早节点的边必须显式标 backward。
  • failReason:仅 condition: "fail" 的边可带,是失败的细分路由键(对应 work_graph_edges.fail_reason 列)。 省略即为该节点的兜底边。取值域不由引擎约束,推荐取值见 27-P3/P4 §四.D.2.2 (impl_bug / input_underspecified / skill_defect / infra_flake)。
  • nodeKind:"agent_call"(默认,可省略)| "subgraph"。work graph 节点默认是一次 agent 调用; 只有确实要再下钻一层图的节点才写 subgraph。
  • writesArtifacts / readsArtifacts:可选声明,对应 §四.A.5 的共享产物区读写权。 写 artifacts/ 是特权动作,只有被显式标记的节点才有,防止任意节点往共享区乱写。
  • produces / needs:边上的数据契约,见下一节。四张骨架的每条 forward 边都必须有其中之一。

produces / needs:边只带控制流的话,图就不传数据

condition / edgeKind / failReason 表达的全是执行流。下游节点的提示词里能不能出现 「上游产物在这个绝对路径」,只取决于边上的数据契约声明——第四次真 CLI 冒烟里 org-planner-prd 的归因原话是「不是上游没产出,是它的产出结构上到不了我手上」, 根因就是这条边上什么都没写。

这个声明有两个读回方,都在 server/services/graph-engine.js:

环 谁读 读到之后干什么
输入侧 describeInputs → inboundContracts('work', def.edges, defNodeId) 按声明的 kind 去产物索引查实例,把绝对路径拼进下游节点的提示词;声明了却查不到就显式标 ⚠️
产出侧 indexNodeOutputs → 读本节点出边上的契约 用声明的 kind 给这个节点刚落盘的文件定 kind(文件名认不出来时靠声明一对一认领),否则一律记成 artifact,下游按 kind 就查不到

写法与规则(权威说明在 system/agents/workgraph-planning/tools.md §6bis,这里只记要点):

  • kind 取值域只有 7 个:brd / prd / sdd / acceptance / contract / artifact / log, 自创取值会被 rule=schema 打回。
  • produces 写在产出方的出边上(绝大多数情况只用它);needs 写在消费方的入边上, 仅当产出方不是紧邻上游(跨节点中继)时才用。
  • backward 边一律不声明(rule=schema):引擎解析输入只看 forward 入边, 写在回退边上等于没写,而「看起来写了」比「没写」更难排查。
  • 规则 6 data-contract:声明要吃的 kind(needs,required 不为 false) 在图上必须有产出方,否则整张图被拒。产出方确实不在本图(上一层 org graph 或 artifacts/ 给的)时, 写成 "needs":[{"kind":"artifact","required":false}] —— 这是唯一的逃生口, validator.json 用的就是它。
  • 任务级共享的 acceptance / contract 不要沿边声明:引擎按 task + kind 自动递给每个节点 (graph-engine 的 TASK_SHARED_KINDS),不受拓扑相邻性限制。只有当本图里某个节点自己产出它、 再交给本图的下游时才写 produces(planner.json 的 acceptance 就是这种情形)。

🛡️ 防线:server/tests/unit/work-graph-skeletons.test.js 逐张骨架查四件事—— 过 validateWorkGraph(含规则 6)、每条 forward 边都有非空声明、 createWorkGraph 落库后 graph_edge_contracts 的 kind/role/required 与 JSON 逐条一致、 每个非入口节点的 inboundContracts 非空。改本目录任何 JSON 后跑:

npx vitest run server/tests/unit/work-graph-skeletons.test.js

加一个新模板

  1. 新建 system/work-graph-templates/{id}.json,按上面的格式填。
  2. 节点名遵循 {宿主 org 节点全名}-{职能},本体名不带前缀(27-NAMING §3.1 / §3.2,见上文对照表)。
  3. 避开这 6 个命名冲突 id:agent-creator / digester / evolver / evolver-youtube-crawler / innovator-* 系列。它们是造 agent / 训练 agent 的元 agent,产出是「一个新本体」而不是「任务的下一步」。 (原理由「两个目录下同名不同步」已随 28 号改造消失,见本目录 §🛡️ 与 org-graph-templates/README §3。)
  4. workgraph-planning 自己不要出现在图里——它规划图,但不是图上的节点。
  5. 不要把 RESEARCH → PLAN → EXECUTE → VERIFY 四段式画进图里,也不要指望节点内部去跑它 (27-P3/P4 §九bis)。work graph 本身就是那张图的泛化版,两层并存会让两层 Context Flush 互相清对方的上下文。详见下一节。
  6. 人工验算五项校验(写入 API 也会校验,但模板先自查能省一轮 LLM 重试):

| # | 规则 | 怎么自查 | |---|---|---| | 1 | 无环(只看 forward 边) | 把所有 backward 边划掉,剩下的边必须能排出拓扑序 | | 2 | 恰好一个入口 | 只数 forward 边的入度,入度为 0 的节点必须恰好一个 | | 3 | 全可达 | 从入口沿 forward 边 BFS,必须覆盖 nodes 里每一个节点 | | 4 | 回退边显式标注 | 任何 to 在拓扑序上早于 from 的边,必须带 "edgeKind": "backward" | | 5 | fail 兜底边 | 有 fail 出边的节点,其中必须至少有一条不带 failReason | | 6 | 声明要吃的 kind 有产出方 | 每条 needs(未标 required:false)的 kind,图上必须有某条出边 produces 它 |

  1. 自验:直接拿引擎真正用的那套校验跑一遍本目录全部模板(占位符不影响校验,它只做名字匹配):
   node -e "
   const fs=require('fs'),p=require('path'),{validateWorkGraph}=require('./server/services/work-graph-db.js');
   const d='system/work-graph-templates';let bad=0;
   for(const f of fs.readdirSync(d).filter(f=>f.endsWith('.json'))){
     const t=JSON.parse(fs.readFileSync(p.join(d,f),'utf-8'));
     const r=validateWorkGraph(t.nodes,t.edges);
     console.log(f,r.ok?'OK':'FAIL',r.ok?'':JSON.stringify(r.errors));
     if(!r.ok)bad++;
   }
   process.exit(bad?1:0);"

🚫 不要在节点内部再套四段式(27-P3/P4 §九bis)

system/agents/_base/ 的基座员工被要求跑 RESEARCH → PLAN → EXECUTE → VERIFY, 每次流转靠 node bin/lemon.js task transition 清空上下文(Context Flush)。 work graph 的节点本身就是这套东西的泛化版——同一次执行里两层都跑,两层 Context Flush 会互相清对方的上下文。

定案:图是唯一的流转层。 阶段顺序由边定义提供,阶段间产物由 output_ref 传递, 失败回退由 backward 边 + 熔断提供,不需要节点内部再来一遍。

❌ 不要 ✅ 要
一个节点的 prompt 里写「先 research 再 plan 再 execute 再 verify」 一个节点只做一件能一句话说清「做完什么算成功」的事
让节点自己调 task transition 换阶段 让引擎按边推进到下一个节点
把四段式画成图里的四个节点,再让每个节点内部也跑四段式 要拆调研/规划就直接拆成图上的相邻节点,只拆一层

把四段式改写成引擎上的一张标准图模板是 P3 之后的待办,本目录不预先实现它。