PicoBot/docs/SUB_AGENT_ORCHESTRATION_DESIGN.md
xiaoxixi 5501c539fc feat: remove agent run groups, add WebUI agent definition management
- drop agent_run_groups table and group_id/scope_kind/scope_id columns (schema v8)
- remove group_id from AgentExecutionContext and recovery group counters
- flatten TasksPage background tab into a per-run list
- add WebUI Agents page with definition CRUD and inline provider/model
- bump version to 1.11.0
2026-08-13 14:03:01 +08:00

1315 lines
65 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。Phase 1 的具名 Definition/Catalog、Provider profile、工具与 Skill 裁剪、显式执行上下文、委托图校验和批量 foreground 已落地durable run/inbox、signal/steer 与可唤醒 sleep 仍按本文后续阶段实施。
>
> 本文定义具名子 Agent、委托图、多 Provider、`delegate`、`emit_signal`、后台结果收件箱、`queue`/`steer` 投递以及可唤醒 `sleep` 的目标架构。各阶段是否已经实现以代码、测试和 `docs/ARCHITECTURE.md` 为准;未落地章节仍是目标设计。
>
> 2026-08 评审提出的 A1A5、B1B6、C1C4 已纳入本文;逐项决策与理由见 [`SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md`](SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md),代码级落地方案与验收门槛见 [`SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md`](SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md)。
## 1. 背景与现状
PicoBot 已经具备一版子 Agent 能力:根交互 Agent 通过 `delegate` 创建临时 Agent支持 `inline``background``parallel`,可以过滤工具、绑定计划子项、持久化后台任务,并在后台任务完成后向原 Channel 发送通知。
当前实现适合作为“一次性子任务执行器”,但还不能表达完整的多 Agent 编排:
1. 所有子 Agent 复用同一个 `LLMProviderConfig`,不能按角色选择 Provider/Model。
2. 子 Agent 没有具名、可校验的角色文件;工具权限由 `delegate.allowed_tools` 临时决定。
3. 子 Agent 被统一移除 `delegate`,不能按受控委托图继续委派。
4. `DelegateContext` 只有 session/channel/chat没有 caller、current agent、parent run、depth 和 ancestry无法执行多级授权。
5. `parallel` 把“委托方是否等待”和“子任务是否并发”混成一个模式。
6. 后台完成通知直接走 `MessageBus.outbound` 发给用户,不会自动成为主 Agent 的输入。
7. 当前 steering mailbox 只建模用户输入Agent 信号若直接复用会被误标为用户消息,并继承 `/stop` 丢弃语义。
8. `SleepTool` 只等待定时器;除整个 Turn 被取消外,不能被新用户输入或 Agent 信号唤醒。
9. `send_message` 同时覆盖跨 Channel 消息、目标会话历史写入和同 Turn 附件暂存,不能作为子 Agent 内部信号的安全替代。
本设计在保留 `AgentLoop` 无状态、每 Session 单 Turn、TurnSnapshot latest-wins、持久化后才 Completed 等既有不变量的基础上,引入显式编排层。
## 2. 设计目标
### 2.1 功能目标
- 每个子 Agent 由独立 Markdown 文件定义角色、工具、Provider profile、委托目标和资源上限。
- 主 Agent 和子 Agent、子 Agent 与子 Agent 之间可以按有向权限图委托任务。
- 主 Agent 是特殊根身份,只能委托其他 Agent永远不能成为委托目标。
- 委托执行模式收敛为成对概念 `foreground | background`
- 单任务/批量任务与串行/并发属于调度维度,不再作为第三种执行模式。
- Background Agent 可以主动发出非终态重要信号,最终完成/失败/超时/取消由运行时自动生成终态事件。
- Background 事件使用 `queue | steer` 决定进入下一 Turn 还是当前 Turn。
- 主 Agent 忙碌、空闲、等待 sleep、正在模型调用或工具调用时都有明确、无丢失的投递语义。
- Agent 信号和后台完成结果先持久化,再通过 SessionManager 投递Gateway 崩溃或内存唤醒丢失后可以恢复。
- `sleep` 可以由当前 session 的新输入唤醒,但不能因此破坏 queue/steer 的内容可见性边界。
- 工具、任务树、事件和结果具备有界并发、取消、超时、去重、审计和可观测性。
### 2.2 架构目标
- Agent 编排属于 `agent`/`session` 领域,不把内部 Agent 信号塞入外部 Channel 数据面。
- ToolRegistry 只提供经过角色定义和系统策略共同裁剪后的能力。
- 所有慢 I/O 在 Session 锁外执行;投递 admission、Turn 关闭和可靠 fallback 保持原子。
- Background 任务与旧运行代绑定配置热重载不在任务中途切换角色、Provider 或工具权限。
- 完成事件、主 Agent 消费确认和 Turn 持久化使用事务与条件更新,避免内存/数据库静默分叉。
## 3. 非目标
- 不把 PicoBot 改成分布式 Agent 集群;所有运行仍在单 Gateway 进程内。
- 不恢复 Gateway 崩溃前正在进行的 Provider 流或工具调用现场。
- 不承诺外部 LLM 调用严格 exactly-once崩溃恢复可能重新调用 Provider。
- 不允许模型在委托参数中直接指定 API key、base URL、任意工具或任意目标 session。
- 不让子 Agent 直接访问 SessionManager 内部状态。
- 不把 Agent reasoning、Provider 私有 replay state 或完整工具轨迹转发给其他 Agent、客户端或日志。
- 不默认硬中断正在进行的 Provider 请求或有副作用工具;`steer` 只保证最近安全边界注入。
- 不用本功能替代跨进程可靠监控。小时/天级持久监控仍应优先使用 Scheduler 的 monitor job。
## 4. 术语与核心语义
| 术语 | 定义 |
|------|------|
| Root Agent | 当前用户会话的主 Agent运行时身份为 `ROOT`,不属于可寻址子 Agent 目录 |
| Agent Definition | 从一个 Markdown 文件解析出的具名角色、模型、工具、委托边和限制 |
| Agent Catalog | 当前 Gateway 运行代中全部有效 Agent Definition 的不可变快照 |
| Agent Run | 一个 Agent 对一个具体任务的执行实例 |
| Run Group | 一次批量委托创建的多个同级 Agent Run |
| Foreground | 委托方等待任务终态并直接取得结果;不代表子任务串行 |
| Background | 委托调用立即返回 run ID任务独立执行结果通过事件投递 |
| Queue | 输入属于后续 Turn不改变当前 Turn 的模型上下文 |
| Steer | 输入尝试进入当前 Turn并在最近安全边界注入失败时可靠退化为 queue |
| Agent Signal | Background Agent 在运行中主动发出的非终态重要事件 |
| Agent Completion | Agent Run 进入 completed/failed/timed_out/cancelled/interrupted 时由运行时自动产生的终态事实background 逐 run 投影为 inbox completion event |
| Agent Inbox | 持久化的主 Agent 内部收件箱,是 Background 结果与信号的权威来源 |
| Turn Mailbox | 当前 Turn 接受 steer 输入的有界内存邮箱,保留来源、顺序和 durable event ID |
### 4.1 两个正交维度
执行方式和结果投递必须分开:
```text
执行生命周期foreground | background
后台事件投递queue | steer
```
多个 foreground 子任务可以并发运行,父 Agent 仍同步等待全部结果。多个 background 子任务也可以并发运行,但 `delegate` 立即返回 run IDs。并发与否由批量请求、Coordinator 调度和并发配额决定,不由 mode 名称决定。
### 4.2 Foreground 与 Background
| 行为 | Foreground | Background |
|------|------------|------------|
| `delegate` 返回时机 | 子任务进入终态后 | 任务持久化并成功接纳后 |
| 直接返回 | 结构化结果 | run ID / group ID |
| 父 Agent 当前 Turn | 阻塞等待 | 继续运行 |
| 最终结果路径 | 当前 delegate tool result | Agent Inbox event |
| 适用场景 | 当前工作依赖结果 | 长任务、监控、可稍后处理的任务 |
若当前工作必须及时依赖结果,应使用 foreground。Background 的 queue completion 不保证参与发起它的原 Turn需要抢占式关注的重要信号应显式使用 steer。
## 5. Agent Markdown 定义
### 5.1 文件位置
第一版只从受信任配置目录加载:
```text
~/.picobot/
├── config.json
└── agents/
├── researcher.md
├── coder.md
└── reviewer.md
```
角色文件决定工具权限和委托能力,属于安全配置,不应默认从可被普通 Agent 写入的 workspace 自动加载。未来若支持 workspace 角色目录,必须由配置显式开启,并说明它不是硬安全边界。
### 5.2 文件格式
```md
---
id: researcher
description: 搜索、阅读并整理技术资料
llm_profile: research-sonnet
tools:
- file_read
- file_search
- content_search
- web_fetch
delegates:
- reviewer
skills:
- technical-research
limits:
timeout_secs: 900
max_iterations: 24
max_children: 4
max_depth: 3
max_concurrent_runs: 2
max_concurrent_provider_steps: 1
max_concurrent_tool_steps: 4
max_result_chars: 16000
---
# Role
你是一名严谨的研究 Agent。
- 优先使用原始资料。
- 明确区分事实、推断与建议。
- 只返回与任务有关的结论、证据和不确定性。
- 不修改项目文件。
```
Frontmatter 只保存非秘密引用和限制API key、base URL、headers 继续保存在 `config.json`/`.env``llm_profile` 引用现有 `config.agents` key`Config::get_provider_config()` 解析 Provider 与 Model。
`skills` 是可选的受信任 allowlist只有 Definition 工具集包含 `get_skill` 时才向子 Agent注入这些 Skill。子 Agent不继承主会话临时启用的 Skill、完整历史或 memory recall。调用方需要传递的事实必须进入显式 task/context未来若支持 memory只能通过管理员配置的只读 scope 开放。
### 5.3 配置扩展
```json
{
"agent_orchestration": {
"definitions_dir": "agents",
"root_delegates": ["researcher", "coder", "reviewer"],
"max_tree_depth": 4,
"max_runs_per_tree": 16,
"max_concurrent_runs": 6,
"max_concurrent_runs_per_session": 4,
"max_concurrent_provider_steps": 8,
"max_concurrent_provider_steps_per_session": 4,
"max_concurrent_tool_steps": 16,
"max_concurrent_tool_steps_per_session": 8,
"max_pending_inbox_events_per_session": 128,
"inbox_event_ttl_hours": 168,
"max_inbox_delivery_attempts": 8,
"max_user_turn_burst_before_inbox": 4,
"max_inbox_wait_secs": 30
}
}
```
`root_delegates` 是 Root Agent 的出边白名单。Root 不需要也不允许出现在 definitions 目录中。
`definitions_dir` 的相对路径按实际加载的 `config.json` 所在目录解析;第一版要求 canonical path 保持在该受信任配置目录内。默认值 `agents` 对应 `~/.picobot/config.json` 旁的 `~/.picobot/agents/`,也能让仓库内 fallback `./config.json` 使用同仓库配置目录而不跨越信任边界。
### 5.4 加载与校验
AgentCatalog 在 Gateway 候选运行代准备阶段完成全部校验,但候选代不得在此时扫描或修改 inbox/run 恢复状态;恢复扫描、旧 run 收敛和 Router 启动只能在候选代成为活动运行代后的 activation 阶段执行:
- 文件大小、UTF-8、frontmatter 格式和必填字段。
- ID 格式、重复 ID、保留 ID`ROOT``main` 等)。
- `llm_profile` 能解析为完整 `LLMProviderConfig`
- 每个工具已注册且允许委托。
- 每个 delegates 目标存在且不是 Root。
- 限制值在系统硬上限内。
- 角色正文和描述长度有界。
- Skill allowlist 中的每个 ID 均存在,且只有允许 `get_skill` 的角色可以声明。
- canonical path 位于允许目录,拒绝越界 symlink。
任一引用错误应拒绝候选运行代激活而不是静默删除工具或委托边。AgentCatalog 以 `Arc` 固定在运行代中,已启动任务不读取修改后的文件。
第一版 Definition 只允许引用候选代准备阶段已经注册的 built-in 工具。MCP 连接按现有架构只能在 activation 阶段发生,无法在不产生外部副作用的候选代准备阶段完成严格校验,因此 MCP 工具委托暂不开放;未来需要先增加可离线校验的 MCP tool manifest再扩展 Catalog。
## 6. 总体组件设计
```mermaid
flowchart LR
Root[Root Agent] --> DT[DelegateTool]
Sub[Sub Agent] --> DT
DT --> C[AgentCoordinator]
C --> AC[AgentCatalog]
C --> RI[runtime-injected tool marker]
C --> PF[ProviderFactory]
C --> TR[Filtered ToolRegistry]
C --> AR[AgentRunner]
AR --> AL[AgentLoop]
AR --> ES[AgentEventSink]
ES --> DB[(agent_runs / agent_inbox_events)]
DB --> RR[AgentResultRouter]
RR --> SM[SessionManager]
SM --> TM[TurnMailbox / Inbox Wake + Worker Claim]
TM --> Root
SM --> DC[DeliveryCoordinator]
```
### 6.1 AgentCatalog
拥有运行代内不可变的 Agent Definition提供按 ID 查找、委托目标描述和 definition hash。主 Agent系统提示只获得 ID 与短 description不加载所有角色正文。
### 6.2 AgentCoordinator
替代当前承担过多职责的 `SubAgentManager`,负责:
- 解析 caller/target 和授权委托边。
- 创建 run ID、父子关系和预算。
- 持久化接纳状态后启动 AgentRunner。
- 管理 foreground await、background spawn、取消和超时。
- 控制全局、session、Agent 与任务树的 run admission quota以及 Provider/普通工具步骤的 execution permit两类配额不共用生命周期。
- 生成自动 completion terminal outcome并逐 run 物化 inbox event。
- 向 WorkManager 条件提交计划子项结果。
### 6.3 AgentRunner
负责一次 Agent Run
1. 从 Definition 和运行代创建 Provider。
2. 构造有效 ToolRegistry。
3. 组装系统规则、角色正文、任务和显式上下文。
4. 调用无状态 AgentLoop。
5. 收集最终正文、媒体、usage、工具次数和子运行引用。
6. 返回类型化终态,不直接向 Channel 发消息。
### 6.4 AgentEventSink / AgentResultRouter
`AgentEventSink` 负责持久化 signal 和 run completion event每个 run 终态独立生成);`AgentResultRouter` 负责把 pending inbox event 送到原 root session。Router 的内存 wakeup 是加速器SQLite inbox 才是权威来源。
### 6.5 ProviderFactory
根据 `llm_profile` 创建 Provider注入 Storage/Observer并复用当前运行代的 workspace、input types 和 token limit。Provider 仍是纯 HTTP client不感知 Session、Channel 或 Agent 图。`cost` 只有在 profile 明确配置 input/output/cache 价格后才计算;当前没有价格来源时持久化为 `NULL`,不能根据模型名猜测价格。
## 7. 委托图与权限模型
### 7.1 基本规则
每次委托必须同时满足:
```text
target != ROOT
target in allowed_targets(caller)
next_depth <= global max_tree_depth
remaining_delegation_depth > 0
tree_run_count < max_runs_per_tree
target not in current_agent_ancestry
remaining budget > 0
runtime admission is open
```
Root 的 allowed targets 来自 `root_delegates`;子 Agent 来自自身 Markdown 的 `delegates`
配置可以出现 A→B 和 B→A允许两者在不同任务树中互相委托但一个执行链默认禁止再次出现同一 Agent ID从而拒绝 A→B→A 的递归乒乓。未来若需要受控返工循环,应设计显式 iteration workflow而不是放开隐式递归。
全局 `max_tree_depth` 是 root-relative 硬上限Definition 的 `limits.max_depth` 表示该 Agent可继续创建的最大相对后代深度。child context 的 remaining depth 取 `min(parent_remaining - 1, target_definition.max_depth)`,任一限制耗尽即拒绝继续委托。
### 7.2 工具权限
工具可用性完全由具名 Agent 定义文件决定:有效工具集为
```text
AgentDefinition.tools ∩ 当前运行代已注册工具
```
不再有工具侧的「可派发」标志。Tool trait 只保留一个运行时注入标记:
```rust
/// 该工具由运行上下文按需注入delegate 目标、信号契约、skill allowlist
/// 不能直接写进 Definition 的 `tools` 列表。普通工具默认 false。
fn runtime_injected(&self) -> bool { false }
```
- `delegate``emit_signal``get_skill``agent_task` 标记 `runtime_injected=true`,由 Coordinator 根据 `delegates`/`signal`/`skills` 字段和运行上下文注入,不能仅靠 Markdown 的 `tools` 声明;`get_skill` 是唯一例外——把它写进 `tools` 表示启用 scoped skill 包装器。
- 其余任何已注册工具(含 `bash``send_message``todo` 等)都可由管理员在定义文件的 `tools` 列表显式授权,这是知情的选择。
`allowed_tools` 调用参数只能收窄 Definition 的工具集,不能扩权;模型不能在单次调用中越过 Definition。
## 8. AgentExecutionContext
当前只含 session/channel/chat 的隐式上下文不足以支持嵌套委托。目标结构为:
```rust
pub struct AgentExecutionContext {
pub root_session_id: String,
pub root_turn_id: Option<String>,
pub run_id: String,
pub parent_run_id: Option<String>,
pub caller_agent_id: String,
pub current_agent_id: String,
pub ancestry: Vec<String>,
pub depth: u16,
pub plan_item_id: Option<String>,
pub cancellation: CancellationToken,
pub budget: AgentBudget,
pub signal_contract: Option<SignalContract>,
}
```
`ToolExecutionContext` 扩展为包含可选 `AgentExecutionContext`、CancellationToken、execution gate、Turn wakeup handle 和资源 scope。Root interactive Agent 没有伪造的 run ID`agent` 字段为 `None`,由 session/turn context 明确识别为 `ROOT`sub-run 的 `agent` 字段必须为 `Some` 且 run ID 非空。Turn wakeup handle 只为 root interactive Turn 提供sub-run 中为 `None``DelegateTool``EmitSignalTool` 必须实现 `execute_with_context`;权限判断不能依赖模型参数或仅依赖 Tokio task-local。
task-local 可以继续作为同一调用栈的便利桥接但不是授权事实来源。Background spawn 必须显式复制所需上下文,不能假设 task-local 跨 `tokio::spawn` 传播。
## 9. Delegate 工具设计
### 9.1 职责
`delegate` 只负责创建 Agent Run不再同时承担查询、取消、列表。任务管理拆给 `agent_task`
```text
delegate → run / run_many
agent_task → get / list / cancel / get_result
```
较小、单一的 schema 能减少模型错误调用,也便于分别授权。
`agent_task` 的每次操作都必须同时校验 `root_session_id` 和调用者在任务树中的位置。Root 只能操作当前 session 的任务树;子 Agent只能读取自身及后代、取消未终态后代不能访问祖先、sibling 或其他 session。run ID 不是授权凭证。
### 9.2 单任务请求
```json
{
"target": "researcher",
"task": "分析当前 Provider 扩展点",
"context": "重点关注热重载和 usage 持久化",
"mode": "foreground",
"plan_item_id": "T2"
}
```
### 9.3 批量请求
```json
{
"mode": "foreground",
"tasks": [
{"target": "researcher", "task": "研究方案 A"},
{"target": "coder", "task": "分析实现 B"},
{"target": "reviewer", "task": "评审风险 C"}
]
}
```
批量 foreground 的三个子任务并发执行delegate 等待全部终态后返回聚合结果。这等价于旧 `parallel`,但不再把 parallel 当作生命周期模式。
批量 background 同样并发接纳,立即返回:
```json
{
"runs": [
{"run_id": "run-a", "agent": "researcher", "status": "queued"},
{"run_id": "run-b", "agent": "coder", "status": "queued"},
{"run_id": "run-c", "agent": "reviewer", "status": "queued"}
]
}
```
### 9.4 Background 投递契约
```json
{
"target": "service-monitor",
"task": "监控服务错误率",
"mode": "background",
"delivery": {
"signal": "steer",
"completion": "queue",
"failure": "steer"
}
}
```
- `signal`:运行中主动事件的投递方式(当前由 Definition 的 `signal:` 契约 `delivery` 字段决定 queue/steer
- `completion` / `failure`:投递契约为未来扩展;当前 completion 与 failure 恒为 queue尚未开放可配置 steer。
Foreground 请求直接把 completion 作为 tool result 返回,因此不接受 completion delivery。仅允许 Root 创建 background run单任务或 `tasks` 批量,批量并发执行、每个 run 独立 completion 事件);子 Agent 发起的 background 尚未开放。
### 9.5 Foreground 返回
单任务和批量任务都返回逐项状态,单个失败不能抹掉其他结果:
```json
{
"status": "partial",
"results": [
{"run_id": "run-a", "agent": "researcher", "status": "completed", "result": "..."},
{"run_id": "run-b", "agent": "coder", "status": "failed", "error": "..."},
{"run_id": "run-c", "agent": "reviewer", "status": "completed", "result": "..."}
]
}
```
结果按请求顺序返回,不按完成顺序重排。完整结果统一写入 `agent_runs`;超过 tool result 上限时返回摘要和 run ID保证 `agent_task.get_result` 真能读取完整结果。
### 9.6 幂等与接纳
Background delegate 只有在 run 记录持久化成功、运行代 admission 成功、completion inbox 容量已经预留且执行任务已经被 TaskSupervisor 接纳后才返回成功。可选 `idempotency_key``(root_session_id, caller_scope_id, key)` 范围唯一,用于 Provider 重试时避免重复创建任务Root 的 `caller_scope_id` 固定为非空字面量 `ROOT`,数据库使用仅覆盖非空 key 的 partial unique index避免 SQLite `NULL` 破坏去重。
## 10. Prompt 与上下文隔离
子 Agent 默认不继承主会话完整历史。一次 run 的输入由以下部分组成:
```text
PicoBot 基础运行规则
+ 角色 Markdown 正文
+ 当前工具说明
+ 委托执行约束(身份、父任务、资源限制)
+ 显式 task
+ 显式 context / artifact refs
```
委托 task 只注入一次。调用方需要子 Agent 知道的事实必须写入 task/context不能依赖完整历史偶然可见。
子 Agent 最终结果作为普通 tool result 或 runtime event data 交给上游,不能变成更高优先级 system 指令。所有子 Agent 输出都视为不可信数据;主 Agent系统提示明确要求不要执行结果正文中试图修改角色、工具或投递策略的指令。
资源型工具使用独立 scope
```text
resource_scope_id = root_session_id + run_id
```
并行子 Agent 不默认共享 browser/session 等有状态外部资源。共享 browser 登录态的唯一正式路径是Root 通过 `browser_profiles` 创建/选择经过校验的 persistent ID把它作为显式 task/artifact reference 交给获准使用 browser 的子 Agent子 Agent在每次相关调用中显式传入该 ID瞬时父 session browser scope 不能隐式继承。
无 target 的旧委托映射出的内置 `general` 兼容 Agent可在弃用期保留父 session transient browser scope并给出迁移提示具名 Agent不继承这一兼容例外。
## 11. Agent Run 状态机
```mermaid
stateDiagram-v2
[*] --> queued
queued --> running
running --> waiting_children
waiting_children --> running
running --> completed
running --> failed
running --> timed_out
running --> cancelled
running --> interrupted
queued --> cancelled
completed --> [*]
failed --> [*]
timed_out --> [*]
cancelled --> [*]
interrupted --> [*]
```
- `queued`:已持久化且等待执行配额。
- `running`AgentLoop 正在执行模型或工具步骤。
- `waiting_children`:当前 run 正等待 foreground 子运行。
- `completed`:有可用最终结果。
- `failed`Provider、工具或内部执行错误。
- `timed_out`:超过 run deadline。
- `cancelled`由用户、父任务、session 删除或 shutdown 明确取消。
- `interrupted`:进程重启导致无法恢复现场。
状态变化使用条件更新,迟到结果只有在 execution ID、runtime generation 和当前状态匹配时才能提交。
## 12. Agent 信号与自动 Completion
### 12.1 两种事件
| 事件 | 产生者 | 是否终止 run | 用途 |
|------|--------|--------------|------|
| Signal | 子 Agent 主动调用 `emit_signal` | 否 | 重要中间状态、监控告警 |
| Completion outcome | AgentCoordinator 自动生成 | 是 | completed/failed/timed_out/cancelled/interrupted |
最终结果不能依赖模型记得调用工具。即使 Provider 异常、超时或任务被取消Coordinator 也必须持久化 run 的终态 outcome。Foreground 将其返回为 tool resultbackground 的每个 run 终态都物化为一个独立的 run completion inbox event无 all/each 策略,批量也只是逐 run 生成)。
### 12.2 EmitSignalTool
```json
{
"key": "service-error-threshold",
"severity": "critical",
"summary": "服务错误率超过 5%",
"details": {
"current": 0.071,
"threshold": 0.05
},
"dedupe_key": "service-a:error-rate"
}
```
`emit_signal` 不接受 target session、channel、chat ID 或 delivery 参数。目标、queue/steer、最大次数、速率和 root session 全部来自 `AgentExecutionContext.signal_contract`
工具调用只有在 signal event 持久化后才成功返回:
```json
{
"signal_id": "signal-123",
"status": "accepted",
"delivery": "steer"
}
```
Coordinator 强制执行:
- 每 run 信号总数与累计字节上限。
- 最小发送间隔和 burst 上限。
- dedupe key 冷却窗口。
- severity allowlist。
- summary/details 大小与 JSON 深度限制。
- 只能投递到创建该 run 的 root session。
未提供 `dedupe_key` 时,每次调用使用 `signal:<event_uuid>` 作为非空 event key提供 key 时使用 `signal:<normalized-key>:<cooldown-window-id>`,只在冷却窗口内去重,不能因数据库唯一约束永久压制同类告警。
普通进度不应滥用 signal。工具调用进度继续通过内部 Observer/TurnEvent 投影到 UI只有需要主 Agent采取行动的事件才使用 `emit_signal`
### 12.3 Completion 去重
run completion payload 包含本 run 已发出的 signal IDs主 Agent 可以识别并避免再次报告。completion/failure 投递当前恒为 queue可配置 steer 为未来扩展)。禁止完全静默丢弃失败,`silent` 若未来开放也只能用于正常 completion。
## 13. SendMessage、EmitSignal 与附件职责
三者方向不同,不合并为一个万能工具:
```text
emit_signal 子 Agent → Agent Inbox → 主 Agent内部输入
send_message Agent → OutboundDispatcher → Channel/用户(外部输出)
attach_artifact 工具产物 → 当前 Turn → DeliveryCoordinator当前回复附件
```
### 13.1 send_message
只负责用户明确授权的跨 Channel/跨会话外部消息,具有真实外部副作用。目标和文件参数继续受 Channel/file transfer 限制;`origin` 不再由模型自由填写,改由 ToolExecutionContext 生成,避免来源伪造。是否对子 Agent 开放由管理员在定义文件的 `tools` 里显式决定。
### 13.2 emit_signal
只负责后台子 Agent 的结构化内部事件。它没有任意目标、媒体或直接用户投递能力,不写目标会话 assistant history。
### 13.3 attach_artifact
当前 `send_message(files=...)` 对同 session 的特殊暂存行为长期应拆成 `attach_artifact` 或统一 ToolResult media side channel。短期保留兼容路径但新子 Agent 信号设计不得依赖它。
### 13.4 自动 completion
Completion 不是工具。AgentRunner 的终结路径统一返回 terminal outcomeCoordinator 保存结果并按 foreground/background 创建相应投递 event避免模型遗漏或重复。
## 14. 持久化模型
### 14.1 agent_runs
```text
agent_runs
----------
id TEXT PRIMARY KEY
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
deadline_at INTEGER NOT NULL
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
started_at INTEGER
finished_at INTEGER
created_at INTEGER NOT NULL
```
```sql
CREATE UNIQUE INDEX agent_runs_idempotency
ON agent_runs(root_session_id, caller_scope_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
```
不保存 API key、Authorization header、Provider 私有 reasoning state 或完整 connection URL。`cost` 是 nullable projectionProvider profile 未配置价格时必须为 `NULL`usage token 不受影响。
### 14.2 agent_run_groups已删除schema v8
批量委托不再建组头:单/批量请求的 `idempotency_key` 都绑定各自的 run 行,批量只是多个独立 run 的集合,使用 `(root_session_id, caller_scope_id, idempotency_key)` partial unique index避免批量 children 互相冲突。
批量 background 的每个 run 终态都立即创建独立 completion event不等待 sibling主 Agent 空闲时收到即处理(完成即返回),忙碌时由公平调度合并或等待。不存在 all/each 策略。
接纳 background 时在每个 run 的 `completion_slot_reserved` 记一个 slot。Storage 用同一写事务统计该 session 的 `pending/leased/admitted` 事件和有效 reservation避免并发接纳越过上限`consumed/dead_letter` 受 TTL 清理但不占 pending 配额。容量不足在创建 run 前拒绝signal 只能使用未预留容量。预留在 completion 事务落库或接纳回滚时释放。
容量判断不能在每次接纳时通过无锁 `COUNT(*)` 推断。新增每 root session 一行的 `agent_session_state`,在同一 SQLite 写事务中以条件 `UPDATE` 维护 `pending_event_count``reserved_completion_slots` 和单调 `revision`。background 接纳先增加 reservationsignal 只有在 `pending + reserved < limit` 时增加 pendingcompletion 将 reservation 原子转换为 pendingconsume/dead-letter 减少 pending。启动恢复会以事件与 run 事实重算计数,发现差异时修复并记录告警。
### 14.3 agent_inbox_events
```text
agent_inbox_events
------------------
id TEXT PRIMARY KEY
root_session_id TEXT NOT NULL
run_id TEXT NOT NULL
event_type signal | completion
event_key TEXT NOT NULL
delivery queue | steer
requires_continuation BOOLEAN NOT NULL DEFAULT TRUE
severity TEXT
payload_json TEXT NOT NULL
status pending | leased | admitted | consumed | superseded | dead_letter
attempt_count INTEGER NOT NULL DEFAULT 0
lease_token TEXT
lease_until INTEGER
next_attempt_at INTEGER
admitted_turn_id TEXT
last_error TEXT
created_at INTEGER NOT NULL
consumed_at INTEGER
dead_lettered_at INTEGER
fallback_notified_at INTEGER
revision INTEGER NOT NULL
UNIQUE(run_id, event_type, event_key)
```
完整结果保存在 `agent_runs.result`inbox payload 默认只放有界摘要、元数据和 result reference避免复制大文本。
event key 始终非空:无 dedupe key 的 signal 用 `signal:<event_uuid>`,有 dedupe key 的 signal 加冷却窗口 IDrun completion 固定为 `completion:<run_id>`。由同一次 `/stop` 产生、无需主 Agent再次解释的 cancelled completion 使用 `requires_continuation=false`,在终态事务中直接记为 consumed但仍保留事件审计和客户端投影。
### 14.4 原子事务
Agent completion 必须在一个 Storage 事务中:
```text
UPDATE agent_runs terminal state/result/usage
CONSUME reserved completion capacity
INSERT run completion ... ON CONFLICT DO NOTHING
UPDATE bound task item by execution_id
COMMIT
```
事务失败时不能对外宣称任务完成。内存 wakeup 只有在 commit 成功后发送。每个 run 的终端事务独立生成自己的 completion 事件;迟到终态只更新自己的 run 行,不影响其它 sibling。
### 14.5 continuation 消息与投递绑定
现有 message 持久化需要增加两个可向后兼容字段:
```text
client_visibility visible | hidden默认 visible
turn_origin user | agent_continuation | scheduled默认 user
```
hidden message 参与 Provider replay 和事务回滚,但 `SessionHistory``TurnCommitted.messages` 与 Channel 投递只投影 visible message。Storage 的 continuation commit API 必须在一个事务中写 hidden trigger、可见 assistant/tool 消息、usage 和 event consumption。
内部历史读取与客户端历史读取必须拆开Session 恢复和 Provider replay 读取 visible+hiddenWebSocket/HTTP 历史、管理面消息查询和 committed delta 默认只读 visible。hidden `role=user` 不增加面向用户的 `message_count`,也不参与自动标题生成阈值;上下文压缩和 Provider token 占用仍必须统计它。
root session 还需持久化最近一次有效 delivery binding`channel``chat_id` 和 Channel 明确标为 durable 的 opaque context。`ChannelContext` 必须把 `durable_private` 与当前仅用于一次回复的 `reply_to`/`private` 分开;核心只持久化 `durable_private`,不通过猜测 key 名过滤现有 `private`。binding 在成功接纳外部用户输入时更新;平台 thread/root 等稳定字段保持 Channel 私有核心只存取和回传不解释。binding 不得包含 message/reaction ID、token、临时上传 ID 或其他短期 credential。
## 15. Background 事件投递
### 15.1 统一输入类型
当前 user-only SteeringMailbox 演进为保留来源的 TurnMailbox
```rust
pub struct TurnInput {
pub id: String,
pub sequence: u64,
pub source: TurnInputSource,
pub delivery: InputDelivery,
pub content: String,
pub media_refs: Vec<MediaRef>,
pub durable_event_id: Option<String>,
pub received_at: i64,
}
pub enum TurnInputSource {
User,
AgentSignal { run_id: String, agent_id: String },
AgentCompletion { run_id: String, agent_id: String },
}
pub enum InputDelivery {
Queue,
Steer,
}
```
`SourceKind` 增加 `agent_signal``agent_result`。Provider 不支持 runtime role 时可以序列化为有明确 envelope 的 user-compatible message但持久化来源、客户端渲染和取消恢复必须保持类型不得显示成用户气泡。
### 15.2 路由规则
| 主 Agent 状态 | queue | steer |
|----------------|-------|-------|
| 无活动 Turn | 保持 pending、唤醒 worker claim启动内部 Turn | 退化为 queue同左 |
| 活动 Turn 接受输入 | 入下一 Turn | 入当前 TurnMailbox |
| TurnMailbox 满/已关闭 | 保持 durable pending唤醒 worker | 可靠退化为 queue |
| Provider 请求进行中 | 等下一 Turn | 等请求结束后的安全边界 |
| 普通工具批次进行中 | 等下一 Turn | 等完整工具批次结束 |
| sleep 进行中 | 唤醒 sleep内容仍留在 queue | 唤醒 sleep并在工具批次后注入当前 Turn |
Steer 不承诺硬实时抢占。最迟可见时间由当前不可分割 Provider 请求或工具步骤决定。需要当前逻辑必然依赖子结果时应使用 foreground不要用 background+steer 模拟同步调用。
### 15.3 原子 admission 与 fallback
AgentResultRouter 对 steer 事件使用不跨 Session 锁做 SQLite I/O 的两阶段 admission
1. 在锁外以条件更新把 event 从 `pending` claim 为 `leased`
2. 在 Session 状态锁内为当前 accepting Turn 分配 sequence 和不可排空的 mailbox reservationclosed/full/不存在则不创建 reservation。
3. 在锁外以 lease token 把 event 条件更新为 `admitted` 并写 `admitted_turn_id`
4. 重新取得 Session 锁;只有同一 Turn/generation 仍 accepting 时才把 reservation 激活为 AgentLoop 可见输入。
5. 任一步失败都删除 reservation并以 lease token 把 event 恢复 `pending`;若恰逢 `/stop`,由 `/stop` 的 admitted-turn 条件释放和 lease expiry 兜底。
AgentLoop 只能排空已经激活的 reservation因此不会在 durable admission 成功前看到事件。若事件不适合当前 TurnRouter 释放 lease、保持 `pending`,只递增该 session 的 inbox wake revision。
任何竞态下事件只能属于当前 Turn 或后续 Turn之一不能同时进入两者也不能两者都不进入。durable event payload 不进入保存用户 `AgentTask` 的 mpsc因此普通 session queue 饱和不影响它。worker 在准备运行 continuation 时才从 SQLite claim lease内存 wake 丢失由 pending/expired lease 扫描恢复。
Session worker 的接收面分为:
```text
user task lane bounded mpsc(32),保存 payload满时明确拒绝
agent inbox wake lane watch revision只合并“SQLite 有待处理事件”的提示
```
watch revision 是延迟优化而不是事实来源;不为每个 event 建立另一个可饱和 payload 队列。Router/worker 遇到瞬时错误按 `next_attempt_at` 退避重试,默认最多 8 次并受 event TTL 限制;永久错误立即进入 dead-letter。
### 15.4 Mailbox 容量与公平性
用户输入和 Agent 事件共享接收顺序,但使用独立容量配额,避免相互挤占:
```text
user steer lane: 32 messages / 64 KiB
agent event lane: 8 messages / 32 KiB
```
排空时按 session sequence 合并。重要 AgentSignal 可以保留专用容量,但不默认越过更早已接受的用户输入。信号洪泛由 emit_signal rate limit 和 inbox 上限共同控制。
queue continuation 在 Turn 调度边界采用有界公平,而不是依赖 UI 保证可见:通常先处理用户任务;连续处理 `max_user_turn_burst_before_inbox`(默认 4个用户 Turn或最老 pending event 等待达到 `max_inbox_wait_secs`(默认 30 秒)后,下一个调度项必须是一个有界 event batch。当前活动 Turn 从不被 queue event 抢占因此等待上限从下一个调度边界计算。UI 未读状态只是投影,不参与正确性。
### 15.5 安全边界注入
AgentLoop 只在以下边界排空 steer
- 一个完整工具批次结束后。
- Provider 返回无工具候选最终回复、但 mailbox 有新输入时。
- 明确可安全取消的等待工具被唤醒后。
输入被排空后保留为 in-flight只有整个 Turn 消息和 event consumption 原子提交成功才确认。Provider/工具/持久化失败时恢复原事件。
## 16. 主 Agent 忙碌与空闲
### 16.1 主 Agent 忙碌
- queue event 只进入后续内部任务,不改变当前上下文。
- steer event 进入 TurnMailbox在安全边界参与当前 Turn。
- 当前 Turn 已 finalizing/closed 时steer 自动退化为 queue。
- UI 可以立即展示“信号已接纳/后台任务已完成”,但用户可见最终结论仍由主 Agent产生。
### 16.2 主 Agent 空闲
Session worker 不做固定频率忙轮询。正常路径由 AgentResultRouter 发出 best-effort session wakeupworker 被唤醒后领取 inbox event 并创建内部 Turn。Gateway 启动、reload 激活和周期恢复任务扫描 pending/expired lease弥补丢失唤醒。
Session worker 调度优先级:
```text
当前活动 Turn
> 已排队用户输入(受 burst/age 公平上限约束)
> queue background result continuation
> 等待新事件
```
Steer event 在没有活动 Turn 时按 queue 处理。用户输入通常优先以避免后台总结打断新请求,但 §15.4 的 burst/age 规则保证结果不会在持续用户流量下无限饥饿。UI 未读状态只展示 pending/dead-letter 数量,不承担调度正确性。
### 16.3 内部 continuation Turn
内部任务不是伪造的 InboundMessage
```rust
enum AgentTaskSource {
UserInput,
BackgroundAgentResults { event_ids: Vec<String> },
ScheduledTask,
}
```
SessionManager 直接把领取的结果构造成 bounded runtime context并加入一个内部触发语义“检查这些后台结果结合原始目标验证和汇总再向用户报告。”内部输入不显示用户气泡主 Agent输出按普通 assistant Turn 持久化和投递。
continuation 必须创建一条 durable hidden trigger message而不是只在内存临时拼 prompt
- 数据库 role 使用 Provider-compatible `user`source 为 `agent_signal`/`agent_result`,并标记 `client_visibility=hidden``turn_origin=agent_continuation`
- hidden content 是有界 runtime envelope包含 event/run references 和不可信数据边界Provider history replay 会保留它,普通 history/WebSocket 投影会过滤它。
- assistant/tool 消息照常可见。`turn_updated`/`turn_committed` 携带 turn origin客户端可以显示“后台结果处理”标签但不创建伪用户气泡。
内部 Turn 的 delivery target 来自 root session 的 durable binding`channel``chat_id` 和可复用的 thread/root context。一次性 `reply_to` 不得复用。没有可用外部 binding 时仍提交历史并等待 WebUI/TUI 读取,不能猜测或改投其他 chat。
### 16.4 消费确认
领取流程:
```text
pending → leased → admitted → consumed
```
`lease_token` 防止重复 worker 处理。同一事务必须保存 hidden trigger、主 Agent Turn、usage并把对应 inbox events 标记 consumed。continuation 持有 `InboxLeaseGuard`AgentLoop 失败、Turn 取消、generation stale 等正常退出会以 token 显式 release只有进程崩溃或强制 abort 才依赖 lease 到期恢复 pending。
外部 LLM 调用无法严格 exactly-once。为降低恢复重跑的副作用background result continuation 默认只开放只读/汇总工具;需要外部写操作时由主 Agent向用户确认或工具自身使用幂等键。
## 17. 可唤醒 Sleep 设计
### 17.1 目标语义
`sleep` 仍是最长 24 小时、不可持久恢复的前台等待工具,但当前 session 接收到任何新输入时立即结束等待:
- user steer
- user queue
- AgentSignal queue/steer
- AgentCompletion queue/steer
- `/stop`、shutdown 和父 cancellation
Sleep 只负责唤醒不负责消费输入。queue 内容仍属于下一 Turnsteer 内容仍由 TurnMailbox 在工具批次后注入。
上述“当前 session 输入”只适用于 root interactive Turn。sub-run 没有独立 session input lane`ToolExecutionContext.turn_wakeup=None`,其 sleep 只响应 timer、run cancellation、timeout 或 shutdown不会因 root session 用户输入或 sibling signal 被唤醒。Root 如需停止 sleeping sub-run应调用 `agent_task.cancel`
### 17.2 Wakeup handle
`ToolExecutionContext` 增加:
```rust
pub struct TurnWakeupHandle {
pub receiver: watch::Receiver<TurnWakeupState>,
}
pub struct TurnWakeupState {
pub revision: u64,
pub pending_steer: usize,
pub pending_queue: usize,
pub latest_source: WakeupSource,
pub latest_preview: Option<String>,
}
```
使用 watch revision 而不是裸 Notify避免输入恰好在 sleep 开始监听前到达而丢失唤醒。执行前先比较当前 revision/pending再进入 select。
### 17.3 Sleep 执行
```rust
tokio::select! {
_ = tokio::time::sleep(duration) => SleepOutcome::Elapsed,
changed = wakeup.changed() => SleepOutcome::InputArrived(changed),
_ = cancellation.cancelled() => SleepOutcome::Cancelled,
}
```
返回示例:
```text
Sleep 提前结束:已等待 37 秒。
收到一条 steer AgentSignalrun_id=run-123服务错误率超过 5%。
该信号将在当前 Turn 的下一个安全边界注入。
```
queue 输入不能把正文泄漏给当前 Turn否则等价于偷偷 steer。其返回只能说明类型和数量
```text
Sleep 提前结束:收到一条排队输入。
内容不会进入当前 Turn将在当前工作结束后的下一 Turn处理。
```
### 17.4 工具中断策略
新增工具元数据:
```rust
enum InputInterruptPolicy {
Never,
WakeOnly,
CancelSafe,
}
```
- `sleep``WakeOnly`,输入使工具正常提前返回。
- 明确只读且可重试的等待工具可标记 `CancelSafe`
- bash、写文件、发送消息和未知外部副作用工具默认 `Never`
Steer 不自动取消 `Never` 工具。未来若需要硬抢占,应新增独立 `interrupt` 策略并定义 partial tool 状态、幂等和恢复;本设计不把它隐含进 steer。
## 18. 取消、停止与恢复
### 18.1 Foreground 结构化取消
Foreground 子 run 是父 run 的结构化子任务:
- 父 Turn 取消会取消所有未终态 foreground 后代。
- timeout token 与父 cancellation token 组合。
- run 进入 waiting_children 时仍保留所有权,但不能长期占用模型执行 permit。
- 父取消后迟到结果不能提交为 completed。
这是 AgentLoop 的显式接口约束root Turn、foreground child、run timeout 和 runtime shutdown 的 `CancellationToken` 必须贯穿 Provider stream、可取消等待和工具批次外层不能只依赖调用 future 被 drop。结构化取消先作为 Phase 2A 独立落点实现并回归现有 root Turn 行为,再接入嵌套 run。
“不默认硬中断 Provider/副作用工具”只约束普通 `steer``steer` 等待安全边界。`/stop` 保持现有强停止语义:取消 token 并使 root Turn future 失效,独立 child 在有界宽限期后仍未退出则由 Coordinator/Supervisor abort。Coordinator 的 terminal condition update 始终阻止取消后的迟到完成提交。
### 18.2 Background 所有权
Background run 归 root session 所有,不归发起它的模型 future 所有。Root Turn结束不会自动取消它。
保留当前 `/stop` 的明确停止语义:取消目标 session 的 active Turn、排队用户输入以及所有非 Scheduler background Agent run。需要跨 `/stop` 和重启长期存在的监控应创建 Scheduler monitor而不是普通 background delegate。
### 18.3 `/stop` 与 durable Agent events
用户 steering 按现有语义可被 `/stop` 丢弃;已经持久化的 AgentSignal/Completion 不能静默消失:
- `/stop` 关闭 TurnMailbox 时收集尚未提交的 event ID并在 generation 失效后按 `lease_token`/`admitted_turn_id` 条件更新立即恢复 pendinglease expiry 只是崩溃兜底。
- continuation 的 `InboxLeaseGuard` 在 worker 正常退出、失败或取消时显式 releasedurable payload 不进入会被 `agent_tx.take()` 丢弃的普通 mpsc。
- 被取消 background run 仍由 Coordinator 条件事务写 cancelled completion。由本次 `/stop` 自身造成的 completion 使用 `requires_continuation=false`,保留审计和 UI 状态但不反向启动新 Turn`/stop` 前已经存在的其他 durable event 恢复 pending 后继续投递。
- 用户显式执行 `agent_task.cancel` 后,可以将该 run 未消费的普通 signal 标记 superseded但保留审计记录。
### 18.4 Gateway reload
AgentCatalog、ProviderFactory、Coordinator 和 inbox router 属于 Gateway runtime generation
- reload 关闭 admission 后不接受新 background run/signal。
- 已进入旧代的 run 固定使用旧 definition hash、Provider 和工具策略。
- 排空期等待前台 Turn、Scheduler 和 background run 到持久化边界。
- 超过总排空期限的 run 被取消/中断并写终态事件。
- pending inbox event 留在 SQLite由新运行代恢复投递。
### 18.5 进程重启
进程退出后无法恢复正在进行的 LLM stream。启动恢复将旧 `running/waiting_children` 标记 `interrupted` 并生成 completion。普通有副作用 Agent run 不自动重试;只读、显式配置 idempotency/restart policy 的监控任务可以创建新 attempt并保留原 run 的中断记录。
### 18.6 Session 归档与删除
- session 被归档后不再启动内部 continuation未消费事件进入 `dead_letter(session_archived)` 并继续在管理面可见,非 Scheduler 所有的未终态 run 被取消。该生命周期原因不发送 system fallback。
- session 被软删除后同样取消未终态 run释放 reservation并将未消费事件收敛为 `dead_letter(session_deleted)`;不得猜测其他 session 或 Channel 作为替代目标,也不发送 system fallback。
- `/stop` 不是归档或删除:它取消当前 Turn 和该 session 的 active background run`/stop` 前已经存在的 durable event 仍恢复为 pending由本次停止产生的取消 completion 仅做 status-only 审计。
## 19. 并发、预算与死锁避免
### 19.1 限制层次
```text
Gateway 全局 active provider/tool permits
└── per-session permits
└── per-agent permits
└── per-tree max runs/depth/children/token/cost
```
批量请求必须有 `maxItems`Coordinator 还会按剩余 tree budget 裁剪/拒绝,不能让模型生成任意数量任务。
### 19.2 父子等待死锁
不能让一个等待 foreground child 的父 run 一直持有唯一执行 permit否则并发上限为 1 时形成:
```text
父 run 持有 permit → 等子 run → 子 run 永远拿不到 permit
```
permit 应限制活跃 Provider/工具步骤,而不是整个 Agent Run 生命周期。父 run 进入 `waiting_children` 前释放执行 permit子 run 完成后父 run 再竞争 permit 继续模型迭代。Task tree ownership、timeout 和 cancellation 不随 permit 释放而消失。
具体归属如下:
- Coordinator 的 run admission quota 统计已接纳且未终态的 run可以跨 `waiting_children` 持有。
- AgentLoop 在每次 Provider 请求前按 global → session → agent 的固定顺序获取 provider step permitsstream 结束/取消即释放。
- tool executor 只为普通工具调用获取 tool step permit`delegate``agent_task``emit_signal` 等 runtime-control 工具不占这种 permit。
- foreground delegate 通过状态 guard 在等待前条件更新 `running → waiting_children`,返回/取消时再条件更新;等待动作本身不持有 provider/tool permit。
### 19.3 预算传播
每次子委托从父 budget 派生硬上限:
```text
child deadline <= parent deadline
child max depth <= remaining depth
sum child token reservation <= remaining tree budget
sum child cost reservation <= remaining tree budget仅有价格配置时
```
调用方可以收紧 timeout/结果大小,但不能超过 Agent Definition 和系统上限。Provider profile 没有价格信息时不启用 cost reservation只执行 token、迭代、deadline 和 run-count 硬预算。
## 20. 可观测性与客户端表现
### 20.1 运行树
WebUI 管理面展示:
- group/run/parent ID。
- Agent ID、Provider profile、Model。
- queued/running/waiting_children/terminal 状态。
- signal 数量和最近 severity。
- duration、usage、cost、工具次数。
- 取消/超时/中断原因。
### 20.2 Chat 表现
- Foreground delegate 继续作为当前 Turn 的可折叠工具块。
- Background delegate 启动后显示 run ID不假装任务已完成。
- AgentSignal 显示为独立运行时信号卡片,不显示成用户气泡。
- queue completion 在主 Agent内部 continuation 后只显示主 Agent汇总回复。
- steer 信号可以在当前 Turn 工具状态中显示“已接纳”,最终历史由 Turn commit 校准。
WebSocket 协议使用通用 run/event 投影,不为 Signal 复制一套状态机:
```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 }
```
`AgentEventUpdated` 覆盖 accepted/admitted/consumed/dead-letter 状态,客户端按 `(session_id, revision, event_id)` 幂等合并;重连后用 `GetAgentRuns` 全量校准。`TurnSnapshot``TurnCommitted` 增加 `turn_origin = user | agent_continuation | scheduled`。第一版取消入口继续使用 `/stop``agent_task.cancel` 或受保护管理 API不增加缺少任务树授权上下文的裸 WebSocket cancel 帧。
### 20.3 隐私与日志
- 不显示/记录 Agent reasoning 和 Provider 私有 state。
- 默认日志只记录 run ID、Agent ID、状态、duration、usage 和截断错误。
- task/result 正文不进入 info 日志。
- API key、headers、临时凭据、含 credential URL 永不落库或日志。
## 21. 失败语义
| 失败点 | 对外语义 |
|--------|----------|
| Agent Definition 无效 | 拒绝候选运行代;旧代继续服务 |
| 委托边不允许 | delegate 立即返回 permission denied不创建 run |
| Background 持久化失败 | delegate 返回失败,不报告 run ID |
| Completion capacity 无法预留 | delegate 在创建 run 前返回 inbox capacity exceeded |
| TaskSupervisor 拒绝 spawn | run 条件更新 cancelled/failed再返回失败 |
| Provider 创建失败 | run failedforeground 返回错误background 生成 failure event |
| Signal inbox 无可用容量 | emit_signal 返回 inbox_fullrun 继续执行 |
| Inbox wakeup 丢失 | pending event 由恢复扫描重新唤醒 |
| TurnMailbox closed/full | steer 可靠退化 queue |
| Session user queue 满 | 用户输入明确拒绝durable event 不经过该队列 |
| Main continuation Provider 失败 | lease guard 显式 release 并退避重试;崩溃时才等 lease 到期 |
| 主 Agent回复持久化失败 | event 不确认,避免结果消失 |
| Event 超过 attempts/TTL | 标 dead_letter并幂等尝试一次 system fallback |
| Channel 最终投递失败 | assistant history已持久化沿用 DeliveryCoordinator terminal fallback |
所有重试必须有次数、退避、deadline 和分类;永久错误立即终态化,不能无界重试。默认最多 8 次,退避为 `1s/5s/30s/2m/10m` 后封顶 10 分钟,并同时受 inbox event TTL 限制。
dead-letter 记录最终原因和时间,并通过 OutboundDispatcher 最多发送一次有界 system fallback只包含 run ID、终态和查询提示`fallback_notified_at` 保证幂等。fallback 渠道失败时SQLite run/event 记录和管理 UI 是最终诊断出口,不能把 dead-letter 伪装成已交付。
## 22. 兼容迁移
### 22.1 Delegate 参数
旧模式映射已在迁移完成后移除:`inline`/`parallel` 别名与 legacy general 委托均不再解析,只保留 canonical `foreground`/`background` 生命周期词与具名 `target`
### 22.2 allowed_tools
`allowed_tools` 只能收紧 Definition.tools不能扩权。没有 `target` 的委托不再支持(旧匿名 general 已移除);内置 `general-purpose` Agent 定义随二进制释放到 `~/.picobot/agents/`,开箱即用。
### 22.3 background_tasks
`background_tasks` 表已在 schema v7 中删除(`DROP TABLE`),旧 adapter 与 direct notification 路径一并移除;`/api/tasks` 只读取 `agent_runs`。无历史兼容需求。
### 22.4 版本与文档
本设计文档本身不改变产品行为。实现功能合并时按项目规则增加中段版本,并同步更新 README、`docs/ARCHITECTURE.md`、AGENTS.md 和 `resources/skills/about-picobot/references/`
## 23. 实现分期
### Phase 1具名 Agent 与 Foreground
- 新增 AgentDefinition/AgentCatalog loader。
- Definition 解析不同 Provider profile 和固定工具集。
- Delegate schema 使用 target + foreground/background canonical modes。
- 批量 foreground 并发执行并聚合。
- 显式 AgentExecutionContext 和委托图授权。
- 明确 skills/memory 不继承、具名 Agent browser scope 隔离。
- 不开放嵌套 background子 Agent 发起的 background
### Phase 2AAgentLoop 结构化取消
- CancellationToken 贯穿 root Turn、Provider stream、工具批次和 AgentRunner。
- 保持 `/stop` 强停止、普通 `steer` 安全边界语义。
- 用现有 sleep cancellation、Provider stream 和 steering recovery tests 锁定回归基线。
### Phase 2B统一 Agent Run 持久化
- 新增 `agent_runs`、Storage transaction API。
- 拆分 `delegate``agent_task`
- Foreground 结果也持久化,修复截断结果不可查询。
- 实现预算、run admission quota 与 step execution permit 释放。
### Phase 3Agent Inbox 与 Queue Completion
- 新增 `agent_inbox_events`、lease、恢复扫描。
- 新增 completion capacity reservation、合并式 wake lane和 worker 有界公平。
- Background completion 从 Channel direct notification 改为主 Agent内部 continuation。
- 增加 hidden continuation trigger、SourceKind::AgentResult、Turn origin 和 WebSocket run/event 投影。
- 批次 completion 合并与 debounce。
- 增加 dead-letter system fallback、UI 未读计数和 reconnect 全量校准。
### Phase 4Emit Signal 与 Steer
- 新增 EmitSignalTool、SignalContract 和 rate/dedupe。
- SteeringMailbox 泛化为来源感知 TurnMailbox。
- 实现 steer admission、queue fallback、durable ack/recovery。
- 添加 AgentSignal UI任务树复用 Phase 3 的 run/event 协议。
### Phase 5可唤醒 Sleep 与工具中断元数据
- ToolExecutionContext 增加 TurnWakeupHandle。
- SleepTool 使用 watch revision + timer + cancellation select。
- queue/steer 唤醒内容边界和测试。
- 为工具增加 InputInterruptPolicy默认 Never。
- sub-run 保持 timer/cancellation-only不获得 root TurnWakeupHandle。
## 24. 预计代码边界
建议模块拆分:
```text
src/agent/
├── definition.rs AgentDefinition / loader
├── catalog.rs immutable AgentCatalog
├── coordinator.rs authorization / lifecycle / budgets
├── run.rs AgentRun types / AgentRunner
├── inbox.rs event types / router contracts
└── sub_agent.rs 迁移兼容层,最终缩减或删除
src/tools/
├── delegate.rs create run only
├── agent_task.rs get/list/cancel/get_result
├── emit_signal.rs constrained internal signal
├── sleep.rs wake-aware wait
└── send_message.rs external delivery only
src/session/
├── turn_mailbox.rs typed steer inputs
├── agent_inbox.rs claim/admit/ack、lease guard、coalesced wake
└── session.rs typed AgentTask scheduling
src/storage/
├── agent_run.rs
└── agent_inbox.rs
```
`AgentLoop` 只需要理解来源感知输入的安全边界追加,不拥有 AgentCatalog、任务树或 inbox persistence。
## 25. 测试矩阵
### 25.1 Definition 与授权
- 解析合法 Markdown、frontmatter 与 Unicode 正文。
- 重复/保留 ID、未知 Provider、未知工具、未知 delegate target 拒绝加载。
- path traversal/symlink 越界拒绝。
- Root 永远不能成为 target。
- 未声明 A→B 时拒绝;声明后允许。
- A→B→A 在单链中拒绝。
- 模型参数不能扩大工具、timeout、depth 或预算。
### 25.2 Foreground/Background
- 单 foreground 结果立即成为父 tool result。
- 三个 foreground task 并发执行、父等待全部、结果按请求顺序。
- 单项失败不丢其他项结果。
- Background 只有持久化并成功 spawn 后才返回 run ID。
- 同 idempotency key 不重复创建 run。
- Root caller scope 的 idempotency key 在 SQLite 中同样去重。
- Background completion 不直接伪装为用户消息。
### 25.3 Provider 与工具
- 不同 Agent 使用不同 provider/model profile。
- Provider storage/observer 正确注入。
- 工具集完全由定义文件的 `tools` 列表决定runtime-injected 工具不能通过 Markdown 声明。
- runtime-injected delegate/emit_signal 只在上下文允许时存在。
- 并行 run 的 browser/resource scope 隔离。
- persistent browser profile 可显式共享;具名 Agent不继承 transient parent scope。
- 子 Agent不隐式继承主会话 history/memory/临时 Skill。
### 25.4 Inbox 与投递竞态
- completion update 与 inbox insert 原子。
- wakeup 丢失后启动扫描恢复。
- active accepting Turn 的 steer 进入 current Turn。
- finalizing/closed/full 时 steer 恰好一次退化 queue。
- 无活动 Turn 的 steer 启动内部 continuation。
- 用户队列优先于 queue completion。
- Turn persist 失败时 event 不 consumed。
- lease 超时后可重领,旧 lease token 不能提交。
- 正常取消/worker 退出由 lease guard 立即 release不等待 lease timeout。
- user mpsc 满不影响 durable wakewake revision 丢失后扫描可恢复。
- 用户持续输入时 burst/age 公平上限仍调度 continuation。
- completion capacity 在 background 接纳时预留signal 不能抢占。
- 每个 run 独立 completion逐 run 投递;无 group 汇总 Turn。
- retries/TTL 耗尽进入 dead-lettersystem fallback 最多发送一次。
### 25.5 Signal
- emit_signal 无上下文或 foreground 禁止策略时失败。
- 不能指定任意 target/channel/delivery。
- dedupe key、速率、数量和大小上限生效。
- signal 不结束 Agent Run。
- Agent 异常退出仍自动生成 failure completion。
- 已发 signal IDs 出现在 completion避免重复汇报。
### 25.6 Sleep
- 无输入时精确等待至 timer。
- user steer、AgentSignal steer 立即唤醒并随后注入当前 Turn。
- user queue、AgentCompletion queue 唤醒但正文不泄漏当前 Turn。
- 输入先于 sleep 订阅时 revision 检查仍立即返回。
- 多条输入只消费一次且顺序稳定。
- `/stop`、parent cancellation、shutdown 取消 sleep 并终态化工具块。
- wakeup 与 timer 同时发生时不丢输入;输入若未入当前 Turn则可靠排队。
- sub-run sleep 不被 root session 输入唤醒,只响应 timer/cancellation。
### 25.7 取消、并发与恢复
- 父 foreground 取消级联后代。
- 父 waiting_children 不持有唯一 permit无死锁。
- `/stop` 取消 session background runs并恢复未提交 durable events。
- `/stop` 产生的 cancelled completion 只投影状态,不启动新的 continuation。
- 迟到结果不能覆盖 cancelled/interrupted。
- reload 关闭 admission 后拒绝新 run/signalpending inbox 由新代恢复。
- Gateway 重启把 running 标记 interrupted 并生成 completion。
- session 删除/归档后的事件按明确 dead-letter/cancel 策略收敛。
### 25.8 消息与客户端
- AgentSignal 不渲染为用户气泡。
- Internal continuation 输入不出现在普通历史assistant 汇总正常持久化。
- hidden trigger 会参与 Provider replay并与 assistant/usage/event consumed 原子提交。
- continuation 使用稳定 delivery binding绝不复用一次性 reply_to。
- reasoning/provider state 不进入信号、API、客户端和日志。
- send_message 仍走外部投递确认emit_signal 不走 OutboundDispatcher。
- 同 Turn 附件兼容路径与未来 attach_artifact 不产生重复历史。
- run/event 增量按 revision 幂等,断线重连后可全量校准。
## 26. 必须保持的架构不变量
1. 同一 Session 最多一个活动主 Agent TurnAgent 子 run 可以并行,但不能并发提交主会话历史。
2. Root Agent 不能成为委托目标,结果回传不等同于反向委托。
3. Foreground/Background 只描述委托方等待行为;并发是独立调度维度。
4. Queue 输入永不泄漏正文到当前 TurnSteer 只在安全边界注入。
5. Signal 先持久化后唤醒;内存通知不是事实来源。
6. Signal 是非终态事件run Completion 由运行时自动生成且恰好对应一个 run 终态(批量背景也逐 run 生成,无 group completion
7. SendMessage 是外部输出EmitSignal 是内部输入,不能用一个公开万能工具混合权限。
8. Durable Agent event 在 `/stop`、Turn 失败或 Gateway 崩溃时不能静默丢失。
9. Agent Definition 和 Provider 绑定 runtime generation运行中不热切换。
10. 不持有 Session mutex 等待 Provider、工具、SQLite 或子 run。
11. 父 run 等待子 run 时不持有会造成递归死锁的执行 permit。
12. 完成状态、结果、usage、计划子项和 inbox event 使用事务/条件更新提交。
13. 客户端、Channel 和日志永不暴露 Provider 私有 reasoning state、secret 或本地内部路径。
14. Durable event payload 只以 SQLite inbox 为权威来源;普通 user task mpsc 和合并式 wake lane 都不能成为确认点。
15. Continuation 的 hidden trigger 必须可供 Provider replay但不得投影为用户消息trigger、回复和 event consumption 原子提交。
16. 已接纳 background run 的 terminal completion 容量已经预留;运行结束不能因 signal 洪泛丢失 completion。
## 27. 设计结论
目标架构把现有“一个 delegate 工具创建临时 Agent”提升为明确的编排系统
```text
Markdown Agent Definition
AgentCatalog + runtime-injected 工具标记
AgentCoordinator
├─ foreground并发执行、父等待、tool result 返回
└─ backgroundrun ID 返回、signal/completion 进入 durable inbox
queue | steer
inbox wake/worker claim | current TurnMailbox
Root Agent
```
Foreground 解决依赖型子任务Background+Queue 解决稍后统一处理Background+Steer 解决长任务期间的重要监控信号;可唤醒 Sleep 为安全等待提供及时响应点。三条消息路径各自保持单一职责:`emit_signal` 内部告警、自动 completion 终态回传、`send_message` 外部投递。该划分能够在不破坏 PicoBot Session/Turn/Delivery 既有不变量的前提下分阶段实现,并为权限、持久化、取消、热重载和客户端表现提供可验证边界。