- drop agent_run_groups table and group_id/scope_kind/scope_id columns (schema v8) - remove group_id from AgentExecutionContext and recovery group counters - flatten TasksPage background tab into a per-run list - add WebUI Agents page with definition CRUD and inline provider/model - bump version to 1.11.0
16 KiB
子 Agent 编排与信号投递设计评审答复
状态:设计方答复(2026-08)。
本文逐项回应
docs/SUB_AGENT_ORCHESTRATION_REVIEW.md。评审原文作为审核记录保留;已接受的结论同时回写到docs/SUB_AGENT_ORCHESTRATION_DESIGN.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:
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 的唯一恢复路径。
调整后的链路为:
/stop在关闭 TurnMailbox 时取回尚未提交的 durable event IDs。- 在 session generation 失效后,以
lease_token/admitted_turn_id条件更新把这些事件立即恢复为pending。 - continuation 执行持有
InboxLeaseGuard;正常失败、取消或 stale generation 会显式 release,只有进程崩溃或任务被强制 abort 才等待lease_until到期。 - 普通内部 continuation 不作为 payload 存在 session mpsc 中,因此
agent_tx.take()不会吞掉 leased event;worker 领取后才在本地构造 typed task source。 - 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:
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 后:
- 保存最终原因和
dead_lettered_at,在任务树/API 中持续可见。 - 通过 OutboundDispatcher 最多发送一次有界 system fallback,内容只包含 run ID、终态和查询提示,不复制大结果。
- 用
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_completionevent,包含全部逐项状态和 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:<event_uuid>。 - 有
dedupe_key的 signal 使用signal:<normalized-key>:<cooldown-window-id>,只在冷却窗口内去重,不会永久压制同类告警。 - run completion 使用固定
completion:terminal-v1;group completion 使用group-completion:terminal-v1。
C2 — 深度、task-tree 授权和上下文继承
答复:接受。
- 全局
max_tree_depth是 root-relative 硬上限;Definitionlimits.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. 最终结论
评审结论“方向通过,需修订后实现”成立。修订后的关键边界是:
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 的生产实现。