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 |
deliverable.json |
deliverable |
内容型交付物类(非代码,如 org-hunt/org-digest/org-evolve 这类调研/知识产出角色) |
{{orgNode}}-impl(单节点,无内部验收) |
前四张图共享一个形状:做 → 自检 → 本层验收,验收失败沿 backward 边退回重做。deliverable.json 是例外——
它服务的任务本质上不是"可运行代码",不适用 executor.json 那套自测+acceptance.json 验收语义,
校验交给宿主节点在 org graph 边上声明的 produces 契约(见该模板 whenToUse 里记录的真实事故:
硬套 executor.json 后 workgraph-planning 两次现场重规划分别选中 qa-judge(critic/judge 合一导致
无限重复确认,判 infra_flake)和 testdata-validator(死等任务本身不会产出的 acceptance.json),
两次都在验证层卡死,烧了 20 次调用/31 分钟才熔断)。
🛡️ 本目录的 systemAgentId 有一道 CI 防线
server/tests/unit/template-refs.test.js 对本目录每个 JSON 做三件事:
- 每个非占位符
systemAgentId在system/agents/下必须有目录; - 同一个 id 在
system/agents/_registry.json里必须有条目; - 真的建一次库、把不含占位符的骨架全部
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→ 子 runblocked→ 父节点拿到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,判决四元组不缺环。
⚠️ 两处已知边界,不是漏画:
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"放行—— 这是常态,不是它没干活。- 两个节点都只读自己那一格(
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
加一个新模板
- 新建
system/work-graph-templates/{id}.json,按上面的格式填。 - 节点名遵循
{宿主 org 节点全名}-{职能},本体名不带前缀(27-NAMING §3.1 / §3.2,见上文对照表)。 - 避开这 6 个命名冲突 id:
agent-creator/digester/evolver/evolver-youtube-crawler/innovator-*系列。它们是造 agent / 训练 agent 的元 agent,产出是「一个新本体」而不是「任务的下一步」。 (原理由「两个目录下同名不同步」已随 28 号改造消失,见本目录 §🛡️ 与 org-graph-templates/README §3。) workgraph-planning自己不要出现在图里——它规划图,但不是图上的节点。- 不要把
RESEARCH → PLAN → EXECUTE → VERIFY四段式画进图里,也不要指望节点内部去跑它 (27-P3/P4 §九bis)。work graph 本身就是那张图的泛化版,两层并存会让两层 Context Flush 互相清对方的上下文。详见下一节。 - 人工验算五项校验(写入 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 它 |
- 自验:直接拿引擎真正用的那套校验跑一遍本目录全部模板(占位符不影响校验,它只做名字匹配):
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 之后的待办,本目录不预先实现它。