PicoBot/docs/SUB_AGENT_ORCHESTRATION_DESIGN.md

47 KiB
Raw Blame History

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

状态提案2026-08

本文定义具名子 Agent、委托图、多 Provider、delegateemit_signal、后台结果收件箱、queue/steer 投递以及可唤醒 sleep 的目标架构。本文描述待实现设计;在实现完成并通过测试前,当前行为仍以代码和 docs/ARCHITECTURE.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 时由运行时自动产生的终态事件
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

limits:
  timeout_secs: 900
  max_iterations: 24
  max_children: 4
  max_depth: 3
  max_result_chars: 16000
---

# Role

你是一名严谨的研究 Agent。

- 优先使用原始资料。
- 明确区分事实、推断与建议。
- 只返回与任务有关的结论、证据和不确定性。
- 不修改项目文件。

Frontmatter 只保存非秘密引用和限制API key、base URL、headers 继续保存在 config.json/.envllm_profile 引用现有 config.agents keyConfig::get_provider_config() 解析 Provider 与 Model。

5.3 配置扩展

{
  "agent_orchestration": {
    "definitions_dir": "~/.picobot/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_pending_inbox_events_per_session": 128,
    "inbox_event_ttl_hours": 168
  }
}

root_delegates 是 Root Agent 的出边白名单。Root 不需要也不允许出现在 definitions 目录中。

5.4 加载与校验

AgentCatalog 在 Gateway 候选运行代准备阶段完成全部校验:

  • 文件大小、UTF-8、frontmatter 格式和必填字段。
  • ID 格式、重复 ID、保留 IDROOTmain 等)。
  • llm_profile 能解析为完整 LLMProviderConfig
  • 每个工具已注册且允许委托。
  • 每个 delegates 目标存在且不是 Root。
  • 限制值在系统硬上限内。
  • 角色正文和描述长度有界。
  • canonical path 位于允许目录,拒绝越界 symlink。

任一引用错误应拒绝候选运行代激活而不是静默删除工具或委托边。AgentCatalog 以 Arc 固定在运行代中,已启动任务不读取修改后的文件。

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 / Session Queue]
    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 与任务树并发。
  • 生成自动 completion 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/completionAgentResultRouter 负责把 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 图。

7. 委托图与权限模型

7.1 基本规则

每次委托必须同时满足:

target != ROOT
target in allowed_targets(caller)
depth < effective_max_depth
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而不是放开隐式递归。

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、Turn wakeup handle 和资源 scope。DelegateToolEmitSignalTool 必须实现 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 能减少模型错误调用,也便于分别授权。

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 成功且执行任务已经被 TaskSupervisor 接纳后才返回成功。可选 idempotency_key(root_session_id, caller_run_id, key) 范围唯一,用于 Provider 重试时避免重复创建任务。

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 等有状态外部资源;确需共享必须由工具定义显式支持。

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 AgentCoordinator 自动生成 completed/failed/timed_out/cancelled/interrupted

最终结果不能依赖模型记得调用工具。即使 Provider 异常、超时或任务被取消Coordinator 也必须产生终态事件。

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。

普通进度不应滥用 signal。工具调用进度继续通过内部 Observer/TurnEvent 投影到 UI只有需要主 Agent采取行动的事件才使用 emit_signal

12.3 Completion 去重

Completion 包含本 run 已发出的 signal 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 的终结路径统一保存结果并创建 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
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
task                   TEXT NOT NULL
context_json           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     TEXT NOT NULL
attempt                INTEGER NOT NULL DEFAULT 1
started_at             INTEGER
finished_at            INTEGER
created_at             INTEGER NOT NULL

不保存 API key、Authorization header、Provider 私有 reasoning state 或完整 connection URL。

14.2 agent_run_groups

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

批量 background 默认 completion_policy=all,等全部 run 进入终态或 group deadline 后只唤醒主 Agent一次。独立 run 可通过 300500ms debounce 合并,避免连续启动多个内部 Turn。

14.3 agent_inbox_events

