diff --git a/docs/README.md b/docs/README.md index 1308388..0aa8cc9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -60,7 +60,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | | [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录,checklist 已查完**。最初以为 `org-coder-impl` 从未产出任何 HTML/CSS,后来发现真实网站其实存在于 `graph_files` 索引不覆盖的「项目根」目录(已顺手修:控制台「产物」面板现在会展示项目根)。追加查清 validator 有没有真的做浏览器级检查——中途自我订正一次:不是"没工具做不到"(`bash` 能走到仓库自带的 Playwright,另一条对照任务证明可行),是**没人教它这么做**;抽查另外两个项目根后发现**验证严谨度完全不稳定**(同类任务有的做了真实 Playwright 级验证、有的只做静态正则、有的没验证就被取消),根因很可能是新招聘的 `unit-tester` 本体撞上了 [32 号文档](./todo/system-design/32-hire-body-fill-reliability.md) 那类"装配没填充完"的真实样本,不是平台工具限制 | -| [43-task-decomposition-granularity.md](./todo/system-design/43-task-decomposition-granularity.md) | 📝 **需求 + 方案定案 + 技术实现计划(5 个 Phase)已写完,尚未开工**。回应"org-planner 出 brd/prd/sdd 后,任务看板能不能展示成 task/sub-task 树,coder-validator 按清单逐条开发验收"。方案:goal→task 流水线自建父子机制、先只拆验收(开发仍整体完成)、`tasks.acceptance_mode` 由 task-planning 判断、子任务复用 `tasks.status` 既有状态语义、任一子任务 fail 父任务即 fail。**Phase 0-4** 逐条核实了代码形态:`createTask()`/`parent_task_id` 迁移模板、task-planning "硬规则 5"需要开例外让 LLM 多写一个字段、`org-planner` 产出的验收清单其实是 `acceptance.md` 不是 `acceptance.json`(且⚠️真实回归里从未被用上,落地前需先验证可靠性)、`coder-validator`(不是 org-validator,文档订正过一次)的 `checks[]` 目前没有任何服务端代码解析、看板树形渲染有 `FileNode.vue` 现成模板可抄。建议实施顺序 Phase 0→1→4→2→3 | +| [44-writes-artifacts-produces-gate-mismatch.md](./todo/system-design/44-writes-artifacts-produces-gate-mismatch.md) | 📝 **真实回归中发现的缺陷记录,未修复**。43 号文档 Phase 2 真实验证时撞见:`goal-benchmark`(`planner.json` 里标 `writesArtifacts:true`)按文档正确把产物写进 root run 共享 `artifacts/` 目录,但产出闸门 `unsatisfiedRequiredProduces`(`graph-engine.js:4430`)只查节点私有目录(`nodeArtifactDir`),`writesArtifacts` 这个字段全仓库零代码读取——这条契约在当前实现下**永远无法通过**,不是偶发。真实证据:`artifacts/goal-benchmark.json` 磁盘上确认存在,闸门仍判「没找到」,强制 fail 沿兜底边回退。很可能正是 snakegame-co 参考任务里 `goal-benchmark`/`planner-validator` 显示 `skipped` 的真实原因。已有的 R-19 `contract.json` 显式声明逃生口(41 号文档修的那个)`allowedRoots` 也够不到共享 `artifacts/` 目录,一并够不到。留了两条候选修法,未实现 | +| [43-task-decomposition-granularity.md](./todo/system-design/43-task-decomposition-granularity.md) | ✅ **Phase 0-4 全部已实现**(2026-08-31)。回应"org-planner 出 brd/prd/sdd 后,任务看板能不能展示成 task/sub-task 树,coder-validator 按清单逐条开发验收"。`tasks` 加 `acceptance_mode`/`parent_task_id`(后者复用既有列);`task-planning` 判断要不要拆(硬规则 5 开例外让 LLM 写这一个字段);org-planner 收尾把 `acceptance.md` 物化成子任务;`coder-validator` 新增 `task subtask-result` 工具逐条上报,`settleNode()` 新增闸门强制核对子任务全 `completed` 才认 success;看板加子任务展示。**落地时订正了一处原计划想岔的架构假设**:org-planner 在 org 层永远是 `subgraph` 节点(不是 agent_call),真跑数据显示它当前展开成的 `planner.json` 5 节点子图里,`goal-benchmark`/`planner-validator` 两节点常被 skip,`acceptance.md` 未必真的产出——钩子已改成扫子 run 全节点、找不到就优雅降级为 holistic,不阻塞主产出,但"子任务一定会长出来"这件事目前是尽力而为不是保证,见文档"实现记录"一节的遗留风险说明。全量单测 1120/1123 通过(3 个失败与本次改动无关,本次改动前即存在) | ## todo/future/(暂缓,非当前重心) diff --git a/docs/todo/system-design/43-task-decomposition-granularity.md b/docs/todo/system-design/43-task-decomposition-granularity.md index 53d241e..738d15c 100644 --- a/docs/todo/system-design/43-task-decomposition-granularity.md +++ b/docs/todo/system-design/43-task-decomposition-granularity.md @@ -1,6 +1,6 @@ # 43 · Goal 分解粒度:规划产出该长成文件,还是长成 task/subtask 树 -> **状态**: 📝 需求 + 现状调研,尚未设计、未实现 +> **状态**: ✅ Phase 0-4 全部已实现(2026-08-31)。全量单测 1120/1123 通过(3 个失败与本次改动无关,改动前即存在) > **日期**: 2026-08-31 > **触发场景**: snakegame-co 公司,goal `G-mtfh23mu-bd63`("HTML 贪吃蛇游戏"),task `T-mtfh2kuf-7c31`。 > 该任务当天被 `missing_required_input` 死循环卡住(根因与修复见 [33 号文档](../../done/system-design/33-needs-gate-role-filter-bug.md) 的复发一例, @@ -136,3 +136,30 @@ org-planner 的 4 阶段 FSM(`system/agents/org-planner/tools.md:19-23`,`nod Phase 4(看板渲染)提前到 Phase 2/3 之前:先用手工插入的测试数据验证树形展示对不对,把"看得见"和"能自动生成"两件事解耦,避免一次性大集成、出问题不知道是生成的问题还是展示的问题。 Phase 2 排在 Phase 3 之前,且内部先做"acceptance.md 是否真的可靠"这次独立验证——这是唯一一个建在未经证实的既有产物之上的环节,值得单独花一次真跑确认,再决定要不要照原计划往下建。 + +--- + +## 实现记录(2026-08-31,Phase 0-4 全部完成) + +按 Phase 0→1→4→2→3 顺序实现,每个 Phase 落地时跑一次相关单测 + 全量回归,全程零静默失败。 +以下记两处**跟原计划不一样的地方**——都是动手时发现原计划想岔了,不是简单照抄执行。 + +### 落地清单 + +| Phase | 改了什么 | 测试 | +|---|---|---| +| 0(schema) | `tasks` 加 `acceptance_mode` 列(`CHECK IN ('holistic','itemized')`,迁移套用 `parent_task_id` 当年的 `hasColumn`/`ALTER TABLE` 模板);`createTask()` 加 `parentTaskId`/`acceptanceMode`,子任务禁止递归声明 `itemized`;`taskView()` 补上这两个字段的输出 | `task-acceptance-mode.test.js`(6 条) | +| 1(task-planning 判断) | `task-planning` 的 JSON schema 从 4 字段扩到 5 字段(`acceptance_mode` 可选),在硬规则 5 上明确开了一条例外(这个字段需要语义判断,不能像 mode/priority/budget 那样服务端注入默认值);`agent.md` 加 Step 2bis + 例子;`buildTaskPlans()` 透传 | `company-goal-task-routes.test.js` 补 2 条断言 | +| 4(看板渲染) | `GoalTaskPane.vue` 按 `parentTaskId` 分层渲染(子任务只读、无操作菜单,因为它们没有独立 run);新出现的 `TaskTable.vue`(同一天另一个 session 并发重写了 `TaskBoard.vue`)用一行"↳ 子任务·属于「…」"指回父任务,不强行画树——它是可排序表格,硬套树会打架 | 用假数据 harness 截图人工验证(渲染逻辑本身足够简单,没再加单测) | +| 2(org-planner 物化子任务) | **见下方"计划外的架构订正"** | `task-decomposition-materialize.test.js`(3 条,真的驱动了 org-planner 展开子图 + 子 run 收敛回传的完整路径) | +| 3(coder-validator 逐条上报) | 新增 `wdb.settleSubtaskResult()`(终态幂等条件更新,同 `settleTaskFromRun` 一个模子);新路由 `POST /:companyId/tasks/:taskId/subtask-result`;`listCompanyTasks`/`GET /:companyId/tasks` 加 `parentTaskId` 过滤;`bin/lemon.js` 加 `task subtasks` / `task subtask-result` 两个子命令;`settleNode()` 新增闸门——`coder-validator` 判 success 前核一遍它验的 task 若有子任务,是否全部 `completed`,没全的话强制改判 fail(新 `ENGINE_FAIL_REASONS.SUBTASKS_INCOMPLETE`);`coder-validator/agent.md` 加 Step 4bis | `task-decomposition-validator-gate.test.js`(6 条);顺带修了 `graph-engine-fail-reason.test.js` 的取值域快照(六→七个引擎取值)和 `template-verdicts.test.js`(agent.md 里一句话的措辞会被误判成"教 agent 写一个引擎不认的 fail_reason",改了措辞不改语义) | + +### 计划外的架构订正:org-planner 不是 agent_call,是 subgraph + +原计划假设 `system/agents/org-planner/` 是一个单体 agent,走 4 阶段 FSM 直接产出 `brd.md/prd.md/sdd.md/acceptance.md`,Phase 2 打算在它自己的 `settleNode()` 成功分支挂一个钩子。**这个假设是错的**——org 层节点在 `readDefGraph` 里全部被标记成 `nodeKind='subgraph'`(无一例外),真正核实 snakegame-co 那条参考任务的生产数据后发现,org-planner 当前总是靠 `workgraph-planning` 展开成 `planner.json` 那张 5 节点子图(`brd-planning → prd-planning → sdd-planning → goal-benchmark → planner-validator`),而**这条参考任务的 `goal-benchmark`/`planner-validator` 两个节点当时都是 `skipped`**——`acceptance.md`(连带 R-19-B 的 `acceptance.json` 契约)从未被真正产出过。这正是文档最初标记的"⚠️ 已知风险",真跑数据把它坐实了。 + +修法:钩子从"`org-planner` 这个 `agent_call` 节点自己成功时触发"改成"`org-planner` 这个 subgraph 节点(不管它是直接执行还是展开成子图)真正结算 success 时触发"——两种形态统一在同一个 `maybeMaterializeItemizedSubtasks()` 里判:agent_call 就在它自己的私有目录找 `acceptance.md`,subgraph 就去它的子 run 里**全节点扫**(不假设固定是哪个节点写的,因为 workgraph-planning 现场选模板,节点名不固定)。找不到就留一条 timeline note、按 `holistic` 收尾,**不阻塞、不拖累 org-planner 这一轮真正有价值的产出**——这与文档最初的"软失败"设计初衷一致,只是触发时机改对了。 + +**遗留风险**(订正后依然成立,写实话):`acceptance.md` 会不会被稳定产出,取决于 `workgraph-planning` 会不会给 org-planner 挑一张真正跑完 `goal-benchmark`/`planner-validator` 的图、以及那张图会不会真的产出这份文件——这两件事都不在本次改动范围内,`maybeMaterializeItemizedSubtasks()` 目前是**优雅降级**(找不到就跳过,不报错),不是**保证生效**。换句话说:`acceptance_mode='itemized'` 现在是"如果规划链路配合,就能自动长出子任务",还没做到"一定会长出子任务"。 + +**2026-08-31 真实回归验证坐实了这条风险**,而且找到了比"未必可靠"更具体的根因:见 [44 号文档](./44-writes-artifacts-produces-gate-mismatch.md)——`goal-benchmark` 按设计把产物写进 root run 共享 `artifacts/` 目录,但产出闸门只查节点私有目录,`writesArtifacts:true` 这个字段全仓库零代码读取,这条契约在当前实现下**结构性地**打不过,不是运气不好。这很可能正是本文档触发场景(snakegame-co)里 `goal-benchmark`/`planner-validator` 显示 `skipped` 的真实原因。44 号文档只记录诊断,未修复;修复它会提高 Phase 2 的实际命中率,但不在 43 号文档范围内,不在这里展开。 diff --git a/docs/todo/system-design/44-writes-artifacts-produces-gate-mismatch.md b/docs/todo/system-design/44-writes-artifacts-produces-gate-mismatch.md new file mode 100644 index 0000000..d70b035 --- /dev/null +++ b/docs/todo/system-design/44-writes-artifacts-produces-gate-mismatch.md @@ -0,0 +1,110 @@ +# 44 · `writesArtifacts` 节点的产出闸门永远打不过:写到共享区,闸门查的是私有目录 + +> **状态**: 📝 真实回归中发现的缺陷记录,已诊断到根因,**未修复**(本文档只记录,不含实现) +> **日期**: 2026-08-31 +> **发现场景**:[43 号文档](./43-task-decomposition-granularity.md) Phase 2 的真实端到端验证——新建 `task-decomp-verify-co` 公司,走一条真实 `itemized` 任务,org-planner 展开成 `system/work-graph-templates/planner.json` 那张 5 节点子图(真实 Claude CLI,非 mock) +> **与 43 号文档的关系**:**完全独立的缺陷**,跟 goal 分解粒度那件事没有任何关系,只是恰好在验证 43 号文档时撞见。43 号文档"实现记录"一节提到的"acceptance.md 未必可靠",这份文档是那句话背后更具体、更确定的根因之一 + +--- + +## 一、现象 + +真实回归任务里,`org-planner` 展开的 `planner.json` 子图跑到 `org-planner-benchmark`(本体 `goal-benchmark`,模板里标了 `"writesArtifacts": true`)这一步: + +- agent 自己的判决(`attempt-0.json`)写的是 `"verdict": "produced"`,`output.artifact` 指向 `artifacts/goal-benchmark.json`; +- 落到 `graph-engine.js` 的产出闸门(`unsatisfiedRequiredProduces`,`server/services/graph-engine.js:4430-4467`)却判定「节点声明会产出 `acceptance`,但产物目录里没找到对应文件」,把这次 `success` 强制改判 `fail`(`fail_reason: produces_missing`); +- 沿 `planner.json` 的兜底回退边退回 `org-planner-sdd` 重跑,`loop_count 1/3`。 + +时间线原文(`data/companies/task-decomp-verify-co/runs/run_edaf3ea3ed69/timeline.ndjson`): + +``` +18:24:17.177 node.output org-planner-benchmark → success +18:24:17.178 node.output 节点声明会产出 acceptance,但产物目录里没找到对应文件,按 fail 结算 + payload: {"missing_kinds":["acceptance"],"degraded":"produces_missing"} +18:24:17.178 node.status org-planner-benchmark: running → fail (failReason: produces_missing) +18:24:17.179 loop.registered 回退备案 loop_count=1/3 · backward_count=1/5 +18:24:17.179 node.status org-planner-benchmark: fail → pending(上游回退,本轮产出作废) +``` + +**关键点**:agent 没有做错任何事——它确实把文件写出来了,就在它自己判决里写的那个路径: + +```bash +$ find data/companies/task-decomp-verify-co/runs/run_edaf3ea3ed69/artifacts -type f +data/companies/task-decomp-verify-co/runs/run_edaf3ea3ed69/artifacts/goal-benchmark.json # 真实存在,内容正常 +``` + +而闸门检查的是这个: + +```bash +$ ls data/companies/task-decomp-verify-co/runs/run_edaf3ea3ed69/nodes/rn_d5fb26f5eede/ +attempt-0.json # 只有这一份,闸门要找的 acceptance 相关文件不在这 +``` + +两个不是同一个目录,而且**永远不会**是同一个目录——下面「根因」一节说明为什么。 + +--- + +## 二、根因:闸门只认节点私有目录,`writesArtifacts:true` 是纯文档、代码里没人读 + +### 2.1 `goal-benchmark` 被明确指示写共享区,这是设计意图不是失误 + +`system/agents/goal-benchmark/agent.md:13,43` 白纸黑字写着: + +> 产物写进 root run 的 `artifacts/` 共享区…… `runs/{rootRunId}/artifacts/`:共享产物区。你对它**有写权**(`writesArtifacts: true`),但只写自己那一段 + +`goal-benchmark/tools.md:25`:`用文件写入工具写 $R/artifacts/goal-benchmark.json`。 + +这是因为 `acceptance.json`(R-19-B 契约)是三个 org node 各写一段合成的(`benchmark`/`test_data`/`test_script`),天然是跨节点共享状态,只能落在 root run 级别的共享区,不可能落在某一个节点自己的私有目录里——落在私有目录里,别的节点根本读不到它那一段。 + +### 2.2 但闸门只查节点私有目录 + +`unsatisfiedRequiredProduces`(`server/services/graph-engine.js:4430-4467`): + +```js +dir = nodeArtifactDir(run, nodeRun.id); // 4452 行:只算节点私有目录 +if (!fs.existsSync(dir) || ...) return declared; +files = fs.readdirSync(dir)... +``` + +`nodeArtifactDir(run, nodeRunId)` = `runRootDir(run)/nodes/{nodeRunId}`(`graph-engine.js:3680`),不包含 `runRootDir(run)/artifacts/`。所以只要 `goal-benchmark` **正确地**按文档写到共享区,它自己节点私有目录下除了 `attempt-N.json`(会被过滤掉,见 4458-4459 行)什么都不会有,`hasCandidates` 恒为 `false`,`acceptance` 这个 kind 恒被判定「缺」。 + +**这条契约在当前实现下,无论 agent 表现多好都无法通过。** + +### 2.3 `writesArtifacts: true` 这个字段,全仓库零代码读取 + +```bash +$ grep -rn "writesArtifacts" server/ system/ --include="*.js" --include="*.json" +system/work-graph-templates/planner.json:11: { ..., "writesArtifacts": true } +``` + +唯一一处命中就是模板里声明它的那一行。**它是纯文档性质的元数据,从没被 `unsatisfiedRequiredProduces`、`indexNodeOutputs`(`graph-engine.js:4469` 起)或任何别的地方读取过。** 模板作者(`planner.json` 的注释)显然知道 `goal-benchmark` 的产出方式跟别的节点不一样,也确实标了这个字段——但从来没人把它接进产出闸门的判断逻辑里。 + +### 2.4 已有的逃生口(R-19 产出索引的显式声明机制)够不到共享区 + +`graph-engine.js` 已经有一条为**同一类**问题准备的机制——`readDeclaredProduces`(`graph-engine.js:4377-4406`):节点可以在自己私有目录写一份 `contract.json`,里面显式声明 `produces: {kind: ["<绝对路径>"]}`,闸门会认这份声明而不是死磕目录扫描。这正是 [41 号文档](./41-graph-vs-fde-reliability-discussion.md) 提到的"R-19 产出索引不覆盖项目根,加 `contract.json` 的 `produces` 显式声明机制"那次修复的产物。 + +但这条逃生口本身也够不到共享 `artifacts/` 目录——`inBounds()` 校验的 `allowedRoots` 只有 `[节点私有目录, 项目根]`(`graph-engine.js:4384`),不包含 `runRootDir(run)/artifacts/`。即使 `goal-benchmark` 主动在自己目录写一份 `contract.json` 声明 `produces.acceptance = ["/abs/path/to/artifacts/goal-benchmark.json"]`,这条声明也会因为路径不在 `allowedRoots` 里被判 `bad=true`,等于没声明。 + +--- + +## 三、影响面 + +**任何一家公司只要 org-planner 走 `planner.json` 骨架(当前的常态,见 43 号文档 Phase 2 的真实数据核实),`org-planner-benchmark` 这一步几乎注定会撞这堵墙。** 每次撞墙都沿兜底回退边退回 `org-planner-sdd` 重来一轮(烧一次 sdd-planning 的真实调用 + 一次 goal-benchmark 的真实调用),直到 `loop_count` 撞到 `planner.json` 兜底边的硬顶(`LOOP_HARD_CAP_RUN` 相关,全树 `backward_count` 硬顶 5),最终大概率被强制判 `blocked` 或者 `planner-validator` 跟着一起被跳过。 + +**这很可能正是本 session 全程调试的参考任务(snakegame-co,[43 号文档](./43-task-decomposition-granularity.md) 的触发场景)里 `goal-benchmark`/`planner-validator` 两个节点显示 `skipped` 的真实原因**——不是没跑到,是跑了、正确地写了产物,却被这道闸门反复判定「没写」,最终在某一轮被放弃跳过。 + +连带后果: + +- `artifacts/acceptance.json`(R-19-B 契约,`benchmark`/`test_data`/`test_script` 三段合成)的 `benchmark` 段大概率永远拿不到真实内容——不是 `goal-benchmark` 没算出来,是它算出来的东西没能真正"通过"这一节点的成功判定,图会不断回退重算,直到耗尽回退预算放弃。 +- 连带 43 号文档 Phase 2:`acceptance.md`(不同于此处的 `acceptance.json`,来源不同)不受这条闸门直接影响,但**同一个 `planner.json` 子图反复在这里回退熔断**,会拖慢/阻塞整条 org-planner 子图的收敛,间接影响 Phase 2 的"org-planner 真正判定 success"这一时机能不能正常到达。 + +--- + +## 四、建议修法(未实现,留给下一次专门处理) + +两个方向,第一个更小、更贴合现有 R-19 设计: + +1. **扩大 `readDeclaredProduces` 的 `allowedRoots`**(`graph-engine.js:4384`),把 `runRootDir(run)/artifacts/` 也纳入合法路径;同时让 `goal-benchmark`(以及未来任何 `writesArtifacts:true` 的节点)在写完共享产物后,**也**在自己节点私有目录写一份 `contract.json` 显式声明 `produces.acceptance` 指向那个共享区绝对路径。这样闸门检查的还是"显式声明 + 文件真实存在",只是放宽了声明能指向的位置——跟 R-19 现有的设计哲学一致,改动面最小。 +2. **让 `writesArtifacts: true` 这个字段真正被消费**:`unsatisfiedRequiredProduces` 读节点定义时,如果这个节点标了 `writesArtifacts: true`,改成同时扫 `runRootDir(run)/artifacts/` 目录(不要求 agent 额外写 `contract.json`)。改动面稍大(要把这个字段从模板一路传到 `def.nodes` 里,目前 `readDefGraph` 会不会保留这个字段未核实),但对 agent 侧零改动。 + +两条路都没有实现,需要先确认哪条更契合现有 `def.nodes` 的数据流(`readDefGraph` 目前是否保留模板里的自定义字段),再动手;本文档只记录诊断结果,不展开设计。