From 2abb08205a3683f6866ce3401b7ab7e50665bf30 Mon Sep 17 00:00:00 2001 From: yibo Date: Tue, 18 Aug 2026 14:37:17 +0800 Subject: [PATCH] docs(27-P3C): 五条收尾定案 —— goal 验收用 org-validator,两套任务树统一 --- docs/todo/system-design/27-p3c-goal-task-model.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- docs/todo/system-design/27-requirement.md | 22 ++++++++++++++++++++++ 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/docs/todo/system-design/27-p3c-goal-task-model.md b/docs/todo/system-design/27-p3c-goal-task-model.md index ed9d40a..0360741 100644 --- a/docs/todo/system-design/27-p3c-goal-task-model.md +++ b/docs/todo/system-design/27-p3c-goal-task-model.md @@ -18,6 +18,9 @@ > > **第三轮(2026-08-18,四条口径由用户定案 → 27-REQ R-20.5)**:① **`task-planning` agent 提进 v1**(原计划 v2),新增 §四.9,批 2 范围与工作量随之变化;② 并行仍按批 4 单独排期,v1 只做排队(`max_parallel_tasks = 1`,唯一索引不动);③ manual 任务不参与自动出队(§五.2bis);④ 上游失败 → 下游自动推导为 `blocked` 并标红(§五.1.2)。 > 定案 ① 连带牵出一处新缺陷:**`task-planning` 跑在 goal 层、任何 run 存在之前,它烧的 token 没有预算池可扣** —— 正是 `defaultPlanWorkGraph` 注释里点名的「隐形的一半花费」的新实例。上一轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以记账字段 `planning_tokens` 的形式回来(§四.1)。 +> +> **第四轮(2026-08-18,五条口径由用户定案 → 27-REQ R-20.6)**:① 退役 `/goals` 两个 OKR 别名**确认执行**;② **goal 级验收仍用 `org-validator`**,在 goal 层再调一次(新增 §四.10,goal 状态机加 `failed`);③ **预算先不考虑**,不做 goal 级闸门,但保留 `planning_tokens` / `validation_tokens` 两个记账字段;④ **`queued` 开放改 `depends_on`**(§六.3);⑤ **两套任务体系统一到 `tasks` 表,删掉会话反推那套**(新增 §十二 = 批 5)。 +> 定案 ⑤ 核实后有个反直觉的前提必须先说清:**两套不是并列的,A 的回调链路长在 B 身上** —— `reportRunOutcome` 收敛后调的正是 `/api/agent/task/report`。所以「删掉另一套」= 删**存储与会话反推**,**不能**删入口与回调(`/task/dispatch` 的 URL 与请求体写在 `tools-base.md` 里,改它等于同时改所有 agent 的提示词)。开放问题 2 / 4 / 6 / 7 全部就此关闭。 --- @@ -93,7 +96,7 @@ goals (G-xxx) 人类下达。一条 = 一个需求 = 一条验收边 | 层 | 回答的问题 | 谁改它 | 状态机 | |---|---|---|---| -| goal | 人想要什么?做到了没有? | 人 | `draft → planning → active → done / cancelled / archived` | +| goal | 人想要什么?做到了没有? | 人 + **goal 级 `org-validator`**(§四.10)| `draft → planning → active → done / failed / cancelled / archived` | | task | 这个需求拆成哪几件事?谁先谁后? | **`task-planning` agent 拆、人可改**(§四.9)| `draft → queued → running → completed / failed / blocked / cancelled` | | run | 这一次跑成什么样? | 引擎 | 既有 `RUN_TRANSITIONS` + 新增 `queued`(§四.3) | @@ -120,13 +123,16 @@ CREATE TABLE IF NOT EXISTS goals ( title TEXT NOT NULL, -- 短标题,列表显示用;可改 description TEXT NOT NULL, -- 人类需求原文 = 验收边界;下达后不可改 status TEXT NOT NULL DEFAULT 'draft', - -- draft | planning | active | done | cancelled | archived + -- draft | planning | active | done | failed | cancelled | archived -- planning = task-planning 正在拆分(§四.9),是个真会持续几十秒的状态 + -- failed = goal 级验收没过,或有 task 未能 completed(§四.10);可回 active 补 task 重下 priority TEXT NOT NULL DEFAULT 'P1', okr_node_id TEXT, -- 可选,挂到 OKR 度量树的哪个节点(O-001.1.1) created_by TEXT, -- 'human' | agent id(Master/CEO 代下单) report_to TEXT, -- 收敛后要唤醒的上级 agent,见 §四.7 - planning_tokens INTEGER NOT NULL DEFAULT 0, -- task-planning 这一次拆分烧掉的 token,见 §四.9 + planning_tokens INTEGER NOT NULL DEFAULT 0, -- task-planning 拆分烧掉的 token(§四.9) + validation_tokens INTEGER NOT NULL DEFAULT 0, -- goal 级 org-validator 烧掉的 token(§四.10) + verdict TEXT, -- goal 级验收判词(org-validator 的原话),§四.10 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, dispatched_at TEXT, -- 首次下达时间;非空 = 描述已冻结 @@ -311,6 +317,42 @@ AgentManager 调 task-planning(输入:goal.description + 公司 org graph æ‘ `POST /goals` 带 `split: false` 时不调 agent,直接建 1 条同描述的 task。存量调用方(`POST /:cid/tasks`)走的就是这条路 —— **它们不该因为这次改动突然多花一次 LLM 调用**。 +### 4.10 goal 级验收 —— 仍然是 `org-validator`(用户定案) + +**不新造验收 agent**:goal 级用的还是 `org-validator` 这个本体,只是**在 goal 层再调一次**。 + +#### 为什么是「再调一次」而不是复用 task 级那次的结论 + +因为两次验的是**基数不同的东西**,这正是 27-NAMING §四 那张表的论证: + +| | 验什么 | 谁问的 | +|---|---|---| +| task 级(既有,org graph 末端节点)| 这**一个 task** 的 `task-goal` 达成了没有 | 引擎,每个 task 各一次 | +| **goal 级(本次新增)** | **N 个 task 合起来,人提的那个需求达成了没有** | goal 收敛时,一次 | + +5 个 task 全 `completed`,只说明每一件事都按各自的描述做完了 —— 不说明拼起来是人要的东西。这个缺口正是 goal 层存在的理由,用别的东西替代不了。 + +#### 触发与判定 + +``` +goal 下所有 task 到终态 + ├─ 有任何一条 ≠ completed → goal 直接 failed,不调 agent(省一次调用:显然没交付) + ├─ 全 completed 且只有 1 条 task → 跳过 goal 级验收,直接采用该 task 的验收结论 + │ (单 task 的 goal,两级验收是同一件事,再调一次纯浪费) + └─ 全 completed 且 ≥2 条 task → 调一次 org-validator + 输入:goal.description(= 验收边界)+ 该 goal 全部 task 的产物索引 + (`graph_files` 按 task_id 查,§P4 §4.6 的表已经能给出这份清单) + 产出:pass / fail + 判词 → goals.status = done / failed,判词落 goals.verdict +``` + +调用形态与 `task-planning` 相同:`AgentManager` 直调本体,不在任何 graph run 里,token 记 `goals.validation_tokens`。 + +#### 挂在哪 + +和调度器同一个位置:task 转终态时顺手判一次「这个 goal 的 task 是不是都到齐了」。**`settleGoal(goalId)` 必须幂等**(并行完成的多个 task 会同时触发它),用 `goals.status` 的条件更新做闸门 —— 与 §五.1 出队用的是同一招。 + +> goal 收敛之后才轮到 §四.7 的两件事:写 OKR 节点(一次,用 goal 的判定结果)、唤醒 `report_to`。**这两件事从 run 级上移到 goal 级**,禁令一说的「task 的 run 一律不带 `okr_node_id`」在这里闭环。 + --- ## 五、排队与并行 @@ -440,7 +482,7 @@ for (const t of queuedTasks) { |---|---|---| | `GET` | `/api/company/:cid/tasks` | **新端点**(核实过:今天这个路径下只有 `POST`,没有 `GET`,不存在兼容问题)。返回 task 表的行,含 `goalId` / `status` / `queuePosition` / `progress` / `runId` | | `POST` | `/api/company/:cid/tasks` | **保留、行为兼容**:不带 `goal_id` 调用时,**自动建一个同描述的 goal**(`created_by` 记调用方)再挂一条 task —— 存量调用方(含 `DispatchTaskButton`、外部 agent 的 curl)一行不用改就还能用,响应体里多出 `goalId` 字段。带 `goal_id` 则挂到既有 goal 下 | -| `PATCH` | `/api/company/:cid/tasks/:taskId` | 改 `title` / `priority`;`depends_on` 仅 `draft` 可改;`queued_at` 非空时带 `description` / `mode` / `budget` 一律 `409` | +| `PATCH` | `/api/company/:cid/tasks/:taskId` | 改 `title` / `priority`;**`depends_on` 在 `draft` 与 `queued` 都可改**(用户定案,见下);`queued_at` 非空时带 `description` / `mode` / `budget` 一律 `409` | | `POST` | `/api/company/:cid/tasks/:taskId/enqueue` | 单条入队:建 `queued` 的 run 行 + 写 `task-goal.json` + `tick()` | | `POST` | `/api/company/:cid/tasks/:taskId/cancel` | `queued` → 撤出队列(run `queued → stopped`);`running` → 转调既有 `POST /api/graph-runs/:id/stop` | | `POST` | `/api/company/:cid/tasks/:taskId/rerun` | 从 `failed` / `blocked` / `completed` 重新入队,`attempt + 1`,**新起一条 run**(旧 run 保留在历史里)。**刻意不叫 `retry`** —— 见下 | @@ -448,6 +490,11 @@ for (const t of queuedTasks) { **`rerun` ≠ `retry`,两者会撞车。** 既有的 `POST /api/graph-runs/:id/nodes/:nodeId/retry` 是**节点级重试**:同一条 run 内把某个节点打回 `ready` 重跑(`blocked` 的唯一出路,`NODE_TRANSITIONS` 里写死的)。新加的是**任务级重跑**:新起一条 run,整张图从头来。两个都叫 retry,实现时必然有人把新端点接到旧逻辑上(或反过来)。所以任务级一律叫 `rerun`,文档、API、界面按钮文案(「重跑这个任务」vs 节点详情里的「重试这一步」)三处同时区分。 +**`queued` 状态开放改依赖**(用户定案)。真实场景是「B 已经排上了,才发现它得等 C」,此前只能取消重建、丢掉排队位次。三条细则: +- 环检测照跑(只能引用同 goal 内的 task),有环 `400`,库里不留半条脏数据; +- **位次不受影响**:`queuePosition` 只按 `priority → seq → created_at` 排,`depends_on` 决定的是「够不够格出队」,两者正交 —— 当初担心的「改完位次怎么算」不成立; +- 新加的依赖若已经 `failed` / `cancelled`,下一次 `tick` 会自然把它推导成 `blocked`(§五.1.2),不需要额外处理。 + **`POST /:cid/tasks` 的 409 消失了**:并发冲突不再是错误,是排队。响应体改为 `201 { taskId, goalId, runId, status: 'queued', queuePosition: 2 }`。`queuePosition` 是这次改动里对用户最直观的那个字段 —— **它的排序口径必须与调度器第 4 步逐字一致**(`priority → seq → created_at`),两处各写一份的下场已经有先例:`canDispatch` 就是因为前端自己复述了一遍取值域、还多写了一个 `blocked`,导致公司失败一次后再也下不了任务。**口径只留调度器一处,端点调用它算位置。** ### 6.4 不动的端点 @@ -585,9 +632,9 @@ for (const t of queuedTasks) { 2. **拆分质量决定预算消耗**。拆出 5 条 task 就是 5 份 `DEFAULT_TASK_BUDGET`(各 100 次调用 / 500 万 token)。这是「产出一律 `draft`、必须人确认」的根本理由 —— 那一步是唯一一道成本闸门。界面上要显式提示「N 个任务 ≈ N × 单任务成本」。 3. **新本体不登记 `_registry.json` 会直接把 CI 跑红**。27-P3/P4 §八.21 有前科(六个本体建了目录没登记,`template-refs.test.js` 当场红)。建目录与登记必须在同一个 commit 里。 -### 5. 两套 `T-` id +### 5. 两套 `T-` id —— 由 §十二 的统一消解,但在那之前是真风险 -`agent-chat.js` 的 tasklog 也发 `T-` 开头的 id,本次不动它。两者从此**长得像但不同源**,读日志时别混;本文档的 task id 一律带 `-{4hex}` 后缀,肉眼可分。 +`agent-chat.js` 的 `/task/dispatch` 也发 `T-` 开头的 id 并写 `system/tasklog/`。**批 1-4 期间两套并存**,长得像但不同源,读日志时别混(本文档的 id 一律带 `-{4hex}` 后缀,肉眼可分)。批 5(§十二)把存储统一到 `tasks` 表之后,这个风险随之消失 —— 那时 `T-` 只有一个来源。 ### 6. 并行是最后一批,且要单独验 @@ -603,6 +650,7 @@ for (const t of queuedTasks) { | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 | | **3 · 排队** | `run_status='queued'` + `RUN_TRANSITIONS` + `syncRunNodesToDef` + `task-scheduler`(漏斗钩子 + 三个辅助触发 + 重入保护 + 上游失败推导 blocked)+ `depends_on` 环检测 + 入队/取消/rerun 端点 + §四.7 两条禁令 + §九.1 全部回归点 | 2 / 3 / 5 / 6 / 7 / 8 / 9 / 13-18 / 23 | | **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit | 另立验收 | +| **5 · 任务体系统一**(单独排期)| 见 §十二:`executor` / `assignee_agent_id` / `parent_task_id` 三字段 + `/task/dispatch` 换存储 + 删会话反推 + `tasklog` 回填 | 25-30 | > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。 @@ -613,9 +661,76 @@ for (const t of queuedTasks) { ## 十一、开放问题 1. ~~谁把 goal 拆成 task?~~ **已定案(2026-08-18):`task-planning` agent 进 v1**,产出 `draft` 任务清单由人确认后下达,详见 §四.9。 -2. **goal 级的验收谁做?** 现在 `org-validator` 验的是 task 级(`task-goal`)。一个 goal 的 5 个 task 全 `completed`,不等于人的需求达成了。要不要在 goal 收敛时再跑一次 goal 级验收(`goal-benchmark` 天然就是干这个的)?倾向做,但需要先定「goal 级验收的输入是什么」—— 是 N 个 task 的产物合集吗?留到 P5。 +2. ~~goal 级的验收谁做?~~ **已定案(2026-08-18):仍然是 `org-validator`**,在 goal 层再调一次同一个本体,输入是该 goal 全部 task 的产物合集。详见 §四.10。 3. ~~失败的 task 挡不挡住依赖它的下游?~~ **已定案(2026-08-18):自动转 `blocked` 并在界面标红**,给「重跑上游」与「强制放行」两个出路;`blocked` 每次 `tick` 重算而不落死,上游恢复后下游自己回 `queued`。详见 §五.1.2。 -4. **跨公司任务总览**。`/api/tasks` + `TaskTimeline` 是从会话反推的另一套任务树,与本文档的 `tasks` 表同名不同源。将来是把它改成读这张表,还是并存?本次不碰,但这是个明确待收的口子。 +4. ~~跨公司任务总览 / 两套任务树并存~~ **已定案(2026-08-18):统一到 `tasks` 表一套,删掉会话反推那套。** 详见 §十二(批 5)。 5. **`max_parallel_tasks` 放 `company.json` 还是建 `company_settings` 表?** v1 选前者(`companyService.get` 已经在读那个文件,零新增读路径)。公司级配置项超过 3 个时再考虑建表。 -6. **goal 级总预算怎么定口径?** §四.1 砍掉了 `goals.budget_json`,因为预算闸门只认 root run,goal 级没有执行机制。真要做,三选一:(a) 建 task 时按条数平分 goal 额度;(b) 预算池上移到 goal,`spendBudget` 从 `root_run_id` 改成上溯 `goal_id`(改动最大但语义最正);(c) 不做 goal 级,只在界面上把 N 个 task 的消耗加起来显示。**倾向 (c)** —— 一个 goal 的 task 数会变(rerun、后补 task),平分和总池都会让"还剩多少"变成一个解释不清的数。 -7. **排队中的任务能不能改依赖?** 现在 `depends_on` 只有 `draft` 可改。但真实场景是「B 已经排上了,才发现它得等 C」—— 这时只能取消重建,会丢掉排队位次。要不要给 `queued` 开一个只改 `depends_on` 的口子?开的话要防「改出环」和「改完位次怎么算」。 +6. ~~goal 级总预算怎么定口径?~~ **已定案(2026-08-18):预算先不考虑,不做 goal 级闸门。** `goals.budget_json` 保持砍掉。**但 `planning_tokens` / `validation_tokens` 两个记账字段保留** —— 它们不是闸门,是让 goal 层那两次 agent 调用的花费**可见**;不记就是 `defaultPlanWorkGraph` 注释点名的「隐形的一半」,而且界面上「本次拆分消耗 ~N tokens」这个确认步骤要用它。 +7. ~~排队中的任务能不能改依赖?~~ **已定案(2026-08-18):可以改。** `queued` 状态开放 `depends_on` 的 `PATCH`,见 §六.3。当初担心的「改完位次怎么算」不成立:`queuePosition` 只按 `priority → seq → created_at` 排,`depends_on` 决定的是**够不够格出队**,两者正交,改依赖不动位次。 + +--- + +## 十二、批 5 · 任务体系统一(用户定案:统一到一套,删掉另一套) + +### 12.1 现状:两套并存,而且**新体系已经依赖旧体系的一半** + +| | A · 本文档的体系 | B · 会话反推的体系 | +|---|---|---| +| 存储 | `goals` / `tasks` 表(SQLite) | `system/tasklog/{taskId}.json` 一任务一文件 | +| 入口 | `POST /api/company/:cid/goals`、`/tasks` | `POST /api/agent/task/dispatch`(**agent 之间派单**,`tools-base.md` 里写着让 agent curl 它)| +| 收敛 | `settleGoal` → OKR + `report_to` | `POST /api/agent/task/report`(更新 todo/plan → 更新 OKR → 推进 Pipeline → 唤醒上级)| +| 任务树 | `goal_id` / `depends_on` 显式存 | `TaskService.getAggregatedTasks()` **扫全部 agent 的 conversations 反推**父子关系(391 行)| +| 读接口 | `GET /:cid/goals`、`/tasks` | `GET /api/tasks`、`/api/tasks/:id/timeline`(59 行)| +| 前端 | `GoalTaskPane`(§七)| `TaskTimeline.vue`(736 行,唯一消费方)| +| id | `T-{base36}-{4hex}` | `T-{base36}`(同前缀不同源,§九.5)| + +**关键事实:`reportRunOutcome` 收敛后调的正是 B 的 `/api/agent/task/report`。** 两套不是并列关系,A 的回调链路长在 B 身上。所以「删掉另一套」不能理解成整片删除 —— 要删的是**存储与反推**,不是入口与回调。 + +### 12.2 删什么、留什么 + +**删**: +- `system/tasklog/` 目录及其读写(`agent-chat.js` 的 `saveTaskMeta` / `loadTaskMeta`,`task-service.js` 的 `_loadTaskMetas`); +- `TaskService.getAggregatedTasks()` —— **从会话记录反推任务树**这整套启发式。父子关系改为显式存字段,不再猜。 + +**留(改数据源)**: +- `POST /api/agent/task/dispatch` **URL 与请求体形态一个字都不能动** —— 它写在 `packages/agent-core/base/common/tools-base.md` 里,是所有 agent 的工具说明;改了等于同时改所有 agent 的提示词。只把它的存储从写 JSON 文件改成写 `tasks` 表。 +- `POST /api/agent/task/report` 同上(A 的回调依赖它)。 +- `TaskService.getTaskTimeline()` —— 它读的是 agent 的 `conversations/`,那是**真实的一手数据**,不是反推。留着,只把「有哪些 task、父子是谁」换成查表。 +- `TaskTimeline.vue` 保留,换数据源。 + +### 12.3 `tasks` 表要吸收 B,需要三个字段 + +B 的 task 与 A 的 task 有一处真实差异:**执行载体不同**。A 的 task 跑一张 org graph(走调度器),B 的 task 是直接派给某个 agent 起一次对话(`autoExecute`)。硬说成一回事会骗自己,所以显式建模: + +```sql +executor TEXT NOT NULL DEFAULT 'graph', -- 'graph'(跑 org graph,走 §五 调度器) + -- 'agent'(直接派给某个 agent 对话,不进队列) +assignee_agent_id TEXT, -- executor='agent' 时派给谁(B 的 `to`) +parent_task_id TEXT REFERENCES tasks(id), -- B 的任务链;A 的 depends_on 是同级依赖,两者不同 +``` + +**调度器只管 `executor='graph'`**(§五.1 的候选筛选加这一条),`executor='agent'` 的 task 由 `/task/dispatch` 自己起对话,进表只为了「可见、可查、可串成树」。 + +`goal_id NOT NULL` 怎么办:**agent 派单时自动建一个 goal**(`title = '来自 {from} 的派单'`、`created_by = from`),与 §六.3 已经为 `POST /:cid/tasks` 设计的兼容路径**同一条**。模型保持干净,不给 `goal_id` 开可空的口子。 + +### 12.4 顺带修好的两件事 + +1. **30 分钟去重从「扫全目录 JSON」变成一条 SQL**。现在 `/task/dispatch` 每次派单都 `readdirSync` + 逐个 `JSON.parse` 整个 `tasklog/` 来找相似任务 —— 任务一多就是 O(n) 次磁盘读,且相似度判断本身也没索引可用。改成 `WHERE company_id=? AND assignee_agent_id=? AND status='queued' AND created_at > ?` 一条查询。 +2. **两套 `T-` id 合一**(§九.5 那条风险就此消失)。id 生成统一走 §四.5 的 `T-{base36}-{4hex}`。 + +### 12.5 迁移 + +`system/tasklog/*.json` 回填进 `tasks` 表,与 §四.8 的 root run 回填**放在同一个脚本**(同一次数据形态变更,分两个脚本只会让「跑了哪个没跑哪个」变成新问题)。字段映射:`to → assignee_agent_id`、`parentTaskId → parent_task_id`、`objectiveId/goalId → 所属 goal 的 okr_node_id`、`status` 按 `dispatched/done/failed/blocked` 映射。**回填后目录改名为 `system/tasklog.bak-unify/` 而不是直接删**,跑顺一个版本再清。 + +### 12.6 排在批 5 的理由 + +它依赖 `tasks` 表存在(批 1)、依赖 `executor` 语义在调度器里被正确排除(批 3)。而且它**改的是 agent 的生产链路**(派单 / 汇报 / 唤醒),风险面与前四批完全不同 —— 前四批坏了是控制台不好用,这一批坏了是**所有 agent 互相派不了活**。必须单独一批、单独验收。 + +### 12.7 验收 + +25. `POST /api/agent/task/dispatch` 请求体不变,派单成功后 `tasks` 表出现 `executor='agent'` 的行,`system/tasklog/` 不再产生新文件。 +26. 派单的 task **不进调度队列**(`executor='agent'` 被 §五.1 排除),不占 `max_parallel_tasks` 的坑。 +27. 30 分钟去重仍然拦得住重复派单,且不再读目录(改成 SQL)。 +28. `POST /api/agent/task/report` 汇报链路四步(todo/plan → OKR → Pipeline → 唤醒上级)全部照常;**`reportRunOutcome` 那条依赖它的路也照常**。 +29. `TaskTimeline.vue` 任务列表与时间线都正常,父子关系来自 `parent_task_id` 而非会话反推。 +30. 存量 `system/tasklog/*.json` 全部回填进表,目录改名保留。 diff --git a/docs/todo/system-design/27-requirement.md b/docs/todo/system-design/27-requirement.md index 98edb8d..b8fc648 100644 --- a/docs/todo/system-design/27-requirement.md +++ b/docs/todo/system-design/27-requirement.md @@ -541,6 +541,27 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file **定案 1 连带牵出一处新缺陷**:`task-planning` 跑在 **goal 层、任何 run 存在之前**,它烧的 token **没有预算池可扣**。这正是 `graph-engine.defaultPlanWorkGraph` 注释里点过名的那个病——「规划调用也是真花钱的一次 agent 调用,token 必须计入同一个 root run 预算池,否则『一层规划 + 一层执行』里有一半的花费是隐形的」——在 goal 层的新实例。复核第二轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以**记账**字段 `planning_tokens` 的形式回来(闸门仍不做,见 P3C §十一.6)。 +### R-20.6 · 五条收尾定案(2026-08-18) + +| # | 问题 | 定案 | 落点 | +|---|---|---|---| +| 1 | 退役 `/api/goals` 与 `/api/company/:cid/goals` 两个 OKR 兼容别名(仓库内零调用方,但属对外可见的破坏性变更) | **可以退役** | P3C §二.3 / §六.1 | +| 2 | goal 级验收谁做? | **仍然是 `org-validator`**,在 goal 层再调一次同一个本体 | P3C §四.10 | +| 3 | goal 级总预算口径? | **先不考虑**,不做 goal 级闸门 | P3C §十一.6 | +| 4 | 排队中的任务能不能改依赖? | **可以改**,`queued` 开放 `depends_on` 的 PATCH | P3C §六.3 | +| 5 | `/api/tasks` + `TaskTimeline` 那套同名不同源的任务树怎么收? | **统一到 `tasks` 表一套,删掉另一套** | P3C §十二(批 5)| + +**定案 2 的要点是「不新造验收 agent」**:task 级与 goal 级验的是**基数不同的东西**(R-12 / NAMING §四 那张表)——task 级问「这一个 task 的 `task-goal` 达成没有」,goal 级问「N 个 task 合起来,人提的需求达成没有」。5 个 task 全 `completed` 不等于人要的东西做出来了,这个缺口正是 goal 层存在的理由。所以是**同一个本体在 goal 层再调一次**,不是复用 task 级的结论。单 task 的 goal 跳过这次调用(两级验收此时是同一件事)。 + +**定案 3 保留记账、只砍闸门**:`planning_tokens` / `validation_tokens` 两个字段留着。它们不是预算闸门,是让 goal 层那两次 agent 调用(拆分 + 验收)的花费**可见** —— 不记就是 `defaultPlanWorkGraph` 注释点名的「隐形的一半」,而且界面上「本次拆分消耗 ~N tokens」这个成本确认步骤要用它。 + +**定案 5 有一个反直觉的前提,实现前必须先读**:核实后确认**两套体系不是并列的,新体系的回调链路长在旧体系身上** —— `reportRunOutcome` 收敛后调的正是 `POST /api/agent/task/report`。所以「删掉另一套」的准确含义是: + +- **删**:`system/tasklog/` 文件存储 + `TaskService.getAggregatedTasks()`(从会话记录**反推**任务树的那套启发式,391 行里的主体); +- **留但换数据源**:`POST /api/agent/task/dispatch` 与 `/task/report` —— 它们的 URL 与请求体形态**一个字都不能动**,因为写在 `packages/agent-core/base/common/tools-base.md` 里,是所有 agent 的工具说明,改了等于同时改所有 agent 的提示词;`getTaskTimeline()` 读的是 agent 真实会话记录(一手数据,不是反推),留。 + +统一还顺带修好两件事:30 分钟派单去重从「`readdirSync` 整个 tasklog 目录 + 逐个 JSON.parse」变成一条 SQL;两套同前缀不同源的 `T-` id 合一。 + **落点**:[27-P3C goal / task 两级模型](./27-p3c-goal-task-model.md)(本条派生出的工作项文档)。 --- @@ -584,3 +605,4 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file | R-20.3 | goal / task 描述**一经下达不可改**(artifact 已被 agent 消费,改库等于篡改审计输入)| P3C §四.4 | | R-20.4 | 新增 run status **`queued`**,出队原子性靠既有唯一索引兜底 | P3C §四.3 | | R-20.5 | 四条口径:**`task-planning` 进 v1** / 并行留批 4 / manual 不自动出队 / 上游失败下游自动 blocked | P3C §四.9 / §五.1.2 / §五.2bis | +| R-20.6 | 五条收尾:退役别名 / goal 验收用 `org-validator` / 预算不做 / `queued` 可改依赖 / **两套任务树统一删一套** | P3C §四.10 / §六.3 / §十二 | -- libgit2 0.21.4