Commit 88fe3e446f7ee7a0b9b8f49de4b0a0e567c98f08
1 parent
87a2cee7
docs(task-decomposition): 46号文档 Phase 3(任务池认领)详细设计
跟用户对齐关键架构决定:可认领子任务复用 tasks 表(新增 subtask_kind 字段区分 'acceptance_item'/'claimable_work',不新开表)。 设计中定位两处要点: - 必须同步改的坑:settleNode 的 SUBTASKS_INCOMPLETE 硬闸门要收窄成只查 subtask_kind='acceptance_item',否则新物化的 claimable_work 行会让所有 开了 subtask-planning 的 task 验收判定失控。 - 重要化简:认领成功后直接复用现成的 executor='agent' 派单闭环(认领人 自己搞定、自己 /task/report 上报),不需要给 graph-engine.js 加认领 调度层,比文档原先设想的"改动面最大的一条"轻得多。 只是设计,未写代码。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H85SS5hVC4u1PbVhjmrhH8
Showing
1 changed file
with
89 additions
and
1 deletions
docs/todo/system-design/46-mandatory-planning-and-task-decomposition.md
| 1 | # 46 · Planning 应作为每个任务的默认前置步骤,覆盖三类信息来源 | 1 | # 46 · Planning 应作为每个任务的默认前置步骤,覆盖三类信息来源 |
| 2 | 2 | ||
| 3 | -> **状态**: 📝 设计中——四个关键决策已拍板。**Phase 0 已完成**(2026-09-01 真实回归验证):链路能收敛、44 号文档修复确认生效,发现的独立缺陷已拆到 [47 号文档](./47-itemized-subtask-materialization-goal-benchmark-fallback.md) 并修复。**Phase 1 代码已实现**(2026-09-02,见「Phase 1 实现记录」):新 agent `subtask-planning` + `enqueueTaskRun` 挂钩(收窄为显式 opt-in)+ 复用 R-9.3 任务级共享索引对接下游,单测覆盖,**但还没做真实端到端回归**(且过程中出过一次真实 CLI 子进程泄漏事故,已修复并加了测试环境防护,见该节)。**Phase 2 已实现且已做真实端到端回归(收窄版,2026-09-02,见「Phase 2 实现记录」)**:经验检索(新查询能力,扫已雇 agent 的 dream 沉淀)+ 按需 deep research(图外调用 org-hunt,默认关闭)。真实回归第一次就跑出一个真缺陷(关键词打分噪音压过信号,导致"经验库有答案"分支误触发)并已修复验证;两条真实任务的分解内容质量都好,但 deep research 分支目前更接近"多问一个角色的第二意见"而非真的做了外部检索(org-hunt 那次调用没有工具调用痕迹),如实记在实现记录里。"历史同类 goal 检索"这条来源本轮明确收窄未做。**Phase 3-5 涉及核心调度引擎范式级改动(任务池认领)+ 全新子任务级预算模型,风险量级不同,本轮不做,另起设计讨论** | 3 | +> **状态**: 📝 设计中——四个关键决策已拍板。**Phase 0 已完成**(2026-09-01 真实回归验证):链路能收敛、44 号文档修复确认生效,发现的独立缺陷已拆到 [47 号文档](./47-itemized-subtask-materialization-goal-benchmark-fallback.md) 并修复。**Phase 1 代码已实现**(2026-09-02,见「Phase 1 实现记录」):新 agent `subtask-planning` + `enqueueTaskRun` 挂钩(收窄为显式 opt-in)+ 复用 R-9.3 任务级共享索引对接下游,单测覆盖,**但还没做真实端到端回归**(且过程中出过一次真实 CLI 子进程泄漏事故,已修复并加了测试环境防护,见该节)。**Phase 2 已实现且已做真实端到端回归(收窄版,2026-09-02,见「Phase 2 实现记录」)**:经验检索(新查询能力,扫已雇 agent 的 dream 沉淀)+ 按需 deep research(图外调用 org-hunt,默认关闭)。真实回归第一次就跑出一个真缺陷(关键词打分噪音压过信号,导致"经验库有答案"分支误触发)并已修复验证;两条真实任务的分解内容质量都好,但 deep research 分支目前更接近"多问一个角色的第二意见"而非真的做了外部检索(org-hunt 那次调用没有工具调用痕迹),如实记在实现记录里。"历史同类 goal 检索"这条来源本轮明确收窄未做。**Phase 3 设计已细化(2026-09-02,见「Phase 3 详细设计」,未实现)**:跟用户对齐了最关键的架构决定——可认领子任务复用 `tasks` 表(加 `subtask_kind` 字段区分类型,不新开表)。设计过程中发现一处必须同步改的向后兼容坑(`settleNode` 的 `SUBTASKS_INCOMPLETE` 闸门要收窄,否则会被新物化的行误伤),以及一处重要化简(认领后复用现成的 `executor='agent'` 派单闭环,不需要碰 `graph-engine.js`,比最初设想的"要给核心调度引擎加认领层"轻得多)。Phase 4(独立开发验证循环)、Phase 5(看板 UI)仍未设计,留给后续 |
| 4 | > **日期**: 2026-09-01 | 4 | > **日期**: 2026-09-01 |
| 5 | > **触发场景**: 用户复盘 [43 号文档](./43-task-decomposition-granularity.md)(goal 分解粒度)时,追问"为什么本地和线上所有公司的任务看板都没有子任务分解"。核实后发现:不是巧合或配置遗漏,`acceptance_mode='itemized'` 在当前系统里**从未被真正触发并落库过一次**——追问引出了比 43 号文档更大的架构缺口:43 号文档只解决"验收要不要拆细",而用户认为**规划(把一件事拆成第一步/第二步/第三步)本身应该是每个任务默认必经的前置步骤,不是一个可选的、可以被判"不需要"的例外**。 | 5 | > **触发场景**: 用户复盘 [43 号文档](./43-task-decomposition-granularity.md)(goal 分解粒度)时,追问"为什么本地和线上所有公司的任务看板都没有子任务分解"。核实后发现:不是巧合或配置遗漏,`acceptance_mode='itemized'` 在当前系统里**从未被真正触发并落库过一次**——追问引出了比 43 号文档更大的架构缺口:43 号文档只解决"验收要不要拆细",而用户认为**规划(把一件事拆成第一步/第二步/第三步)本身应该是每个任务默认必经的前置步骤,不是一个可选的、可以被判"不需要"的例外**。 |
| 6 | > **格式对齐** `27-requirement.md`/`43-task-decomposition-granularity.md`:原话照搬 + 结构化复述,现状调研另起一节。 | 6 | > **格式对齐** `27-requirement.md`/`43-task-decomposition-granularity.md`:原话照搬 + 结构化复述,现状调研另起一节。 |
| @@ -315,3 +315,91 @@ goal 创建 | @@ -315,3 +315,91 @@ goal 创建 | ||
| 315 | **两条真实任务的分解内容质量都相当好**:贪吃蛇任务给出"先定义状态 → 渲染循环 → 输入监听 → 移动逻辑 → 吃食物 → 碰撞检测"六步,依赖关系合理;Markdown 工具任务甚至体现出"代码块围栏必须最先扫描保护,否则块内的 `#`/`**`/`[]` 会被后续正则误转换"这种真实的解析器实现顺序知识。这直接回答了 Phase 1/2 一直悬而未决的"内容有没有用"——回答是有用。 | 315 | **两条真实任务的分解内容质量都相当好**:贪吃蛇任务给出"先定义状态 → 渲染循环 → 输入监听 → 移动逻辑 → 吃食物 → 碰撞检测"六步,依赖关系合理;Markdown 工具任务甚至体现出"代码块围栏必须最先扫描保护,否则块内的 `#`/`**`/`[]` 会被后续正则误转换"这种真实的解析器实现顺序知识。这直接回答了 Phase 1/2 一直悬而未决的"内容有没有用"——回答是有用。 |
| 316 | 316 | ||
| 317 | **一个诚实的局限,没有掩盖**:查了 org-hunt 那次调用的日志,全程没有 `l1-web-fetch` 工具调用痕迹——它大概率是直接用自己训练知识回答的,没有真的去查外部资料。轻量提示词只要求"给一段总结",没有强制要求引用可验证来源、没有强制要求真的调用检索工具。这意味着当前的 `"deep_research"` 分支实际提供的价值更接近"多问一个角色的第二意见",不是名副其实的外部调研——如果以后要坐实"真的查了外部资料"这件事,需要在提示词里明确要求工具调用证据(比如强制要求 sources 是真实可验证 URL),这次没有额外去做,留给以后需要更强调研可信度时再补。 | 317 | **一个诚实的局限,没有掩盖**:查了 org-hunt 那次调用的日志,全程没有 `l1-web-fetch` 工具调用痕迹——它大概率是直接用自己训练知识回答的,没有真的去查外部资料。轻量提示词只要求"给一段总结",没有强制要求引用可验证来源、没有强制要求真的调用检索工具。这意味着当前的 `"deep_research"` 分支实际提供的价值更接近"多问一个角色的第二意见",不是名副其实的外部调研——如果以后要坐实"真的查了外部资料"这件事,需要在提示词里明确要求工具调用证据(比如强制要求 sources 是真实可验证 URL),这次没有额外去做,留给以后需要更强调研可信度时再补。 |
| 318 | + | ||
| 319 | +--- | ||
| 320 | + | ||
| 321 | +## Phase 3 详细设计(2026-09-02,设计已细化,未实现) | ||
| 322 | + | ||
| 323 | +### 前置决策(跟用户对齐) | ||
| 324 | + | ||
| 325 | +Phase 3 最关键的架构分叉——可认领子任务要不要复用现有 `tasks` 表的 `parent_task_id` 机制——**已跟用户对齐:复用 `tasks` 表,加字段区分类型**。原因见下节,这条决定直接决定了后面所有设计。 | ||
| 326 | + | ||
| 327 | +### 数据模型:新增 `subtask_kind` 字段 + 两个新 `status` 值 | ||
| 328 | + | ||
| 329 | +`tasks` 表新增列 `subtask_kind`(`NULL` | `'acceptance_item'` | `'claimable_work'`),CHECK 约束仿 `acceptance_mode` 那次迁移的写法(batch 迁移): | ||
| 330 | + | ||
| 331 | +- `NULL`:普通 task(现状不变,绝大多数行)。 | ||
| 332 | +- `'acceptance_item'`:43 号文档现有机制——`maybeMaterializeItemizedSubtasks` 从 `acceptance.md`/`goal-benchmark.json` 物化出的验收条目,**行为完全不变**,只是把它们打上一个明确的类型标签(之前隐含在"parent_task_id 非空"里,现在显式区分)。 | ||
| 333 | +- `'claimable_work'`:Phase 3 新增——`subtask-planning` 产出的 `subtask-plan.json` 里 `steps[]` 物化出的可认领工作项。 | ||
| 334 | + | ||
| 335 | +两者都用 `parent_task_id` 指向同一条父 task,语义/生命周期完全独立,靠 `subtask_kind` 分流——这是为什么不需要开新表:现有 `tasks` 表本来就是"通用工作项容器",`subtask_kind` 只是给容器里已经混杂的两种东西正名,不是往一个干净的模型里硬塞第二套语义。 | ||
| 336 | + | ||
| 337 | +`status` 枚举新增两个值:`'claimable'`(已物化、开放认领、还没人要)、`'claimed'`(已被认领,等待被处理——处理方式见下"认领后怎么办"一节)。只对 `subtask_kind='claimable_work'` 的行有意义,其余行继续用现有 `draft|queued|running|completed|failed|blocked|cancelled` 六态,不受影响。新增 `claimed_at` 时间戳列(认领发生的时间,用于超时/僵尸认领巡检——复用心跳巡检已经在用的"时间戳 + 巡检" 模式,不新造一套监控)。 | ||
| 338 | + | ||
| 339 | +### 必须同步改的一处:`settleNode` 的 `SUBTASKS_INCOMPLETE` 硬闸门要收窄 | ||
| 340 | + | ||
| 341 | +`graph-engine.js:1881-1889`(43 号文档实现)现在是: | ||
| 342 | + | ||
| 343 | +```js | ||
| 344 | +// coder-validator 报 success 时,查 parent_task_id 指向该 task 的所有行是否 completed | ||
| 345 | +SELECT id, status FROM tasks WHERE parent_task_id = ? | ||
| 346 | +``` | ||
| 347 | + | ||
| 348 | +Phase 3 一旦开始物化 `claimable_work` 行,这条查询会立刻把它们也扫进去——一堆天然不会是 `completed`(它们的终态是 Phase 4 才要定义的东西)的 `claimable_work` 行会让这条闸门对**所有**开了 `subtask-planning` 的 task 都误判 `SUBTASKS_INCOMPLETE`,把本该正常通过的验收打成失败。**这是实现时第一件必须做的事,不是可选项**:查询加一条 `AND subtask_kind = 'acceptance_item'`,把两套子任务的完成判定彻底分开。 | ||
| 349 | + | ||
| 350 | +### 触发/物化时机:跟 `subtask-planning` 同一时刻,不等 org-planner | ||
| 351 | + | ||
| 352 | +43 号文档的验收条目物化时机是"org-planner 完成后"(依赖它的产出)。Phase 3 的物化对象不一样——`subtask-plan.json` 在 `enqueueTaskRun` 时就异步产出了(`triggerSubtaskPlanning`),比 org-planner 开始规划早得多。**物化时机定在 `triggerSubtaskPlanning` 成功写完 `subtask-plan.json` 之后立刻做**,不拖到 org-planner 跑完: | ||
| 353 | + | ||
| 354 | +- 呼应用户原始诉求"分解任务 → 所有 agents 围绕这个任务看板来工作,一层一层地接子任务"——如果拖到 org-planner 整个流程跑完才有"看板","围绕看板协作"这句话在 org-planner 执行期间根本无从谈起。 | ||
| 355 | +- 技术上更省事:`triggerSubtaskPlanning` 已经是 best-effort、绝不 throw 的收尾逻辑,加一步物化不引入新的失败模式(复用同一套"软失败,只打日志"的处理)。 | ||
| 356 | + | ||
| 357 | +具体实现:`steps[].depends_on_hint` 是这批 steps 内部的 1-based `seq` 引用,物化时先建好全部行拿到真实 `task.id`,再回填 `depends_on`(复用 `tasks.depends_on` 现有的"JSON 数组存 task id"格式,不新造依赖表示法)——`depends_on_hint` 本身"是提示不是硬依赖"这条设计(Phase 1 agent.md 就这么写的)延续下来:Phase 3 物化后的 `depends_on` 同样只做展示,**不做认领前置校验**(见下"明确不做")。 | ||
| 358 | + | ||
| 359 | +### 认领 API:这是本轮唯一真正的新机制 | ||
| 360 | + | ||
| 361 | +调研确认:**全仓库目前没有任何"认领/接单(pull)"语义的端点**——`executor='agent'` 那条路径是纯粹的"派发(push)":发送方在 `/task/dispatch` 请求体里显式指定 `to`,从没有过"多方竞争同一条任务,谁先抢到算谁的"这种场景,所以也没有任何 CAS 并发保护先例可抄。这是 Phase 3 唯一必须新写的东西: | ||
| 362 | + | ||
| 363 | +``` | ||
| 364 | +POST /api/company/:companyId/tasks/:taskId/claim {agentId} | ||
| 365 | +``` | ||
| 366 | + | ||
| 367 | +服务端逻辑(仿 `claimQueuedTask` 的 CAS 写法,`work-graph-db.js:2209`): | ||
| 368 | + | ||
| 369 | +```sql | ||
| 370 | +UPDATE tasks SET status='claimed', assignee_agent_id=?, claimed_at=? | ||
| 371 | +WHERE id=? AND subtask_kind='claimable_work' AND status='claimable' | ||
| 372 | +``` | ||
| 373 | + | ||
| 374 | +`changes === 0` 就是"没抢到"(要么已被别人认领,要么状态不对),返回 409,不是 500——这是预期会发生的正常竞争结果,不是错误。对称补一个 `POST .../unclaim`(`status='claimed' → 'claimable'`,`assignee_agent_id` 清空)——认领了发现做不了要能退回去,成本很低,没理由不做。 | ||
| 375 | + | ||
| 376 | +**认领角色范围**:不引入能力匹配(调研确认 `system_agents`/`company_agents` 表没有任何结构化角色能力字段,能不能干一件事完全靠 agent 自己读 `agent.md` 判断,代码层不做匹配)。Phase 3 用最简单的白名单:`claimable_work` 行默认只对 `role_name='org-coder'` 开放认领(当前系统唯一的通用"写代码"角色,且物化出的 steps 基本都是实现类工作),认领 API 校验调用者的 `role_name` 是否在白名单里,不在直接拒绝。这个白名单先写死,不是留数据库字段——真需要扩展到多角色再加,不为假想的未来预先建模。 | ||
| 377 | + | ||
| 378 | +### 认领后怎么办:复用现成的 agent 派单路径,不新起执行机制 | ||
| 379 | + | ||
| 380 | +这是这轮设计里最重要的一处化简,值得单独说清楚:最初以为"认领后要真正驱动执行"必然要碰 `graph-engine.js`(比如给每个 `claimed` 的子任务起一次独立 work-graph run),那就是 Phase 4 的范畴,而且是范式级改动。但调研 `executor='agent'` 那条路径后发现——**它本来就是"一条 task 归某个 agent 负责、这个 agent 自己想办法搞定、搞定后自己调 `/task/report` 上报"的完整闭环**,跟"认领了之后谁去干"这个问题的答案高度重合。 | ||
| 381 | + | ||
| 382 | +所以 Phase 3 的落地方案是:`claim` 成功后,除了状态转成 `claimed`,**同时把这条行的 `executor` 设成 `'agent'`**——这样它立刻变成一条普通的"agent 自主任务",认领它的 agent(多半是 org-coder)用它已经在用的既有机制(自己决定怎么干、干完调 `/task/report`)走完剩下的生命周期,`META_TO_TASK_STATUS` 状态机(`agent-chat.js:1795`)直接接管后续的 `dispatched → done/failed/blocked/cancelled` 转移,**不需要 Phase 3 再发明一套新的执行触发逻辑**。`task-scheduler.js` 明确只认 `executor='graph'`(367 行注释已经写了这条判据),所以这条行天然不会被现有的图排队/并发逻辑碰到,两套系统互不干扰。 | ||
| 383 | + | ||
| 384 | +这个方案的好处:Phase 3 做完,"认领"不是一个没人会因此真正干活的静态标记,是一个立刻能跑的真实闭环,而且**零新增执行机制、不碰 graph-engine 一行代码**,完全落在 Phase 1 已经定案的边界内("图本身怎么跑不变")。代价是诚实的:`org-coder` 认领到的这一步"怎么干"完全靠它自己(可能是直接写代码、可能是走它熟悉的其他工具),没有 Phase 4 设想的那种"独立开发 + 独立验证"的正式生命周期(写代码有没有经过验证、有没有走测试,全凭 org-coder 自己负责)——这正是 Phase 4 真正要解决的问题:现在这条最小可用路径能跑,但"跑得糙",要不要、以及怎么给它补上正式的验证循环,留给 Phase 4 设计时决定。 | ||
| 385 | + | ||
| 386 | +### 明确不做(本轮范围边界) | ||
| 387 | + | ||
| 388 | +- **不做依赖顺序强制**:`depends_on` 只展示,不阻止认领——判断"前一步是否真的做完了"需要一个可靠的完成判据,Phase 3 阶段(`executor='agent'` 自主执行)没有这个判据,强行卡会卡出一堆认领不了的死锁。 | ||
| 389 | +- **不做能力匹配**:认领角色白名单写死 `['org-coder']`,不引入任何"这一步该给谁"的推断逻辑。 | ||
| 390 | +- **不碰 `graph-engine.js` 执行/调度模型**:认领这件事完全在 `tasks` 表 + 一个新端点里完成,不新增图节点类型、不改拓扑推进逻辑。 | ||
| 391 | +- **不做 Phase 4 设想的"独立开发+验证循环"**:认领后的执行完全复用现有 `executor='agent'` 闭环,`org-coder` 自己对交付质量负责,没有独立验证阶段。 | ||
| 392 | +- **不做看板 UI(Phase 5)**:可认领池先靠 `GET /:companyId/tasks?subtaskKind=claimable_work&status=claimable` 这类查询参数(复用现有 `parentTaskId` 过滤模式加一个新参数)暴露数据,UI 展示留给 Phase 5。 | ||
| 393 | + | ||
| 394 | +### Phase 3 验收标准(后续实现完要检查的) | ||
| 395 | + | ||
| 396 | +1. `subtask-plan.json` 产出后,`claimable_work` 子任务是否正确物化(`depends_on` 回填正确、`status='claimable'`)? | ||
| 397 | +2. 有 `subtask-planning` 挂钩的 task,`coder-validator` 的验收判定是否完全不受这些新增 `claimable_work` 行影响(`SUBTASKS_INCOMPLETE` 闸门收窄生效)? | ||
| 398 | +3. 并发认领同一条子任务,是否只有一方成功(CAS 生效,409 不是 500)? | ||
| 399 | +4. 认领成功后,这条行是否真的能走完 `executor='agent'` 的既有生命周期(`org-coder` 自己完成、`/task/report` 上报、状态正确落到终态)? | ||
| 400 | +5. 非白名单角色尝试认领,是否被正确拒绝? | ||
| 401 | + | ||
| 402 | +### 遗留给 Phase 4 的问题 | ||
| 403 | + | ||
| 404 | +- 现在"认领后怎么干"完全靠 `org-coder` 自主决定,质量没有保障——要不要给 `claimed` 状态补一段正式的"开发 + 验证"生命周期?如果要,是新起一次简化版 work-graph run,还是在现有 `executor='agent'` 闭环里加一道可选的验证钩子(成本更低,但仍然不是"独立 run")?两个方向哪个都不是这次 Phase 3 该决定的事,需要单独评估。 | ||
| 405 | +- 子任务级预算:`executor='agent'` 现有闭环目前完全没有预算概念(`budget_json` 只在 `executor='graph'` 的 run 上有意义)——如果 Phase 4 决定要给认领的子任务设预算,需要先补这个空白。 |