PicoBot/docs/SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md
xiaoxixi ac201a3949 feat: durable agent orchestration with run persistence, inbox continuation, and signal/steer
- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph,
  fail-closed tool scoping, and signal contracts
- structured cancellation (AgentError::Cancelled/TimedOut) across provider
  streams, tool batches, and sleep; /stop drives the same terminal state
- schema v6 run/group/inbox persistence with execution-ID conditional
  transitions and completion-slot reservations
- ExecutionGate separating run quota from provider/tool step permits
- background completion inbox with hidden-trigger continuation turns,
  fairness scheduling, lease release, dead-lettering, and activation recovery
- typed TurnMailbox with two-phase steer admission and atomic consumption at
  turn commit; /stop releases admitted steer events back to pending
- emit_signal tool with contract-enforced rate/dedupe/severity/size limits
- WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated),
  /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree
  and signal cards
- ChannelContext.durable_private persisted for continuation delivery reuse

Version 1.7.0
2026-08-11 11:51:20 +08:00

1114 lines
60 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.

# 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-08Phase 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 gateglobal→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 wakespawn 拒绝执行补偿事务;`agent_inbox_events`/`agent_session_state` 容量条件更新与 claim/lease/admit/release/supersede/dead-letter API`AgentInboxNotifier`(弱引用 late-bound wake与 Session 双 lane workeruser mpsc + inbox watch 合并 wake公平调度按 `max_user_turn_burst_before_inbox` 与 `max_inbox_wait_secs` 强制 continuationcontinuation 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 收敛旧代 runinterrupted + 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 与 steerAgent 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 + deduplicatedCoordinator `emit_signal` 校验 run 活跃与 execution ID、容量条件插入、投影+notifier wake取消 run 时未消费 signal 自动 supersedeSteeringMailbox 泛化为来源感知 TurnMailboxuser lane 32/64KiB 与 agent lane 8/32KiB 独立容量,`TurnInput{source,delivery,durable_event_id,lease_token}`agent steer 投影为 hidden user 消息保留 source 元数据steer 两阶段 admissionclaim → 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 unionWebUI TasksPage 后台 tab 渲染 run treegroup 折叠、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` 表示 Rootchild 必须携带完整 `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<AgentCatalog>,
agent_coordinator: Arc<AgentCoordinator>,
agent_result_router: Arc<AgentResultRouter>,
agent_projection_hub: Arc<AgentProjectionHub>,
}
```
装配顺序必须明确:
```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<SessionManager wake interface>
→ 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<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 边界,按顺序执行:
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<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 解析安全配置。建议类型:
```rust
#[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,
}
```
加载算法:
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<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 增加默认安全元数据:
```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<Arc<dyn Tool>>,
) -> Result<Arc<ToolRegistry>, AgentCatalogError>;
```
### 5.2 ToolExecutionContext
目标类型:
```rust
#[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`
```rust
pub struct AgentLoopExecution {
pub cancellation: CancellationToken,
pub gate: Arc<ExecutionGate>,
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 descendantsbackground 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 与状态机
建议公开最小接口:
```rust
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 接纳顺序
严格顺序:
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 runactivation 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<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 扩展
```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 的 groupkey 直接写在 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<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
```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` 可在 300500ms 内 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 和新 revisioncommit 后发布 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 countcompletion 事实不能 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 的其他事件仍保持 pendingSession 下次可调度时处理。
现有 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 分批执行,避免长事务:
1. 将旧 generation 的 queued/running/waiting_children 条件更新为 interrupted。
2. 为 background interrupted outcome 转换预留并插入 failure completionforeground 只保存终态。
3. group counter 收敛并生成必要的 group completion。
4. expired leased/admitted events 恢复 pendingattempt +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<String>,
pub private: HashMap<String, String>,
pub durable_private: HashMap<String, String>,
}
```
Channel adapter 负责分类:
- CLI/WebUI通常为空。
- Feishuthread/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 bubbleSignal 卡片来自 `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 同样需要唤醒:
```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<String>,
}
```
以下 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 在既定安全边界排空 steerqueue 留给下一个 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 1Catalog 与安全裁剪
- `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 2AAgentLoop 结构化取消
- CancellationToken 进入 root Turn 和 AgentLoop。
- Provider stream、工具批次、sleep 观察 token。
- `AgentError::Cancelled/TimedOut`
- `/stop` oneshot 兼容桥接和迟到结果测试。
完成条件root 行为不变Provider、tool、preparation 各阶段 stop 均得到一致 Cancelled 终态。
### Phase 2BCoordinator、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 3durable 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 4signal 与 steer
- EmitSignalTool、rate/dedupe/capacity。
- typed TurnMailbox 和两阶段 steer admission。
- `/stop` 立即 release、supersede 和 Signal UI。
完成条件steer 在所有 admission 竞态中严格属于当前 Turn 或未来 queue 之一,不能重复或消失。
### Phase 5wake-aware sleep 与清理
- TurnWakeupHandle watch revision。
- root sleep 响应 user/agent queue/steerchild sleep 仅 timer/cancel。
- 工具 interrupt policy。
- 移除 direct background notification 默认路径;观察一个版本后删除旧 adapter 和旧表写入。
完成条件sleep 不丢 pre-listen wakequeue 内容不泄漏进当前 Turnsteer 在下一个安全边界可见。
## 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 permitglobal provider permit=1 仍能完成。
- foreground batch 并发执行但结果按 request index 返回。
- parent cancel 取消 foreground descendants不取消已独立接纳的 background run。
- TaskSupervisor spawn reject 执行补偿事务且 delegate 不返回可用 ID。
- crash window 留下 queued old-generation runactivation 恢复为 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 runbatch 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。