# PicoBot 子 Agent 编排实施细节与可实施性审查 > 状态:实施基线(2026-08)。 > > 本文以 [`SUB_AGENT_ORCHESTRATION_DESIGN.md`](SUB_AGENT_ORCHESTRATION_DESIGN.md) 为产品与架构规范,以 [`SUB_AGENT_ORCHESTRATION_REVIEW.md`](SUB_AGENT_ORCHESTRATION_REVIEW.md) 和 [`SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md`](SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md) 为评审记录,并逐项对照当前代码给出可以直接拆分为开发任务的实现方案。 > > 本文不是“代码已经实现”的声明。实现完成前,运行时事实仍以当前代码、测试和 [`ARCHITECTURE.md`](ARCHITECTURE.md) 为准。 > 实施进度(2026-08):Phase 1 已落地具名 Definition/Catalog、不同 Provider profile、工具/Skill fail-closed 裁剪、显式 `AgentExecutionContext`、父子委托边与 ancestry 校验、canonical `foreground/background` schema 以及批量 foreground 并发。旧 general background 仅作为兼容路径保留。Phase 2A 已落地结构化取消:`AgentError::Cancelled/TimedOut`、CancellationToken 贯穿 root Turn、Provider 连接/stream、并行与串行工具批次以及 sleep;`/stop` 保留 oneshot 兼容桥接并同时取消 Turn token,协作式与强制路径提交同一 Cancelled 终态;树级 `max_runs_per_tree` 由共享原子计数在 foreground 委托接纳时强制。Phase 2B 已落地 durable foreground 编排:schema v6(`agent_run_groups`/`agent_runs`/`agent_session_state`/`agent_inbox_events` 与 messages/sessions 扩展列)在单一迁移事务中原子创建;Storage 领域 API(接纳、running/waiting_children 条件转换、execution-ID 条件 terminal commit、plan item 原子领取/完成、游标分页);`ExecutionGate` 分离 run quota 与 provider/tool step gate(global→session 顺序获取、弱引用键控回收、取消可中断等待),root Turn 步骤同样占用 step gate;`AgentCoordinator` 持久化全部具名 foreground run(单任务不建 group、批量建 group 并按请求顺序返回)、父 run `waiting_children` 转换、迟到结果丢弃与树位置授权;`agent_task` 工具提供 scoped get/list/get_result/cancel。foreground 不占用 run quota,嵌套 foreground 在并发上限为 1 时不死锁。Phase 3 已落地 durable inbox 与 queue continuation:具名 background 单任务经 Coordinator 接纳(completion slot 预留 → 持久化 queued → TaskSupervisor 托管 runner → terminal commit 原子转换 reservation 为 completion 事件 → notifier wake),spawn 拒绝执行补偿事务;`agent_inbox_events`/`agent_session_state` 容量条件更新与 claim/lease/admit/release/supersede/dead-letter API;`AgentInboxNotifier`(弱引用 late-bound wake)与 Session 双 lane worker(user mpsc + inbox watch 合并 wake),公平调度按 `max_user_turn_burst_before_inbox` 与 `max_inbox_wait_secs` 强制 continuation;continuation Turn 使用 hidden trigger、只读工具集,`commit_continuation_turn` 同事务提交 hidden trigger + assistant/tool/usage + event consume,失败显式 release lease;`client_visibility`/`turn_origin` 贯穿 ChatMessage/MessageMeta/协议 DTO,客户端历史查询默认过滤 hidden,`message_count` 只统计可见用户输入;activation recovery 收敛旧代 run(interrupted + failure completion)、过期 lease、group counter 与容量计数;`/stop`/archive/delete 走 `cancel_session`(suppress_continuation 写 consumed completion)并 dead-letter。background completion 不再直接通知 Channel。Phase 3 审查修复:worker 通过 `next_pending_due_at` 定时器在 release backoff 到期后重新 claim(不再依赖 wake);达到 `max_inbox_delivery_attempts` 的事件在正常运行中即 dead-letter 而非无限重试;`recover_on_activation` 对含 due 事件的在内存 session 发送合并 wake;修正 MIN 聚合无行时 NULL 被解码为 0 导致 worker 空转的缺陷(`oldest_pending_due`/`next_pending_due_at` 用 `Option>` 显式解码)。Phase 4 已落地 emit_signal 与 steer:Agent Definition frontmatter 新增 `signal:` 块(`SignalContract`:delivery queue/steer、总数/字节/间隔/burst/severity allowlist/dedupe 冷却窗/JSON 深度,全部由工具与 Coordinator 强制,模型只提供 key/severity/summary/details/dedupe_key);`EmitSignalTool` 仅在带 contract 的 run 注册(fail-closed),`insert_agent_signal` 支持同冷却窗 dedupe(返回原 event + deduplicated);Coordinator `emit_signal` 校验 run 活跃与 execution ID、容量条件插入、投影+notifier wake,取消 run 时未消费 signal 自动 supersede;SteeringMailbox 泛化为来源感知 TurnMailbox(user lane 32/64KiB 与 agent lane 8/32KiB 独立容量,`TurnInput{source,delivery,durable_event_id,lease_token}`,agent steer 投影为 hidden user 消息保留 source 元数据);steer 两阶段 admission(claim → mailbox 预留 → DB admit(turn_id) → 同 Turn/generation 激活),任何失败 release lease 并 wake queue lane;`/stop`/generation 变更时已 admit 的 steer 事件按 lease token 条件释放回 pending(绝不静默丢弃),用户 Turn commit 与 steer 事件 consume 同事务(`persist_turn_batch_with_steer_consumption`)。Phase 3 收尾已落地:`ChannelContext.durable_private`(Feishu 仅 thread/root/chat_type 进入)持久化到 `sessions.delivery_context` 并被 continuation 投递复用;WS 协议 `GetAgentRuns`/`GetAgentRun` 与 `SessionAgentRuns`/`AgentRunUpdated`/`AgentEventUpdated`(有界 `AgentRunView`/`AgentEventView`,不暴露 budget/contract/delivery context/execution id);`AgentProjectionHub` broadcast(复用 plan-change 模式,lag 由客户端 GetAgentRuns 校准);HTTP `/api/agent-runs*`(列表游标分页/详情/events/cancel)+ `/api/tasks` legacy/new union;WebUI TasksPage 后台 tab 渲染 run tree(group 折叠、agent 标签、深度缩进),ChatPage 显示 continuation 标签与 Signal 卡片(投影事件,不插入 history)。Phase 5 尚未完成:wake-aware sleep 与工具中断策略。 ## 1. 审查结论 结论为:**设计可实施,没有需要推翻总体方案的阻断项,但必须按依赖顺序实施,不能在当前 `SubAgentManager` 上直接追加 durable inbox 或 steer signal。** 当前代码已经提供以下可复用基础: - `AgentLoop` 已经是跨 Turn 无状态执行器,工具执行统一经过 `ToolExecutionContext` 和 `ToolOutputProcessor`。 - Session 已有单 worker、用户输入有界队列、活动 Turn steering、generation/state version 和 persistence lock。 - `TurnController`、`DeliveryCoordinator` 已建立“持久化成功后才 Completed”的边界。 - Storage 已有单事务迁移、批量消息与 usage 原子提交模式。 - `TaskSupervisor`、`RuntimeAdmission` 和 Gateway 候选运行代已经提供重载与有界关闭框架。 - WorkManager 已有 `execution_id` 条件更新和计划变更广播。 - WebSocket 已有 request/event 路由与断线后全量校准的现成模式。 必须先解决的结构性差距如下: | 领域 | 当前实现 | 实施要求 | 风险等级 | |------|----------|----------|----------| | Agent 身份 | 一个共享 Provider 的临时子 Agent | 不可变 AgentCatalog、具名 definition、显式 caller/run context | 高 | | 委托生命周期 | `inline/background/parallel` 混合建模 | `foreground/background` 与批量并发正交 | 中 | | 取消 | Session 在 AgentLoop 外 drop future | CancellationToken 贯穿 Provider stream、工具批次和 child tree | 高 | | 并发 | background run 持有一个 Semaphore permit | run admission 与 provider/tool step permit 分离 | 高 | | 结果投递 | background 终态直接发 Channel | SQLite inbox 为事实源,Session wake 仅为加速器 | 高 | | steering | mailbox 只接受用户 ChatMessage | 来源感知 TurnInput、durable admission 和可靠 queue fallback | 高 | | continuation | 用户任务在模型调用前落库 | hidden trigger 与 assistant/tool/usage/event consume 同事务提交 | 高 | | history | 所有 role=user 都作为用户消息 | internal replay 读 hidden,客户端默认过滤 hidden | 高 | | Channel context | `reply_to` 与 opaque `private` 未区分稳定性 | Channel 显式提供 `durable_private`,核心不得猜 key | 高 | | 重载恢复 | 候选代创建完整 SessionManager | Catalog 在 prepare 校验,run/inbox 恢复只能 activation 后执行 | 高 | | 客户端 | `/api/tasks` 只读旧 `background_tasks` | 新 run/event 投影、revision、旧表 union 过渡 | 中 | 实现的关键路径为: ```text Catalog/Tool policy ↓ AgentLoop cancellation + execution gate ↓ Run/group durable lifecycle ↓ Inbox state/capacity + Session wake + queue continuation ↓ Typed TurnMailbox + emit_signal/steer ↓ Wake-aware sleep + UI/legacy cleanup ``` Phase 3 依赖前面全部基础。若跳过 cancellation、持久化状态机或 hidden message 分层,结果会在 `/stop`、reload、SQLite 失败或进程重启时出现无法修复的丢失与重复。 ## 2. 实施中固定的架构决策 以下决策在编码前固定,不留给各模块自行解释: 1. Root Agent 是运行时身份,不创建虚假的 root run。`ToolExecutionContext.agent=None` 表示 Root;child 必须携带完整 `AgentExecutionContext`。 2. `foreground/background` 只描述调用方是否等待。批量是否并发由 `tasks[]` 和调度器决定;不再存在 canonical `parallel` 模式。 3. 每个 Agent Run 都落 `agent_runs`,包括 foreground。这样截断结果、失败审计和 `agent_task.get_result` 对两种模式一致。 4. Background 接纳时预留 completion slot。signal 可以因 inbox 满而失败,已经接纳的 run completion 不得因容量耗尽而丢失。 5. SQLite 是 run、event、容量计数和消费状态的唯一事实源。watch/broadcast/内存 map 只负责降低延迟或缓存投影。 6. queue continuation 不伪造 `InboundMessage`。它通过 Session 内部 typed task 启动,并在成功时原子保存 hidden trigger、可见结果和 event consumption。 7. `steer` 不取消 Provider 或普通工具;它只在安全边界注入。`/stop` 和显式 cancel 才触发 cancellation token。 8. 候选运行代只解析和校验 Catalog,不扫描或修改数据库运行状态。恢复动作仅在新代 activation 后执行。 9. 新工具默认 `RootOnly`。只有经过逐项审计的工具可以标记 `Delegatable`;runtime control 工具由 Coordinator 注入。 10. session 归档/删除、run 取消、event supersede 和 dead-letter 都保留审计事实,不通过物理删除表达状态变化。 ## 3. 目标运行时组件与依赖装配 ### 3.1 GatewayState 新增成员 建议增加: ```rust pub struct GatewayState { // existing fields ... agent_catalog: Arc, agent_coordinator: Arc, agent_result_router: Arc, agent_projection_hub: Arc, } ``` 装配顺序必须明确: ```text Config + registered built-in tools + SkillsLoader → parse/validate AgentCatalog → create AgentProjectionHub → create late-bound AgentInboxNotifier → create AgentCoordinator(Storage, Catalog, ProviderFactory, tools, Supervisor, Admission) → create SessionManager(..., notifier) → bind notifier to Weak → create AgentResultRouter(Storage, notifier, projection hub) ``` `AgentInboxNotifier` 使用 late-bound weak target 解决 Coordinator/SessionManager 的环形依赖: ```rust #[async_trait] pub trait AgentInboxWakeTarget: Send + Sync { async fn wake_agent_inbox(&self, session_id: &str, revision: i64); } pub struct AgentInboxNotifier { target: RwLock>, } ``` 事件提交后 notifier 失败不回滚事务;周期扫描会重新唤醒。SessionManager 也不能反向持有强 `Arc` 形成释放环。 ### 3.2 prepare 与 activation 分离 `GatewayState::from_config()` 是候选运行代准备阶段,只允许: - 解析配置和 Agent Markdown。 - 校验 Provider profile、工具、skill 和委托图。 - 创建无外部副作用的内存对象。 - 运行 Storage schema migration。 `start_message_processing()` 成为 activation 边界,按顺序执行: 1. 连接 MCP;第一版 Agent Definition 不允许引用 MCP 工具,因此此步不改变已经校验完成的 Catalog。 2. 启动 Session worker/router、projection relay 和 inbox recovery scanner。 3. 调用 `AgentCoordinator::recover_on_activation(runtime_generation)`,收敛旧代 queued/running/waiting runs。 4. 扫描 pending/expired leases,修复 `agent_session_state` 计数并发出合并式 wake。 5. 最后打开新代的 Agent run admission。 为避免候选代构造期间与旧代同时修改 run 状态,Coordinator 初始处于 closed 状态,activation 成功后才 `open()`。 ## 4. 配置与 Agent Definition ### 4.1 配置类型 在 `src/config/mod.rs` 增加: ```rust #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default, deny_unknown_fields)] pub struct AgentOrchestrationConfig { pub enabled: bool, pub definitions_dir: String, pub root_delegates: Vec, pub max_tree_depth: u16, pub max_runs_per_tree: usize, pub max_concurrent_runs: usize, pub max_concurrent_runs_per_session: usize, pub max_concurrent_provider_steps: usize, pub max_concurrent_provider_steps_per_session: usize, pub max_concurrent_tool_steps: usize, pub max_concurrent_tool_steps_per_session: usize, pub max_pending_inbox_events_per_session: usize, pub inbox_event_ttl_hours: u64, pub max_inbox_delivery_attempts: u32, pub max_user_turn_burst_before_inbox: usize, pub max_inbox_wait_secs: u64, } ``` `Config` 增加 `#[serde(default)] pub agent_orchestration: AgentOrchestrationConfig`。所有默认值必须保持现有配置可加载。旧 `gateway.max_concurrent_background_tasks` 在迁移期只控制 legacy adapter;新实现不复用该字段表达三类不同配额。 配置校验需要拒绝 0 容量、session 上限大于 global 上限、TTL/timeout 超过硬上限,以及 definitions 目录越界。配置示例、README 和运行时 config reference 在功能合并时同步更新。 热重载允许把 inbox 上限降低到当前占用以下:已有 pending 和 reservation 仍受保护,不删除也不拒绝其 completion;新 background 接纳和 signal 在计数回落到新上限前返回 capacity exceeded。 ### 4.2 Markdown parser 新增 `serde_yaml` 依赖,禁止用现有 Skill frontmatter 的宽松字符串 parser 解析安全配置。建议类型: ```rust #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct AgentFrontmatter { id: AgentId, description: String, llm_profile: String, #[serde(default)] tools: Vec, #[serde(default)] delegates: Vec, #[serde(default)] skills: Vec, #[serde(default)] limits: AgentLimits, } ``` 加载算法: 1. canonicalize definitions root,确认它是受信任目录。 2. 只读取 root 第一层的 `*.md`,按文件名排序,保证错误顺序和 hash 稳定。 3. 对每个文件先用 metadata 检查上限,再做有界读取;拒绝非 UTF-8、越界 symlink 和非普通文件。 4. frontmatter 必须以第一行 `---` 开始并有独立结束 `---`;正文为空、重复 key、未知 key 均报错。 5. `id` 使用 `[a-z][a-z0-9_-]{0,63}`,拒绝大小写折叠冲突和保留名。 6. 对规范化 frontmatter JSON 与原始 role body 计算 SHA-256 `definition_hash`。 7. 第一遍建立 ID map,第二遍解析 delegate target、Provider、工具和 skill 引用。 Catalog 最终类型不可变: ```rust pub struct AgentCatalog { definitions: BTreeMap>, root_delegates: BTreeSet, runtime_generation: u64, } ``` 第一版 Catalog 只接受候选代准备阶段已经注册的 built-in 工具。现有 MCP 连接只允许在 activation 发生,为维持候选代无外部副作用和“引用错误拒绝整代”的不变量,MCP 工具不得出现在 Agent Definition;未来只有在 MCP 提供可离线校验的 tool manifest 后才能开放。 ### 4.3 legacy general 当 orchestration 未配置或旧调用没有 `target` 时,兼容层提供代码内置 `general` definition: - Provider 使用当前 root agent profile。 - 工具只取旧 default list 与 `Delegatable` 的交集。 - 不能继续 delegate,不能 emit signal。 - 保留旧 transient browser scope 的兼容行为并返回弃用提示。 显式配置的具名 Agent 永远不继承该例外。 ## 5. ToolRegistry 与执行上下文改造 ### 5.1 Tool 元数据 在 `Tool` trait 增加默认安全元数据: ```rust fn delegation_policy(&self) -> DelegationPolicy { DelegationPolicy::RootOnly } fn input_interrupt_policy(&self) -> InputInterruptPolicy { InputInterruptPolicy::Never } ``` `DelegationPolicy` 为 `RootOnly | Delegatable | RuntimeInjected`;`InputInterruptPolicy` 为 `Never | WakeOnly | CancelSafe`。二者不能从 Markdown 覆盖。 第一轮审计建议: | 工具类型 | 初始策略 | 说明 | |----------|----------|------| | file/content search、file read | Delegatable | 仍受现有进程文件权限约束,不代表硬 sandbox | | browser 普通动作 | Delegatable | 具名 run 使用独立 transient resource scope | | `get_skill` | RuntimeInjected wrapper | 只能读取 definition 的 skill allowlist | | HTTP request | RootOnly | 当前实现支持写方法;以后可增加 delegated GET-only wrapper | | bash、file write/edit | RootOnly | 需单独威胁建模后才开放 | | send_message、todo、cron、reload、browser_profiles、管理工具 | RootOnly | 有外部或全局状态副作用 | | delegate、agent_task、emit_signal | RuntimeInjected | 依据 caller context 动态注入 | | sleep | Delegatable + WakeOnly | child 无 TurnWakeupHandle,只响应 timer/cancel | `ToolRegistry` 增加只读构建方法,不在共享 registry 上删除工具: ```rust pub fn scoped_for_agent( &self, definition: &AgentDefinition, runtime_tools: Vec>, ) -> Result, AgentCatalogError>; ``` ### 5.2 ToolExecutionContext 目标类型: ```rust #[derive(Clone)] pub struct ToolExecutionContext { pub session_id: Option, pub turn_id: Option, pub agent: Option>, pub cancellation: CancellationToken, pub execution_gate: Option>, pub turn_wakeup: Option, pub resource_scope_id: Option, pub turn_origin: TurnOrigin, } ``` Root interactive context 的 `agent=None`;Coordinator 只允许在 session/turn 身份完整时把它解释成 ROOT。缺少两者的 context 不能调用 delegate。child 的 `agent=Some`,其中 run、parent、ancestry、budget 和 signal contract 是授权事实。 不要再依赖 `DELEGATE_CONTEXT` task-local 作授权。它可以暂时保留为旧 adapter 桥接,但所有新工具必须从 `ToolExecutionContext` 读取身份。 ## 6. AgentLoop:取消、许可与 typed input ### 6.1 CancellationContext 给 AgentLoop 的所有 process 入口增加 `AgentLoopExecution`: ```rust pub struct AgentLoopExecution { pub cancellation: CancellationToken, pub gate: Arc, pub tool_context: ToolExecutionContext, } ``` Provider 请求必须在获取 provider-step permit 后执行: ```rust let _permit = execution.gate.acquire_provider(&execution.cancellation).await?; let response = tokio::select! { _ = execution.cancellation.cancelled() => Err(AgentError::Cancelled), result = self.stream_completion(...) => result, }; ``` 普通工具调用同样取得 tool-step permit。runtime-control 工具不取得普通 tool permit,防止父 run 等 child 时占住唯一许可。工具 future 被取消只表示 PicoBot 不再等待;对 bash、HTTP 写入等外部副作用不能宣称已回滚,因此迟到结果必须被 execution ID 条件提交挡住。 工具批次当前使用 `join_all`。改造后每项 future 自行获取 permit,批次仍可并发;取消时等待一个很短的 cooperative grace,然后由外层 task abort。`AgentError` 增加结构化 `Cancelled` 和 `TimedOut`,不要用字符串判断终态。 ### 6.2 ExecutionGate 配额分成两组: - `RunQuota`:统计 queued/running/waiting_children 的非终态 run,直到 terminal commit 才释放。 - `StepGate`:provider/tool 步骤执行期间持有,步骤结束立即释放。 permit 固定按 global → session → agent 顺序获取;失败或取消逆序释放。为了避免动态 semaphore 缓存泄漏,session/agent gate 使用带弱引用的 keyed registry,并在没有 run/permit 时清理。 Root Turn 不是 Agent Run,不占 run quota,但它的 Provider/tool 步骤占 global/session step gate,这样 background run 不会绕过整个 Gateway 的资源上限。 ### 6.3 waiting_children `delegate` foreground 执行前创建 `WaitingChildrenGuard`: 1. child 接纳成功后,条件更新 parent `running → waiting_children`。 2. 等待期间父没有 provider/tool step permit。 3. 全部 child 终态、父取消或错误退出时 guard 尝试 `waiting_children → running`;父已经 terminal 时不覆盖。 4. 父取消递归取消未终态 foreground descendants;background run 归 root session token 所有,不因创建它的 Turn 正常结束而取消。 ### 6.4 typed Turn input Phase 4 将 `SteeringMailbox` 替换为 `TurnMailbox`。在此之前 Phase 2A 只做 cancellation,不改变用户 steering 行为,降低一次改动的回归面。 `AgentLoop::append_steering_messages` 不再强制构造 user source;改为由 serializer 把 typed source 转成 Provider-compatible role,同时保留 durable source metadata。Agent event envelope 明确标注为不可信数据,不能改变 system/tool 权限。 ## 7. AgentCoordinator API 与状态机 建议公开最小接口: ```rust pub struct DelegateRequest { pub mode: ExecutionMode, pub tasks: Vec, pub completion_policy: CompletionPolicy, pub idempotency_key: Option, } impl AgentCoordinator { pub async fn delegate(&self, caller: CallerContext, request: DelegateRequest) -> Result; pub async fn get_run(&self, caller: CallerContext, run_id: &RunId) -> Result; pub async fn list_runs(&self, caller: CallerContext, query: AgentRunQuery) -> Result<_, _>; pub async fn get_result(&self, caller: CallerContext, run_id: &RunId) -> Result<_, _>; pub async fn cancel_run(&self, caller: CallerContext, run_id: &RunId) -> Result<_, _>; pub async fn emit_signal(&self, context: &AgentExecutionContext, signal: SignalInput) -> Result; pub async fn cancel_session(&self, session_id: &str, reason: CancelReason) -> Result<(), _>; pub async fn recover_on_activation(&self, generation: u64) -> Result; } ``` ### 7.1 Background 接纳顺序 严格顺序: 1. 从 context 解析 caller,校验 root session、委托边、ancestry、depth 和 budget。 2. 解析全部 target definition,派生 deadline、token/run reservation 和 delivery contract。 3. 取得 RuntimeAdmission activity guard 和内存 run quota reservation;尚未写库前任何失败均直接释放。 4. Storage 事务条件增加 completion reservation、领取 plan item、插入 group/run queued 记录。 5. 把 cancellation token 和 execution ID 注册到 Coordinator active map。 6. 通过 `TaskSupervisor::spawn_graceful` 接纳 runner。 7. spawn 被拒绝时执行补偿事务:queued → cancelled、释放 completion reservation、回滚/阻塞已领取 plan item;不得向模型返回可用 run ID。 8. spawn 成功后返回 run/group ID。 第一版在步骤 1 强制 `background caller == ROOT`;child 只能 foreground 委托。该限制属于 Coordinator policy,不仅是 delegate schema 提示。以后开放 nested background 时仍必须把 run 归属到原 root session,并重新审查取消所有权和 completion reservation。 SQLite commit 与 task spawn 无法成为同一原子操作。崩溃发生在步骤 4 与 6 之间时,会留下旧 generation 的 queued run;activation recovery 必须将其收敛为 `interrupted` 并生成预留过的 completion,不能无限保持 queued。 ActivityGuard 必须移动进已接纳的 background runner,直到 terminal transaction 完成后才 drop;不能在 `delegate` 返回时提前释放。Runner 等待 run quota/provider permit 时同样受 deadline 和 cancellation 约束,排队时间计入 run timeout。 ### 7.2 Foreground 执行 Foreground 同样先持久化 run,但不预留 inbox slot,也不创建 completion event。单任务直接 await;批量使用 `FuturesUnordered` 并保存原 request index,最终按请求顺序组装结果。每个 child 有独立取消 token;父取消时全部 token 同时取消。 完整结果先写 `agent_runs.result`,tool result 只返回有界 projection。这样无论是否截断,`agent_task.get_result` 都能读取相同事实。 ### 7.3 terminal commit Runner 只返回 `AgentTerminalOutcome`,不能自己发 Channel 或更新 WorkManager: ```rust pub enum AgentTerminalOutcome { Completed { result: String, usage: Usage, tool_calls: u32, iterations: u32 }, Failed { error: AgentRunError, usage: Option }, TimedOut { deadline_at: i64 }, Cancelled { reason: CancelReason }, Interrupted { reason: String }, } ``` Coordinator 用 `(run_id, execution_id, runtime_generation, nonterminal status)` 条件提交。只有更新行数为 1 的赢家可以: - 写终态和完整 result/error。 - 更新 group terminal counter。 - 转换或释放 completion reservation。 - 插入 run/group inbox event。 - 更新绑定的 task item。 - 递增 session agent revision。 迟到结果返回 `StaleExecution` 并丢弃,不改变已保存终态。 Coordinator 还需启动一个由 TaskSupervisor 管理、观察 shutdown token 的 deadline reaper。正常 runner 使用 `timeout_at(deadline)`;reaper 负责没有活跃内存 task 的 queued run 和 group deadline,分批条件终态化未完成 children,再走同一个 terminal transaction,不能维护第二套完成逻辑。 ## 8. Storage schema v6 实现时把 `SCHEMA_VERSION` 从 5 升到 6。表创建、旧列增加、索引和 `PRAGMA user_version=6` 必须在当前 `migrate_schema()` 的同一事务中完成。下面的 `ALTER TABLE` 表达目标列;实际代码必须同时更新 fresh-schema 的 `CREATE TABLE`,迁移时沿用现有 `PRAGMA table_info` 检查,仅对缺失列执行 ALTER,不能在 fresh database 上重复加列。 SQLite 使用 INTEGER 表示布尔值;应用层枚举仍用 Rust enum,读到未知值要报 corruption,不能静默当成 completed。 ### 8.1 session 与 message 扩展 ```sql ALTER TABLE sessions ADD COLUMN delivery_context TEXT; ALTER TABLE sessions ADD COLUMN delivery_context_updated_at INTEGER; ALTER TABLE messages ADD COLUMN client_visibility TEXT NOT NULL DEFAULT 'visible'; ALTER TABLE messages ADD COLUMN turn_origin TEXT NOT NULL DEFAULT 'user'; CREATE INDEX IF NOT EXISTS idx_messages_session_visibility_seq ON messages(session_id, client_visibility, seq); ``` `delivery_context` 只序列化 `ChannelContext.durable_private`。现有 `reply_to` 和 `private` 永不写入该字段。 ### 8.2 agent_session_state ```sql CREATE TABLE IF NOT EXISTS agent_session_state ( root_session_id TEXT PRIMARY KEY, revision INTEGER NOT NULL DEFAULT 0, pending_event_count INTEGER NOT NULL DEFAULT 0, reserved_completion_slots INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL, CHECK (revision >= 0), CHECK (pending_event_count >= 0), CHECK (reserved_completion_slots >= 0) ); ``` 这是 inbox 容量和客户端 revision 的权威计数行。每个改变客户端 run/group/event 投影的事务只分配一个新 revision,并把同一 revision 写回本事务改变的所有行;仅内部读取不递增。所有容量增加操作使用条件更新: ```sql UPDATE agent_session_state SET reserved_completion_slots = reserved_completion_slots + ?, revision = revision + 1, updated_at = ? WHERE root_session_id = ? AND pending_event_count + reserved_completion_slots + ? <= ? RETURNING revision; ``` 若 session 尚无行,先 `INSERT ... ON CONFLICT DO NOTHING`,仍在同一写事务中执行条件更新。 ### 8.3 agent_run_groups ```sql CREATE TABLE IF NOT EXISTS agent_run_groups ( id TEXT PRIMARY KEY, root_session_id TEXT NOT NULL, caller_run_id TEXT, caller_scope_id TEXT NOT NULL, idempotency_key TEXT, mode TEXT NOT NULL, completion_policy TEXT NOT NULL, expected_runs INTEGER NOT NULL, terminal_runs INTEGER NOT NULL DEFAULT 0, abnormal_runs INTEGER NOT NULL DEFAULT 0, completion_slot_reserved INTEGER NOT NULL DEFAULT 0, completion_delivery TEXT, failure_delivery TEXT, deadline_at INTEGER NOT NULL, status TEXT NOT NULL, runtime_generation INTEGER NOT NULL, revision INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, finished_at INTEGER, CHECK (mode IN ('foreground', 'background')), CHECK (completion_policy IN ('all', 'each')), CHECK (status IN ('queued', 'running', 'completed', 'partial', 'failed', 'timed_out', 'cancelled', 'interrupted')), CHECK (expected_runs > 0), CHECK (terminal_runs >= 0 AND terminal_runs <= expected_runs), CHECK (completion_slot_reserved IN (0, 1)) ); CREATE INDEX IF NOT EXISTS idx_agent_groups_session_created ON agent_run_groups(root_session_id, created_at DESC); CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_groups_idempotency ON agent_run_groups(root_session_id, caller_scope_id, idempotency_key) WHERE idempotency_key IS NOT NULL; ``` 批量请求把 request idempotency key 写在 group,所有 child run 的 `idempotency_key` 为 NULL;单任务请求不创建只含一个 child 的 group,key 直接写在 run。这样 retry 能返回原 group/run,又不会让同一批 children 触发唯一索引冲突。 ### 8.4 agent_runs ```sql CREATE TABLE IF NOT EXISTS agent_runs ( id TEXT PRIMARY KEY, group_id TEXT, root_session_id TEXT NOT NULL, root_turn_id TEXT, parent_run_id TEXT, caller_agent_id TEXT NOT NULL, caller_scope_id TEXT NOT NULL, idempotency_key TEXT, agent_id TEXT NOT NULL, definition_hash TEXT NOT NULL, provider_profile TEXT NOT NULL, provider_name TEXT NOT NULL, model_id TEXT NOT NULL, mode TEXT NOT NULL, depth INTEGER NOT NULL, plan_item_id TEXT, execution_id TEXT NOT NULL, task TEXT NOT NULL, context_json TEXT, budget_json TEXT NOT NULL, signal_contract_json TEXT, signal_delivery TEXT, completion_delivery TEXT, failure_delivery TEXT, status TEXT NOT NULL, result TEXT, error TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, cost REAL, tool_calls_count INTEGER NOT NULL DEFAULT 0, iterations INTEGER NOT NULL DEFAULT 0, runtime_generation INTEGER NOT NULL, attempt INTEGER NOT NULL DEFAULT 1, completion_slot_reserved INTEGER NOT NULL DEFAULT 0, deadline_at INTEGER NOT NULL, revision INTEGER NOT NULL, started_at INTEGER, finished_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, CHECK (mode IN ('foreground', 'background')), CHECK (status IN ('queued', 'running', 'waiting_children', 'completed', 'failed', 'timed_out', 'cancelled', 'interrupted')), CHECK (depth >= 1), CHECK (completion_slot_reserved IN (0, 1)), FOREIGN KEY (group_id) REFERENCES agent_run_groups(id) ON DELETE RESTRICT, FOREIGN KEY (parent_run_id) REFERENCES agent_runs(id) ON DELETE RESTRICT ); CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_runs_execution ON agent_runs(execution_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_runs_idempotency ON agent_runs(root_session_id, caller_scope_id, idempotency_key) WHERE idempotency_key IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_agent_runs_session_created ON agent_runs(root_session_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_agent_runs_parent ON agent_runs(parent_run_id, created_at); CREATE INDEX IF NOT EXISTS idx_agent_runs_recovery ON agent_runs(runtime_generation, status, deadline_at); ``` Session 使用软删除,agent 表不对 `root_session_id` 建级联外键;生命周期由 `cancel_session` 和恢复扫描显式收敛。 ### 8.5 agent_inbox_events ```sql CREATE TABLE IF NOT EXISTS agent_inbox_events ( id TEXT PRIMARY KEY, root_session_id TEXT NOT NULL, scope_kind TEXT NOT NULL, scope_id TEXT NOT NULL, run_id TEXT, group_id TEXT, event_type TEXT NOT NULL, event_key TEXT NOT NULL, delivery TEXT NOT NULL, requires_continuation INTEGER NOT NULL DEFAULT 1, severity TEXT, payload_json TEXT NOT NULL, status TEXT NOT NULL, attempt_count INTEGER NOT NULL DEFAULT 0, lease_token TEXT, lease_until INTEGER, next_attempt_at INTEGER, admitted_turn_id TEXT, last_error TEXT, revision INTEGER NOT NULL, created_at INTEGER NOT NULL, consumed_at INTEGER, superseded_at INTEGER, dead_lettered_at INTEGER, fallback_notified_at INTEGER, fallback_suppressed_reason TEXT, CHECK (scope_kind IN ('run', 'group')), CHECK (event_type IN ('signal', 'completion', 'group_completion')), CHECK (delivery IN ('queue', 'steer')), CHECK (requires_continuation IN (0, 1)), CHECK (status IN ('pending', 'leased', 'admitted', 'consumed', 'superseded', 'dead_letter')), CHECK ( (scope_kind = 'run' AND run_id IS NOT NULL AND group_id IS NULL AND scope_id = run_id) OR (scope_kind = 'group' AND group_id IS NOT NULL AND run_id IS NULL AND scope_id = group_id) ), UNIQUE(scope_kind, scope_id, event_type, event_key), FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE RESTRICT, FOREIGN KEY (group_id) REFERENCES agent_run_groups(id) ON DELETE RESTRICT ); CREATE INDEX IF NOT EXISTS idx_agent_inbox_claim ON agent_inbox_events(root_session_id, status, next_attempt_at, created_at); CREATE INDEX IF NOT EXISTS idx_agent_inbox_lease ON agent_inbox_events(status, lease_until); CREATE INDEX IF NOT EXISTS idx_agent_inbox_revision ON agent_inbox_events(root_session_id, revision); ``` run/group 外键使用 RESTRICT。清理默认只清理大正文或归档整组记录;若未来需要物理删除,必须先按明确保留策略删除 terminal inbox event,再删除 run/group,不能留下失去审计来源的事件。 ### 8.6 事务 API 不要把 SQL 分散在 Coordinator、Router 和 WorkManager。Storage 增加领域 API: ```rust accept_agent_group(request) -> AcceptedGroup mark_agent_run_running(run_id, execution_id) mark_agent_run_waiting_children(run_id, execution_id, expected_status) commit_agent_terminal(run_id, execution_id, outcome) -> TerminalCommit insert_agent_signal(run_id, execution_id, signal) -> EventCommit claim_inbox_batch(session_id, now, lease) -> InboxLease admit_inbox_event(event_id, lease_token, turn_id) release_inbox_lease(event_ids, lease_token, retry) commit_continuation_turn(batch) -> CommittedTurnDelta recover_agent_state(active_generation, now) -> RecoveryReport ``` `commit_agent_terminal` 在一个事务内更新 run/group、容量计数、event、plan item 和 revision。提交后 Coordinator 调用 `WorkManager::refresh_after_external_commit()` 刷新 cache 并广播;WorkManager 不再为这条路径另开事务。 ## 9. Agent Inbox、唤醒与公平调度 ### 9.1 Session worker 接收面 Session 增加: ```rust agent_tx: Option>, agent_inbox_wake: watch::Sender, consecutive_user_turns: usize, ``` 把 worker 创建逻辑抽为 `ensure_agent_worker_locked()`,用户 enqueue 与 `wake_agent_inbox()` 共用。worker 同时持有 user receiver 和 wake receiver: ```rust tokio::select! { biased; user = task_rx.recv(), if should_take_user => { ... } changed = inbox_wake.changed() => { ... } } ``` 不能仅靠 `biased` 实现公平。每个 Turn 结束后查询最老 due pending event;满足以下任一条件时下一项必须是 continuation: - `consecutive_user_turns >= max_user_turn_burst_before_inbox`; - oldest pending age >= `max_inbox_wait_secs`。 watch value 是最新 durable revision,只合并 wake,不携带 payload。worker 收到 wake 后从 SQLite claim;发送方从不等待 Session user mpsc 容量。 ### 9.2 queue continuation claim batch 必须有大小与总字节上限,例如 8 events/32 KiB envelope。`completion_policy=each` 可在 300–500ms 内 debounce 已经 pending 的 siblings,但不能等待未终态 sibling。 执行过程: 1. Storage 把 due events `pending → leased`,返回 lease token。 2. worker 构造 `AgentTaskSource::BackgroundAgentResults` 和 hidden trigger,但暂不落 messages。 3. root Agent 使用只读 continuation ToolRegistry 执行。默认允许内容读取、检索和 scoped `agent_task.get/get_result`,不允许 send_message、todo、写文件、delegate 或其他外部副作用。 4. 成功后 `commit_continuation_turn` 原子插入 hidden trigger、tool/assistant messages、usage,并把同 token events `leased/admitted → consumed`。 5. commit 后更新 Session 内存、Turn Completed、projection 和 Channel delivery。 6. 执行失败、取消或 stale generation 时 `InboxLeaseGuard` 显式 release 到 pending 并写 `next_attempt_at`。 这一流程与当前“先持久化用户消息再调用模型”不同,必须走独立分支。不能先写 hidden trigger,否则失败重试会在 history 中累积无 assistant 配对的内部输入。 ### 9.3 hidden history `ChatMessage` 和 `MessageMeta` 增加: ```rust pub client_visibility: ClientVisibility, pub turn_origin: TurnOrigin, ``` 默认构造器生成 `Visible/User`。需要拆分查询: - `load_messages_for_replay(session_id)`:visible + hidden。 - `load_visible_messages(session_id, ...)`:WebSocket/HTTP/管理历史。 - `persist_message_batch_inner()`:支持两字段并保持向后兼容默认值。 - `CommittedTurnDelta`:过滤 hidden,并携带 turn origin。 Session 的 `message_count` 只统计 visible external user input;`total_message_count` 和压缩/token 估算统计全部 replay messages。自动标题只由 visible user input 触发。 ### 9.4 steer admission Router 的两阶段流程必须由测试锁定: ```text pending --claim(token)--> leased leased --reserve current Turn--> memory reservation (not drainable) leased --DB admit(token, turn)--> admitted admitted --activate same turn/generation--> drainable TurnInput ``` 任何失败都取消 reservation,并以 token 把 event 恢复 pending。若 active Turn 已关闭、mailbox 满或 generation 改变,直接释放 lease并 wake queue;这就是可靠 steer→queue fallback。 TurnMailbox 分 user lane 与 agent lane 容量,最终按 session sequence 合并排空。durable agent reservation 在 DB admit 前不能被 AgentLoop 看见;`/stop` 关闭 mailbox 时返回尚未提交的 event IDs/tokens,由 SessionManager 立即 release。 ## 10. emit_signal 与 completion 投影 `EmitSignalTool` 只注册到携带 background signal contract 的 runner。foreground run、Root 或没有 contract 的 child 看不到该工具。 `emit_signal` 流程: 1. 从 AgentExecutionContext 取得唯一 root session、run、execution ID 和 delivery。 2. 校验 run 仍为 running/waiting_children 且 execution ID 匹配。 3. 校验 severity、JSON 深度、summary/details 字节、信号总数、burst、最小间隔和 dedupe window。 4. 条件增加 session pending count;容量必须扣除 reserved completion slots。 5. 插入 event 和新 revision;commit 后发布 projection + wake。 6. 返回 accepted event ID。事件已存在时返回同一 ID 和 `deduplicated=true`。 Completion 永远由 Coordinator 生成。`completion_policy=all` 时每个 run terminal outcome 只存在于 `agent_runs`,不生成 per-run inbox completion;最后一个 terminal child 或 group deadline 的事务赢家生成唯一 group completion。group 有任一 failed/timed_out/interrupted/cancelled 时使用 `failure_delivery`,否则使用 `completion_delivery`。 显式 `agent_task.cancel` 可以把该 run 尚未消费的普通 signal 更新为 `superseded` 并减少 pending count;completion 事实不能 supersede。 ## 11. Session 生命周期、停止与恢复 ### 11.1 `/stop` 执行顺序: 1. 在 Session 锁内关闭 active Turn admission、取出 mailbox durable reservations、递增 worker generation、取出 root Turn cancellation token。 2. 锁外取消 root token,条件 release admitted/leased inbox events。 3. 调用 Coordinator cancel 当前 session 的 active background runs。 4. cancellation terminal event 使用 `requires_continuation=false` 并直接 consumed,防止 stop 后又自动启动“已取消”Turn。 5. stop 前已经 pending 的其他事件仍保持 pending;Session 下次可调度时处理。 现有 oneshot 可在过渡期由 token adapter 驱动,最终移除 `current_cancel: Option>`,统一为 `CancellationToken`。 ### 11.2 archive/delete Session 归档或软删除必须调用 `AgentCoordinator::cancel_session`。提交事务: - 非 Scheduler 所有且未终态 run → cancelled。 - 释放所有 completion reservation。 - pending/leased/admitted events → `dead_letter(session_archived|session_deleted)`。 - 清理 mailbox reservation,不启动 continuation。 客户端仍可通过管理面看到 run/event 审计。当前没有可靠 unarchive 投递语义,因此归档事件不保留 pending。归档/删除属于用户生命周期操作,写 `fallback_suppressed_reason` 并禁止 system fallback,避免用户删除对话后又收到后台通知。 ### 11.3 restart/reload recovery activation recovery 分批执行,避免长事务: 1. 将旧 generation 的 queued/running/waiting_children 条件更新为 interrupted。 2. 为 background interrupted outcome 转换预留并插入 failure completion;foreground 只保存终态。 3. group counter 收敛并生成必要的 group completion。 4. expired leased/admitted events 恢复 pending,attempt +1,写 next retry。 5. 按每 session 重算 `pending_event_count` 和有效 reservation;差异修复并记录 structured warning。 6. 对有 pending due events 的 session 只发一次合并 wake。 旧代排空期间每个 run 持有 RuntimeAdmission activity guard。超出 reload grace 后 cancellation token 先触发;Supervisor 强制 abort 后留下的非终态记录由新代 recovery 标 interrupted。 ## 12. ChannelContext 与结果投递 当前 `ChannelContext.private` 同时可能含稳定 thread/root ID 和一次性 message/reaction ID,核心无法安全猜测。类型改为: ```rust pub struct ChannelContext { pub reply_to: Option, pub private: HashMap, pub durable_private: HashMap, } ``` Channel adapter 负责分类: - CLI/WebUI:通常为空。 - Feishu:thread/root/chat type 等是否可复用由 Feishu adapter 决定;message ID、reaction ID 和本次 reply target 留在 `private/reply_to`。 Session 在成功接纳外部用户输入时保存 `durable_private`。continuation 的 `TurnTarget` 使用 session 自身 channel/chat ID 和 durable context,`reply_to=None`。没有 binding 时照常提交历史和 WebSocket 投影,只跳过外部 Channel delivery。 ## 13. WorkManager 原子性 当前 `assign_sub_agent` 和 `finish_sub_agent` 各自开事务,无法与 run 接纳/终态保持原子。实施方案: - 把 plan item 条件 SQL 移入 Storage 的 agent run transaction。 - run 接纳时按 `(plan_id, item_id, status=pending)` 领取,并把 `execution_id` 写为 run execution ID。 - terminal commit 时按同 execution ID 更新 completed/blocked 和 plan version。 - WorkManager 新增 `refresh_after_external_commit(session_id, reason, item_ids)`,只负责重新查询、刷新 cache 和广播,不写数据库。 - 若 plan item 已被其他执行领取,整个 run 接纳事务回滚;不得创建一个与计划脱节的 run。 ## 14. 协议、管理 API 与 WebUI ### 14.1 Rust protocol 增加 DTO 而不直接序列化 Storage row: ```text 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`、`CommittedTurnDelta`、`turn_updated` 和 `turn_committed` 增加 `turn_origin`。DTO 不暴露 task/result 全文以外的敏感 context、reasoning、Provider state、budget internals 或 delivery context。 `AgentProjectionHub` 复用 WorkManager 的 broadcast 模式。广播 lag 不补逐条事件,客户端收到 lag 或重连后调用 `GetAgentRuns` 校准。revision 来自 `agent_session_state`,客户端只接受更大的 revision;分页 cursor 使用 `(created_at,id)`,不能用 offset。 ### 14.2 HTTP 管理 API 过渡期 `/api/tasks` 返回统一 projection: - 新 `agent_runs` 映射为 `source=agent_run`。 - 旧 `background_tasks` 映射为 `source=legacy_background_task`。 - 按 created_at 合并排序,旧记录只读。 新增受设备鉴权保护的: ```text GET /api/agent-runs?session_id=&cursor=&limit= GET /api/agent-runs/:id GET /api/agent-runs/:id/events POST /api/agent-runs/:id/cancel ``` cancel endpoint 仍通过 Coordinator 做 root session/task-tree 授权,不直接执行 SQL。 ### 14.3 WebUI 现有 `TasksPage.svelte` 的“后台任务”tab 演进为运行树: - group 可折叠展示 children。 - status、Agent、Provider/model、duration、usage、工具次数。 - signal 数量、最近 severity、pending/dead-letter 标记。 - terminal result 有界预览,完整内容按需加载。 - cancel 只对允许取消的非终态 run 显示。 ChatPage 对 `turn_origin=agent_continuation` 显示轻量标签,不创建 user bubble;Signal 卡片来自 `AgentEventUpdated`,不能把 payload 插入普通聊天 history。所有新 Markdown/result 仍经过现有 sanitize 流程。 ## 15. 可唤醒 sleep 的代码实现 `TurnWakeupHandle` 只由 root interactive Turn 创建并放入 ToolExecutionContext。它持有 `watch::Receiver`;state 使用独立于 durable inbox revision 的 session-local 单调 revision,因为用户 queue/steer 同样需要唤醒: ```rust pub struct TurnWakeupState { pub revision: u64, pub pending_user_steer: usize, pub pending_user_queue: usize, pub pending_agent_steer: usize, pub pending_agent_queue: usize, pub latest_source: WakeupSource, pub latest_safe_preview: Option, } ``` 以下 admission 成功后递增 revision 并 `send_replace`:用户进入 TurnMailbox、用户进入 next-turn mpsc、Agent event durable admitted、Agent event 保持 pending 并成功 wake Session。先更新事实状态再发 wake,不能让 sleep 醒来却查询不到输入。 `SleepTool::execute_with_context`: 1. 解析并限制 duration 到 24 小时。 2. child context 的 `turn_wakeup=None`,只 select timer 与 cancellation。 3. root context 先读取 `receiver.borrow_and_update()`;若 pending 总数已大于 0,立即返回,不进入等待。 4. 否则同时等待 timer、`receiver.changed()` 和 cancellation。watch 保留最新 revision,因此 input 在检查与 select 之间到达也不会丢。 5. wake 后再次读取 state 并构造有界结果。steer 可以返回来源、run ID 和安全摘要;queue 只返回类型/数量,不返回正文。 6. cancellation 映射为统一 `AgentError::Cancelled`,不能让模型把它当普通 sleep 完成后继续执行。 Sleep 只提前结束工具 future,不消费 mailbox/inbox,也不自行改变 queue/steer。工具批次返回后,AgentLoop 在既定安全边界排空 steer;queue 留给下一个 Turn。 第一版 `InputInterruptPolicy` 只驱动 sleep 的 `WakeOnly`。不要顺带让 bash、browser、HTTP 或文件工具响应 input wake;以后开放 `CancelSafe` 必须逐工具证明取消、重试和副作用语义。 ## 16. 分阶段实施清单 ### 文件级落点 | 文件/模块 | 主要改动 | |-----------|----------| | `src/config/mod.rs` | orchestration config、默认值、边界校验 | | `src/agent/definition.rs`(新) | Markdown/frontmatter 类型、严格 parser、definition hash | | `src/agent/catalog.rs`(新) | immutable catalog、委托图与引用校验 | | `src/agent/run.rs`(新) | run/group/outcome DTO、AgentRunner、ProviderFactory | | `src/agent/coordinator.rs`(新) | 授权、预算、接纳、取消、deadline、terminal commit | | `src/agent/inbox.rs`(新) | event/lease/projection/notifier contracts | | `src/agent/agent_loop.rs` | cancellation、step gate、typed input safe boundaries | | `src/agent/steering.rs` | 迁移为 typed TurnMailbox 与 reservation | | `src/agent/sub_agent.rs` | legacy adapter,逐阶段缩减并最终删除旧 manager | | `src/tools/traits.rs` | ToolExecutionContext、DelegationPolicy、interrupt policy | | `src/tools/delegate.rs` | 仅 run/run_many 和兼容参数转换 | | `src/tools/agent_task.rs`(新) | scoped get/list/cancel/get_result | | `src/tools/emit_signal.rs`(新) | contract-bound signal | | `src/tools/sleep.rs` | watch-aware wait 和 cancellation | | `src/session/session.rs` | dual lane worker、internal task、atomic continuation、stop release | | `src/session/agent_inbox.rs`(新) | claim/admit/release、fair scheduler、lease guard | | `src/storage/mod.rs` | schema v6 migration 和领域 API re-export | | `src/storage/agent_run.rs`(新) | run/group transaction SQL | | `src/storage/agent_inbox.rs`(新) | capacity/event/lease/recovery SQL | | `src/storage/message.rs`、`session.rs` | visibility/origin/durable delivery context | | `src/gateway/mod.rs`、`reload.rs` | prepare/activation、recovery task、projection relay | | `src/protocol.rs`、`src/session/commands.rs` | run query/event DTO 和 Turn origin | | `src/gateway/http.rs` | 管理 API、legacy/new projection union | | `webui/src/pages/TasksPage.svelte` | run tree、event/dead-letter/cancel UI | | `webui/src/pages/ChatPage.svelte` | continuation origin 和 Signal 卡片 | 不要让 `session.rs` 继续吸收所有 inbox SQL 和状态机;先抽 Storage 领域 API 与 `session/agent_inbox.rs`,再接 worker,能显著降低竞态代码的审查难度。 ### Phase 0:契约与 schema 冻结 - 固定本文 enums、状态转换、DDL 和默认值。 - 给现有 delegate、background notification、Session stop/reload 行为补基线测试。 - 为 config 示例准备 backward-compatible fixture。 完成条件:所有后续 PR 可以只引用固定 DTO/Storage contract,不再各自发明状态名。 ### Phase 1:Catalog 与安全裁剪 - `definition.rs`、`catalog.rs`、严格 YAML parser。 - Config 扩展与候选代校验。 - Tool delegation metadata、scoped registry、skill wrapper。 - `foreground/background` schema 和 legacy parameter adapter。 - built-in general compatibility。 完成条件:具名 foreground Agent 能使用不同 Provider 和固定工具集;尚不开放 background 新路径。 ### Phase 2A:AgentLoop 结构化取消 - CancellationToken 进入 root Turn 和 AgentLoop。 - Provider stream、工具批次、sleep 观察 token。 - `AgentError::Cancelled/TimedOut`。 - `/stop` oneshot 兼容桥接和迟到结果测试。 完成条件:root 行为不变;Provider、tool、preparation 各阶段 stop 均得到一致 Cancelled 终态。 ### Phase 2B:Coordinator、run persistence 与 execution gate - schema v6 的 run/group 部分。 - ProviderFactory、AgentRunner、Coordinator。 - run quota/provider gate/tool gate。 - foreground 单/批量、parent waiting_children、agent_task。 - WorkManager 原子绑定。 完成条件:foreground 全量持久化、截断结果可查询、并发上限为 1 时嵌套 foreground 不死锁。 ### Phase 3:durable inbox 与 queue continuation - `agent_session_state`、inbox Storage API、容量 reservation。 - late-bound notifier、Session watch wake、公平调度。 - hidden messages、turn origin、atomic continuation commit。 - recovery/dead-letter/fallback。 - WebSocket/HTTP run projection 和 WebUI 基础运行树。 完成条件:background completion 不直接通知 Channel;空闲/忙碌/队列满/重启/reload 下最终都能被主 Agent处理或进入可诊断 dead-letter。 ### Phase 4:signal 与 steer - EmitSignalTool、rate/dedupe/capacity。 - typed TurnMailbox 和两阶段 steer admission。 - `/stop` 立即 release、supersede 和 Signal UI。 完成条件:steer 在所有 admission 竞态中严格属于当前 Turn 或未来 queue 之一,不能重复或消失。 ### Phase 5:wake-aware sleep 与清理 - TurnWakeupHandle watch revision。 - root sleep 响应 user/agent queue/steer,child sleep 仅 timer/cancel。 - 工具 interrupt policy。 - 移除 direct background notification 默认路径;观察一个版本后删除旧 adapter 和旧表写入。 完成条件:sleep 不丢 pre-listen wake,queue 内容不泄漏进当前 Turn,steer 在下一个安全边界可见。 ## 17. 测试与故障注入 除主设计测试矩阵外,实施必须增加以下代码级场景。 ### 17.1 Storage - fresh database 创建 schema v6。 - v5 fixture 升级到 v6,原消息默认 visible/user。 - migration 任一步失败时 `user_version` 和全部表结构回滚。 - 两个并发 background 接纳只有一个能占最后 completion slot。 - completion 把 reservation 转 pending,不出现中间负数或超限。 - terminal transaction 在 plan update/event insert/usage 任一点失败时全回滚。 - execution ID 不匹配的迟到结果更新 0 行。 - startup reconciliation 能修复故意破坏的 state counters。 ### 17.2 AgentLoop/Coordinator - cancellation 发生在 permit wait、Provider connect、stream、工具批次、child wait。 - parent 等 child 时不持 step permit;global provider permit=1 仍能完成。 - foreground batch 并发执行但结果按 request index 返回。 - parent cancel 取消 foreground descendants,不取消已独立接纳的 background run。 - TaskSupervisor spawn reject 执行补偿事务且 delegate 不返回可用 ID。 - crash window 留下 queued old-generation run,activation 恢复为 interrupted。 ### 17.3 Inbox/Session - watch wake 在 worker 开始等待前发生也不会丢。 - user mpsc 满不影响 durable event。 - 连续用户输入达到 burst 上限后强制执行一个 event batch。 - continuation Provider 失败显式 release;进程模拟崩溃后 lease expiry 恢复。 - hidden trigger 与 assistant/event consume 原子;客户端 history 永远不返回 hidden。 - hidden user 不增加 title threshold/message_count。 - `/stop` 与 admit 的每个交错点都只产生 pending 或 consumed/admitted ownership之一。 - archived/deleted session 不启动 continuation且 reservation 归零。 ### 17.4 Channel/protocol/UI - durable context round trip 不包含 reply_to、message/reaction ID。 - 无 delivery binding 时 history commit 成功且不发送其他 chat。 - event broadcast lag/reconnect 后 full query 校准 revision。 - legacy/new tasks union 不重复并稳定分页。 - Signal payload、result Markdown 和错误信息通过 sanitize/redaction。 ### 17.5 Sleep - input 在 sleep 读取 revision 前、读取后但 select 前、select 后三个时点到达都能提前结束。 - user/agent 的 queue wake 只返回数量,不泄漏正文;对应输入仍由下一 Turn处理。 - steer wake 返回后由 AgentLoop 安全边界注入,SleepTool 自身不消费输入。 - child sleep 不因 root user input 或 sibling signal 唤醒,但 run cancel/timeout 会立即结束。 - duration 超过 24 小时拒绝;Gateway shutdown 不留下未托管等待任务。 建议把 SQLite failpoint、barrier 和暂停钩子限制在 `#[cfg(test)]`,用于精确制造 admission、stop、commit 与 reload 竞态,避免依赖随机 sleep。 ## 18. 验证命令与合并门槛 每个 Rust 阶段至少执行: ```bash cargo fmt --check cargo test --lib cargo test --test test_scheduler cargo test --test test_request_format cargo clippy --all-targets --all-features -- -D warnings cargo build ``` 涉及 WebUI 时另外执行: ```bash cd webui npm ci npm run check npm run build cd .. cargo build ``` 文档或配置变更执行 `git diff --check`,并核对 README、配置模板、ARCHITECTURE、AGENTS 和 runtime knowledge 是否需要同步。API-backed ignored tests 只在配置了真实凭据时运行。 功能首次合并时按仓库规则提升产品中段版本;本文档本身不改变行为,不单独修改 `1.5.1`(当前功能合并版本 1.7.0)。 ## 19. 主要风险与回滚 | 风险 | 预防 | 回滚方式 | |------|------|----------| | hidden history 过滤错误泄漏内部输入 | 查询 API 分层和协议测试 | 禁用 continuation worker,保留事件 pending | | completion 容量计数漂移 | 单行条件更新 + 启动重算 | reconcile 修复后重启 Router | | steer admission 竞态重复 | lease token + 不可排空 reservation | 配置强制所有 Agent delivery=queue | | background 新路径影响现有用户 | legacy adapter 与新表分离 | 一个版本内启用 direct-notification rollback flag,二者互斥 | | reload 双代同时恢复 | prepare/activation 硬边界 | 关闭新代 admission,旧代继续服务 | | 工具权限误放大 | default RootOnly + Catalog fail closed | 从 definition 移除工具并 reload;已启动 run 固定旧快照 | | continuation 重试重复外部副作用 | 默认只读 registry | dead-letter,要求用户显式处理 | rollback flag 只能在 Phase 3 过渡期存在,并保证新 inbox continuation 与旧 direct notification 互斥。即使回滚投递方式,新 `agent_runs`/inbox 表仍保留审计事实,不能降 schema 或删除记录。 ## 20. 最终实施判据 只有同时满足下列条件,才能把主设计状态从“提案”改为“已实现”: - 具名 definition、不同 Provider、委托图和工具裁剪全部由运行时代码强制。 - foreground/background 都有 durable run,batch foreground 真正并发且父同步等待。 - run quota 与 provider/tool step gate 分离,嵌套委托无 permit 死锁。 - background completion 和 signal 先落 SQLite,丢 wake、queue 满、重启和 reload 均可恢复。 - queue continuation 使用 hidden trigger 原子提交;客户端历史没有伪用户消息。 - steer admission 与 `/stop` 竞态经过故障注入证明无丢失、无双重归属。 - sleep 的 wake 只改变等待,不破坏 queue/steer 内容边界。 - session archive/delete、dead-letter、fallback 和 management UI 均可诊断。 - 全部离线测试、Clippy、build、WebUI check/build 通过,并同步公开文档和产品版本。 在达到这些判据前,可以按 Phase 逐步合并内部基础,但不能提前对外宣称已经具备可靠的 background Agent orchestration。