agent_inbox_events
------------------
id                    TEXT PRIMARY KEY
root_session_id       TEXT NOT NULL
run_id                TEXT NOT NULL
group_id              TEXT
event_type            signal | completion
event_key             TEXT NOT NULL
delivery              queue | steer
severity              TEXT
payload_json           TEXT NOT NULL
status                 pending | leased | admitted | consumed | dead_letter
attempt_count          INTEGER NOT NULL DEFAULT 0
lease_token            TEXT
lease_until            INTEGER
admitted_turn_id       TEXT
created_at             INTEGER NOT NULL
consumed_at            INTEGER

UNIQUE(run_id, event_type, event_key)

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

14.4 原子事务

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

UPDATE agent_runs terminal state/result/usage
UPDATE agent_run_groups terminal count/status
INSERT agent_inbox_events ... ON CONFLICT DO NOTHING
UPDATE bound task item by execution_id
COMMIT

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

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 },
}

pub enum InputDelivery {
    Queue,
    Steer,
}

SourceKind 增加 agent_signalagent_result。Provider 不支持 runtime role 时可以序列化为有明确 envelope 的 user-compatible message但持久化来源、客户端渲染和取消恢复必须保持类型不得显示成用户气泡。

15.2 路由规则

主 Agent 状态 queue steer
无活动 Turn 入 session queue启动内部 Turn 退化为 queue启动内部 Turn
活动 Turn 接受输入 入下一 Turn 入当前 TurnMailbox
TurnMailbox 满/已关闭 保持 durable pending 后排队 可靠退化为 queue
Provider 请求进行中 等下一 Turn 等请求结束后的安全边界
普通工具批次进行中 等下一 Turn 等完整工具批次结束
sleep 进行中 唤醒 sleep内容仍留在 queue 唤醒 sleep并在工具批次后注入当前 Turn

Steer 不承诺硬实时抢占。最迟可见时间由当前不可分割 Provider 请求或工具步骤决定。需要当前逻辑必然依赖子结果时应使用 foreground不要用 background+steer 模拟同步调用。

15.3 原子 admission 与 fallback

AgentResultRouter 对 steer 事件执行与用户 steering 相同级别的原子判定:

  1. 获取目标 Session。
  2. 在 Session 状态锁内分配单调 sequence。
  3. 若 active Turn 正在 accepting尝试 push TurnMailbox。
  4. 若 closed/full/不存在,创建内部 AgentTask 放入有界 session queue。
  5. 内存 admission 结果与 durable event lease 关联。

任何竞态下事件只能属于当前 Turn 或后续 Turn之一不能同时进入两者也不能两者都不进入。Session queue 饱和时事件继续保持 durable pending由 Router 重试;不能像普通瞬时通知一样丢弃。

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 上限共同控制。

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
> 已排队用户输入
> queue background result continuation
> 等待新事件

Steer event 在没有活动 Turn 时按 queue 处理。用户输入优先避免后台总结打断新请求UI 未读状态避免持续用户流量下结果不可见。

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 持久化和投递。

16.4 消费确认

领取流程:

pending → leased → admitted → consumed

lease_token 防止重复 worker 处理。同一事务必须保存主 Agent Turn 和把对应 inbox events 标记 consumed。若 AgentLoop 失败、Turn 取消或 Gateway 崩溃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 在工具批次后注入。

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。

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 不能静默消失:

  • 已进入 current Turn 但尚未提交的 durable event 恢复 pending。
  • 被取消 background run 产生 cancelled completion供 UI/主 Agent获知。
  • 用户显式执行 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 的中断记录。

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 释放而消失。

19.3 预算传播

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

child deadline <= parent deadline
child max depth <= remaining depth
sum child token/cost reservation <= remaining tree budget

调用方可以收紧 timeout/结果大小,但不能超过 Agent Definition 和系统上限。

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 校准。

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
TaskSupervisor 拒绝 spawn run 条件更新 cancelled/failed再返回失败
Provider 创建失败 run failedforeground 返回错误background 生成 failure event
Inbox wakeup 丢失 pending event 由恢复扫描重新唤醒
TurnMailbox closed/full steer 可靠退化 queue
Session queue 满 durable event 保持 pendingRouter 有界重试
Main continuation Provider 失败 event lease 到期并重试;不标 consumed
主 Agent回复持久化失败 event 不确认,避免结果消失
Channel 最终投递失败 assistant history已持久化沿用 DeliveryCoordinator terminal fallback

所有重试必须有次数、退避、deadline 和分类;永久错误立即终态化,不能无界重试。

22. 兼容迁移

