- RunQuota now enforced: background admission acquires the run permit (global→session) and the runner holds it until terminal commit; foreground never takes run permits so nested limit=1 cannot deadlock - RuntimeAdmission activity guard held by background runners for the whole run; admission checked before any durable write - signal_contract_json/signal_delivery persisted at run admission so definition-level steer delivery actually takes effect (was silently falling back to queue) - terminal completion payload carries the run's emitted signal IDs (design §12.3) so the main Agent can recognise duplicates - ROOT caller_scope_id is the literal "ROOT" (design §9.6) - legacy general delegation returns a deprecation/migration hint Cleanup: - remove unused AgentCaller enum and run_parallel wrapper - remove unreachable http_get_only prompt machinery (http_request is RootOnly and never enters sub-agent registries) - drop never-read EmittedSignal fields; derive WakeupSource from TurnInputSource instead of duplicating match arms - replace the /stop oneshot compatibility bridge with a plain turn_busy flag; the worker's forced-cancel path now selects on the cancellation token (implementation doc §11.1) Version 1.8.0
62 KiB
PicoBot 子 Agent 编排实施细节与可实施性审查
状态:实施基线(2026-08)。
本文以
SUB_AGENT_ORCHESTRATION_DESIGN.md为产品与架构规范,以SUB_AGENT_ORCHESTRATION_REVIEW.md和SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md为评审记录,并逐项对照当前代码给出可以直接拆分为开发任务的实现方案。本文不是“代码已经实现”的声明。实现完成前,运行时事实仍以当前代码、测试和
ARCHITECTURE.md为准。
实施进度(2026-08):Phase 1 已落地具名 Definition/Catalog、不同 Provider profile、工具/Skill fail-closed 裁剪、显式
AgentExecutionContext、父子委托边与 ancestry 校验、canonicalforeground/backgroundschema 以及批量 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 并按请求顺序返回)、父 runwaiting_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<Option<i64>>显式解码)。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);Coordinatoremit_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);AgentProjectionHubbroadcast(复用 plan-change 模式,lag 由客户端 GetAgentRuns 校准);HTTP/api/agent-runs*(列表游标分页/详情/events/cancel)+/api/taskslegacy/new union;WebUI TasksPage 后台 tab 渲染 run tree(group 折叠、agent 标签、深度缩进),ChatPage 显示 continuation 标签与 Signal 卡片(投影事件,不插入 history)。Phase 5 已落地 wake-aware sleep:TurnWakeupPublisher/TurnWakeupHandle随 active Turn 创建(root interactive Turn 专用,sub-run 与 continuation 永远没有 handle);watch revision 单调,四个 admission 点(用户进 TurnMailbox、用户进 next-turn mpsc、Agent steer durable admitted、Agent event 保持 pending 且 wake Session)在事实可见后 publish(先更新状态再发 wake,sleep 醒来必能查到输入);SleepToolroot 上下文先borrow_and_update预检 pending>0 立即返回,否则 select timer/changed/cancellation,steer 唤醒消息携带 run_id/agent_id 与安全摘要、queue 唤醒只给类型/数量并明确不泄漏正文;child/continuation sleep 仅 timer/cancel(不受 root 输入或 sibling signal 影响)。InputInterruptPolicy元数据沿用(sleep=WakeOnly,其余默认 Never,不扩展)。legacy general background 的 direct notification 兼容路径按设计保留一个版本观察,随后删除旧 adapter 与background_tasks写入。
三方对齐审查(2026-08)修复:RunQuota 接线(background admission 前按 global→session 获取 run permit,随 runner 持有至 terminal commit;foreground 不占 run permit,嵌套 limit=1 永不死锁,与 gate 注释语义一致);signal_contract_json/signal_delivery 在 run 接纳时持久化(此前 steer delivery 静默回退为 queue);terminal completion payload 携带本 run 已发出的 signal IDs(§12.3);ROOT 的 caller_scope_id 固定为字面量 "ROOT"(§9.6);legacy general 委托返回弃用迁移提示(§4.3)。已知剩余偏差:resource_scope_id 未建模为显式字段(browser 瞬态隔离经 session_id 惯例达成,行为等效);idempotency_key 仅 schema 预留、工具未开放入口;all policy 的 group completion 事件在 background 批量开放前不可达;legacy 适配器与 background_tasks 写入按设计保留一个版本后删除。
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 过渡 | 中 |
实现的关键路径为:
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. 实施中固定的架构决策
以下决策在编码前固定,不留给各模块自行解释:
- Root Agent 是运行时身份,不创建虚假的 root run。
ToolExecutionContext.agent=None表示 Root;child 必须携带完整AgentExecutionContext。 foreground/background只描述调用方是否等待。批量是否并发由tasks[]和调度器决定;不再存在 canonicalparallel模式。- 每个 Agent Run 都落
agent_runs,包括 foreground。这样截断结果、失败审计和agent_task.get_result对两种模式一致。 - Background 接纳时预留 completion slot。signal 可以因 inbox 满而失败,已经接纳的 run completion 不得因容量耗尽而丢失。
- SQLite 是 run、event、容量计数和消费状态的唯一事实源。watch/broadcast/内存 map 只负责降低延迟或缓存投影。
- queue continuation 不伪造
InboundMessage。它通过 Session 内部 typed task 启动,并在成功时原子保存 hidden trigger、可见结果和 event consumption。 steer不取消 Provider 或普通工具;它只在安全边界注入。/stop和显式 cancel 才触发 cancellation token。- 候选运行代只解析和校验 Catalog,不扫描或修改数据库运行状态。恢复动作仅在新代 activation 后执行。
- 新工具默认
RootOnly。只有经过逐项审计的工具可以标记Delegatable;runtime control 工具由 Coordinator 注入。 - session 归档/删除、run 取消、event supersede 和 dead-letter 都保留审计事实,不通过物理删除表达状态变化。
3. 目标运行时组件与依赖装配
3.1 GatewayState 新增成员
建议增加:
pub struct GatewayState {
// existing fields ...
agent_catalog: Arc<AgentCatalog>,
agent_coordinator: Arc<AgentCoordinator>,
agent_result_router: Arc<AgentResultRouter>,
agent_projection_hub: Arc<AgentProjectionHub>,
}
装配顺序必须明确:
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<SessionManager wake interface>
→ create AgentResultRouter(Storage, notifier, projection hub)
AgentInboxNotifier 使用 late-bound weak target 解决 Coordinator/SessionManager 的环形依赖:
#[async_trait]
pub trait AgentInboxWakeTarget: Send + Sync {
async fn wake_agent_inbox(&self, session_id: &str, revision: i64);
}
pub struct AgentInboxNotifier {
target: RwLock<Weak<dyn AgentInboxWakeTarget>>,
}
事件提交后 notifier 失败不回滚事务;周期扫描会重新唤醒。SessionManager 也不能反向持有强 Arc<AgentCoordinator> 形成释放环。
3.2 prepare 与 activation 分离
GatewayState::from_config() 是候选运行代准备阶段,只允许:
- 解析配置和 Agent Markdown。
- 校验 Provider profile、工具、skill 和委托图。
- 创建无外部副作用的内存对象。
- 运行 Storage schema migration。
start_message_processing() 成为 activation 边界,按顺序执行:
- 连接 MCP;第一版 Agent Definition 不允许引用 MCP 工具,因此此步不改变已经校验完成的 Catalog。
- 启动 Session worker/router、projection relay 和 inbox recovery scanner。
- 调用
AgentCoordinator::recover_on_activation(runtime_generation),收敛旧代 queued/running/waiting runs。 - 扫描 pending/expired leases,修复
agent_session_state计数并发出合并式 wake。 - 最后打开新代的 Agent run admission。
为避免候选代构造期间与旧代同时修改 run 状态,Coordinator 初始处于 closed 状态,activation 成功后才 open()。
4. 配置与 Agent Definition
4.1 配置类型
在 src/config/mod.rs 增加:
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct AgentOrchestrationConfig {
pub enabled: bool,
pub definitions_dir: String,
pub root_delegates: Vec<String>,
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 解析安全配置。建议类型:
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct AgentFrontmatter {
id: AgentId,
description: String,
llm_profile: String,
#[serde(default)] tools: Vec<String>,
#[serde(default)] delegates: Vec<AgentId>,
#[serde(default)] skills: Vec<String>,
#[serde(default)] limits: AgentLimits,
}
加载算法:
- canonicalize definitions root,确认它是受信任目录。
- 只读取 root 第一层的
*.md,按文件名排序,保证错误顺序和 hash 稳定。 - 对每个文件先用 metadata 检查上限,再做有界读取;拒绝非 UTF-8、越界 symlink 和非普通文件。
- frontmatter 必须以第一行
---开始并有独立结束---;正文为空、重复 key、未知 key 均报错。 id使用[a-z][a-z0-9_-]{0,63},拒绝大小写折叠冲突和保留名。- 对规范化 frontmatter JSON 与原始 role body 计算 SHA-256
definition_hash。 - 第一遍建立 ID map,第二遍解析 delegate target、Provider、工具和 skill 引用。
Catalog 最终类型不可变:
pub struct AgentCatalog {
definitions: BTreeMap<AgentId, Arc<AgentDefinition>>,
root_delegates: BTreeSet<AgentId>,
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 增加默认安全元数据:
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 上删除工具:
pub fn scoped_for_agent(
&self,
definition: &AgentDefinition,
runtime_tools: Vec<Arc<dyn Tool>>,
) -> Result<Arc<ToolRegistry>, AgentCatalogError>;
5.2 ToolExecutionContext
目标类型:
#[derive(Clone)]
pub struct ToolExecutionContext {
pub session_id: Option<String>,
pub turn_id: Option<String>,
pub agent: Option<Arc<AgentExecutionContext>>,
pub cancellation: CancellationToken,
pub execution_gate: Option<Arc<ExecutionGate>>,
pub turn_wakeup: Option<TurnWakeupHandle>,
pub resource_scope_id: Option<String>,
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:
pub struct AgentLoopExecution {
pub cancellation: CancellationToken,
pub gate: Arc<ExecutionGate>,
pub tool_context: ToolExecutionContext,
}
Provider 请求必须在获取 provider-step permit 后执行:
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:
- child 接纳成功后,条件更新 parent
running → waiting_children。 - 等待期间父没有 provider/tool step permit。
- 全部 child 终态、父取消或错误退出时 guard 尝试
waiting_children → running;父已经 terminal 时不覆盖。 - 父取消递归取消未终态 foreground descendants;background run 归 root session token 所有,不因创建它的 Turn 正常结束而取消。
6.4 typed Turn input
Phase 4 将 SteeringMailbox<ChatMessage> 替换为 TurnMailbox<TurnInput>。在此之前 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 与状态机
建议公开最小接口:
pub struct DelegateRequest {
pub mode: ExecutionMode,
pub tasks: Vec<DelegateTask>,
pub completion_policy: CompletionPolicy,
pub idempotency_key: Option<String>,
}
impl AgentCoordinator {
pub async fn delegate(&self, caller: CallerContext, request: DelegateRequest)
-> Result<DelegateResponse, AgentCoordinatorError>;
pub async fn get_run(&self, caller: CallerContext, run_id: &RunId) -> Result<AgentRunView, _>;
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<SignalAccepted, _>;
pub async fn cancel_session(&self, session_id: &str, reason: CancelReason) -> Result<(), _>;
pub async fn recover_on_activation(&self, generation: u64) -> Result<RecoveryReport, _>;
}
7.1 Background 接纳顺序
严格顺序:
- 从 context 解析 caller,校验 root session、委托边、ancestry、depth 和 budget。
- 解析全部 target definition,派生 deadline、token/run reservation 和 delivery contract。
- 取得 RuntimeAdmission activity guard 和内存 run quota reservation;尚未写库前任何失败均直接释放。
- Storage 事务条件增加 completion reservation、领取 plan item、插入 group/run queued 记录。
- 把 cancellation token 和 execution ID 注册到 Coordinator active map。
- 通过
TaskSupervisor::spawn_graceful接纳 runner。 - spawn 被拒绝时执行补偿事务:queued → cancelled、释放 completion reservation、回滚/阻塞已领取 plan item;不得向模型返回可用 run ID。
- 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:
pub enum AgentTerminalOutcome {
Completed { result: String, usage: Usage, tool_calls: u32, iterations: u32 },
Failed { error: AgentRunError, usage: Option<Usage> },
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 扩展
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
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 写回本事务改变的所有行;仅内部读取不递增。所有容量增加操作使用条件更新:
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
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
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
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:
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 增加:
agent_tx: Option<mpsc::Sender<UserAgentTask>>,
agent_inbox_wake: watch::Sender<i64>,
consecutive_user_turns: usize,
把 worker 创建逻辑抽为 ensure_agent_worker_locked(),用户 enqueue 与 wake_agent_inbox() 共用。worker 同时持有 user receiver 和 wake receiver:
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。
执行过程:
- Storage 把 due events
pending → leased,返回 lease token。 - worker 构造
AgentTaskSource::BackgroundAgentResults和 hidden trigger,但暂不落 messages。 - root Agent 使用只读 continuation ToolRegistry 执行。默认允许内容读取、检索和 scoped
agent_task.get/get_result,不允许 send_message、todo、写文件、delegate 或其他外部副作用。 - 成功后
commit_continuation_turn原子插入 hidden trigger、tool/assistant messages、usage,并把同 token eventsleased/admitted → consumed。 - commit 后更新 Session 内存、Turn Completed、projection 和 Channel delivery。
- 执行失败、取消或 stale generation 时
InboxLeaseGuard显式 release 到 pending 并写next_attempt_at。
这一流程与当前“先持久化用户消息再调用模型”不同,必须走独立分支。不能先写 hidden trigger,否则失败重试会在 history 中累积无 assistant 配对的内部输入。
9.3 hidden history
ChatMessage 和 MessageMeta 增加:
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 的两阶段流程必须由测试锁定:
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 流程:
- 从 AgentExecutionContext 取得唯一 root session、run、execution ID 和 delivery。
- 校验 run 仍为 running/waiting_children 且 execution ID 匹配。
- 校验 severity、JSON 深度、summary/details 字节、信号总数、burst、最小间隔和 dedupe window。
- 条件增加 session pending count;容量必须扣除 reserved completion slots。
- 插入 event 和新 revision;commit 后发布 projection + wake。
- 返回 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
执行顺序:
- 在 Session 锁内关闭 active Turn admission、取出 mailbox durable reservations、递增 worker generation、取出 root Turn cancellation token。
- 锁外取消 root token,条件 release admitted/leased inbox events。
- 调用 Coordinator cancel 当前 session 的 active background runs。
- cancellation terminal event 使用
requires_continuation=false并直接 consumed,防止 stop 后又自动启动“已取消”Turn。 - stop 前已经 pending 的其他事件仍保持 pending;Session 下次可调度时处理。
现有 oneshot 可在过渡期由 token adapter 驱动,最终移除 current_cancel: Option<oneshot::Sender<()>>,统一为 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 分批执行,避免长事务:
- 将旧 generation 的 queued/running/waiting_children 条件更新为 interrupted。
- 为 background interrupted outcome 转换预留并插入 failure completion;foreground 只保存终态。
- group counter 收敛并生成必要的 group completion。
- expired leased/admitted events 恢复 pending,attempt +1,写 next retry。
- 按每 session 重算
pending_event_count和有效 reservation;差异修复并记录 structured warning。 - 对有 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,核心无法安全猜测。类型改为:
pub struct ChannelContext {
pub reply_to: Option<String>,
pub private: HashMap<String, String>,
pub durable_private: HashMap<String, String>,
}
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:
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 合并排序,旧记录只读。
新增受设备鉴权保护的:
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<TurnWakeupState>;state 使用独立于 durable inbox revision 的 session-local 单调 revision,因为用户 queue/steer 同样需要唤醒:
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<String>,
}
以下 admission 成功后递增 revision 并 send_replace:用户进入 TurnMailbox、用户进入 next-turn mpsc、Agent event durable admitted、Agent event 保持 pending 并成功 wake Session。先更新事实状态再发 wake,不能让 sleep 醒来却查询不到输入。
SleepTool::execute_with_context:
- 解析并限制 duration 到 24 小时。
- child context 的
turn_wakeup=None,只 select timer 与 cancellation。 - root context 先读取
receiver.borrow_and_update();若 pending 总数已大于 0,立即返回,不进入等待。 - 否则同时等待 timer、
receiver.changed()和 cancellation。watch 保留最新 revision,因此 input 在检查与 select 之间到达也不会丢。 - wake 后再次读取 state 并构造有界结果。steer 可以返回来源、run ID 和安全摘要;queue 只返回类型/数量,不返回正文。
- 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/backgroundschema 和 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。/stoponeshot 兼容桥接和迟到结果测试。
完成条件: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 阶段至少执行:
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 时另外执行:
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。