PicoBot/docs/SUB_AGENT_ORCHESTRATION_REVIEW.md
xiaoxixi ac201a3949 feat: durable agent orchestration with run persistence, inbox continuation, and signal/steer
- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph,
  fail-closed tool scoping, and signal contracts
- structured cancellation (AgentError::Cancelled/TimedOut) across provider
  streams, tool batches, and sleep; /stop drives the same terminal state
- schema v6 run/group/inbox persistence with execution-ID conditional
  transitions and completion-slot reservations
- ExecutionGate separating run quota from provider/tool step permits
- background completion inbox with hidden-trigger continuation turns,
  fairness scheduling, lease release, dead-lettering, and activation recovery
- typed TurnMailbox with two-phase steer admission and atomic consumption at
  turn commit; /stop releases admitted steer events back to pending
- emit_signal tool with contract-enforced rate/dedupe/severity/size limits
- WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated),
  /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree
  and signal cards
- ChannelContext.durable_private persisted for continuation delivery reuse

Version 1.7.0
2026-08-11 11:51:20 +08:00

154 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 子 Agent 编排与信号投递设计审核报告
> 状态审核完成2026-08。审核对象为设计提案 `docs/SUB_AGENT_ORCHESTRATION_DESIGN.md`,该设计尚未实现;本文所有"现状"描述以当前代码和测试为准。
>
> 本文结合现有实现逐条核实设计的现状诊断,评估架构合理性,并按严重程度列出缺陷与落地前必须补齐的定义。行号基于审核时的代码快照,后续实现合并后可能过时。
>
> 设计方逐项答复见 [`SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md`](SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md);已接受结论同步写入设计文档。
## 1. 审核范围与依据
### 1.1 审核对象
- 设计文档:`docs/SUB_AGENT_ORCHESTRATION_DESIGN.md`(提案,未实现;`rg` 确认 `src/``webui/` 中无任何 `AgentCatalog`/`AgentCoordinator`/`agent_inbox`/`emit_signal` 相关实现)
- 对照实现:`src/agent/sub_agent.rs``src/agent/agent_loop.rs``src/agent/steering.rs``src/session/session.rs``src/session/turn_input.rs``src/session/persistence.rs``src/tools/delegate.rs``src/tools/sleep.rs``src/tools/send_message.rs``src/tools/traits.rs``src/storage/``src/work/mod.rs``src/scheduler/mod.rs``src/task_supervisor.rs``src/config/mod.rs``src/gateway/reload.rs`
### 1.2 审核依据
- `docs/ARCHITECTURE.md` 与 AGENTS.md 中的架构边界和并发不变量
- 现有相似机制Scheduler durable lease`src/storage/scheduler.rs:310-348`、WorkManager 乐观并发(`src/work/mod.rs:320-342`、TurnController"持久化后才 Completed"`src/session/persistence.rs:147-167`、RuntimeAdmission`src/gateway/reload.rs`
## 2. 总体结论
**设计方向合理,可以按分期推进;但存在 5 处与现有代码强耦合的接缝缺口A1A5落地前必须先补齐定义否则 Phase 2/3 会被迫返工。**
设计的核心决策——`foreground/background``queue/steer` 两个正交维度、先持久化后唤醒、SQLite inbox 为权威来源、lease/consumed 事务提交、ancestry 环检查、AgentCatalog 绑定运行代——与 PicoBot 既有不变量一致,且现状诊断(设计 §1 的 9 条)逐条属实(见第 3 节)。主要问题不在方向,而在设计与现有 session worker、`/stop`、AgentLoop 取消机制的衔接处留白过多。
## 3. 现状诊断核实
设计 §1 的 9 条诊断全部与代码一致:
| # | 设计诊断 | 代码证据 | 结论 |
|---|----------|----------|------|
| 1 | 所有子 Agent 复用同一 `LLMProviderConfig` | `SubAgentManager.provider_config` 单实例(`src/agent/sub_agent.rs:119`inline/background 均用它创建 Provider:204、:477 | 属实 |
| 2 | 无具名角色文件,工具权限由 `allowed_tools` 临时决定 | `delegate` schema 的 `allowed_tools` 数组(`src/tools/delegate.rs:50-54`);未填时用默认只读集(`sub_agent.rs:34-41` | 属实 |
| 3 | 子 Agent 被统一移除 `delegate` | `filter_tools` 硬编码排除 `delegate`/`todo`/`reload_config``sub_agent.rs:177-181` | 属实 |
| 4 | `DelegateContext` 只有 session/channel/chat | `sub_agent.rs:90-95`;无 caller/parent/depth/ancestry | 属实 |
| 5 | `parallel` 混淆"委托方是否等待"与"是否并发" | `run_parallel` 就是 `join_all(run_inline)``sub_agent.rs:332-346` | 属实 |
| 6 | 后台完成通知直发 `MessageBus.outbound`,不成为主 Agent 输入 | `background-task-notifications` 任务格式化后 `publish_outbound` fire-and-forget`src/session/session.rs:1757-1776`),不写会话历史、不触发 Turn | 属实 |
| 7 | Steering mailbox 只建模用户输入,且继承 `/stop` 丢弃语义 | admission 只推 `SourceKind::UserInput``session.rs:2910-2938`AgentLoop 防御性归一 `role=user``src/agent/agent_loop.rs:624-628``/stop``close_and_take_pending` 主动丢弃(`session.rs:2120-2128``src/agent/steering.rs:215-229` | 属实 |
| 8 | `SleepTool` 只等定时器 | `tokio::time::sleep` 单一路径(`src/tools/sleep.rs:72`),无 wakeup/cancel 分支 | 属实 |
| 9 | `send_message` 同时覆盖跨 Channel、目标会话写入和同 Turn 附件暂存 | `src/tools/send_message.rs` 的 target/content/origin/files 参数;`OutboundDelivery::AttachedToCurrentTurn` 同 Turn 分支(`src/tools/traits.rs:127-131` | 属实 |
**补充:设计隐含覆盖了一个现存 bug。** inline 结果截断时提示"完整结果请使用 check_task 查看"`sub_agent.rs:809`),但 `run_inline` 从不写 `background_tasks` 表(只有 `run_background` 写,`sub_agent.rs:373-398``check_task``sub_agent.rs:707-713`)查不到 inline 结果。设计 Phase 2"Foreground 结果也持久化"§23修复此问题分期安排正确。
## 4. 设计合理性评估
以下决策予以肯定:
| 设计点 | 评估 |
|--------|------|
| 执行生命周期与投递方式正交化§4.1 | ✅ 干净消除 `parallel` 的语义混淆;批量 foreground 并发等价旧 parallel 但不作为第三种模式 |
| 先持久化后唤醒、内存 wakeup 仅为加速器§6.4、§14.4 | ✅ 与"持久化后才 Completed"既有不变量(`persistence.rs:147-167`)同构 |
| lease/consumed 与条件更新§14、§16.4 | ✅ 复用 Scheduler durable lease 与 WorkManager 乐观并发的成熟模式 |
| `target not in ancestry` 拒绝 A→B→A§7.1 | ✅ 以显式 iteration workflow 替代隐式递归,边界正确 |
| 授权不依赖 task-local§8 | ✅ 正确诊断现状 `DELEGATE_CONTEXT` task-local`sub_agent.rs:19-28`)不是授权事实来源;`tokio::spawn` 不传播 task-local |
| AgentCatalog 以 Arc 固定运行代§5.4、§18.4 | ✅ 与 RuntimeAdmission 现有集成一致(`SubAgentManager` 已接 admission`sub_agent.rs:157-163、353-358` |
| `llm_profile` 引用现有 `config.agents`§5.2 | ✅ `Config.agents``get_provider_config(agent_name)` 已存在(`src/config/mod.rs:45、712-745`),无需配置重构 |
| Completion 由运行时自动生成、不依赖模型记得调工具§12.1 | ✅ 正确;`emit_signal` 无任意目标参数,收敛了权限面 |
| 可唤醒 sleep 用 watch revision 而非裸 Notify§17.2 | ✅ 正确规避"输入先于订阅到达"的丢失唤醒竞态 |
| §26 不变量清单 | ✅ 与 ARCHITECTURE.md 一致,可作为实现验收标准 |
| 分期顺序§23 | ✅ Phase 1 纯增量Phase 2 顺带修复 inline 截断 bug依赖方向正确 |
## 5. 缺陷清单
严重程度A=主要落地前必须补齐定义B=中等(实现对应 Phase 前补齐C=次要(修订文档即可)。
### 5.1 A 级:主要缺陷
**A1 — Session 队列饱和语义与现状冲突(设计 §15.3**
现状session 队列容量 32`session.rs:27`),满时 `try_send` 失败直接丢弃输入并回复"队列已满"`session.rs:3001-3007`)。设计要求 durable event 在队列饱和时"保持 durable pending由 Router 有界重试,不能丢弃",但未定义:
- agent 事件与用户输入是否共用同一 mpsc共用则用户流量可长期占满队列事件重试无收敛界
- Router 重试的退避、deadline 与最终处置;
- §15.4 只拆分了 TurnMailbox 的 laneuser 32/64KiB、agent 8/32KiBsession 级队列的 agent lane 容量与优先级未定义。
**A2 — `/stop` 与 worker 退出时 durable event 的恢复机制缺失(设计 §18.3**
现状 `/stop``current_cancel.take()``session.rs:2117`)→ `close_and_take_pending` 丢弃 steering:2120-2128`agent_tx.take()` 丢弃全部排队任务(:2136→ bump generation/state_version:2139-2140→ 取消后台子任务(:2144-2148。mpsc 被 drop 时没有逐项回调。设计未定义:
- 被丢弃的内部 `AgentTask`(含 `BackgroundAgentResults`)如何触发 inbox lease 释放——只能靠 lease 超时被动收敛(应明说延迟界),或为 AgentTask 增加 Drop guard未提
- `/stop` 后 worker 退出(`task_rx.recv()` 返回 None 即 break`session.rs:3150-3152`),被取消 run 异步生成的 cancelled completion 由谁、何时消费,完整链路未写。
**A3 — `waiting_children` permit 释放在现有结构中无落点(设计 §19.2**
AgentLoop 目前没有任何 permit/cancellation 原语(`agent_loop.rs` 无 CancellationToken/select取消靠 worker 整体 drop future`session.rs:3778-3804`TaskSupervisor 也没有并发上限,只在 stopping 时拒绝 spawn`task_supervisor.rs:60-89`)。设计只给出原则"permit 限制活跃 Provider/工具步骤",未定义:
- permit 由谁持有与获取/释放AgentLoopAgentRunnerCoordinator
- delegate 在父 run 的 tool batch 内执行(`agent_loop.rs:1187-1242`),父 run 进入等待时释放 permit 的钩子如何嵌入现有批处理流程。
这是 Phase 2 复杂度最高的部分,只给原则不够。
**A4 — 结构化取消是前置条件,但 AgentLoop 当前零支持(设计 §18.1**
新架构中子 run 是 Coordinator spawn 的独立任务,父 future 被 drop 不再传播取消,必须用显式 CancellationToken 树贯穿 AgentLoop——这是横切重构设计只在 Phase 2 列了一行"实现结构化取消"。另有一处表述需要澄清§3 非目标"不默认硬中断正在进行的 Provider 请求"只约束 `steer`;现有 `/stop` 恰是硬 dropdrop `process_future` 连带中断 provider 流,`session.rs:3778-3804`)。文档应显式声明 `/stop` 保持硬语义,避免实现时误读为 `/stop` 也要走安全边界。
**A5 — 内部 continuation Turn 的消息/持久化/渲染模型未定义(设计 §16.3**
现有 Turn 以用户消息为起点:先持久化用户消息(`session.rs:3191``prepare_turn_input` 把 runtime context 附加到最后一条 user message`src/session/turn_input.rs:19-25`WebUI/TUI 按 user/assistant 交替渲染。设计说"内部输入不显示用户气泡",但未定义:
- continuation Turn 写什么消息行(无 user 行?系统行?)、历史 replay 给 provider 时的形态;
- 客户端如何渲染无用户消息的 Turn§15.1 的 SourceKind 扩展只解决标记问题);
- continuation 输出的投递目标:`AgentTask` 的 channel/chat_id/channel_context 来自 InboundMessage`session.rs:631-641`),内部任务没有 channel 上下文,应显式规定投递到 session 最近的 channel/chat。
### 5.2 B 级:中等缺陷
**B1 — browser/resource scope 隔离是行为破坏,且无共享出口(设计 §10。** 现状子 Agent 复用父对话的 browser session`browser_session_id` 回退到 delegate context 的 session_id`sub_agent.rs:264-279`background 路径 :505-508 同)。改为 `root_session_id + run_id` 隔离会破坏依赖父会话登录态/cookie 的场景。"确需共享必须由工具定义显式支持"没有给出机制,应指明 `browser_profiles` persistent ID 为官方共享路径。
**B2 — dead_letter 与重试上限策略空缺(设计 §14.3、§21。** Phase 3 移除直发通知后continuation 反复失败转 dead_letter 时结果对用户彻底不可见,文档未定义 dead-letter 后的用户可见行为(如回退一条系统通知)。`max_pending_inbox_events_per_session=128` 打满后新事件的行为同样未定义。
**B3 — `completion_policy=each` 只有字段没有语义(设计 §14.2)。** 正文只描述了 `all`:一个 run 超时会把全组结果交付拖到 group deadline缺少 per-run 提前交付或分组拆分策略。
**B4 — "UI 未读状态"是防饥饿的关键依赖,但不存在且未立项(设计 §16.2)。** 调度优先级把 queue completion 排在用户输入之后,持续用户流量下后台结果会被无限推迟,设计靠"UI 未读状态"兜底;该 WebUI 功能当前不存在Phase 3 只写了"WebUI 投影",未列为明确工作项。
**B5 — `cost` 字段假设了不存在的定价配置(设计 §6.5、§14.1)。** `agent_runs.cost` 与 ProviderFactory"复用价格信息"的前提不成立config 从不填 `price_input/output_per_million``src/config/mod.rs:742-743` 硬编码 None无配置键解析。要么补定价配置要么注明 cost 暂为 NULL。
**B6 — 子 Agent run 内 sleep 的唤醒语义未定义(设计 §17。** 全章隐含 root session 的 Turnsub-run 没有"当前 session 用户输入"概念。应显式规定 sub-run 内 sleep 只响应自身 cancellation/timeout否则 `TurnWakeupHandle` 的来源不明。
### 5.3 C 级:次要问题
**C1 — SQLite UNIQUE 与 NULL 语义(设计 §14.3、§9.6)。** `UNIQUE(run_id, event_type, event_key)``emit_signal` 未提供 `dedupe_key``event_key` 的生成规则未定义。`idempotency_key` 的唯一范围 `(root_session_id, caller_run_id, key)` 在 ROOT 调用时 `caller_run_id` 为 NULLSQLite 中 NULL≠NULL 会导致去重失效,需要哨兵值(如 `root`)。
**C2 — 授权与上下文的细节留白(设计 §7.1、§9.1、§10。** definition `max_depth` 与全局 `max_tree_depth` 是否取 min 未明说;`agent_task` 工具能否操作本 root session 任务树之外的 run跨 session 越权未明说skills/memory 是否进入子 Agent 上下文未提(现状子 Agent 可带 skills prompt`sub_agent.rs:188-197`memory recall 只在 session Turn`turn_input.rs:47`)。
**C3 — `async` 别名是多余假设(设计 §22.1)。** 现代码从未接受 `async``delegate.rs:151-165` 只解析 inline/background/parallel该迁移条目可删。
**C4 — 客户端协议变更未枚举(设计 §20.2、Phase 4。** AgentSignal 卡片、"已接纳"状态、continuation Turn 都需要新的 `WsOutbound` 消息类型(`src/protocol.rs`Phase 4 只写"添加 AgentSignal UI 和任务树",未列协议变更清单。
## 6. 修订建议
实现启动前,建议在设计文档中补充五个专项定义(对应 A 级缺陷):
1. **Durable event 与 session 队列的 lane 划分**agent 事件是否独立队列、饱和时的重试退避/deadline/dead-letter 策略、与用户输入的优先级关系A1
2. **`/stop`、worker 退出与 lease 释放的衔接**:被丢弃内部任务的 lease 释放路径Drop guard 或明确依赖 lease 超时及延迟界)、`/stop` 后生成的 cancelled completion 的消费链路A2
3. **Permit 归属**:执行 permit 在 AgentLoop/AgentRunner/Coordinator 之间的获取与释放点,特别是 `waiting_children` 前后的钩子位置A3
4. **Continuation Turn 模型**消息行写入形态、provider replay 形态、客户端渲染契约、输出投递目标A5
5. **取消横切方案**CancellationToken 贯穿 AgentLoop 的接口设计,并显式声明 `/stop` 保持硬 drop 语义、安全边界注入只约束 `steer`A4
B 级问题建议在对应 Phase 实现前补齐B1/B6 在 Phase 1B5 在 Phase 2B2/B3/B4 在 Phase 3。
## 7. 分期实施意见
| Phase | 风险 | 意见 |
|-------|------|------|
| 1 具名 Agent 与 Foreground | 低 | 纯增量。`llm_profile` 直接复用 `Config::get_provider_config``config/mod.rs:712-745`),无配置重构。注意 B1browser scope 隔离会改变现有子 Agent 共享父会话 browser 的行为,需要迁移说明 |
| 2 统一 Run 持久化 | 高 | 改动面最大AgentLoop 取消与 permit 均为横切变更。建议先独立原型"CancellationToken 贯穿 AgentLoop",用现有 sleep 取消测试(`sleep.rs:194-237`)与 steering 恢复测试锁定回归基线,再叠加 permit |
| 3 Inbox 与 Queue Completion | 中 | UX 拐点:完成通知从直发 Channel 改为主 Agent continuation。建议保留配置开关回退直发通知覆盖 B2 的 dead-letter 空窗§25.4 测试矩阵是本 Phase 验收关键 |
| 4 Emit Signal 与 Steer | 中 | TurnMailbox 泛化触及 `handle_message` 核心 admission 路径(`session.rs:2815-2953`),与 `/stop` 的原子性必须沿用现有同锁判定模式;先补 C4 协议清单 |
| 5 可唤醒 Sleep | 低 | 相对独立。watch revision 方案正确;先补 B6 的 sub-run 语义 |
## 8. 结论
设计的现状诊断准确、核心决策与既有架构不变量兼容、分期依赖方向正确,**审核结论为"方向通过,需修订后实现"**。A1A5 五个接缝缺口不是方向错误,而是设计与 `session worker`/`/stop`/`AgentLoop` 取消机制的衔接定义不足;按第 6 节补齐专项定义后,可按第 7 节顺序分期实施。