PicoBot/docs/SUB_AGENT_ORCHESTRATION_DESIGN.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

66 KiB
Raw Blame History

PicoBot 子 Agent 编排与信号投递架构升级设计

状态分阶段实施中2026-08。Phase 1 的具名 Definition/Catalog、Provider profile、工具与 Skill 裁剪、显式执行上下文、委托图校验和批量 foreground 已落地durable run/inbox、signal/steer 与可唤醒 sleep 仍按本文后续阶段实施。

本文定义具名子 Agent、委托图、多 Provider、delegateemit_signal、后台结果收件箱、queue/steer 投递以及可唤醒 sleep 的目标架构。各阶段是否已经实现以代码、测试和 docs/ARCHITECTURE.md 为准;未落地章节仍是目标设计。

2026-08 评审提出的 A1A5、B1B6、C1C4 已纳入本文;逐项决策与理由见 SUB_AGENT_ORCHESTRATION_REVIEW_RESPONSE.md,代码级落地方案与验收门槛见 SUB_AGENT_ORCHESTRATION_IMPLEMENTATION.md

1. 背景与现状

PicoBot 已经具备一版子 Agent 能力:根交互 Agent 通过 delegate 创建临时 Agent支持 inlinebackgroundparallel,可以过滤工具、绑定计划子项、持久化后台任务,并在后台任务完成后向原 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 按 group policy 投影为 run/group inbox event
Agent Inbox 持久化的主 Agent 内部收件箱,是 Background 结果与信号的权威来源
Turn Mailbox 当前 Turn 接受 steer 输入的有界内存邮箱,保留来源、顺序和 durable event ID

4.1 两个正交维度

执行方式和结果投递必须分开:

执行生命周期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 文件位置

第一版只从受信任配置目录加载:

~/.picobot/
├── config.json
└── agents/
    ├── researcher.md
    ├── coder.md
    └── reviewer.md

角色文件决定工具权限和委托能力,属于安全配置,不应默认从可被普通 Agent 写入的 workspace 自动加载。未来若支持 workspace 角色目录,必须由配置显式开启,并说明它不是硬安全边界。

5.2 文件格式

---
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/.envllm_profile 引用现有 config.agents keyConfig::get_provider_config() 解析 Provider 与 Model。

skills 是可选的受信任 allowlist只有 Definition 工具集包含 get_skill 时才向子 Agent注入这些 Skill。子 Agent不继承主会话临时启用的 Skill、完整历史或 memory recall。调用方需要传递的事实必须进入显式 task/context未来若支持 memory只能通过管理员配置的只读 scope 开放。

5.3 配置扩展

