Commit 2abb08205a3683f6866ce3401b7ab7e50665bf30
1 parent
f5e1a220
docs(27-P3C): 五条收尾定案 —— goal 验收用 org-validator,两套任务树统一
R-20.6: 1. 退役 /api/goals 与 /api/company/:cid/goals 两个 OKR 别名,确认执行 2. goal 级验收仍用 org-validator,在 goal 层再调一次同一个本体。 不复用 task 级结论 —— 两次验的是基数不同的东西:task 级问 「这一个 task 的 task-goal 达成没有」,goal 级问「N 个 task 合起来人要的东西做出来没有」。单 task 的 goal 跳过这次调用。 goal 状态机加 failed,判词落 goals.verdict 3. 预算先不考虑,不做 goal 级闸门;但 planning_tokens / validation_tokens 两个记账字段保留 —— 它们不是闸门,是让 goal 层那两次 agent 调用的花费可见 4. queued 开放改 depends_on。当初担心的「改完位次怎么算」不成立: queuePosition 只按 priority→seq→created_at 排,depends_on 决定 的是够不够格出队,两者正交 5. 两套任务体系统一到 tasks 表,新增 §十二(批 5) 定案 5 核实出一个反直觉的前提:两套不是并列的,新体系的回调链路 长在旧体系身上 —— reportRunOutcome 收敛后调的正是 /api/agent/task/report。所以「删掉另一套」= 删存储与会话反推 (system/tasklog + TaskService.getAggregatedTasks),不能删入口 与回调:/task/dispatch 的 URL 与请求体写在 tools-base.md 里, 改它等于同时改所有 agent 的提示词。 tasks 表为此吸收三个字段(executor / assignee_agent_id / parent_task_id):agent 派单与 graph 执行的载体确实不同, 硬说成一回事是骗自己;调度器只管 executor='graph'。 顺带修好:派单去重从扫目录变一条 SQL,两套 T- id 合一。 开放问题 2/4/6/7 全部关闭,只剩 5(max_parallel 放哪)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
2 changed files
with
147 additions
and
10 deletions
docs/todo/system-design/27-p3c-goal-task-model.md
| @@ -18,6 +18,9 @@ | @@ -18,6 +18,9 @@ | ||
| 18 | > | 18 | > |
| 19 | > **第三轮(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)。 | 19 | > **第三轮(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)。 |
| 20 | > 定案 ① 连带牵出一处新缺陷:**`task-planning` 跑在 goal 层、任何 run 存在之前,它烧的 token 没有预算池可扣** —— 正是 `defaultPlanWorkGraph` 注释里点名的「隐形的一半花费」的新实例。上一轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以记账字段 `planning_tokens` 的形式回来(§四.1)。 | 20 | > 定案 ① 连带牵出一处新缺陷:**`task-planning` 跑在 goal 层、任何 run 存在之前,它烧的 token 没有预算池可扣** —— 正是 `defaultPlanWorkGraph` 注释里点名的「隐形的一半花费」的新实例。上一轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以记账字段 `planning_tokens` 的形式回来(§四.1)。 |
| 21 | +> | ||
| 22 | +> **第四轮(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)。 | ||
| 23 | +> 定案 ⑤ 核实后有个反直觉的前提必须先说清:**两套不是并列的,A 的回调链路长在 B 身上** —— `reportRunOutcome` 收敛后调的正是 `/api/agent/task/report`。所以「删掉另一套」= 删**存储与会话反推**,**不能**删入口与回调(`/task/dispatch` 的 URL 与请求体写在 `tools-base.md` 里,改它等于同时改所有 agent 的提示词)。开放问题 2 / 4 / 6 / 7 全部就此关闭。 | ||
| 21 | 24 | ||
| 22 | --- | 25 | --- |
| 23 | 26 | ||
| @@ -93,7 +96,7 @@ goals (G-xxx) 人类下达。一条 = 一个需求 = 一条验收边 | @@ -93,7 +96,7 @@ goals (G-xxx) 人类下达。一条 = 一个需求 = 一条验收边 | ||
| 93 | 96 | ||
| 94 | | 层 | 回答的问题 | 谁改它 | 状态机 | | 97 | | 层 | 回答的问题 | 谁改它 | 状态机 | |
| 95 | |---|---|---|---| | 98 | |---|---|---|---| |
| 96 | -| goal | 人想要什么?做到了没有? | 人 | `draft → planning → active → done / cancelled / archived` | | 99 | +| goal | 人想要什么?做到了没有? | 人 + **goal 级 `org-validator`**(§四.10)| `draft → planning → active → done / failed / cancelled / archived` | |
| 97 | | task | 这个需求拆成哪几件事?谁先谁后? | **`task-planning` agent 拆、人可改**(§四.9)| `draft → queued → running → completed / failed / blocked / cancelled` | | 100 | | task | 这个需求拆成哪几件事?谁先谁后? | **`task-planning` agent 拆、人可改**(§四.9)| `draft → queued → running → completed / failed / blocked / cancelled` | |
| 98 | | run | 这一次跑成什么样? | 引擎 | 既有 `RUN_TRANSITIONS` + 新增 `queued`(§四.3) | | 101 | | run | 这一次跑成什么样? | 引擎 | 既有 `RUN_TRANSITIONS` + 新增 `queued`(§四.3) | |
| 99 | 102 | ||
| @@ -120,13 +123,16 @@ CREATE TABLE IF NOT EXISTS goals ( | @@ -120,13 +123,16 @@ CREATE TABLE IF NOT EXISTS goals ( | ||
| 120 | title TEXT NOT NULL, -- 短标题,列表显示用;可改 | 123 | title TEXT NOT NULL, -- 短标题,列表显示用;可改 |
| 121 | description TEXT NOT NULL, -- 人类需求原文 = 验收边界;下达后不可改 | 124 | description TEXT NOT NULL, -- 人类需求原文 = 验收边界;下达后不可改 |
| 122 | status TEXT NOT NULL DEFAULT 'draft', | 125 | status TEXT NOT NULL DEFAULT 'draft', |
| 123 | - -- draft | planning | active | done | cancelled | archived | 126 | + -- draft | planning | active | done | failed | cancelled | archived |
| 124 | -- planning = task-planning 正在拆分(§四.9),是个真会持续几十秒的状态 | 127 | -- planning = task-planning 正在拆分(§四.9),是个真会持续几十秒的状态 |
| 128 | + -- failed = goal 级验收没过,或有 task 未能 completed(§四.10);可回 active 补 task 重下 | ||
| 125 | priority TEXT NOT NULL DEFAULT 'P1', | 129 | priority TEXT NOT NULL DEFAULT 'P1', |
| 126 | okr_node_id TEXT, -- 可选,挂到 OKR 度量树的哪个节点(O-001.1.1) | 130 | okr_node_id TEXT, -- 可选,挂到 OKR 度量树的哪个节点(O-001.1.1) |
| 127 | created_by TEXT, -- 'human' | agent id(Master/CEO 代下单) | 131 | created_by TEXT, -- 'human' | agent id(Master/CEO 代下单) |
| 128 | report_to TEXT, -- 收敛后要唤醒的上级 agent,见 §四.7 | 132 | report_to TEXT, -- 收敛后要唤醒的上级 agent,见 §四.7 |
| 129 | - planning_tokens INTEGER NOT NULL DEFAULT 0, -- task-planning 这一次拆分烧掉的 token,见 §四.9 | 133 | + planning_tokens INTEGER NOT NULL DEFAULT 0, -- task-planning 拆分烧掉的 token(§四.9) |
| 134 | + validation_tokens INTEGER NOT NULL DEFAULT 0, -- goal 级 org-validator 烧掉的 token(§四.10) | ||
| 135 | + verdict TEXT, -- goal 级验收判词(org-validator 的原话),§四.10 | ||
| 130 | created_at TEXT NOT NULL, | 136 | created_at TEXT NOT NULL, |
| 131 | updated_at TEXT NOT NULL, | 137 | updated_at TEXT NOT NULL, |
| 132 | dispatched_at TEXT, -- 首次下达时间;非空 = 描述已冻结 | 138 | dispatched_at TEXT, -- 首次下达时间;非空 = 描述已冻结 |
| @@ -311,6 +317,42 @@ AgentManager 调 task-planning(输入:goal.description + 公司 org graph | @@ -311,6 +317,42 @@ AgentManager 调 task-planning(输入:goal.description + 公司 org graph | ||
| 311 | 317 | ||
| 312 | `POST /goals` 带 `split: false` 时不调 agent,直接建 1 条同描述的 task。存量调用方(`POST /:cid/tasks`)走的就是这条路 —— **它们不该因为这次改动突然多花一次 LLM 调用**。 | 318 | `POST /goals` 带 `split: false` 时不调 agent,直接建 1 条同描述的 task。存量调用方(`POST /:cid/tasks`)走的就是这条路 —— **它们不该因为这次改动突然多花一次 LLM 调用**。 |
| 313 | 319 | ||
| 320 | +### 4.10 goal 级验收 —— 仍然是 `org-validator`(用户定案) | ||
| 321 | + | ||
| 322 | +**不新造验收 agent**:goal 级用的还是 `org-validator` 这个本体,只是**在 goal 层再调一次**。 | ||
| 323 | + | ||
| 324 | +#### 为什么是「再调一次」而不是复用 task 级那次的结论 | ||
| 325 | + | ||
| 326 | +因为两次验的是**基数不同的东西**,这正是 27-NAMING §四 那张表的论证: | ||
| 327 | + | ||
| 328 | +| | 验什么 | 谁问的 | | ||
| 329 | +|---|---|---| | ||
| 330 | +| task 级(既有,org graph 末端节点)| 这**一个 task** 的 `task-goal` 达成了没有 | 引擎,每个 task 各一次 | | ||
| 331 | +| **goal 级(本次新增)** | **N 个 task 合起来,人提的那个需求达成了没有** | goal 收敛时,一次 | | ||
| 332 | + | ||
| 333 | +5 个 task 全 `completed`,只说明每一件事都按各自的描述做完了 —— 不说明拼起来是人要的东西。这个缺口正是 goal 层存在的理由,用别的东西替代不了。 | ||
| 334 | + | ||
| 335 | +#### 触发与判定 | ||
| 336 | + | ||
| 337 | +``` | ||
| 338 | +goal 下所有 task 到终态 | ||
| 339 | + ├─ 有任何一条 ≠ completed → goal 直接 failed,不调 agent(省一次调用:显然没交付) | ||
| 340 | + ├─ 全 completed 且只有 1 条 task → 跳过 goal 级验收,直接采用该 task 的验收结论 | ||
| 341 | + │ (单 task 的 goal,两级验收是同一件事,再调一次纯浪费) | ||
| 342 | + └─ 全 completed 且 ≥2 条 task → 调一次 org-validator | ||
| 343 | + 输入:goal.description(= 验收边界)+ 该 goal 全部 task 的产物索引 | ||
| 344 | + (`graph_files` 按 task_id 查,§P4 §4.6 的表已经能给出这份清单) | ||
| 345 | + 产出:pass / fail + 判词 → goals.status = done / failed,判词落 goals.verdict | ||
| 346 | +``` | ||
| 347 | + | ||
| 348 | +调用形态与 `task-planning` 相同:`AgentManager` 直调本体,不在任何 graph run 里,token 记 `goals.validation_tokens`。 | ||
| 349 | + | ||
| 350 | +#### 挂在哪 | ||
| 351 | + | ||
| 352 | +和调度器同一个位置:task 转终态时顺手判一次「这个 goal 的 task 是不是都到齐了」。**`settleGoal(goalId)` 必须幂等**(并行完成的多个 task 会同时触发它),用 `goals.status` 的条件更新做闸门 —— 与 §五.1 出队用的是同一招。 | ||
| 353 | + | ||
| 354 | +> goal 收敛之后才轮到 §四.7 的两件事:写 OKR 节点(一次,用 goal 的判定结果)、唤醒 `report_to`。**这两件事从 run 级上移到 goal 级**,禁令一说的「task 的 run 一律不带 `okr_node_id`」在这里闭环。 | ||
| 355 | + | ||
| 314 | --- | 356 | --- |
| 315 | 357 | ||
| 316 | ## 五、排队与并行 | 358 | ## 五、排队与并行 |
| @@ -440,7 +482,7 @@ for (const t of queuedTasks) { | @@ -440,7 +482,7 @@ for (const t of queuedTasks) { | ||
| 440 | |---|---|---| | 482 | |---|---|---| |
| 441 | | `GET` | `/api/company/:cid/tasks` | **新端点**(核实过:今天这个路径下只有 `POST`,没有 `GET`,不存在兼容问题)。返回 task 表的行,含 `goalId` / `status` / `queuePosition` / `progress` / `runId` | | 483 | | `GET` | `/api/company/:cid/tasks` | **新端点**(核实过:今天这个路径下只有 `POST`,没有 `GET`,不存在兼容问题)。返回 task 表的行,含 `goalId` / `status` / `queuePosition` / `progress` / `runId` | |
| 442 | | `POST` | `/api/company/:cid/tasks` | **保留、行为兼容**:不带 `goal_id` 调用时,**自动建一个同描述的 goal**(`created_by` 记调用方)再挂一条 task —— 存量调用方(含 `DispatchTaskButton`、外部 agent 的 curl)一行不用改就还能用,响应体里多出 `goalId` 字段。带 `goal_id` 则挂到既有 goal 下 | | 484 | | `POST` | `/api/company/:cid/tasks` | **保留、行为兼容**:不带 `goal_id` 调用时,**自动建一个同描述的 goal**(`created_by` 记调用方)再挂一条 task —— 存量调用方(含 `DispatchTaskButton`、外部 agent 的 curl)一行不用改就还能用,响应体里多出 `goalId` 字段。带 `goal_id` 则挂到既有 goal 下 | |
| 443 | -| `PATCH` | `/api/company/:cid/tasks/:taskId` | 改 `title` / `priority`;`depends_on` 仅 `draft` 可改;`queued_at` 非空时带 `description` / `mode` / `budget` 一律 `409` | | 485 | +| `PATCH` | `/api/company/:cid/tasks/:taskId` | 改 `title` / `priority`;**`depends_on` 在 `draft` 与 `queued` 都可改**(用户定案,见下);`queued_at` 非空时带 `description` / `mode` / `budget` 一律 `409` | |
| 444 | | `POST` | `/api/company/:cid/tasks/:taskId/enqueue` | 单条入队:建 `queued` 的 run 行 + 写 `task-goal.json` + `tick()` | | 486 | | `POST` | `/api/company/:cid/tasks/:taskId/enqueue` | 单条入队:建 `queued` 的 run 行 + 写 `task-goal.json` + `tick()` | |
| 445 | | `POST` | `/api/company/:cid/tasks/:taskId/cancel` | `queued` → 撤出队列(run `queued → stopped`);`running` → 转调既有 `POST /api/graph-runs/:id/stop` | | 487 | | `POST` | `/api/company/:cid/tasks/:taskId/cancel` | `queued` → 撤出队列(run `queued → stopped`);`running` → 转调既有 `POST /api/graph-runs/:id/stop` | |
| 446 | | `POST` | `/api/company/:cid/tasks/:taskId/rerun` | 从 `failed` / `blocked` / `completed` 重新入队,`attempt + 1`,**新起一条 run**(旧 run 保留在历史里)。**刻意不叫 `retry`** —— 见下 | | 488 | | `POST` | `/api/company/:cid/tasks/:taskId/rerun` | 从 `failed` / `blocked` / `completed` 重新入队,`attempt + 1`,**新起一条 run**(旧 run 保留在历史里)。**刻意不叫 `retry`** —— 见下 | |
| @@ -448,6 +490,11 @@ for (const t of queuedTasks) { | @@ -448,6 +490,11 @@ for (const t of queuedTasks) { | ||
| 448 | 490 | ||
| 449 | **`rerun` ≠ `retry`,两者会撞车。** 既有的 `POST /api/graph-runs/:id/nodes/:nodeId/retry` 是**节点级重试**:同一条 run 内把某个节点打回 `ready` 重跑(`blocked` 的唯一出路,`NODE_TRANSITIONS` 里写死的)。新加的是**任务级重跑**:新起一条 run,整张图从头来。两个都叫 retry,实现时必然有人把新端点接到旧逻辑上(或反过来)。所以任务级一律叫 `rerun`,文档、API、界面按钮文案(「重跑这个任务」vs 节点详情里的「重试这一步」)三处同时区分。 | 491 | **`rerun` ≠ `retry`,两者会撞车。** 既有的 `POST /api/graph-runs/:id/nodes/:nodeId/retry` 是**节点级重试**:同一条 run 内把某个节点打回 `ready` 重跑(`blocked` 的唯一出路,`NODE_TRANSITIONS` 里写死的)。新加的是**任务级重跑**:新起一条 run,整张图从头来。两个都叫 retry,实现时必然有人把新端点接到旧逻辑上(或反过来)。所以任务级一律叫 `rerun`,文档、API、界面按钮文案(「重跑这个任务」vs 节点详情里的「重试这一步」)三处同时区分。 |
| 450 | 492 | ||
| 493 | +**`queued` 状态开放改依赖**(用户定案)。真实场景是「B 已经排上了,才发现它得等 C」,此前只能取消重建、丢掉排队位次。三条细则: | ||
| 494 | +- 环检测照跑(只能引用同 goal 内的 task),有环 `400`,库里不留半条脏数据; | ||
| 495 | +- **位次不受影响**:`queuePosition` 只按 `priority → seq → created_at` 排,`depends_on` 决定的是「够不够格出队」,两者正交 —— 当初担心的「改完位次怎么算」不成立; | ||
| 496 | +- 新加的依赖若已经 `failed` / `cancelled`,下一次 `tick` 会自然把它推导成 `blocked`(§五.1.2),不需要额外处理。 | ||
| 497 | + | ||
| 451 | **`POST /:cid/tasks` 的 409 消失了**:并发冲突不再是错误,是排队。响应体改为 `201 { taskId, goalId, runId, status: 'queued', queuePosition: 2 }`。`queuePosition` 是这次改动里对用户最直观的那个字段 —— **它的排序口径必须与调度器第 4 步逐字一致**(`priority → seq → created_at`),两处各写一份的下场已经有先例:`canDispatch` 就是因为前端自己复述了一遍取值域、还多写了一个 `blocked`,导致公司失败一次后再也下不了任务。**口径只留调度器一处,端点调用它算位置。** | 498 | **`POST /:cid/tasks` 的 409 消失了**:并发冲突不再是错误,是排队。响应体改为 `201 { taskId, goalId, runId, status: 'queued', queuePosition: 2 }`。`queuePosition` 是这次改动里对用户最直观的那个字段 —— **它的排序口径必须与调度器第 4 步逐字一致**(`priority → seq → created_at`),两处各写一份的下场已经有先例:`canDispatch` 就是因为前端自己复述了一遍取值域、还多写了一个 `blocked`,导致公司失败一次后再也下不了任务。**口径只留调度器一处,端点调用它算位置。** |
| 452 | 499 | ||
| 453 | ### 6.4 不动的端点 | 500 | ### 6.4 不动的端点 |
| @@ -585,9 +632,9 @@ for (const t of queuedTasks) { | @@ -585,9 +632,9 @@ for (const t of queuedTasks) { | ||
| 585 | 2. **拆分质量决定预算消耗**。拆出 5 条 task 就是 5 份 `DEFAULT_TASK_BUDGET`(各 100 次调用 / 500 万 token)。这是「产出一律 `draft`、必须人确认」的根本理由 —— 那一步是唯一一道成本闸门。界面上要显式提示「N 个任务 ≈ N × 单任务成本」。 | 632 | 2. **拆分质量决定预算消耗**。拆出 5 条 task 就是 5 份 `DEFAULT_TASK_BUDGET`(各 100 次调用 / 500 万 token)。这是「产出一律 `draft`、必须人确认」的根本理由 —— 那一步是唯一一道成本闸门。界面上要显式提示「N 个任务 ≈ N × 单任务成本」。 |
| 586 | 3. **新本体不登记 `_registry.json` 会直接把 CI 跑红**。27-P3/P4 §八.21 有前科(六个本体建了目录没登记,`template-refs.test.js` 当场红)。建目录与登记必须在同一个 commit 里。 | 633 | 3. **新本体不登记 `_registry.json` 会直接把 CI 跑红**。27-P3/P4 §八.21 有前科(六个本体建了目录没登记,`template-refs.test.js` 当场红)。建目录与登记必须在同一个 commit 里。 |
| 587 | 634 | ||
| 588 | -### 5. 两套 `T-` id | 635 | +### 5. 两套 `T-` id —— 由 §十二 的统一消解,但在那之前是真风险 |
| 589 | 636 | ||
| 590 | -`agent-chat.js` 的 tasklog 也发 `T-` 开头的 id,本次不动它。两者从此**长得像但不同源**,读日志时别混;本文档的 task id 一律带 `-{4hex}` 后缀,肉眼可分。 | 637 | +`agent-chat.js` 的 `/task/dispatch` 也发 `T-` 开头的 id 并写 `system/tasklog/`。**批 1-4 期间两套并存**,长得像但不同源,读日志时别混(本文档的 id 一律带 `-{4hex}` 后缀,肉眼可分)。批 5(§十二)把存储统一到 `tasks` 表之后,这个风险随之消失 —— 那时 `T-` 只有一个来源。 |
| 591 | 638 | ||
| 592 | ### 6. 并行是最后一批,且要单独验 | 639 | ### 6. 并行是最后一批,且要单独验 |
| 593 | 640 | ||
| @@ -603,6 +650,7 @@ for (const t of queuedTasks) { | @@ -603,6 +650,7 @@ for (const t of queuedTasks) { | ||
| 603 | | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 | | 650 | | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 | |
| 604 | | **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 | | 651 | | **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 | |
| 605 | | **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit | 另立验收 | | 652 | | **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit | 另立验收 | |
| 653 | +| **5 · 任务体系统一**(单独排期)| 见 §十二:`executor` / `assignee_agent_id` / `parent_task_id` 三字段 + `/task/dispatch` 换存储 + 删会话反推 + `tasklog` 回填 | 25-30 | | ||
| 606 | 654 | ||
| 607 | > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。 | 655 | > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。 |
| 608 | 656 | ||
| @@ -613,9 +661,76 @@ for (const t of queuedTasks) { | @@ -613,9 +661,76 @@ for (const t of queuedTasks) { | ||
| 613 | ## 十一、开放问题 | 661 | ## 十一、开放问题 |
| 614 | 662 | ||
| 615 | 1. ~~谁把 goal 拆成 task?~~ **已定案(2026-08-18):`task-planning` agent 进 v1**,产出 `draft` 任务清单由人确认后下达,详见 §四.9。 | 663 | 1. ~~谁把 goal 拆成 task?~~ **已定案(2026-08-18):`task-planning` agent 进 v1**,产出 `draft` 任务清单由人确认后下达,详见 §四.9。 |
| 616 | -2. **goal 级的验收谁做?** 现在 `org-validator` 验的是 task 级(`task-goal`)。一个 goal 的 5 个 task 全 `completed`,不等于人的需求达成了。要不要在 goal 收敛时再跑一次 goal 级验收(`goal-benchmark` 天然就是干这个的)?倾向做,但需要先定「goal 级验收的输入是什么」—— 是 N 个 task 的产物合集吗?留到 P5。 | 664 | +2. ~~goal 级的验收谁做?~~ **已定案(2026-08-18):仍然是 `org-validator`**,在 goal 层再调一次同一个本体,输入是该 goal 全部 task 的产物合集。详见 §四.10。 |
| 617 | 3. ~~失败的 task 挡不挡住依赖它的下游?~~ **已定案(2026-08-18):自动转 `blocked` 并在界面标红**,给「重跑上游」与「强制放行」两个出路;`blocked` 每次 `tick` 重算而不落死,上游恢复后下游自己回 `queued`。详见 §五.1.2。 | 665 | 3. ~~失败的 task 挡不挡住依赖它的下游?~~ **已定案(2026-08-18):自动转 `blocked` 并在界面标红**,给「重跑上游」与「强制放行」两个出路;`blocked` 每次 `tick` 重算而不落死,上游恢复后下游自己回 `queued`。详见 §五.1.2。 |
| 618 | -4. **跨公司任务总览**。`/api/tasks` + `TaskTimeline` 是从会话反推的另一套任务树,与本文档的 `tasks` 表同名不同源。将来是把它改成读这张表,还是并存?本次不碰,但这是个明确待收的口子。 | 666 | +4. ~~跨公司任务总览 / 两套任务树并存~~ **已定案(2026-08-18):统一到 `tasks` 表一套,删掉会话反推那套。** 详见 §十二(批 5)。 |
| 619 | 5. **`max_parallel_tasks` 放 `company.json` 还是建 `company_settings` 表?** v1 选前者(`companyService.get` 已经在读那个文件,零新增读路径)。公司级配置项超过 3 个时再考虑建表。 | 667 | 5. **`max_parallel_tasks` 放 `company.json` 还是建 `company_settings` 表?** v1 选前者(`companyService.get` 已经在读那个文件,零新增读路径)。公司级配置项超过 3 个时再考虑建表。 |
| 620 | -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),平分和总池都会让"还剩多少"变成一个解释不清的数。 | ||
| 621 | -7. **排队中的任务能不能改依赖?** 现在 `depends_on` 只有 `draft` 可改。但真实场景是「B 已经排上了,才发现它得等 C」—— 这时只能取消重建,会丢掉排队位次。要不要给 `queued` 开一个只改 `depends_on` 的口子?开的话要防「改出环」和「改完位次怎么算」。 | 668 | +6. ~~goal 级总预算怎么定口径?~~ **已定案(2026-08-18):预算先不考虑,不做 goal 级闸门。** `goals.budget_json` 保持砍掉。**但 `planning_tokens` / `validation_tokens` 两个记账字段保留** —— 它们不是闸门,是让 goal 层那两次 agent 调用的花费**可见**;不记就是 `defaultPlanWorkGraph` 注释点名的「隐形的一半」,而且界面上「本次拆分消耗 ~N tokens」这个确认步骤要用它。 |
| 669 | +7. ~~排队中的任务能不能改依赖?~~ **已定案(2026-08-18):可以改。** `queued` 状态开放 `depends_on` 的 `PATCH`,见 §六.3。当初担心的「改完位次怎么算」不成立:`queuePosition` 只按 `priority → seq → created_at` 排,`depends_on` 决定的是**够不够格出队**,两者正交,改依赖不动位次。 | ||
| 670 | + | ||
| 671 | +--- | ||
| 672 | + | ||
| 673 | +## 十二、批 5 · 任务体系统一(用户定案:统一到一套,删掉另一套) | ||
| 674 | + | ||
| 675 | +### 12.1 现状:两套并存,而且**新体系已经依赖旧体系的一半** | ||
| 676 | + | ||
| 677 | +| | A · 本文档的体系 | B · 会话反推的体系 | | ||
| 678 | +|---|---|---| | ||
| 679 | +| 存储 | `goals` / `tasks` 表(SQLite) | `system/tasklog/{taskId}.json` 一任务一文件 | | ||
| 680 | +| 入口 | `POST /api/company/:cid/goals`、`/tasks` | `POST /api/agent/task/dispatch`(**agent 之间派单**,`tools-base.md` 里写着让 agent curl 它)| | ||
| 681 | +| 收敛 | `settleGoal` → OKR + `report_to` | `POST /api/agent/task/report`(更新 todo/plan → 更新 OKR → 推进 Pipeline → 唤醒上级)| | ||
| 682 | +| 任务树 | `goal_id` / `depends_on` 显式存 | `TaskService.getAggregatedTasks()` **扫全部 agent 的 conversations 反推**父子关系(391 行)| | ||
| 683 | +| 读接口 | `GET /:cid/goals`、`/tasks` | `GET /api/tasks`、`/api/tasks/:id/timeline`(59 行)| | ||
| 684 | +| 前端 | `GoalTaskPane`(§七)| `TaskTimeline.vue`(736 行,唯一消费方)| | ||
| 685 | +| id | `T-{base36}-{4hex}` | `T-{base36}`(同前缀不同源,§九.5)| | ||
| 686 | + | ||
| 687 | +**关键事实:`reportRunOutcome` 收敛后调的正是 B 的 `/api/agent/task/report`。** 两套不是并列关系,A 的回调链路长在 B 身上。所以「删掉另一套」不能理解成整片删除 —— 要删的是**存储与反推**,不是入口与回调。 | ||
| 688 | + | ||
| 689 | +### 12.2 删什么、留什么 | ||
| 690 | + | ||
| 691 | +**删**: | ||
| 692 | +- `system/tasklog/` 目录及其读写(`agent-chat.js` 的 `saveTaskMeta` / `loadTaskMeta`,`task-service.js` 的 `_loadTaskMetas`); | ||
| 693 | +- `TaskService.getAggregatedTasks()` —— **从会话记录反推任务树**这整套启发式。父子关系改为显式存字段,不再猜。 | ||
| 694 | + | ||
| 695 | +**留(改数据源)**: | ||
| 696 | +- `POST /api/agent/task/dispatch` **URL 与请求体形态一个字都不能动** —— 它写在 `packages/agent-core/base/common/tools-base.md` 里,是所有 agent 的工具说明;改了等于同时改所有 agent 的提示词。只把它的存储从写 JSON 文件改成写 `tasks` 表。 | ||
| 697 | +- `POST /api/agent/task/report` 同上(A 的回调依赖它)。 | ||
| 698 | +- `TaskService.getTaskTimeline()` —— 它读的是 agent 的 `conversations/`,那是**真实的一手数据**,不是反推。留着,只把「有哪些 task、父子是谁」换成查表。 | ||
| 699 | +- `TaskTimeline.vue` 保留,换数据源。 | ||
| 700 | + | ||
| 701 | +### 12.3 `tasks` 表要吸收 B,需要三个字段 | ||
| 702 | + | ||
| 703 | +B 的 task 与 A 的 task 有一处真实差异:**执行载体不同**。A 的 task 跑一张 org graph(走调度器),B 的 task 是直接派给某个 agent 起一次对话(`autoExecute`)。硬说成一回事会骗自己,所以显式建模: | ||
| 704 | + | ||
| 705 | +```sql | ||
| 706 | +executor TEXT NOT NULL DEFAULT 'graph', -- 'graph'(跑 org graph,走 §五 调度器) | ||
| 707 | + -- 'agent'(直接派给某个 agent 对话,不进队列) | ||
| 708 | +assignee_agent_id TEXT, -- executor='agent' 时派给谁(B 的 `to`) | ||
| 709 | +parent_task_id TEXT REFERENCES tasks(id), -- B 的任务链;A 的 depends_on 是同级依赖,两者不同 | ||
| 710 | +``` | ||
| 711 | + | ||
| 712 | +**调度器只管 `executor='graph'`**(§五.1 的候选筛选加这一条),`executor='agent'` 的 task 由 `/task/dispatch` 自己起对话,进表只为了「可见、可查、可串成树」。 | ||
| 713 | + | ||
| 714 | +`goal_id NOT NULL` 怎么办:**agent 派单时自动建一个 goal**(`title = '来自 {from} 的派单'`、`created_by = from`),与 §六.3 已经为 `POST /:cid/tasks` 设计的兼容路径**同一条**。模型保持干净,不给 `goal_id` 开可空的口子。 | ||
| 715 | + | ||
| 716 | +### 12.4 顺带修好的两件事 | ||
| 717 | + | ||
| 718 | +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 > ?` 一条查询。 | ||
| 719 | +2. **两套 `T-` id 合一**(§九.5 那条风险就此消失)。id 生成统一走 §四.5 的 `T-{base36}-{4hex}`。 | ||
| 720 | + | ||
| 721 | +### 12.5 迁移 | ||
| 722 | + | ||
| 723 | +`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/` 而不是直接删**,跑顺一个版本再清。 | ||
| 724 | + | ||
| 725 | +### 12.6 排在批 5 的理由 | ||
| 726 | + | ||
| 727 | +它依赖 `tasks` 表存在(批 1)、依赖 `executor` 语义在调度器里被正确排除(批 3)。而且它**改的是 agent 的生产链路**(派单 / 汇报 / 唤醒),风险面与前四批完全不同 —— 前四批坏了是控制台不好用,这一批坏了是**所有 agent 互相派不了活**。必须单独一批、单独验收。 | ||
| 728 | + | ||
| 729 | +### 12.7 验收 | ||
| 730 | + | ||
| 731 | +25. `POST /api/agent/task/dispatch` 请求体不变,派单成功后 `tasks` 表出现 `executor='agent'` 的行,`system/tasklog/` 不再产生新文件。 | ||
| 732 | +26. 派单的 task **不进调度队列**(`executor='agent'` 被 §五.1 排除),不占 `max_parallel_tasks` 的坑。 | ||
| 733 | +27. 30 分钟去重仍然拦得住重复派单,且不再读目录(改成 SQL)。 | ||
| 734 | +28. `POST /api/agent/task/report` 汇报链路四步(todo/plan → OKR → Pipeline → 唤醒上级)全部照常;**`reportRunOutcome` 那条依赖它的路也照常**。 | ||
| 735 | +29. `TaskTimeline.vue` 任务列表与时间线都正常,父子关系来自 `parent_task_id` 而非会话反推。 | ||
| 736 | +30. 存量 `system/tasklog/*.json` 全部回填进表,目录改名保留。 |
docs/todo/system-design/27-requirement.md
| @@ -541,6 +541,27 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file | @@ -541,6 +541,27 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file | ||
| 541 | 541 | ||
| 542 | **定案 1 连带牵出一处新缺陷**:`task-planning` 跑在 **goal 层、任何 run 存在之前**,它烧的 token **没有预算池可扣**。这正是 `graph-engine.defaultPlanWorkGraph` 注释里点过名的那个病——「规划调用也是真花钱的一次 agent 调用,token 必须计入同一个 root run 预算池,否则『一层规划 + 一层执行』里有一半的花费是隐形的」——在 goal 层的新实例。复核第二轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以**记账**字段 `planning_tokens` 的形式回来(闸门仍不做,见 P3C §十一.6)。 | 542 | **定案 1 连带牵出一处新缺陷**:`task-planning` 跑在 **goal 层、任何 run 存在之前**,它烧的 token **没有预算池可扣**。这正是 `graph-engine.defaultPlanWorkGraph` 注释里点过名的那个病——「规划调用也是真花钱的一次 agent 调用,token 必须计入同一个 root run 预算池,否则『一层规划 + 一层执行』里有一半的花费是隐形的」——在 goal 层的新实例。复核第二轮刚以「零消费方」为由砍掉的 goal 级预算字段,因此以**记账**字段 `planning_tokens` 的形式回来(闸门仍不做,见 P3C §十一.6)。 |
| 543 | 543 | ||
| 544 | +### R-20.6 · 五条收尾定案(2026-08-18) | ||
| 545 | + | ||
| 546 | +| # | 问题 | 定案 | 落点 | | ||
| 547 | +|---|---|---|---| | ||
| 548 | +| 1 | 退役 `/api/goals` 与 `/api/company/:cid/goals` 两个 OKR 兼容别名(仓库内零调用方,但属对外可见的破坏性变更) | **可以退役** | P3C §二.3 / §六.1 | | ||
| 549 | +| 2 | goal 级验收谁做? | **仍然是 `org-validator`**,在 goal 层再调一次同一个本体 | P3C §四.10 | | ||
| 550 | +| 3 | goal 级总预算口径? | **先不考虑**,不做 goal 级闸门 | P3C §十一.6 | | ||
| 551 | +| 4 | 排队中的任务能不能改依赖? | **可以改**,`queued` 开放 `depends_on` 的 PATCH | P3C §六.3 | | ||
| 552 | +| 5 | `/api/tasks` + `TaskTimeline` 那套同名不同源的任务树怎么收? | **统一到 `tasks` 表一套,删掉另一套** | P3C §十二(批 5)| | ||
| 553 | + | ||
| 554 | +**定案 2 的要点是「不新造验收 agent」**:task 级与 goal 级验的是**基数不同的东西**(R-12 / NAMING §四 那张表)——task 级问「这一个 task 的 `task-goal` 达成没有」,goal 级问「N 个 task 合起来,人提的需求达成没有」。5 个 task 全 `completed` 不等于人要的东西做出来了,这个缺口正是 goal 层存在的理由。所以是**同一个本体在 goal 层再调一次**,不是复用 task 级的结论。单 task 的 goal 跳过这次调用(两级验收此时是同一件事)。 | ||
| 555 | + | ||
| 556 | +**定案 3 保留记账、只砍闸门**:`planning_tokens` / `validation_tokens` 两个字段留着。它们不是预算闸门,是让 goal 层那两次 agent 调用(拆分 + 验收)的花费**可见** —— 不记就是 `defaultPlanWorkGraph` 注释点名的「隐形的一半」,而且界面上「本次拆分消耗 ~N tokens」这个成本确认步骤要用它。 | ||
| 557 | + | ||
| 558 | +**定案 5 有一个反直觉的前提,实现前必须先读**:核实后确认**两套体系不是并列的,新体系的回调链路长在旧体系身上** —— `reportRunOutcome` 收敛后调的正是 `POST /api/agent/task/report`。所以「删掉另一套」的准确含义是: | ||
| 559 | + | ||
| 560 | +- **删**:`system/tasklog/` 文件存储 + `TaskService.getAggregatedTasks()`(从会话记录**反推**任务树的那套启发式,391 行里的主体); | ||
| 561 | +- **留但换数据源**:`POST /api/agent/task/dispatch` 与 `/task/report` —— 它们的 URL 与请求体形态**一个字都不能动**,因为写在 `packages/agent-core/base/common/tools-base.md` 里,是所有 agent 的工具说明,改了等于同时改所有 agent 的提示词;`getTaskTimeline()` 读的是 agent 真实会话记录(一手数据,不是反推),留。 | ||
| 562 | + | ||
| 563 | +统一还顺带修好两件事:30 分钟派单去重从「`readdirSync` 整个 tasklog 目录 + 逐个 JSON.parse」变成一条 SQL;两套同前缀不同源的 `T-` id 合一。 | ||
| 564 | + | ||
| 544 | **落点**:[27-P3C goal / task 两级模型](./27-p3c-goal-task-model.md)(本条派生出的工作项文档)。 | 565 | **落点**:[27-P3C goal / task 两级模型](./27-p3c-goal-task-model.md)(本条派生出的工作项文档)。 |
| 545 | 566 | ||
| 546 | --- | 567 | --- |
| @@ -584,3 +605,4 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file | @@ -584,3 +605,4 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file | ||
| 584 | | R-20.3 | goal / task 描述**一经下达不可改**(artifact 已被 agent 消费,改库等于篡改审计输入)| P3C §四.4 | | 605 | | R-20.3 | goal / task 描述**一经下达不可改**(artifact 已被 agent 消费,改库等于篡改审计输入)| P3C §四.4 | |
| 585 | | R-20.4 | 新增 run status **`queued`**,出队原子性靠既有唯一索引兜底 | P3C §四.3 | | 606 | | R-20.4 | 新增 run status **`queued`**,出队原子性靠既有唯一索引兜底 | P3C §四.3 | |
| 586 | | R-20.5 | 四条口径:**`task-planning` 进 v1** / 并行留批 4 / manual 不自动出队 / 上游失败下游自动 blocked | P3C §四.9 / §五.1.2 / §五.2bis | | 607 | | R-20.5 | 四条口径:**`task-planning` 进 v1** / 并行留批 4 / manual 不自动出队 / 上游失败下游自动 blocked | P3C §四.9 / §五.1.2 / §五.2bis | |
| 608 | +| R-20.6 | 五条收尾:退役别名 / goal 验收用 `org-validator` / 预算不做 / `queued` 可改依赖 / **两套任务树统一删一套** | P3C §四.10 / §六.3 / §十二 | |