22.1 Delegate 参数

旧模式映射:

inline     → foreground
parallel   → foreground + tasks[]
background → background
async      → background若曾接受该别名

过渡期解析旧参数并在 tool result/日志中给出弃用提示;新 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 和委托图授权。
  • 保持旧 background 通知路径作为兼容,但不开放嵌套 background。

Phase 2统一 Agent Run 持久化

  • 新增 agent_runsagent_run_groups、Storage transaction API。
  • 拆分 delegateagent_task
  • Foreground 结果也持久化,修复截断结果不可查询。
  • 实现结构化取消、预算与 permit 释放。

Phase 3Agent Inbox 与 Queue Completion

  • 新增 agent_inbox_events、lease、恢复扫描。
  • Background completion 从 Channel direct notification 改为主 Agent内部 continuation。
  • 增加 SourceKind::AgentResult、内部 AgentTask 和 WebUI 投影。
  • 批次 completion 合并与 debounce。

Phase 4Emit Signal 与 Steer

  • 新增 EmitSignalTool、SignalContract 和 rate/dedupe。
  • SteeringMailbox 泛化为来源感知 TurnMailbox。
  • 实现 steer admission、queue fallback、durable ack/recovery。
  • 添加 AgentSignal UI 和任务树。

Phase 5可唤醒 Sleep 与工具中断元数据

  • ToolExecutionContext 增加 TurnWakeupHandle。
  • SleepTool 使用 watch revision + timer + cancellation select。
  • queue/steer 唤醒内容边界和测试。
  • 为工具增加 InputInterruptPolicy默认 Never。

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 orchestration
└── 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。
  • 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 隔离。

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 不能提交。
  • 多 run group 只触发一次汇总 Turn。

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则可靠排队。

25.7 取消、并发与恢复

  • 父 foreground 取消级联后代。
  • 父 waiting_children 不持有唯一 permit无死锁。
  • /stop 取消 session background runs并恢复未提交 durable events。
  • 迟到结果不能覆盖 cancelled/interrupted。
  • reload 关闭 admission 后拒绝新 run/signalpending inbox 由新代恢复。
  • Gateway 重启把 running 标记 interrupted 并生成 completion。
  • session 删除/归档后的事件按明确 dead-letter/cancel 策略收敛。

25.8 消息与客户端

  • AgentSignal 不渲染为用户气泡。
  • Internal continuation 输入不出现在普通历史assistant 汇总正常持久化。
  • reasoning/provider state 不进入信号、API、客户端和日志。
  • send_message 仍走外部投递确认emit_signal 不走 OutboundDispatcher。
  • 同 Turn 附件兼容路径与未来 attach_artifact 不产生重复历史。

26. 必须保持的架构不变量

  1. 同一 Session 最多一个活动主 Agent TurnAgent 子 run 可以并行,但不能并发提交主会话历史。
  2. Root Agent 不能成为委托目标,结果回传不等同于反向委托。
  3. Foreground/Background 只描述委托方等待行为;并发是独立调度维度。
  4. Queue 输入永不泄漏正文到当前 TurnSteer 只在安全边界注入。
  5. Signal 先持久化后唤醒;内存通知不是事实来源。
  6. Signal 是非终态事件Completion 由运行时自动生成且恰好对应一个 run 终态。
  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 或本地内部路径。

27. 设计结论

目标架构把现有“一个 delegate 工具创建临时 Agent”提升为明确的编排系统

Markdown Agent Definition
        ↓
AgentCatalog + DelegationPolicy
        ↓
AgentCoordinator
        ├─ foreground并发执行、父等待、tool result 返回
        └─ backgroundrun ID 返回、signal/completion 进入 durable inbox
                                   ↓
                             queue | steer
                                   ↓
                    Session queue | current TurnMailbox
                                   ↓
                              Root Agent

Foreground 解决依赖型子任务Background+Queue 解决稍后统一处理Background+Steer 解决长任务期间的重要监控信号;可唤醒 Sleep 为安全等待提供及时响应点。三条消息路径各自保持单一职责:emit_signal 内部告警、自动 completion 终态回传、send_message 外部投递。该划分能够在不破坏 PicoBot Session/Turn/Delivery 既有不变量的前提下分阶段实现,并为权限、持久化、取消、热重载和客户端表现提供可验证边界。