# 子 Agent 编排与信号投递设计评审答复 > 状态:设计方答复(2026-08)。 > > 本文逐项回应 `docs/SUB_AGENT_ORCHESTRATION_REVIEW.md`。评审原文作为审核记录保留;已接受的结论同时回写到 `docs/SUB_AGENT_ORCHESTRATION_DESIGN.md`,后者仍是后续实现的规范来源。代码级实施方案见 [`SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md`](SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md)。 ## 1. 总体答复 接受评审的总体结论:方案方向成立,但 A1–A5 必须在实现前成为明确契约。全部 A、B、C 项均采纳;其中 A2、B2 和 B4 不只补充文字,还调整了原方案的数据流: - durable Agent event 不进入普通 session 消息队列,而由 SQLite inbox 保存 payload、独立的合并式 wake lane 只传递“有待处理事件”的提示。 - `/stop` 清理瞬时用户工作,但不通过丢弃内存队列来确认 durable event;事件由条件更新立即释放,lease expiry 只作为崩溃兜底。 - 后台结果调度采用有界公平策略,UI 未读状态降为可观察性能力,不再承担防饥饿正确性。 - inbox 容量在接纳 background run 时预留终态事件空间;signal 可以因容量不足被拒绝,completion 不能在 run 结束时才发现无处落库。 - continuation 使用“持久化但对客户端隐藏”的内部触发消息,保证 Provider replay、事务提交和客户端渲染三者一致。 | 评审项 | 答复 | 设计处理 | |--------|------|----------| | A1 | 接受 | 独立 durable wake lane、claim-on-run、公平调度和有界重试 | | A2 | 接受 | `/stop` 条件释放、lease guard、取消 completion 的 status-only 语义 | | A3 | 接受 | run admission 与 step execution permit 分离 | | A4 | 接受 | CancellationToken 贯穿 AgentLoop;明确 `/stop` 与 `steer` 不同 | | A5 | 接受 | hidden trigger message、Turn origin、稳定 delivery binding | | B1 | 接受 | 默认隔离;persistent browser profile 是唯一显式共享路径 | | B2 | 接受 | 容量预留、dead-letter fallback、重试边界 | | B3 | 接受 | `all` 与 `each` 的事件生成和 deadline 语义 | | B4 | 接受 | worker 有界公平;UI 未读只负责呈现 | | B5 | 接受 | 未配置价格时 `cost=NULL` | | B6 | 接受 | sub-run sleep 只响应 timer/cancellation | | C1 | 接受 | 非空 event key、非空 caller scope、partial unique index | | C2 | 接受 | 深度、task-tree 授权、skills/memory 继承规则 | | C3 | 接受 | 删除不存在的 `async` 迁移别名 | | C4 | 接受 | 枚举 WebSocket 请求、投影和 Turn origin 变更 | ## 2. A 级意见答复 ### A1 — Session 队列饱和语义 **答复:接受。durable event 不与用户 `AgentTask` 共用 payload mpsc。** 实现采用两条不同语义的 lane: ```text user task lane bounded mpsc(32),保存任务;满时明确拒绝新用户输入 agent inbox wake lane watch revision,合并通知;payload 始终留在 SQLite ``` Router 对活动 Turn 的 `steer` 使用 lease → mailbox reservation → durable admitted → activate 的两阶段 admission;reservation 在 durable 更新成功前不可被 AgentLoop 排空,且 SQLite I/O 不跨 Session 锁。失败或 `queue` 事件都恢复/保持 `pending`,只递增 wake revision。worker 在真正准备执行 continuation 时才领取 lease,不先把已 leased 的事件塞进可能被丢弃的 mpsc。 `watch` 只负责降低延迟:发送失败、revision 被合并或进程退出都不影响事实状态。Gateway 启动、reload 激活和周期恢复扫描会重新发现 `pending`/expired lease。普通 session 队列满不再构成 durable event 丢失或无限重试问题。 worker 在每个 Turn 调度边界执行有界公平:通常先处理用户任务;连续处理 4 个用户 Turn,或最老 pending event 已等待 30 秒后,必须先领取一批 continuation。它仍不能中断当前不可分割的 Turn,因此时限从下一个调度边界计算。 ### A2 — `/stop`、worker 退出与事件恢复 **答复:接受。lease expiry 只能是崩溃兜底,不能是正常 `/stop` 的唯一恢复路径。** 调整后的链路为: 1. `/stop` 在关闭 TurnMailbox 时取回尚未提交的 durable event IDs。 2. 在 session generation 失效后,以 `lease_token`/`admitted_turn_id` 条件更新把这些事件立即恢复为 `pending`。 3. continuation 执行持有 `InboxLeaseGuard`;正常失败、取消或 stale generation 会显式 release,只有进程崩溃或任务被强制 abort 才等待 `lease_until` 到期。 4. 普通内部 continuation 不作为 payload 存在 session mpsc 中,因此 `agent_tx.take()` 不会吞掉 leased event;worker 领取后才在本地构造 typed task source。 5. Coordinator 独立拥有 background run。`/stop` 取消 run token 后,Coordinator 仍负责用条件事务写入 `cancelled` 终态,迟到的 completed 结果不能覆盖它。 由本次 `/stop` 自身造成的 cancelled completion 设为 `requires_continuation=false`:事件和 run 状态会持久化并投影到任务树,但事件在同一事务中记为 status-only consumed,不会在 `/stop` 后反向启动一个“任务已取消”的主 Agent Turn。`/stop` 前已经存在、尚未处理的 signal/completion 不被确认或删除,恢复为 pending 后仍可继续投递。 ### A3 — `waiting_children` permit 归属 **答复:接受。permit 不由整个 Agent Run 持有,也不由 `delegate` 等编排工具持有。** Coordinator 管理两类限制: - **run admission quota**:限制树、session、Agent 的已接纳/未终态 run 数量;可以跨 `waiting_children` 持有。 - **step execution permit**:限制当前正在占用 Provider 或普通工具执行资源的步骤;只在一个步骤期间持有。 AgentLoop 在每次 Provider 请求前按固定顺序获取 global → session → agent provider permits,流结束或取消后立即释放。普通工具调用由 tool executor 获取 tool permit;`delegate`、`agent_task`、`emit_signal` 等 runtime-control 工具不获取这种稀缺执行 permit。 foreground `delegate` 在创建 child 前通过状态 guard 把父 run 从 `running` 条件更新为 `waiting_children`,等待期间没有 provider/tool permit。children 终态后 guard 把父状态恢复为 `running`;父 Agent 的下一次模型迭代重新竞争 permit。这样即使 provider 并发上限为 1,父等待 child 也不会死锁。 ### A4 — AgentLoop 结构化取消 **答复:接受,并把它提升为 Phase 2 的独立前置里程碑。** `AgentLoop` 的执行入口将显式接收 cancellation context,而不是只依赖父 future 被 drop: ```text root Turn token └── foreground run token └── descendant foreground run token root session token ── independently owns background run tokens ``` Provider stream、可取消等待和工具批次外层都观察 token;AgentRunner 的终结路径把取消归一为类型化 `cancelled`,Coordinator 再用 execution ID 条件提交。父取消、run timeout、reload/shutdown 可以组合为任一触发即取消。 “不默认硬中断”只约束普通 `steer`:它等待安全边界,不取消 Provider 或副作用工具。`/stop` 保持现有强停止语义,会取消 token 并使 root Turn future 失效;未在宽限期内自行退出的独立 child task 由 Coordinator/Supervisor abort。无论 future 如何结束,terminal condition update 都阻止迟到结果提交。 ### A5 — continuation Turn 模型 **答复:接受。continuation 需要同时满足 durable replay、无伪用户气泡和正常 Turn 投递。** 每个 continuation 生成一条内部触发消息: - 数据库 role 使用 Provider 可兼容的 `user`,source 为 `agent_signal`/`agent_result`。 - 增加 `client_visibility=hidden` 和 `turn_origin=agent_continuation`;普通历史 API 和 `turn_committed.messages` 不投影这条消息。 - 内容是有界、带 event/run reference 的 runtime envelope,不伪造外部 sender,也不直接采用子 Agent 输出中的指令优先级。 - Provider 历史 replay 会包含该隐藏消息,因此后续 assistant 回复不会成为无来源的悬空历史。 - 隐藏触发消息、assistant/tool 结果、usage 和 inbox `consumed` 在同一事务中提交;失败时全部不确认。 客户端继续使用 `turn_updated`/`turn_committed` 展示 assistant Turn,但帧增加 `turn_origin`,从而可以显示“后台结果处理”标记且不创建用户气泡。 内部 Turn 的出站目标来自 root session 的 durable delivery binding:`channel`、`chat_id` 以及可复用的 thread/root 上下文。一次性 `reply_to` 不得复用。若没有可用的外部 binding,结果仍持久化并供 WebUI/TUI 历史读取,不猜测其他目标。 ## 3. B 级意见答复 ### B1 — browser/resource scope 隔离 **答复:接受。** 新具名 Agent 默认使用 `root_session_id + run_id` 的瞬时资源 scope,不继承父会话 browser cookie。需要共享登录态时,官方路径是由 Root 创建/选择经过校验的 `browser_profiles` persistent ID,并把该 ID 作为显式 task/artifact reference 交给获准使用 browser 的子 Agent;子 Agent必须在每次相关调用中显式传入该 ID。 为平滑迁移,由无 target 的旧调用映射出的内置 `general` 兼容 Agent 可在弃用期保留父 session transient scope;具名 Agent不继承这个例外。文档和 tool result 会明确提示两种 scope 的差异。 ### B2 — dead letter、重试和 inbox 上限 **答复:接受。** 接纳 background run 时按 completion policy 预留不可抢占的终态 event slot:`each` 每个 run 一个,`all` 每个 group 一个。容量不足时 `delegate` 在创建 run 前拒绝。signal 只使用未预留容量,满时 `emit_signal` 返回 `inbox_full`,但不终止 run。因此已接纳 run 的 completion 永远不会在终结时因 inbox 满而丢失。 暂定恢复策略为 8 次可配置尝试,退避 `1s/5s/30s/2m/10m` 后封顶 10 分钟,并同时受 event TTL 限制。瞬时 Storage/worker/Provider continuation 失败可重试;session 已删除、授权事实失效或 payload 永久损坏立即 dead-letter。lease timeout 不单独计作永久错误,但会记录 attempt 和原因。 事件进入 dead-letter 后: 1. 保存最终原因和 `dead_lettered_at`,在任务树/API 中持续可见。 2. 通过 OutboundDispatcher 最多发送一次有界 system fallback,内容只包含 run ID、终态和查询提示,不复制大结果。 3. 用 `fallback_notified_at` 保证 fallback 幂等;渠道也失败时仍以 SQLite 记录和管理 UI 为最终可诊断出口。 ### B3 — `completion_policy=each` **答复:接受。** 语义修订为: - `each`:每个 run 进入终态即创建独立 completion event;Router 可在 300–500ms debounce 窗口合并一次 continuation,但不能等待其他 sibling。 - `all`:单 run 终态只更新 group 计数,不创建可投递 completion;全部终态或 group deadline 到达后创建一个 `group_completion` event,包含全部逐项状态和 result references。 - group deadline 到达时,未终态 children 被取消并条件更新为 `timed_out`,随后生成唯一 group completion。 因此 inbox schema 允许 run-scoped 或 group-scoped event 二选一,而不是强制 `run_id NOT NULL`。 ### B4 — UI 未读状态与防饥饿 **答复:接受。** 正确性由 A1 的 worker 有界公平策略保证;UI 未读只呈现尚未汇总/已 dead-letter 的事件数量,不参与调度。Phase 3 明确包含未读计数、event revision 和 reconnect 后全量校准。 ### B5 — `cost` 与定价配置 **答复:接受。** Phase 2 保留 nullable `cost` 字段,但只有 Provider profile 明确提供 input/output/cache 价格时才计算;当前配置没有价格来源,因此写 `NULL`。usage token 仍照常持久化。价格配置和历史重算不属于本次编排功能的前置条件。 ### B6 — sub-run 内 sleep **答复:接受。** `TurnWakeupHandle` 只存在于 root interactive Turn。sub-run 的 `ToolExecutionContext.turn_wakeup=None`,其 `sleep` 只等待 timer、run cancellation、timeout 或 shutdown;不会监听 root session 用户输入或其他 Agent signal。需要被主 Agent立即控制时使用 `agent_task.cancel`,由 cancellation token 唤醒。 ## 4. C 级意见答复 ### C1 — SQLite NULL 与事件去重 **答复:接受。** 所有参与唯一约束的 scope/key 都规范化为非空值: - background idempotency 使用 `caller_scope_id TEXT NOT NULL`;Root 固定为字面量 `ROOT`。 - `idempotency_key` 仍可为空,但使用 `CREATE UNIQUE INDEX ... WHERE idempotency_key IS NOT NULL` 的 partial unique index。 - 无 `dedupe_key` 的 signal 使用 `signal:`。 - 有 `dedupe_key` 的 signal 使用 `signal::`,只在冷却窗口内去重,不会永久压制同类告警。 - run completion 使用固定 `completion:terminal-v1`;group completion 使用 `group-completion:terminal-v1`。 ### C2 — 深度、task-tree 授权和上下文继承 **答复:接受。** - 全局 `max_tree_depth` 是 root-relative 硬上限;Definition `limits.max_depth` 是该 Agent可继续创建的最大相对后代深度。child 的 remaining depth 为 `min(parent_remaining - 1, target_definition.max_depth)`,任何一项为 0 都不能继续委托。 - `agent_task` 查询必须匹配当前 `root_session_id`。Root 可操作本 session 的整棵树;子 Agent只能读取自身与后代,只能取消其未终态后代,不能通过猜测 run ID 跨 session 或操作祖先/sibling。 - 子 Agent不继承主会话 history、memory recall 或临时 activated skills。第一版仅在 Definition 工具集中包含 `get_skill` 时注入受信任 Skill catalog;调用方只能通过显式 task/context 传递事实。若未来开放 memory,必须新增管理员配置的只读 scope,不能默认继承。 ### C3 — `async` 别名 **答复:接受。** 删除 `async → background`。迁移只接受代码中确实存在的 `inline`、`parallel`、`background`;新 prompt/schema 只公布 `foreground`、`background`。 ### C4 — 客户端协议清单 **答复:接受。** 协议按通用运行投影设计,不为 Signal 单独复制一套模型: - `WsInbound::GetAgentRuns { session_id, cursor, limit }` - `WsInbound::GetAgentRun { session_id, run_id }` - `WsOutbound::SessionAgentRuns { session_id, revision, runs, next_cursor }` - `WsOutbound::AgentRunUpdated { session_id, revision, run }` - `WsOutbound::AgentEventUpdated { session_id, revision, event }` - `TurnSnapshot` 与 `WsOutbound::TurnCommitted` 增加 `turn_origin = user | agent_continuation | scheduled` `AgentEventUpdated` 同时承载 accepted、admitted、consumed、dead-letter 等状态,按 `(session_id, revision, event_id)` 幂等合并。断线重连后客户端用 `GetAgentRuns` 全量校准,实时帧只是增量。取消操作第一版继续通过 `/stop`、`agent_task.cancel` 或受保护管理 API,不额外开放一个缺少权限上下文的裸 WebSocket cancel 帧。 ## 5. 对分期的调整 | Phase | 调整后的完成条件 | |-------|------------------| | 1 | 除原内容外,明确 browser 兼容 scope、skills/memory 规则和 sub-run sleep 行为 | | 2A | CancellationToken 贯穿 AgentLoop,先以现有 root Turn/sleep/Provider tests 锁定取消语义 | | 2B | run 持久化、step execution gate、foreground child cancellation 和结果查询 | | 3 | durable wake lane、capacity reservation、hidden continuation trigger、bounded fairness、dead-letter fallback 与 WebSocket run/event projection | | 4 | typed TurnMailbox、emit_signal、steer admission,以及同一 `AgentEventUpdated` 的 Signal 卡片呈现 | | 5 | root Turn wake-aware sleep;sub-run 保持 timer/cancellation-only | Phase 3 上线时保留旧 direct notification 的受控兼容开关,但仅作为 rollback 手段,默认路径必须是 inbox/continuation;不能同时投递两条用户通知。开关移除前必须验证 dead-letter fallback、重启恢复和 reconnect 校准。 ## 6. 最终结论 评审结论“方向通过,需修订后实现”成立。修订后的关键边界是: ```text SQLite inbox 保存事实 ├── direct steer admission → 当前 TurnMailbox └── coalesced wake → worker claim → hidden continuation Turn 普通 user mpsc 满 ≠ durable event 丢失 /stop 丢弃瞬时用户工作 ≠ 确认 durable event run 存活配额 ≠ Provider/tool step permit UI 未读提示 ≠ 调度正确性 ``` 在 A1–A5 的专项契约和上述 schema/protocol 调整落地前,不应开始 Phase 3/4 的生产实现。