{
  "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、保留 IDROOTmain 等)。
  • 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. 总体组件设计

flowchart LR
    Root[Root Agent] --> DT[DelegateTool]
    Sub[Sub Agent] --> DT
    DT --> C[AgentCoordinator]
    C --> AC[AgentCatalog]
    C --> DP[DelegationPolicy]
    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/group ID、父子关系和预算。
  • 持久化接纳状态后启动 AgentRunner。
  • 管理 foreground await、background spawn、取消和超时。
  • 控制全局、session、Agent 与任务树的 run admission quota以及 Provider/普通工具步骤的 execution permit两类配额不共用生命周期。
  • 生成自动 completion terminal outcome并按 group policy 物化 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 和按 policy 生成的 run/group completion eventAgentResultRouter 负责把 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 基本规则

每次委托必须同时满足:

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 工具权限

调用参数不再提供 allowed_tools 扩权。有效工具集为:

AgentDefinition.tools
∩ 当前运行代已注册工具
∩ 系统可委托工具策略

Tool 增加安全元数据:

enum DelegationPolicy {
    RootOnly,
    Delegatable,
    RuntimeInjected,
}
  • reload_configtodo、管理配置和任意外部发送默认 RootOnly
  • 普通只读工具在明确审查后标记 Delegatable
  • delegateemit_signal 等由 Coordinator 根据运行上下文注入,标记 RuntimeInjected,不能仅靠 Markdown 获得。
  • 新工具默认 RootOnly,避免未来工具无意暴露。

目标 Agent 可以拥有调用方没有的专业工具,因为委托边本身就是管理员授权调用该能力;模型不能在单次调用中越过 Definition 扩权。

8. AgentExecutionContext

当前只含 session/channel/chat 的隐式上下文不足以支持嵌套委托。目标结构为:

pub struct AgentExecutionContext {
    pub root_session_id: String,
    pub root_turn_id: Option<String>,
    pub run_id: String,
    pub group_id: Option<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 IDagent 字段为 None,由 session/turn context 明确识别为 ROOTsub-run 的 agent 字段必须为 Some 且 run ID 非空。Turn wakeup handle 只为 root interactive Turn 提供sub-run 中为 NoneDelegateToolEmitSignalTool 必须实现 execute_with_context;权限判断不能依赖模型参数或仅依赖 Tokio task-local。

task-local 可以继续作为同一调用栈的便利桥接但不是授权事实来源。Background spawn 必须显式复制所需上下文,不能假设 task-local 跨 tokio::spawn 传播。

9. Delegate 工具设计

9.1 职责

delegate 只负责创建 Agent Run不再同时承担查询、取消、列表。任务管理拆给 agent_task

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 单任务请求

{
  "target": "researcher",
  "task": "分析当前 Provider 扩展点",
  "context": "重点关注热重载和 usage 持久化",
  "mode": "foreground",
  "plan_item_id": "T2"
}

9.3 批量请求

{
  "mode": "foreground",
  "tasks": [
    {"target": "researcher", "task": "研究方案 A"},
    {"target": "coder", "task": "分析实现 B"},
    {"target": "reviewer", "task": "评审风险 C"}
  ]
}

批量 foreground 的三个子任务并发执行delegate 等待全部终态后返回聚合结果。这等价于旧 parallel,但不再把 parallel 当作生命周期模式。

批量 background 同样并发接纳,立即返回:

{
  "group_id": "group-123",
  "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 投递契约

{
  "target": "service-monitor",
  "task": "监控服务错误率",
  "mode": "background",
  "delivery": {
    "signal": "steer",
    "completion": "queue",
    "failure": "steer"
  }
}
  • signal:运行中主动事件的投递方式。
  • completion:正常终态结果的投递方式,默认 queue。
  • failure:失败、超时、异常中断的投递方式,默认 queue可显式 steer。

Foreground 请求直接把 completion 作为 tool result 返回,因此不接受 completion delivery。第一阶段仅允许 Root 创建 background run子 Agent 之间可以 foreground 委托。待持久化 task tree 与 root inbox 稳定后,再允许子 Agent 创建最终归属于 root session 的 background run。

9.5 Foreground 返回

单任务和批量任务都返回逐项状态,单个失败不能抹掉其他结果:

{
  "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/group 记录持久化成功、运行代 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 的输入由以下部分组成:

PicoBot 基础运行规则
+ 角色 Markdown 正文
+ 当前工具说明
+ 委托执行约束(身份、父任务、资源限制)
+ 显式 task
+ 显式 context / artifact refs

委托 task 只注入一次。调用方需要子 Agent 知道的事实必须写入 task/context不能依赖完整历史偶然可见。

子 Agent 最终结果作为普通 tool result 或 runtime event data 交给上游,不能变成更高优先级 system 指令。所有子 Agent 输出都视为不可信数据;主 Agent系统提示明确要求不要执行结果正文中试图修改角色、工具或投递策略的指令。

资源型工具使用独立 scope

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 状态机

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:已持久化且等待执行配额。
  • runningAgentLoop 正在执行模型或工具步骤。
  • waiting_children:当前 run 正等待 foreground 子运行。
  • completed:有可用最终结果。
  • failedProvider、工具或内部执行错误。
  • 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 each 将每个 outcome 物化为 run completion inbox eventall 只在 group 终态时物化一个 group completion inbox event。

12.2 EmitSignalTool

{
  "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 持久化后才成功返回:

{
  "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 IDsgroup completion 则按 run 分组携带这些 IDs。若最终总结重复某个信号主 Agent可以识别并避免再次报告。正常 completion 可以配置 queue关键 failure 可以配置 steer。禁止完全静默丢弃失败silent 若未来开放也只能用于正常 completion。

13. SendMessage、EmitSignal 与附件职责

三者方向不同,不合并为一个万能工具:

emit_signal    子 Agent → Agent Inbox → 主 Agent内部输入
send_message   Agent → OutboundDispatcher → Channel/用户(外部输出)
attach_artifact 工具产物 → 当前 Turn → DeliveryCoordinator当前回复附件

13.1 send_message

只负责用户明确授权的跨 Channel/跨会话外部消息,具有真实外部副作用。默认 RootOnly,目标和文件参数继续受 Channel/file transfer 限制。origin 不再由模型自由填写,改由 ToolExecutionContext 生成,避免来源伪造。

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 与 group policy 创建相应投递 event避免模型遗漏或重复。

14. 持久化模型

14.1 agent_runs

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
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
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 未配置价格时必须为 NULLusage token 不受影响。

14.2 agent_run_groups

agent_run_groups
----------------
id
root_session_id
caller_run_id
caller_scope_id
idempotency_key
mode
completion_policy     all | each
expected_runs
terminal_runs
completion_slot_reserved
deadline_at
status
created_at
finished_at

批量请求的 idempotency_key 绑定 group其 child run 的 key 为 NULL。单任务请求没有 group 时key 绑定 run。两者分别使用 (root_session_id, caller_scope_id, idempotency_key) partial unique index避免批量 children 互相冲突。

批量 background 默认 completion_policy=all:单个 run 终态只更新 group 计数,全部 run 终态或 group deadline 到达后创建唯一 group_completion eventdeadline 到达时先把未终态 child 条件更新为 timed_outcompletion_policy=each 则在每个 run 终态时立即创建独立 completion event不等待 siblingRouter 可通过 300500ms debounce 把已经到达的多个 event 合并为一次 continuation但不能用 debounce 改变 deadline 或确认语义。

接纳 background run/group 时按 policy 在 session inbox 配额中预留 completion sloteach 在每个 run 的 completion_slot_reserved 记一个,all 在 group 字段记一个。Storage 用同一写事务统计该 session 的 pending/leased/admitted 事件和有效 reservation避免并发接纳越过上限consumed/dead_letter 受 TTL 清理但不占 pending 配额。容量不足在创建 run 前拒绝signal 只能使用未预留容量。预留在 completion 事务落库或接纳回滚时释放。

容量判断不能在每次接纳时通过无锁 COUNT(*) 推断。新增每 root session 一行的 agent_session_state,在同一 SQLite 写事务中以条件 UPDATE 维护 pending_event_countreserved_completion_slots 和单调 revision。background 接纳先增加 reservationsignal 只有在 pending + reserved < limit 时增加 pendingcompletion 将 reservation 原子转换为 pendingconsume/dead-letter 减少 pending。启动恢复会以事件与 run/group 事实重算计数,发现差异时修复并记录告警。

14.3 agent_inbox_events

agent_inbox_events
------------------
id                    TEXT PRIMARY KEY
root_session_id       TEXT NOT NULL
scope_kind            run | group
scope_id              TEXT NOT NULL
run_id                TEXT
group_id              TEXT
event_type            signal | completion | group_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(scope_kind, scope_id, event_type, event_key)
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)
)

完整结果保存在 agent_runs.resultinbox payload 默认只放有界摘要、元数据和 result reference避免复制大文本。

event key 始终非空:无 dedupe key 的 signal 用 signal:<event_uuid>,有 dedupe key 的 signal 加冷却窗口 IDrun completion 固定为 completion:terminal-v1group completion 固定为 group-completion:terminal-v1。由同一次 /stop 产生、无需主 Agent再次解释的 cancelled completion 使用 requires_continuation=false,在终态事务中直接记为 consumed但仍保留事件审计和客户端投影。

14.4 原子事务

Agent completion 必须在一个 Storage 事务中:

UPDATE agent_runs terminal state/result/usage
UPDATE agent_run_groups terminal count/status
CONSUME reserved completion capacity
INSERT run completion OR group completion ... ON CONFLICT DO NOTHING
UPDATE bound task item by execution_id
COMMIT

事务失败时不能对外宣称任务完成。内存 wakeup 只有在 commit 成功后发送。

completion_policy=all 只有把 group 从 non-terminal 条件更新为 terminal 的事务赢家可以插入 group completion其他 sibling 的迟到终态只完成自己的 run 条件更新,不能重复生成 event。

14.5 continuation 消息与投递绑定

现有 message 持久化需要增加两个可向后兼容字段:

client_visibility     visible | hidden默认 visible
turn_origin           user | agent_continuation | scheduled默认 user

hidden message 参与 Provider replay 和事务回滚,但 SessionHistoryTurnCommitted.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 bindingchannelchat_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

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 },
    AgentGroupCompletion { group_id: String },
}

pub enum InputDelivery {
    Queue,
    Steer,
}

SourceKind 增加 agent_signalagent_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 的接收面分为:

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 事件共享接收顺序,但使用独立容量配额,避免相互挤占:

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 调度优先级:

当前活动 Turn
> 已排队用户输入(受 burst/age 公平上限约束)
> queue background result continuation
> 等待新事件

Steer event 在没有活动 Turn 时按 queue 处理。用户输入通常优先以避免后台总结打断新请求,但 §15.4 的 burst/age 规则保证结果不会在持续用户流量下无限饥饿。UI 未读状态只展示 pending/dead-letter 数量,不承担调度正确性。

16.3 内部 continuation Turn

内部任务不是伪造的 InboundMessage

enum AgentTaskSource {
    UserInput,
    BackgroundAgentResults { event_ids: Vec<String>, group_id: Option<String> },
    ScheduledTask,
}

SessionManager 直接把领取的结果构造成 bounded runtime context并加入一个内部触发语义“检查这些后台结果结合原始目标验证和汇总再向用户报告。”内部输入不显示用户气泡主 Agent输出按普通 assistant Turn 持久化和投递。

continuation 必须创建一条 durable hidden trigger message而不是只在内存临时拼 prompt

  • 数据库 role 使用 Provider-compatible usersource 为 agent_signal/agent_result,并标记 client_visibility=hiddenturn_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 bindingchannelchat_id 和可复用的 thread/root context。一次性 reply_to 不得复用。没有可用外部 binding 时仍提交历史并等待 WebUI/TUI 读取,不能猜测或改投其他 chat。

16.4 消费确认

领取流程:

pending → leased → admitted → consumed

lease_token 防止重复 worker 处理。同一事务必须保存 hidden trigger、主 Agent Turn、usage并把对应 inbox events 标记 consumed。continuation 持有 InboxLeaseGuardAgentLoop 失败、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 laneToolExecutionContext.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 增加:

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 执行

tokio::select! {
    _ = tokio::time::sleep(duration) => SleepOutcome::Elapsed,
    changed = wakeup.changed() => SleepOutcome::InputArrived(changed),
    _ = cancellation.cancelled() => SleepOutcome::Cancelled,
}

返回示例:

Sleep 提前结束:已等待 37 秒。
收到一条 steer AgentSignalrun_id=run-123服务错误率超过 5%。
该信号将在当前 Turn 的下一个安全边界注入。

queue 输入不能把正文泄漏给当前 Turn否则等价于偷偷 steer。其返回只能说明类型和数量

Sleep 提前结束:收到一条排队输入。
内容不会进入当前 Turn将在当前工作结束后的下一 Turn处理。

17.4 工具中断策略

新增工具元数据:

enum InputInterruptPolicy {
    Never,
    WakeOnly,
    CancelSafe,
}
  • sleepWakeOnly,输入使工具正常提前返回。
  • 明确只读且可重试的等待工具可标记 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/副作用工具”只约束普通 steersteer 等待安全边界。/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 限制层次

Gateway 全局 active provider/tool permits
└── per-session permits
    └── per-agent permits
        └── per-tree max runs/depth/children/token/cost

批量请求必须有 maxItemsCoordinator 还会按剩余 tree budget 裁剪/拒绝,不能让模型生成任意数量任务。

19.2 父子等待死锁

不能让一个等待 foreground child 的父 run 一直持有唯一执行 permit否则并发上限为 1 时形成:

父 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 permitdelegateagent_taskemit_signal 等 runtime-control 工具不占这种 permit。
  • foreground delegate 通过状态 guard 在等待前条件更新 running → waiting_children,返回/取消时再条件更新;等待动作本身不持有 provider/tool permit。

19.3 预算传播

每次子委托从父 budget 派生硬上限:

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/group ID不假装任务已完成。
  • AgentSignal 显示为独立运行时信号卡片,不显示成用户气泡。
  • queue completion 在主 Agent内部 continuation 后只显示主 Agent汇总回复。
  • steer 信号可以在当前 Turn 工具状态中显示“已接纳”,最终历史由 Turn commit 校准。

WebSocket 协议使用通用 run/event 投影,不为 Signal 复制一套状态机:

WsInbound::GetAgentRuns { session_id, cursor, limit }
WsInbound::GetAgentRun  { session_id, run_id }

WsOutbound::SessionAgentRuns { session_id, revision, runs, next_cursor }
WsOutbound::AgentRunUpdated  { session_id, revision, run }
WsOutbound::AgentEventUpdated { session_id, revision, event }

AgentEventUpdated 覆盖 accepted/admitted/consumed/dead-letter 状态,客户端按 (session_id, revision, event_id) 幂等合并;重连后用 GetAgentRuns 全量校准。TurnSnapshotTurnCommitted 增加 turn_origin = user | agent_continuation | scheduled。第一版取消入口继续使用 /stopagent_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/group ID、终态和查询提示fallback_notified_at 保证幂等。fallback 渠道失败时SQLite run/event 记录和管理 UI 是最终诊断出口,不能把 dead-letter 伪装成已交付。

22. 兼容迁移

22.1 Delegate 参数

旧模式映射:

inline     → foreground
parallel   → foreground + tasks[]
background → background

过渡期只解析代码中确实存在的旧值并在 tool result/日志中给出弃用提示;async 从未是有效值,不新增该别名。新 system prompt 只描述 canonical 值 foreground/background

22.2 allowed_tools

allowed_tools 首先变成只能收紧 Definition.tools 的兼容字段,不能扩权;随后从 schema 删除。没有 target 的旧委托映射到内置 general Agent Definition。

22.3 background_tasks

新增 agent_runs 后:

  • 新任务只写新表。
  • 管理 API 在过渡期 union 读取旧 background_tasks 与新 agent_runs
  • 旧终态记录按原 TTL 清理,不强制迁移正文。
  • 旧 pending/running 记录在升级启动时按 interrupted/cancelled 规则收敛。

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 隔离和 legacy general scope 兼容。
  • 保持旧 background 通知路径作为兼容,但不开放嵌套 background。

Phase 2AAgentLoop 结构化取消

  • CancellationToken 贯穿 root Turn、Provider stream、工具批次和 AgentRunner。
  • 保持 /stop 强停止、普通 steer 安全边界语义。
  • 用现有 sleep cancellation、Provider stream 和 steering recovery tests 锁定回归基线。

Phase 2B统一 Agent Run 持久化

  • 新增 agent_runsagent_run_groups、Storage transaction API。
  • 拆分 delegateagent_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. 预计代码边界

建议模块拆分:

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/group 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 正确注入。
  • RootOnly 工具不能通过 Markdown 或兼容 allowed_tools 获得。
  • 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 不能抢占。
  • each 逐 run 提前投递;all 只触发一次 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 终态,all policy 的 Group Completion 恰好对应一个 group 终态。
  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”提升为明确的编排系统

Markdown Agent Definition
        ↓
AgentCatalog + DelegationPolicy
        ↓
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 既有不变量的前提下分阶段实现,并为权限、持久化、取消、热重载和客户端表现提供可验证边界。