PicoBot/docs/SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md
xiaoxixi 5501c539fc feat: remove agent run groups, add WebUI agent definition management
- 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
2026-08-13 14:03:01 +08:00

223 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_REVIEW.md`。评审原文作为审核记录保留;已接受的结论同时回写到 `docs/SUB_AGENT_ORCHESTRATION_DESIGN.md`,后者仍是后续实现的规范来源。代码级实施方案见 [`SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md`](SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md)。
## 1. 总体答复
接受评审的总体结论:方案方向成立,但 A1A5 必须在实现前成为明确契约。全部 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 的两阶段 admissionreservation 在 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 eventworker 领取后才在本地构造 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、可取消等待和工具批次外层都观察 tokenAgentRunner 的终结路径把取消归一为类型化 `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 eventRouter 可在 300500ms 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:<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 硬上限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 sleepsub-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 未读提示 ≠ 调度正确性
```
在 A1A5 的专项契约和上述 schema/protocol 调整落地前,不应开始 Phase 3/4 的生产实现。