From a77934e726b749895ab7fa89e351e559811e5612 Mon Sep 17 00:00:00 2001 From: yibo Date: Mon, 31 Aug 2026 01:08:47 +0800 Subject: [PATCH] fix(work-graph): kind='log' 契约永远落成 required=false --- server/services/work-graph-db.js | 15 ++++++++++++++- server/tests/unit/graph-edge-contracts.test.js | 21 +++++++++++++++++++++ system/agents/workgraph-planning/tools.md | 8 +++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/services/work-graph-db.js b/server/services/work-graph-db.js index 21de6f5..88f4136 100644 --- a/server/services/work-graph-db.js +++ b/server/services/work-graph-db.js @@ -2558,10 +2558,23 @@ function findFiles({ companyId, taskId = null, kinds = null }) { return rows.filter((r) => want.has(r.kind)); } +/** + * kind='log' 永远不能被声明成 required:`indexNodeOutputs()` 的三级认领规则 + * (文件名匹配声明 kind → 已知文件名 → 唯一无歧义兜底)对 test/integration 类节点 + * 结构性地认不出一个 kind='log' 的文件——它们落盘的是 coder.log/test.log 之类, + * 全部被归到 kind='artifact'。required:true 的 log 契约因此**永远无法满足**, + * 触发 requiredInputsMissing() 把节点打成 missing_required_input 死循环 + * (33 号文档 + snakegame-co T-mtfh2kuf-7c31 复现两次:一次模板 executor.json 里 + * 手滑漏了 required:false,一次 workgraph-planning agent 的 tools.md 例句没跟着改)。 + * 与其指望每个模板作者 / 每次 LLM 规划都记得写对,不如把这条底线收在唯一的落库口。 + */ +const NEVER_REQUIRED_KINDS = new Set(['log']); + /** 在边上声明「这条边要传什么 kind」。规划时写,只有 kind 没有实例。 */ function declareEdgeContract({ edgeLayer, edgeId, kind, required = true, role = 'produces' }) { if (!['org', 'work'].includes(edgeLayer)) throw new Error(`edgeLayer 非法: ${edgeLayer}`); if (!edgeId || !kind) throw new Error('declareEdgeContract 需要 edgeId / kind'); + const effectiveRequired = required && !NEVER_REQUIRED_KINDS.has(kind); // role 默认 produces:出边声明产出是压倒性多数,needs 必须显式写。 // 去重键仍是 (layer, edge, kind) 而不含 role —— 同一条边对同一个 kind 既声明 produces // 又声明 needs 是自相矛盾的输入(「我产它」和「我要别人产它」), @@ -2574,7 +2587,7 @@ function declareEdgeContract({ edgeLayer, edgeId, kind, required = true, role = ON CONFLICT(edge_layer, edge_id, kind) DO NOTHING `) .run( - newId('gec'), edgeLayer, String(edgeId), kind, required ? 1 : 0, + newId('gec'), edgeLayer, String(edgeId), kind, effectiveRequired ? 1 : 0, role === 'needs' ? 'needs' : 'produces', new Date().toISOString() ); } diff --git a/server/tests/unit/graph-edge-contracts.test.js b/server/tests/unit/graph-edge-contracts.test.js index 2ae40b6..e9bcd8f 100644 --- a/server/tests/unit/graph-edge-contracts.test.js +++ b/server/tests/unit/graph-edge-contracts.test.js @@ -156,6 +156,27 @@ describe('R-19.A 边上的数据契约:声明 → 落库 → 读回', () => { expect(rows.map((r) => [r.kind, r.required])).toEqual([['acceptance', 0]]); }); + it('① kind=log 永远落成 required=0,哪怕调用方(模板/LLM 规划)写的是 required:true(33 号文档 + snakegame-co T-mtfh2kuf-7c31 两次复现)', () => { + const companyId = 'co-contract-log-floor'; + ensureAgents(['unit-tester', 'coder-validator']); + const g = wdb.createWorkGraph({ + companyId, + taskId: 'T-log-floor', + nodes: [ + { nodeName: 'l-test', systemAgentId: 'unit-tester' }, + { nodeName: 'l-val', systemAgentId: 'coder-validator' }, + ], + edges: [ + // 裸数组写法 = normalizeContracts 默认 required:true,正是漂移复现的形状 + { from: 'l-test', to: 'l-val', condition: 'success', produces: ['log'] }, + ], + }); + expect(g.errors || []).toEqual([]); + const def = wdb.readDefGraph('work', g.id); + const rows = wdb.edgeContracts('work', def.edges[0].id); + expect(rows.map((r) => [r.kind, r.required])).toEqual([['log', 0]]); + }); + it('① kind 大小写/空格被归一(" PRD " 与 "prd" 是同一个契约,不产生两行)', () => { const companyId = 'co-contract-case'; ensureAgents(['prd-planning', 'sdd-planning']); diff --git a/system/agents/workgraph-planning/tools.md b/system/agents/workgraph-planning/tools.md index 65dd074..a2991cd 100644 --- a/system/agents/workgraph-planning/tools.md +++ b/system/agents/workgraph-planning/tools.md @@ -126,11 +126,17 @@ ls data/companies/<公司ID>/agents/ (或写成一个 `.json` 文件、在回复里给出**绝对路径**),引擎自己取出来校验 + 落库。 ```json -{"nodes":[{"nodeName":"org-coder-impl","systemAgentId":"coder"},{"nodeName":"org-coder-test","systemAgentId":"unit-tester"},{"nodeName":"org-coder-validator","systemAgentId":"coder-validator"}],"edges":[{"from":"org-coder-impl","to":"org-coder-test","condition":"success","produces":["artifact"]},{"from":"org-coder-test","to":"org-coder-validator","condition":"success","produces":["log"],"needs":["artifact"]},{"from":"org-coder-test","to":"org-coder-impl","condition":"fail","edgeKind":"backward"},{"from":"org-coder-validator","to":"org-coder-impl","condition":"fail","edgeKind":"backward"}]} +{"nodes":[{"nodeName":"org-coder-impl","systemAgentId":"coder"},{"nodeName":"org-coder-test","systemAgentId":"unit-tester"},{"nodeName":"org-coder-validator","systemAgentId":"coder-validator"}],"edges":[{"from":"org-coder-impl","to":"org-coder-test","condition":"success","produces":["artifact"]},{"from":"org-coder-test","to":"org-coder-validator","condition":"success","produces":[{"kind":"log","required":false}],"needs":["artifact"]},{"from":"org-coder-test","to":"org-coder-impl","condition":"fail","edgeKind":"backward"},{"from":"org-coder-validator","to":"org-coder-impl","condition":"fail","edgeKind":"backward"}]} ``` > `produces` / `needs` 就是 §6bis 那两个键。**forward 边上少了它们,下游节点的提示词里 > 就没有上游产物的路径** —— 先读 §6bis 再写这份 JSON。 +> +> `"kind":"log"` 必须写成 `{"kind":"log","required":false}` 这个对象形式,不能简写成 +> `["log"]`(33 号文档 + snakegame-co T-mtfh2kuf-7c31 两次真实事故):test/integration +> 类节点从不产出一个能被引擎认作 `kind='log'` 的文件(落盘的是 coder.log/test.log, +> 全被归到 `kind='artifact'`),required 的 log 契约会永远无法满足,把节点卡进 +> missing_required_input 死循环。 引擎取图的死规矩(完整版在 `agent.md` Step 7):**严格 JSON**(注释 / 尾逗号 / 省略号一律解析失败, 整块当没写)· 顶层要有 **`nodes` 数组** · 文件路径必须**绝对** · -- libgit2 0.21.4