> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/architecture/agent-harness.md).

# 代理执行器

代理一次运行是如何进行的：工具调用循环、子代理分发、原型、分诊、钩子，以及围绕它们的成本和预算机制。

`crates/openhuman-core/src/agent/` 是一个代理轮次的宿主端。TinyAgents 负责通用的工具调用循环、调用解析、会话身份和转录回放，以及可复用的轮次运行器机制。OpenHuman 将这些原语适配到产品策略和宿主端口：提供方选择、访问与审批、会话持久化、子代理产品行为、中断策略以及成本核算。TinyChannels 负责提供方实现，以及规范化的 typed channel 和 web-event 契约。

## 将 OpenHuman 作为库嵌入

`openhuman_embed` 提供一个两步式 API。 `Runtime::builder()` 在每个进程中启动一个进程内核心、后台服务、已注册的领域族、后端 URL 和 TinyHumans API key，并且 `Runtime::agent(spec)` 在其上实例化任意数量的代理。每个 `AgentSpec` 完整描述一个代理：提供方端点和模型、访问层级， `action_dir`、MCP 服务器、它自己的进程内工具、技能包、系统提示、工具作用域、沙箱模式、允许列表，以及一个收窄后的 `DomainSet` / `ToolGroups`。一个运行时标识为 `HostKind::Library`：推理不依赖于 OpenHuman 应用登录，包括工作流代理节点的推理就绪性检查。

### 嵌入方自己的工具

`AgentSpec::tools` 接收宿主自己的 `Box<dyn Tool>` 对象，这些对象会作为真实工具进入模型，其自身的 schema 也会随线传输，并以它们自己的名字被调用。在它出现之前，唯一的路径是 `AgentSpec::mcp`，而模型要为这种间接性付出代价：先做一次发现调用来了解服务器提供什么，再发出一个 `mcp_call_tool` 信封，其内部 `参数` 对象没有任何提供方可以对其解码进行验证或约束。

它接收的是一个工厂，而不是一条流水线。 `Agent` 是 `Clone` ，并且 `Box<dyn Tool>` 不是，且一个 spec 背后的会话会从 `Config` 在每一轮重建，因此任何持有 `dyn Tool` 的东西都不可能在轮次之间存活。因此该闭包每轮只运行一次，这也意味着，某个宿主如果其工具属于比代理更短生命周期的东西（一个 episode、一个房间、一次分配），可以每次返回不同的工具带，而不是注册第二个代理。

实现 `Tool` 通过 `openhuman_embed::Tool`，而不是依赖 `tinytools` 直接：到那个 crate 的另一条路径会产生不兼容的 Rust 类型，而基于它构建的工具根本无法交给会话。

关于变化中的工具带有一个注意事项。提示词的工具目录是在同一构建中从同一个工具带渲染出来的，因此在任意一次组合提示词的轮次里两者保持一致，但恢复的会话会复用其已持久化的系统消息，所以长期线程下发生变化的工具带，会由该线程开启时所用的提示词来描述。只有在运行于各自独立会话上的轮次里才变更工具带。

每个代理的隔离是一个上下文，而不是第二个核心。 `Runtime::agent` 克隆运行时的基础 `Config`，应用 spec，并派生一个子级 `CoreContext` (`CoreContext::derive_with`）携带该配置、该代理的领域集、工具组和技能根策略。每一轮都会在该上下文下分发（`CoreRuntime::run_in` → `agent_chat_for` 并显式传入 `AgentDefinition`），因此配置加载器、领域门控、工具组过滤器和技能发现都读取该代理自己的设置。转录按代理 id 作为键，一次轮次只会恢复它自己的线程。

位于运行时拥有的根目录下的布局： `<root>/config.toml` 以及凭证存储； `<root>/workspace/` 包含会话数据库、 `session_raw/` 转录，以及每个代理的 `agents/<id>/skills/`；以及 `<root>/agents/<id>/action/` 作为每个代理默认的工作根目录（workspace 的同级，从不位于其内部）。

`Harness` 是单代理速记：一个运行时加上一个名为 `harness`的代理。构建一个运行时，并在其代理上并发地发起 `run` 或 `turn(...).send()` 调用；不要为每个代理构建一个核心。除非提供了先前的会话 id，否则每次调用都拥有一个独立会话。

库模式下的身份验证是 TinyHumans API key（`RuntimeBuilder::api_key`），它被保存在运行时旁边的一个 `api-key` 认证配置中，位于运行时的 `config.toml`旁边。托管推理会将其作为 bearer 发送到兼容 OpenAI 的端点；后端 REST 调用会发送 `x-api-key`；没有会话 JWT，也没有任何需要过期的东西。命名自己 `Provider` （BYOK）的代理根本不会接触它。该密钥覆盖所有托管特性，但受后端作用域限制：推理、嵌入、语音、搜索、媒体、Composio 以及其他集成、通道和 socket。只有受会话约束的 `/auth/*` 流程（OAuth 连接、通道链接 token）仍然使用 `HarnessBuilder::session`.

`Workspace::Inherit` 连同 `Provider::inherit()` 一起，而且有意不通过库路由推理 API key。它继承已安装的 OpenHuman 配置，因此保留已安装应用的会话检查。当在没有应用登录的情况下嵌入时，请提供显式的提供方或 API key。

有些设置对于每个代理仍然是运行时级别的（实时自治策略的 `auto_approve*`、审批门控开关、子代理目录、配置子代理重读）；crate README 将它们列在“Still runtime-wide”之下。

每个代理轮次都通过发布的 [`tinyagents`](https://crates.io/crates/tinyagents) harness 运行。有三个入口点： `Agent::turn`、channel 和 CLI 总线路径，以及 `run_subagent`。它们都通过 [`crates/openhuman-core/src/agent/tinyagents/`](https://github.com/tinyhumansai/openhuman/tree/main/crates/openhuman-core/src/agent/tinyagents/README.md) (`TurnSpec::run`）。上下文裁剪由 `MessageTrimMiddleware`负责，而取消由 tinyagents steering channel 负责。策略停止钩子（预算、线程目标和迭代上限）使用上游的 `tinyagents-hooks` `StopHookMiddleware` 并在首次停止投票时暂停运行。Web 聊天接入和队列机制使用 TinyAgents 的 `TurnRunner`；OpenHuman 的 `web_chat` 宿主提供 ingress、会话签出、审批、持久化以及终端事件策略。通道提供方传输和 typed web-event schema/projection 由 TinyChannels v2 模块通过 TinyBus 提供。OpenHuman 在其进程事件总线上发布这些规范化事件，并只保留宿主路由元数据。共享的 `TurnProgress` 连接点位于 `agent/session_host/tool_progress.rs`.

TinyAgents 的 `tinyagents-orchestration` crate 负责与宿主无关的子代理生命周期。OpenHuman 为启动策略、持久化会话解析、取消、进度和完成交付提供产品适配器。多代理编排运行在 TinyAgents 的图层上，通过 `tinyagents-orchestration`的 `parallel` 模块、宿主的 `spawn_parallel` 适配器，以及从 `graph::orchestration` `TaskStore` 生命周期原语中重新导出的共享原语。 [`orchestration/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/agent/orchestration/mod.rs)。当前宿主表面在 [编排所有权映射](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/agent/orchestration/README.md); [`spawn_subagent_host_impl.rs`](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/agent/orchestration/tools/spawn_subagent_host_impl.rs) 将子运行绑定到其调用者的执行上下文：

* OpenHuman 的子代理适配器、取消、进度和面向 UI 的投影记录在 [编排所有权映射](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/agent/orchestration/README.md)；TinyAgents 负责通用图和任务执行。
* 工作流阶段引擎会将每个阶段的代理在图上展开（`with_max_concurrency`）。持久化的 `WorkflowRun` 账本仍然是恢复的事实来源。
* `spawn_parallel_agents` 通过宿主的 `spawn_parallel` 适配器在 `tinyagents_orchestration::parallel`.
* 上运行其 fan-out。 `TaskStore` 分离子代理注册表由一个类型化的

## TinyAgents crate：特性与兼容性

OpenHuman 通过路径依赖 vendored git submodule `vendor/tinyagents`中的拆分 TinyAgents crate 家族，包括 `tinyagents-harness` （特性 `multimodal`, `builtin-tools`), `tinyagents-definition`, `tinyagents-registry`, `tinyagents-session`, `tinyagents-runtime`, `tinyagents-orchestration` 以及可选的 `tinyagents-live`，再加上 `tinyinference` 来自 `vendor/tinyagents/vendor/tinyinference`，因此在上游之前可以在树内测试 SDK 变更（参见 [`crates/openhuman-core/Cargo.toml`](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/Cargo.toml)）。TinyFlows 负责工作流图检查点及其 `graph.*` 存储 schema。原因如下，以便未来升级不会回归：

* 原生 TinyAgents 模型接口，OpenHuman 拥有的产品策略。每条 live 路由都是一个 `Arc<dyn ChatModel<()>>`：TinyAgents 兼容 OpenAI 的客户端覆盖线等价的托管、本地和 BYOK 路由，而宿主 `ChatModel` 实现覆盖 Claude SDK/Code 和 Codex 特定传输。OpenHuman 仍然负责凭证解析、OAuth、访问门、端点选择、出站披露、计费元数据和错误分类。
* 一条原生 SQLite 链承载 OpenHuman 的持久化。OpenHuman 根目录和 Tauri Cargo 世界固定了 `rusqlite = "=0.40.2"` (`bundled`）；锁定的 Rust 1.99 工具链（1.96 或更新）提供 `cfg_select!` 宏，这是其构建脚本所需。两个世界都解析为单一的 `libsqlite3-sys` 链。TinyFlows 通过其驱动拥有的 checkpointer 和 schema 持久化工作流检查点。
* WhatsApp Web 位于 TinyChannels 模块中（`tinychannels/whatsapp-web`），不在 OpenHuman 中。OpenHuman 在其正常依赖图中没有 `whatsapp-web` 特性，也没有 `whatsapp-rust` 或 `wacore` 依赖。该模块的存储桥接在 tinychannels 仓库中有文档说明。
* 没有脚本运行时。不存在 `repl` 或 Rhai 脚本特性，也没有 `rhai_workflows` 工具。Rhai 之所以仍作为 `tinyflows` 的依赖出现，只是为了其自身的 JSON 查询栈，与代理 harness 无关。
* 所有权映射：模型构建 → `inference::provider::create_chat_model*`；工作流检查点 → TinyFlows 的驱动 schema；通用分离执行器状态 → `DetachedTaskRegistry`；面向控制器的持久化 → OpenHuman SQL/JSON 运行账本（`running_subagents`, `workflow_runs`, `agent_teams`, `command_center`）。通用的 harness/middleware/event 原语按原样使用。

代理 harness 会把用户消息、webhook 事件或 cron tick 变成一次完整的、使用工具的交互。TinyAgents 负责通用循环和可复用的执行原语。OpenHuman 提供产品特定的子代理和触发工作流、策略钩子以及宿主能力。提供方传输和通道归一化属于 TinyChannels；工具实现、提示词组装和内存存储属于各自的领域。

本页先讲解一次轮次中发生了什么，然后再深入每个活动部件。

## 轮次的形态

无论用户刚输入了一条消息、Telegram webhook 触发了，还是早上 9 点 cron 触发了，每一次轮次都遵循同一个生命周期：

消息通道入口由已配置的 TinyChannels v2 模块通过类型化总线操作和共享线格式类型来准备。OpenHuman 应用账户授权和宿主策略，然后提供 TinyAgents 的会话和转录机制所使用的持久化端口。TinyAgents 负责通用轮次执行、会话身份、转录回放和可复用的轮次运行器。提供方传输、归一化和规范化的 web 输出投影保留在 TinyChannels 中；OpenHuman 负责账户授权、审批、会话与交付策略，以及宿主适配器和持久化实现。

```
┌─ 入站 ─────────────────────────────────────────────────────────┐
│ 用户消息 · 通道入站 · webhook · cron · composio event │
└──────────────────────────┬────────────────────────────────────────┘
                           │
                           ▼  （仅外部触发）
                ┌──────────────────────┐
                │   触发分流     │  分类 → 丢弃 / 通知 /
                │   （本地小型 LLM）  │  启动反应器 / 启动编排器
                └──────────┬───────────┘
                           │
                           ▼
            ┌──────────────────────────────┐
            │      Agent::turn()           │
            │  1. 恢复转录        │
            │  2. 构建系统提示词*     │
            │  3. 召回记忆包      │
            │  4. 进入工具调用循环 ────┼──► 提供方调用
            │  5. 分发工具调用  ────┼──► 工具执行 / 子代理启动
            │  6. 上下文守卫 / 压缩  │
            │  7. 停止钩子检查          │
            │  8. 最终助手文本     │
            └──────────┬───────────────────┘
                       │ 异步，在用户看到回复之后
                       ▼
              ┌─────────────────┐
              │  轮次后      │  成本日志 · 对话
              │  钩子          │  用于记忆的缓冲
              └─────────────────┘

* 系统提示词只在第一轮构建。后续
  轮次逐字复用已渲染的提示词，因此推理
  后端的 KV-cache 前缀仍然有效。
```

本页其余部分是同一张图的展开版。

## 会话和 `Agent::turn`

会话是 `Agent` 实例正在运行的实时对话。该 `Agent` struct 拥有：

* 对话历史（system + user + assistant + tool 消息）。
* 要调用的提供方客户端（模型由 [模型路由器](/openhuman/zh/gong-neng/model-routing.md)).
* 解析）
* 模型可见的工具注册表。 `记忆` 工具（当记忆引擎开启时），模型通过它按需回忆、获取、学习和遗忘。
* 每轮预算：最大工具迭代次数、最大负载大小、最大 USD 成本。
* 本地动作预算：针对会产生副作用的工具动作的滚动小时上限，读取自 `config.autonomy.max_actions_per_hour`.

`Agent::turn(user_message)` 是热路径。在一次轮次中它：

1. 如果这是一个新进程，则恢复会话转录，从磁盘重新加载精确的提供方消息，以便推理后端的 KV-cache 前缀仍然命中。
2. 构建系统提示词（仅在第一轮）。这会拉入身份、灵魂、档案、记忆、已连接的集成、可用工具、安全前言，由提示词部分构建器组装。
3. 召回一个记忆包。TinyMemory 的代理生命周期会记录用户轮次，并召回一个受 token 预算约束的包（学习与信念、脑文档、该代理的历史、其他代理的轮次），包装在 `<memory-context>` 中，并以临时方式添加到该轮次的模型请求里；它从不持久化到转录中，因此不会影响提示词缓存。模型仍然可以调用 `记忆` 工具来获取引用答案（参见 [记忆架构](/openhuman/zh/kai-fa/architecture/memory.md)).
4. 进入工具调用循环（下一节）。
5. 在后台启动轮次后钩子：用户会在成本日志和对话日志完成之前先拿到答案。

后续轮次不会重建系统提示词。哪怕只是字节级的表面变化也会使 KV-cache 前缀失效，并强制完整重新预填，因此动态的每轮上下文（工具结果、记忆召回答案）会作为用户可见的消息内容追加，而不是拼接进系统提示词。

#### 可缓存前缀比系统提示词更宽

冻结提示词只是契约的三分之一，而另外两部分更容易被破坏，因为它们看起来都不像缓存：

* 工具块计算在先，且它 *先*。生产环境中的每个前缀缓存都会在对话之前渲染工具目录，聊天模板必须把它放在模型在第一个用户轮次之前会读取到的位置。OpenAI 的自动缓存、Anthropic 的 `cache_control` （tools → system → messages）、DeepSeek 的上下文缓存以及任何 vLLM/SGLang radix cache 都是这样工作的。所以一个在轮次之间变化的 `tools` 数组也会使冻结的系统提示词失效，而请求体的 JSON key 顺序（它把 `messages` 放在 `tools`前面）对此毫无说明。会话中的 Composio reconcile 仍然是正确的；一个谎报模型可调用内容的工具表面，比冷启动预填更糟，但它必须只在真实能力变化时触发，并且除此之外必须保持字节稳定。 `connected_set_hash` 在哈希前排序，因此重新排序后的后端响应不会触发重建，而 `collect_orchestrator_tools` 出于同样原因对已连接工具包枚举排序。
* 历史必须只追加。第 N 轮的序列化必须逐字保留，作为第 N+1 轮的开头。 `pair_tool_cycles` 在序列化时丢弃未完成的工具循环，因此它对某条记录的判断必须只依赖该记录及其直接邻居，绝不能依赖后面追加的任何内容，否则更早消息的存在会被追溯翻转，前缀就会在缓存下移动。上下文压缩是唯一有意的例外；它会重写中间部分，并为重新预填付费。

恢复的会话的回放前缀会折叠进 `Agent::history` ，而不是拼接进一次请求，因此请求、随后的轮次以及持久化转录都会读取相同的序列。把它拼接进去会使对话付出两次代价：下一轮发出去时没有它，而之后写入的转录（从 `history`序列化而来）只包含新的轮次，而 *下一次* 恢复又会读回它，每次重启都会把线程再截短一点。

#### 只有当线路层明确这么说时，稳定前缀才真正发挥作用

保持字节稳定是必要条件，但还不够：提供方仍然必须被 *告知* 进行缓存，而且有两个提供方需要明确告知。

* 没有……，Anthropic 不会缓存任何内容 `cache_control` 标记，而且其兼容 OpenAI 的端点也无法携带这些标记；Anthropic 文档将该路径上的提示缓存列为不受支持，并报告 `prompt_tokens_details` 始终为空。一个 `cloud_providers` 包含 `auth_style = "anthropic"` 因此被构建为该 crate 的原生 Messages 适配器（`tinyinference_llm::providers::anthropic`），它会在最后一个工具、最后一个系统块和最终消息上放置标记，以便不断增长的工具循环复用前一次迭代的缓存，而不只是系统提示。唯一的例外是文本模式（`native_tools = false`，即提示引导工具），只有 Chat Completions 适配器实现，并且会继续使用兼容客户端，因此仍然按全价计费。
* OpenRouter 会把标记转发给 Anthropic 和 Gemini，但自身不会新增任何标记；托管的 OpenAI 会拒绝未知的内容分片字段。OpenRouter 的 slug（以及任何位于 `openrouter.ai`）上的端点可启用 `OpenAiModel::with_explicit_cache_control`，它会标记最后一个系统消息和最后一个用户消息；其他所有 Chat Completions 端点都不会被标记。
* 路由提示与标记是分开的。该 harness 会从……推导出 `prompt_cache_key` ，基于声明的稳定前缀，并将其放入 `provider_options`，因此线程的每一轮，以及共享其系统提示和工具集的每个子代理，都会在支持分片的提供方上路由到同一缓存分片（OpenAI 的 `prompt_cache_key`）。没有这种概念的适配器会直接丢弃它。受管后端会把它作为顶层 body 字段接收，并与 `thread_id`.

这三者都依赖于 `RunPolicy::cache.protect_prompt_prefix`，宿主无条件设置该项；而可缓存前缀则由 tinyagents 循环在每次请求中基于会话冻结的系统前缀声明（系统提示层级，然后是工具目录）；宿主没有自己的中间件来处理这一点。直到 tinyagents 开始将该有效策略写入发出的请求之前，两个读取方都只查看 `request.cache_policy` 本身，总是 `None` ，因此这个标志仅用于诊断，标记或键从未真正传到线上。 `[cache]` 现在每次模型调用的调试行都会报告是否注入了键，以及声明了多少可缓存片段； `cache_read_tokens` 返回的 usage 中的该值就是证明它起作用的数字。

要测量这个，而不是凭推理判断。 `CAPTURE_ALL=1 node scripts/debug/capture-first-inference.mjs` 会记录整个会话的请求， `scripts/debug/run-multi-turn-capture.mjs` 会通过生产 RPC 驱动一个多轮线程，且 `scripts/debug/audit-inference-prefix.mjs` 会报告第一次分歧并给出归因。单轮捕获看不到这些；问题从来不是第 1 轮花费多少，而是第 2 轮能否复用它。

### AGENTS.md 项目说明

除了 identity/soul/profile/memory 之外，系统提示还会拉入 AGENTS.md 指令文件，这是 OpenHuman 对 Claude Code 的 `CLAUDE.md` / Codex 的 `AGENTS.md`。两个层级只会在系统提示构建时加载一次（不会按轮重新读取，因此冻结前缀 / KV 缓存契约成立）：

* 全局， `<workspace_dir>/AGENTS.md`，用户的 OpenHuman 工作区（其中 `SOUL.md` / `USER.md` 位于）。适用于每次运行。
* 项目， `<action_dir>/AGENTS.md`，即代理正在操作的文件夹。对于带有 git worktree 覆盖的子代理运行（`SubagentRunOptions.worktree_action_dir`），该覆盖目录将作为项目层。

全局层先渲染，项目层后渲染（项目指令后叠加，冲突时优先），并置于 `## 项目指令（AGENTS.md）` 标题下。当两个目录解析为同一路径时，该文件只加载一次（去重）。缺失 / 无法读取 / 为空的文件会被静默跳过，并且每一层都上限为 `BOOTSTRAP_MAX_CHARS` （约 20,000 字符），并附带一个 `[... 已截断]` 标记，这样大文件就不会挤占提示。

加载器是 `agent::prompts::agents_md` （返回预加载字符串的纯函数，在读取时就做边界限制，因此异常的多 MB 文件不会在渲染上限之前耗尽内存）；这些字符串被传递到 `PromptContext` (`agents_md_global` / `agents_md_local`）并由 `AgentsInstructionsSection`进行渲染，它位于默认和子代理构建器中的用户文件部分之后、工具目录之前。主 / 编排代理以及其他内建动态代理（`PromptSource::Dynamic`）通过 `render_*` 辅助函数组装各自的主体，因此相同的 `AgentsInstructionsSection` 会由 `SystemPromptBuilder::from_dynamic`中央注入，追加在代理自身主体之后、中央 grounding 契约之前，而不是由每个 `agents/<id>/prompt.rs` 构建器分别处理。该功能由 `agent.agents_md_enabled` （默认开启）；关闭时，不会加载或注入任何 AGENTS.md 内容。

## 工具调用循环

在 `Agent::turn`内部，工具调用循环是内核引擎。它是已发布的 tinyagents crate 的 `AgentHarness` 循环，由以下代码按轮组装： `TurnSpec::run` ([`crates/openhuman-core/src/agent/tinyagents/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/agent/tinyagents/mod.rs)）。它会运行到该轮的迭代上限，该上限在 `session_host/builder/iteration_cap.rs`中解析：显式的 `[agent] max_tool_iterations_override` （或 `OPENHUMAN_AGENT_MAX_TOOL_ITERATIONS`）优先，然后是代理定义中的上限（编排器的是 200），再然后是 `max_tool_iterations` （默认 10），适用于没有定义的轮次。模型会在预算的 50% 和 80% 时被告知还剩多少次调用，并且 `inference_agent_chat` 会报告一个受上限约束的轮次，带有 `hit_cap: true` 以及其 `检查点` 文本：

```
loop {
    1. 上下文守卫      - 如果历史过大，进行微压缩 / 自动压缩
    2. 停止钩子检查    - 预算上限、最大迭代次数、自定义终止开关
    3. 提供方调用      - 发送消息 + 工具规格，流式接收响应
    4. 解析响应     - 将助手文本与工具调用分离
    5. 如果没有工具调用   - 返回最终文本
    6. 执行工具调用 - 分发每一个调用（下一节）
    7. 总结超大内容 - 将巨大的工具输出交由摘要代理处理
    8. 追加结果     - 将工具结果写入历史，再次循环
}
```

每次迭代都会发出一个实时 `AgentProgress` 事件，以便 UI 渲染逐 token 流式输出、“正在调用工具 X”状态，以及每次迭代的成本更新。

一个引擎，三个入口。该循环存在于 TinyAgents 中。OpenHuman 的聊天宿主、channel/CLI 路由以及子代理宿主都会在适配器接缝处提供产品策略；生成的子代理通过 `agent/subagent_host`进入，该路径驱动中立的 `tinyagents_orchestration::subagent::SubagentDriver` ，而不是 OpenHuman 运行器。宿主负责提供方/模型选择、工具与安全收窄、工作区/制品处理、进度以及持久化产品投影。中立驱动负责生命周期顺序、任务键合并以及“暂停还是终止”的唯一持久化决策。不存在 `harness/subagent_runner` 路径，它也绝不能作为外观层或兼容导出返回。

### 工具分发与工具调用方言

`agent.tool_dispatcher` （可在单次启动中通过 `OPENHUMAN_TOOL_DISPATCHER`覆盖）决定工具如何对模型表述。 `auto` （默认）使用原生工具调用，通过 `ChatModel` 适配器传递结构化工具规格并接收结构化回调，只要提供方配置文件支持；对于本地 Ollama 之类的提示引导型提供方，则回退为标签内 JSON。 `python` ，并且 `typescript` 是可选的：它们把目录渲染为代码签名，并按代码风格读取回调，但在某些模型上会解析错误。会话会为所选方言组装提示，并在轮次 harness 中固定相同方言，因此文本方言不会把其 schema 发到线上，而 harness 会用匹配的语法恢复调用。

规范的 `tinytools_agent::dialect::ToolDialect` 实现直接提供与转录兼容的解析和渲染；OpenHuman 只在这些 I/O 边界转换持久化/提供方记录：

* 原生（`native`），结构化工具调用字段。
* XML（`xml`), `<tool_call>{...}</tool_call>` 在助手文本中的标签，提示中包含完整的 JSON schema。
* P-Format（`pformat`），紧凑的定位式 `<tool_call>name[0|a|1|b]</tool_call>` 带有 `name[0|<a>|1|<b>]` 的提示签名；可选启用。
* 代码（`python` / `typescript`），目录是一个函数签名列表（`def read_file(path: str, limit: int = None) -> str` 或 `function read_file(path: string, limit?: number): string;`），模型会在标签内写出函数调用： `read_file(path="src/main.rs", limit=20)` 或 `read_file({path: "src/main.rs", limit: 20})`。像 P-Format 一样紧凑，但语法是小型代码训练模型已经会写的； `python` 这是默认值。

所有文本方言共用一个解析器：一个 `<tool_call>` 主体会先按 P-Format 尝试，再按代码调用尝试，然后按 JSON 尝试，因此混用形式的模型也能被理解。持久化的会话历史可以包含这些任意形状的后缀，因此会话外壳会保留分发器，以便在恢复转录时忠实地解析并重放它们。

### 工具规则：允许、拒绝、隐藏、批准

你可以用模式规则限制代理能看到和调用哪些工具。规则语言是 `tinytools::ToolRules`，而 tinyagents 循环通过 `RunPolicy::tool_rules`来执行它。一组规则决定三个表面：请求中的工具 schema、 `tool_search` 以及延迟目录，以及每一次调用，包括嵌套调用。因此，被规则移除的工具既无法通过搜索找到，也无法用猜测的名称调用。

规则来自三个地方，并按层叠加。每一层都必须先允许一个工具：

1. **`[tool_rules]` 在 `config.toml`.** 操作员层，适用于每个代理，包括子代理。
2. **代理定义。**
   * `disallowed_tools` ，并且 `tool_rules` 在定义 TOML 中。
   * `tool_rules` 在代理注册项上，而该项的 `agent.registry` 可以由补丁设置。
   * `AgentDefinitionSpec::tool_rules` 供嵌入器使用。
3. **子代理。** 子级会继承操作员的层，但不会继承其父代理自身的限制，因为编排器通常会恰好禁止它所委派的工具。然后它再添加自己定义的层。

```toml
[[tool_rules.rules]]
id = "only-github-mcp"
effect = "deny"                  # allow | deny | hide | require_approval | auto_approve
match = { name = "mcp_*" }       # name / family / tags globs, category, exposure,
except = { family = "github" }   # permission bounds, side_effects, arg
reason = "仅批准 GitHub MCP 服务器。"

[[tool_rules.rules]]
effect = "deny"
match = { name = "shell" }
when = { channel = "telegram" }  # context: channel, agent
```

规则的行为：

* **优先级。** `deny` 在同一层内优先。 `hide` 使工具仍可调用但不列出。 `require_approval` 延后调用，方式与声明了 `approval_required`; `auto_approve` 时完全一样，只是免除该声明。
* **模式。** 通配符支持 `*` ，并且 `?` 并忽略 ASCII 大小写，因此 `gmail_*` 匹配 `GMAIL_SEND_EMAIL`。定义中的名称列表（`disallowed_tools`）通过 `tools::rules::glob_list_matches`。该函数现在是匹配器的唯一实现。
* **拒绝。** 被拒绝的调用会返回该规则的 id、层和原因。

代码： `crates/openhuman-core/src/tools/rules/`。每个会话只组装一次其层（`session_rule_set`）；每一轮都在自己的上下文中评估它们（`OpenHumanRunContext::tool_rules`).

### 循环中的上下文管理

长工具调用链可能会突破上下文窗口。对此有两层处理：

* 工具结果预算：每个工具结果都会按每次调用的字节预算检查，由 tinyagents 工具中间件强制执行。超出的内容会被硬截断，并附上说明标记，以便模型知道自己没有看到完整输出。
* 微压缩 / 自动压缩：当总历史逐渐逼近上下文窗口时，tinyagents 中间件（消息裁剪 + 安装在 `tinyagents/middleware.rs` ，并且 `tinyagents/context_wiring.rs`中的压缩钩子，阶梯顺序和大小由 `tinyagents-harness` `ContextLadder`）会在下一次提供方调用前将较早的轮次压缩为摘要。压缩后的历史会保持系统提示和最近的轮次不变（KV 缓存稳定性），并重写中间部分。摘要由该轮自己的模型写出，除非 `[context] summarizer_model` (`OPENHUMAN_CONTEXT_SUMMARIZER_MODEL`）指定了另一个模型；该模型会在该轮的提供方路由上调用，使用相同的凭据和端点（`inference/turn_models.rs`）。每一轮都会在调试日志中记录其选择，格式为 `[context_compression] summarizer model=<id> source=config|turn`。摘要失败时会回退到确定性的裁剪，而不是回退到该轮的模型。

### 过大的工具结果：摘要器的旁路处理

有些工具调用会返回巨大的负载：比如某个 Composio 动作转储了 200 KB 的 JSON、一次网页抓取返回了 50 KB 的 markdown，或者一个 `file_read` 读取了一个数千行日志。若在负载中间硬截断，就会丢掉截断点之后碰巧出现的内容。

当工具结果超过摘要器阈值（`summarizer_payload_threshold_tokens`，默认 4,000）时，编译后的 TinyJuice 模块会决定是否在其进入父级历史之前对其进行摘要。该模块负责提取提示、摘要缓存和按线程的失败熔断器，并将原始内容存储在其 CCR 存储中，以便 `juice_retrieve` 可以恢复它。宿主负责模型调用，因为它了解模型和轮次上下文。 `ToolOutputMiddleware` 通过以下方式绑定当前轮的一个一元子项： `PayloadSummarizer::prepare`，在一次性 harness 上注册受限模型（`tinyagents/payload_summarizer.rs`），然后模块通过类型化的 `tinyjuice-bus` `MlHost.Generate` 契约回调。只有编排器携带摘要模型，而且它总是会看到 `juice_retrieve`，即使关闭压缩路由也是如此，因为摘要的页脚会标明它。符合摘要条件却没有得到摘要的结果会标记为未摘要，无论是模块失败还是宿主失败，因此模型不会为了它重新运行工具。摘要复用和熔断器的作用域是线程；若没有线程，则作用于整个运行。当摘要失败，或者负载大到让对其调用 LLM 在经济上毫无意义时，硬截断仍然是下游最后一道防线。

压缩、CCR 存储与恢复、内容查询、HTML 提取，以及静态工具声明和线上的 DTO 都属于 TinyJuice 和 `tinyjuice-bus`。OpenHuman 的工具是适配器：它们将类型化请求传给模块，并将文件系统授权保留在宿主侧。句柄查询针对模块的 CCR 存储执行；制品查询会把已授权内容（而不是路径）发送到同一个模块操作。宿主没有并行的 CCR 存储，也没有本地算法回退。如果模块不可用，必需查询和提取会报告该操作不可用；可选的工具输出压缩则保持其文档化的透传行为。恢复的线程也会保留其已记录的 Juice 声明和 schema，即使当前设置不再创建新句柄；当前策略和模块可用性仍然决定执行。

调用者聚焦。工具可以选择加入一个可选的 `summary_focus` 参数，方法是把 `tokenjuice::focus::summary_focus_property()` 加入到其 schema 中； `web_fetch` ，并且 `web_search_tool` 会这样做。模型用它来说明自己需要结果中的什么，例如“速率限制”。 `before_tool` 会在验证之前从调用中移除该参数，因此工具永远看不到它。只有其 schema 携带完全相同属性的工具才会失去它；任何其他带有同名参数的工具都会保留它。它会随着结果传到 TinyJuice，在那里引导摘要、作为缓存键，并为确定性压缩器对文本排序。自身会限制输出的工具，例如 `web_fetch`，通常会交由 cap-and-spill 处理。当调用者给出聚焦时，它也会被摘要，因为翻阅原始页面无法回答问题。

### 文件系统卸载： `outputs/` ，并且 `workspace/`

仅靠压缩无法支撑长周期运行。摘要仍会一步步累积，而无论压缩多少，都无法恢复已丢失的保真度。因此，对于持续数分钟到数小时的任务，harness 会把大结果移出上下文并写入磁盘，然后把路径交给下一步。

代理现有的两个目录下 `action_dir` 在运行时（实现位于 `crates/openhuman-core/src/agent/harness/artifact_offload/`):

| 目录                      | 包含                          |
| ----------------------- | --------------------------- |
| `action_dir/outputs/`   | 交付物。旨在比生成它们的步骤活得更久，并通过路径传递。 |
| `action_dir/workspace/` | 临时区。工作进程需要但不会交回的中间文件。       |

注意 `action_dir/workspace/` 是代理 action 根目录中的临时文件夹。它不是核心内部的 `workspace_dir`，代理写入可能永远不会触及它。

两部分共同强制执行这一约定：

* 提示。实际拥有 `file_write` 的子代理会在其系统提示中获得一份“长周期制品卸载”契约：大约超过 2,000 个 token 的结果会写入以下路径下的文件： `outputs/`，回复则是该相对路径加上一段简短摘要。这个门禁是刻意设计的：提示只能列出代理真正能调用的工具，否则模型会发出失败的调用。经过技能过滤的专才不会获得契约文本，且有专门的守卫断言其提示中从不提及文件系统工具。它们仍由下面的 harness 部分覆盖，而该部分不需要模型配合。 `规划器` 提示词阐明了这一约定对其自身工作的含义；规划器被要求引用 DAG 节点之间的工件路径，而不是把负载向前粘贴。
* Harness。 `offload_oversized_result` 会在每个子代理结果上运行，所以即使工作器已经内联了结果，超大结果也会被卸载。它会在定义的 `max_result_chars` 上限之前触发，因此完整正文会落盘而不是被截断。

父级收到的是一个指针，而不是有效载荷：

```
[artifact] kind=output path=outputs/presentation_agent/sub-1234-result.md bytes=52318
read_with: file_read {"path":"outputs/presentation_agent/sub-1234-result.md"}
注：完整结果已写入动作工作区，而不是被内联。 …

[abstract]
头条发现 …
```

`SubagentRunOutcome.artifact_paths` 在结构上携带相同的路径，已从 `[artifact]` 输出中的指针解析出来，因此无论 harness 卸载了结果还是工作器自己写了文件，交接都会携带路径。父级接收的任何路径都可以用普通的 `file_read` 在子级上下文消失很久之后仍可恢复。

摘要器是后备，而不是首选。卸载捕获常见情况；摘要器绕行以及 `tool_result_budget_bytes` 上的截断仍然会处理它处理不了的一切，包括这里的每一种失败模式。被拒绝或失败的卸载是故意软处理的：调用方保留其内联负载，并退回到这些兜底机制。

路径加固采用失败即关闭。 `resolve_artifact_path` 会拒绝绝对路径， `..` 穿越，以及在词法规范化后逃离其约定根目录的任何内容。当 `SecurityPolicy` 可用时，它还会拒绝位于 `workspace_dir`之下的任何内容，既通过全局包容检查，也通过 `is_workspace_internal_path`。卸载目标解析到 `action_dir`下，而不是 `workspace_dir`，包括有人把 `action_dir` 配置到工作区根目录内部的情况，在这种情况下，每次卸载都会被拒绝，而不是悄悄写入内部状态。

写入日志 `[artifact] 在 action_dir 下写入了工作器工件`；每个交接携带的路径都会记录 `[artifact] 交接向父级携带了一个工件路径`，在生产端和消费端都会记录，因此运行日志会显示每个指针的两端。

### TokenJuice：内容感知的工具输出压缩

在新的工具结果进入历史记录之前（并且早于字节预算兜底机制），它会通过编译后的 TinyJuice 模块中的 TokenJuice 内容路由器，并在 OpenHuman 适配器中使用 `crates/openhuman-core/src/inference/tokenjuice/`。受 Headroom 启发，路由器 *根据字节和/或从工具名称与参数派生的提示检测内容类型* （JSON、代码、日志、搜索、diff、HTML、纯文本），然后分发给专用压缩器：

* JSON → SmartCrusher：对象数组 → 表格（每个键只保留一次），保留包含错误或数值异常值的行。
* 代码 → tree-sitter（Rust/TS/JS/Python）签名保留器，折叠函数体；使用大括号深度启发式作为后备。
* 日志 → 面向 *命令* 输出（git/cargo/npm/…）的 100 规则引擎，否则基于信号保留失败。
* 搜索 → 按文件以相关性排序的 top-K 匹配，并带有 `+N 更多` 计数。
* Diff → 保留变更块，折叠未变更的上下文，概括 lockfile 变更块。
* HTML → 去除标记，转换为可读文本。
* 纯文本 → 确定性的 TextCrusher。

每次有损压缩都会把原始内容卸载到 CCR（Compress-Cache-Retrieve）存储中，并用 `⟦tj:<hash>⟧` 标记，因此压缩在效果上是无损的：代理可按需调用 `juice_retrieve` （token + 可选字节/行范围）来获取完整原文。相同引擎也以通用形式暴露为 `compress_content(content, hint, opts)` ，适用于任何大型负载（文件读取、网页获取），并作为只读 `tokenjuice.*` 调试 RPC。默认启用压缩（`context.compaction_enabled`; `OPENHUMAN_COMPACTION=0` 可选择退出）。大型结果会被存储，并替换为一行统计信息、一个 500 字符的头部和一个由只读 `juice_find` / `juice_extract` / `juice_summarize` 工具查询（`tokenjuice.repl_handle_enabled`），因此对于获得句柄的结果会跳过 LLM 摘要阶段。通过 `[tokenjuice]` 块 / `OPENHUMAN_TOKENJUICE_*` 环境变量配置。代理定义可以用 `tokenjuice_compression = "auto" | "full" | "light" | "off"`; `auto` 将编码型模型代理（`[model] hint = "coding"`）解析为 `light`，这会禁用 CCR 支持的有损压缩，因此编码代理会保留原始的构建/测试/diff/搜索文本，除非某次缩减真的是无损的。其他代理默认使用 `full`。纯文本压缩使用确定性的 TextCrusher。

### 缺失的能力

没有自愈原型。缺失的宿主命令会反馈给用户，而不是进行 polyfill。

## 子代理：编排器模式

OpenHuman 是多代理的。用户正在与之聊天的代理是主代理（稳定内部 id： `orchestrator`），这是一个能力很强的默认代理，能够直接回答并完成普通工作，包括 inspect → edit → verify 的编码循环。当并行性、更深层推理或专门能力能显著带来帮助时，它会生成专门的子代理。

### 为什么采用多代理

一个什么都懂的单一代理，也会有一份像小书一样大的系统提示词。把工作拆分给专门代理意味着：

* 每个子代理都只获得它需要的窄系统提示词（身份 / 记忆 / 安全前言可以被剥离）。
* 每个子代理都获得经过过滤的工具注册表：图像代理不需要文件系统工具，任务管理器不需要 Composio 目录。
* 子代理历史永远不会泄露回父级：父级看到的是一个紧凑的工具结果，而不是内部对话。
* 更便宜的模型也能做叶子工作。编排器运行在强推理模型上；研究子代理可能运行在更快、更便宜的模型上。

### 内置原型

每个原型都位于 `agents/<name>/` 下，带有一个 `agent.toml` （元数据、工具范围、模型提示）和一个提示词。编排器的聊天委派就是其 `[subagents]` 允许列表中的那些：

| 原型                   | 当编排器选择它时                                    |
| -------------------- | ------------------------------------------- |
| `orchestrator`       | 主代理：顶层、可直接执行的默认项。不会被其他编排器生成。                |
| `task_manager_agent` | 添加、预览、获取、更新或移除任务源、工作流包、工件。                  |
| `vision_agent`       | 任何依赖图像内容的东西（描述、OCR、UI 元素）。                  |
| `image_agent`        | 生成或编辑图像。                                    |
| `video_agent`        | 生成视频或为图像制作动画。                               |
| `presentation_agent` | 构建带有依据和引文的幻灯片（功能 `文档`).                     |
| `技能设置`               | 查找、安装和管理来自注册表的技能（`setup_skills`).           |
| `workflow_builder`   | 编写或编辑已保存的工作流（`build_workflow`）；仅返回提案，绝不会保存。 |
| `flow_discovery`     | 建议值得设置的工作流（`discover_workflows`).           |

其他内置项绝不会作为聊天委派：

| 原型                                   | 谁运行它                                                                                                 |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `规划器`, `critic`                      | 只有 `parallel_research_cross_check` 工作流运行模板（decompose and research = `规划器`, cross\_check = `critic`). |
| `summarizer`                         | harness，用于压缩过大的工具结果；也是该 workflow-run 模板的 synthesize 阶段。                                              |
| `trigger_triage` / `trigger_reactor` | 对传入的外部事件进行分类，以及对此的轻量级响应。                                                                             |
| `morning_briefing`                   | Cron：每日摘要，位于一个命名的只读工具带上。                                                                             |

编排器本身通过内联技能处理编码、加密、设置、日程、产品文档和 MCP 服务器： `use_skill` 加载一个工具包的指南和 schema（`coding`, `web3`, `system`, `scheduling`, `docs`, `mcp`），并且相同工具是 `延迟的` ，因此 `tool_search` 找到任何一个单独项。运行已安装的技能是编排器自己的 `run_workflow`。参见 [`tools/toolpacks/README.md`](https://github.com/tinyhumansai/openhuman/blob/main/crates/openhuman-core/src/tools/toolpacks/README.md).

自定义原型作为 TOML 文件随包位于 `$OPENHUMAN_WORKSPACE/agents/*.toml` （或 `~/.openhuman/agents/*.toml` （用于用户全局的专门代理）。自定义定义在 id 冲突时会覆盖内置项。

### 运行可复用的子代理

当编排器调用 `spawn_subagent`时，默认契约是持久且异步的。该工具会从父会话/线程、代理 id、模型覆盖、沙箱模式、动作根目录以及规范化后的任务键/标题构建一个确定性的兼容选择器。然后它会在生成之前检查 `orchestration::subagent_sessions` ：

* 如果已有兼容工作器正在运行，指令会通过其 `RunQueue` 注入，而父级会得到一个快速的 `subagent_session_id` / `task_id` 引用。
* 如果兼容工作器处于空闲或暂停且有可复用历史记录，harness 会为相同的持久 `subagent_session_id` 启动一个新的临时运行，并通过 `SubagentRunOptions.initial_history`传入保存的子级历史，并将新的指令作为对用户可见的后续附加上去。
* 如果形状不兼容、工作器已关闭， `fresh: true` 被传入，或者不存在会话，则 harness 会创建一个新的持久会话和工作线程。

子级运行由 OpenHuman 主机通过中立的 TinyAgents 生命周期进行准备和执行：

1. 从其类型化运行上下文中接收父级取消和工作区；其余遗留的 provider、沙箱和转录输入仍保持作用域，直到各自明确的迁移落地。
2. 解析子代理的模型：先看内联 `model` 覆盖，然后看配置级别的固定项（`[orchestrator].model`, `[teams.*].lead_model`, `[teams.*].agent_model`），然后是原型提示或继承的父级模型。
3. 按定义的 `tools`, `disallowed_tools`过滤父级的工具注册表， `skill_filter`。在 `fork` 模式下，父级的完整注册表会原样继承。
4. 构建窄系统提示词，省略定义要求剥离的部分。
5. 将准备好的子级交给 `tinyagents-orchestration` 以进行生命周期排序，并且只执行一次暂停或终止持久化动作。
6. 在持久化的 `subagent_session_id` 下保存子级历史和工作线程指针，以便之后的轮次可以恢复或检查它。暂停继续也会保留其完整的 TinyAgents 任务键。

`wait_subagent` ，并且 `steer_subagent` 可以接受持久的 `subagent_session_id` 或临时的 `task_id`；跨轮次更偏好持久 id。 `list_subagents` 显示当前父线程中可复用的子级，而 `close_subagent` 会把工作器标记为不可复用，并在其仍在运行时取消它。通过 `blocking: true`.

合成的原型委派（`delegate_*`, `build_workflow`，以及其他 `delegate_name` 工具）遵循相同契约：默认通过持久的异步路径路由，立即返回一个 `[async_subagent_ref]` （带有 `subagent_session_id` + `task_id`） `立刻，并且完成结果会通过`/`background_delivery`以新的系统轮次插入父级聊天。其背后的队列是 harness 的 `tinyagents_tasks::CompletionRouter` ，运行在每工作区一个 `JsonlCompletionStore` (`.openhuman/background_completions.jsonl`之上，以聊天线程为键）：它按任务 id 去重，为父级已内联收集的结果或被 Stop/delete 取消的结果保留墓碑，统计投递尝试次数，并返还那些已放弃的记录。记录会一直保持租用，直到投递轮次落地（`mark_delivered`），因此即使某个结果已完成但从未投递，重启后也会重新投递。宿主保留 `<background_agent_*>` 措辞（一个 `CompletionFormatter`），3 秒空闲去抖、系统轮次及其 web\_chat 事件，以及把已放弃结果持久化到线程中的写入器。投递轮次会持久化自己的结束消息， `sender: "agent"`，id `agent:<run_id>`, `extraMetadata.requestId = run_id`，然后再发出 `chat_done` 作为 `client_id: "system"`；前端会复用该 id，因此其通常的 `chat_done` append 会合并到同一行上（对这些确定性的 `agent:`前缀 id，conversation store 是幂等的；所有其他 id 都是全新的 UUID，并保持恒定时间的 append 路径）而不是第二次持久化已投递的结果。交互式轮次遵循相同契约： `web_chat::presentation::deliver_response` 将回复存储在 `agent:<request_id>` 下，然后再发布 `chat_done`，因此核心生成的答案无论是否有客户端在那里接收公告，都会存在于磁盘上；断开的 socket 或重新加载的 webview 只会带来一次重绘，而不是丢失回复。例外是分段投递，此时客户端为每个分段拥有一行，而核心不存储任何内容；前端正是出于这个原因在那儿保留生成的 id。当没有父代理轮次或没有可投递到其中的聊天线程（cron/CLI）时，或者当 `blocking: true` 被传入时，它们会自动回退到内联阻塞。跨轮次连续性来自三部分：每轮的 `[active_subagents]` 名册将实时内存注册表与持久 `subagent_sessions` 存储合并（因此冷启动的编排器仍能看到较早的工作器）； `continue_subagent` 从暂停检查点回退到持久存储，用其持久化历史恢复空闲工作器；以及一个 `workflow_proposal` 负载若在已完成子级的历史中被找到，则会作为父线程消息持久化（`extraMetadata.scope = "workflow_proposal"`），前端在加载线程时会将其重新水合为提案卡片。

### 生成层级与层级

并非每个代理都允许生成每个其他代理。harness 建模了一个三层层级，它反映了模型之间的成本 / 延迟 / 思考深度划分：

```
Primary     (可直接执行，带 `coding` 提示的主代理)
  │
  ├─► Worker      ◄─── 快路径：一次委派，叶子执行工作
  │
  └─► Reasoning   (慢、深思考，例如带 `reasoning` 提示的 planner)
        │
        └─► Worker  ◄─── 深路径：推理分解，工作器执行
```

每个 `AgentDefinition` 都携带一个 `agent_tier` 字段（`chat` / `reasoning` / `worker`，default `worker`）。契约：

| 层级          | 可生成                   | 不得生成                     | 典型成员                                          |
| ----------- | --------------------- | ------------------------ | --------------------------------------------- |
| `chat`      | `reasoning`, `worker` | 另一个 `chat`               | `orchestrator`                                |
| `reasoning` | `worker`              | 另一个 `reasoning`任何 `chat` | `规划器` （今天的规范项）                                |
| `worker`    | 无[^1]                 | 任何                       | critic, image\_agent, task\_manager\_agent, … |

为何要有这些规则。

* *聊天 → 聊天没有意义。* 聊天层存在是为了快速 UX。一个聊天代理再生成另一个聊天代理，只会把 TTFT 翻倍并浪费 token，却没有买到任何新的能力。
* *推理 → 推理会爆炸式增加深度。* 推理层很昂贵。推理代理链往往会反复分解同一个问题，并创建失控的层级。
* *工作器 → 任何事会混合执行与编排。* 工作器是叶子节点，因此父级始终看到的是一个紧凑结果，而不是嵌套委派的转录。

强制执行。两层：

1. 加载时（静态）。 `tinyagents_definition::DefinitionSet::validate_tier_hierarchy` 会遍历合并后的注册表（内置项 + 工作区 TOML），并拒绝启动一个列出了同层级或 worker-with-subagents 条目的注册表。内置原型在加载时检查；用户随包分发的 TOML 在工作区加载时检查。
2. 运行时深度门控（动态）。与层级无关，子代理运行器将总的生成链深度上限设为 `MAX_SPAWN_DEPTH = 3` ，通过一个任务局部计数器递增实现 `run_subagent`，并表现为一个 `SpawnDepthExceeded` 代理错误。这使得即便用户随包分发的 TOML 去掉了层级标注，也仍然不能递归超过三跳。

> 状态：加载时的层级检查， `agent_tier` 字段，以及运行时深度计数器的 task-local 都已启用。深度同时受到静态加载器契约和运行时 `MAX_SPAWN_DEPTH = 3` 保护的约束。

### 工具包特定专家

对于拥有数百个操作的 Composio 工具包（仅 GitHub 就有 500+），把每个操作都加载进子代理的工具集会让提示词规模膨胀。该 harness 用一个廉价的纯 CPU 过滤器（动词检测、词元重叠、动词对齐加成）把工具包的操作与经父级精炼后的任务提示词进行排名，只把排名靠前的子集加载到子代理中。无需模型调用，纯启发式，快速且可解释。

## 分诊：处理外部触发

当 webhook 触发、cron 走时，或 Composio 事件到达时，系统不能直接把它交给编排器。大多数触发只是噪声；有些值得通知；只有少数值得完整走一次代理回合。触发分诊流水线就是这道门。

```
TriggerEnvelope ──► run_triage ──► TriageDecision ──► apply_decision
                       │                                     │
                       │                                     ├─► 丢弃（噪声）
                       │                                     ├─► 仅通知
                       │                                     ├─► 生成 trigger_reactor
                       │                                     └─► 生成 orchestrator
                       │
                       └── 小型本地 LLM（失败后回退到云端 LLM 重试）
```

评估器刻意做得很便宜：优先使用本地小模型，重试时回退到远程模型。该决策会被缓存，因此相同触发不会重复分类。只有升级为“生成 orchestrator”的触发才会经过完整的 `Agent::turn` 机制。

## 钩子：可观测性与策略控制旋钮

两个钩子面包裹着循环，位于相对的两端：

### 停止钩子（回合中）

停止钩子在工具调用循环的各次迭代之间触发。它们是预算上限、速率限制和自定义熔断开关的策略控制旋钮。内置钩子：

* 预算停止钩子：使用每次迭代的费用累加器，将单回合累计成本上限限定为 USD。
* 单次调用支出预算：每个提供方调用在发送前都会先为该回合的共享账本预留一个保守上限，并根据实际使用量结算。它在附件渲染之下生效，因此照片、贴纸或文档回合会按其渲染后的文本准入，而图像读数的费用也记入同一账本。在预算启用期间，模型接收的是图像读数和提取出的文档文本，而不是原生媒体。
* 最大迭代次数停止钩子：从代理持久配置之外限制迭代次数。
* 动作预算策略： `SecurityPolicy` 强制执行 `config.autonomy.max_actions_per_hour` 对会产生副作用的工具操作。用户可在 Settings、Advanced、Agent autonomy 中调整，或由运维通过 `OPENHUMAN_MAX_ACTIONS_PER_HOUR`.

返回 `Stop` 会以一个清晰的原因中止循环，调用方可将该原因展示给用户。停止钩子与中断（下一节）不同：它们由策略驱动，而非用户驱动。

### 回合后钩子

回合后钩子在回合完成后于后台触发。它们会拿到一个 `TurnContext` 快照、用户消息、助手响应、每次工具调用及其参数和结果、总墙钟时间、迭代次数、会话 ID。内置消费者：

* 成本日志：最终的每回合成本行。
* 对话日志： `记忆` 生命周期会在回合结束后记录回复，并附带一行工具结果（绝不包含参数）；没有批处理或空闲刷新。见 [记忆架构](/openhuman/zh/kai-fa/architecture/memory.md).

长期事实会被显式写入：代理会调用 `记忆` 工具的 `learn` 动作。没有自动档案员。

钩子通过 `tokio::spawn`运行，因此用户会先拿到回答，然后这些钩子才全部结束。

## 中断：优雅取消

取消是 tinyagents 的转向通道。当用户按下 Ctrl+C 或发送 `/stop`时，运行器会把请求转入 harness 的转向/取消接缝，在安全点停止循环：在每次工具执行之前、在每次子代理生成之前，以及在每次提供方调用之前：

* 每个正在运行的子代理都共享取消作用域，并会在下一个检查点退出。
* 正在进行中的提供方流会被丢弃。
* 缓冲的对话回合仍会被存储（空闲刷新，或退出时的退出刷新）。

中断由用户驱动；停止钩子由策略驱动。二者都进入同一套 harness 的暂停/停止管线，但来源不同。

## 成本核算

每个提供方响应都带有一个 `UsageInfo` 块，包括输入 token、输出 token、缓存输入 token，以及由 OpenHuman 后端填充的权威 `charged_amount_usd` 。 `TurnCost` 会把一次回合中所有提供方调用的这些值相加，以便 harness 能够：

* 通过进度通道发出每次迭代的成本遥测。
* 为预算停止钩子供给数据，使失控回合能在循环中途自行切断。
* 记录准确的回合结束成本行。

当后端没有暴露 charged amount（旧版本、没有通过它计费的提供方）时，一个按层级划分的小费率表会提供 token 费率下限估算。只要后端可用，直接成本永远优先。

## Web 通道事件和 RPC（assistant-ui 元素通过）

assistant-ui-elements 集成增加了一批增量式 `WebChannelEvent`和 RPC，使前端无需轮询就能渲染工具参数/时序、计划/目标/队列状态，以及回合生命周期。 `EVENTS_VERSION` (`core/bus.rs`）是 `1.4.0`；所有新增字段都是可选/有默认值的，因此旧订阅者仍能解析新发布者发出的内容。

新增/扩展的 socket 事件（通过以下方式从 `DomainEvent` 桥接到 `WebChannelEvent` ，由 `web_chat::event_bus` ，并且 `openhuman_rpc::server::socketio`):

* `ts` （epoch 毫秒）会由 `publish_web_channel_event` 在生产者未设置时为每个事件写入，因此前端可以在不猜测接收时间的情况下排序/测量延迟。
* `chat_done.timing`: `{ first_token_ms, first_tool_ms, total_ms, tokens_per_second }`，由进度桥接器每回合的 `TurnTiming` 通过 `ProgressBridgeHandle::timing_snapshot()`. `tokens_per_second` 由以下公式得出 `output_tokens / (total_ms / 1000)` ，当两者都已知且 `total_ms > 0`.
* `chat_done.usage.context_tokens`：根代理最终模型调用的输入加输出 token。它用于上下文窗口仪表，不包含更早的调用和子代理； `input_tokens` ，并且 `output_tokens` 仍然保留为回合总支出。
* `chat_cancelled`: `{ thread_id, client_id, request_id, cancel_reason: "user_stop" | "superseded", superseded_by }`。与现有的 `chat_error{error_type:"cancelled"}`一起发出，而不是替代它；适用于无范围/有范围的停止路径、被更新请求取代的路径，以及并行回合的协作取消路径（后者此前根本不会发布终止事件）。
* `chat_error{error_type:"guardrail"}`：携带一个 `guardrail: { verdict, score, reasons: [{code,message}] }` 负载，当 `start_chat` 通过以下方式拒绝一条消息时： `StartChatError::Guardrail` （提示注入/安全护栏）。其余所有拒绝都保持为 `error_type:"inference"`。RPC 表面（`channel.web_chat`的 `Result<_, String>`）以同样的结构化裁决编码为一个 `GUARDRAIL:<json>` 哨兵字符串（`web_chat::ops::start_chat::{GUARDRAIL_ERROR_PREFIX, is_guardrail_error_message}`，与现有的 `BACKEND_UNAVAILABLE:` 模式相呼应）。
* `turn_cost`：来自 `AgentProgress::TurnCostUpdated`的实时每回合成本读数，按每回合最多 750 毫秒一次的频率节流（第一次更新总是立即发出）： `{ thread_id, client_id, request_id, round, usage: { input_tokens, output_tokens, cached_input_tokens, cost_usd, context_window, context_tokens, subagents } }`. `context_tokens` 是根代理最终调用的输入加输出，用于其上下文窗口仪表；不按调用或子代理求和。 `subagents` 在这个实时事件中始终为空（这里只是父级的累计汇总）；按子代理的归因仍只会出现在终端 `chat_done.usage`.
* `approval_decided` / `plan_review_decided`：由以下内容桥接而来 `DomainEvent::ApprovalDecided` / `PlanReviewDecided`，其新增了 `thread_id`, `client_id`, `tool_call_id`过滤父级的工具注册表， `resolution` (`"expired"` 在 TTL/清扫时， `"cancelled"` 在决策通道被丢弃时， `None` 对于普通用户决策，则沿用现有的线上 `cancel_reason` 字段）。只有当原始 park 同时具备 `thread_id` ，并且 `client_id` （聊天路由）。
* `run_mode_changed`: `{ thread_id, client_id: "", message: "plan" | "build" }`，由以下内容桥接： `DomainEvent::ThreadRunModeChanged`，由以下函数发布： `threads::run_mode::set_mode`.
* `thread_todos_changed`, `queue_item_queued` / `queue_item_delivered` / `queue_item_removed`, `queue_item` 携带 `{ id, lane, text_preview }`.
* 子代理关联： `subagent_spawned`/`subagent_completed`/`subagent_failed`/`subagent_awaiting_user` 携带 `subagent.parent_call_id` （生成它的工具调用的 id：某个 `spawn_parallel_agents` call 的每个 worker 都共享它）； `subagent_completed` 还携带 `subagent.output` （截断后的最终文本）。 `tool_call.args`/`tool_result.args`+`elapsed_ms` 现在携带真实参数和时序，而不是 `null` （来自 tinyagents 的 `AgentEvent::ToolStarted.input`).
* 工件事件（`ArtifactPending`/`ArtifactReady`/`ArtifactFailed`）携带 `tool_call_id` ，并且 `turn_request_id`，后者从起始回合的 `ApprovalChatContext::request_id` 填充，因此前端可以把工件卡片绑定到生成它的确切回合上（`None` 适用于没有聊天上下文的 CLI/cron/子代理生产者）。

新 RPC：

* `agent.set_run_mode { thread_id, mode }` / `agent.get_run_mode { thread_id }`，读取/切换线程的 Plan/Build `RunModeHandle`. `channel.web_chat` 还接受一个可选的 `run_mode: "plan" | "build"` 参数（与 socket `chat:start` 负载相呼应），因此一个回合可以在线程已处于所需模式时直接开始，而不是与另一个 RPC 调用竞争；无法识别的值会被记录并忽略。
* `threads.token_usage { thread_id }`：返回持久化的线程支出总计，以及 `last_turn_context_tokens`，即用于上下文窗口仪表的根代理最终调用输入加输出。没有逐调用用量的旧回合会回退到其输入加输出之和。
* `threads.goal_get` / `threads.todos_get`：直接读取线程当前的目标/todo 状态。
* `threads.token_usage { thread_id }`：返回持久化的线程支出总计，以及 `last_turn_context_tokens`，即用于上下文窗口仪表的根代理最终调用输入加输出。没有逐调用用量的旧回合会回退到其输入加输出之和。
* `threads.edit_message { thread_id, message_id, content, client_id? }` / `threads.regenerate { thread_id, message_id?, client_id? }`，二者都返回 `{ request_id }`。它们会取消线程中正在进行的回合，通过 `TranscriptLocator::truncate_into_next_generation` 在切点处分叉会话转录（被截断的封存代不会被触碰，同样保持“压缩不会擦除”的保证），截断对话存储中的消息日志以及所有被丢弃回合的回合状态快照，然后用编辑后的内容（`edit_message`）或原始提示词（`regenerate`）重新开始回合。完整的 UI-message-id -> transcript-cut-point 映射见 `threads::ops::edit`的模块文档。
* `channel.web_queue_remove { client_id, thread_id, item_id }`，从线程的运行队列中移除一个特定的排队项。
* `agent.context_breakdown { agent_id?, thread_id? }`，这是一个面向 UI 的视图，展示代理渲染后的提示词规模（system/tools/history 拆分），用于 composer's 上下文使用指示器。
* `commands.list`：将内置命令与 `skills.list` ，并且 `flows.list` 合并成一个斜杠命令目录。

`plan_exit` （把线程从 Plan 切回 Build 的工具）只有在该线程上没有仍然停放着的计划审查时才会切换模式（`plan_review::gate::PlanReviewGate::parked_review_for_thread`），因为审查会在 `request_plan_review` 把控制权交还给代理之前完成，因此这只在一种时机不对/并发的 `plan_exit` 调用与仍待处理的审查赛跑时才重要，而在用户实际上批准任何内容之前，不能让每个工具都解锁。

## 显式运行上下文与宿主能力

`OpenHumanRunContext` 是共享聊天、通道和子代理回合接缝处的实时承载体。根节点会快照其当前作用域下的 origin、progress、stop hooks、dispatch 状态、thread、route slot 和 workspace grant，然后在把上下文传给运行器之前持有或显式接收一个取消 token；递归子代理会通过 `child()`对其分叉，在隔离路由观测和用量核算的同时保留共享的取消/策略句柄。共享运行器会把 `RunContext<OpenHumanRunContext>` 通过 `into_tinyagents`，而 OpenHuman 的组装和中间件注册表会直接消费这个类型化上下文。对于类型化 harness 边界之外的旧工具/模型 API，路由和线程作用域仍然只是在驱动周围包裹。递归分流会从其类型化父级 `RunContext` 通过 `ToolDispatch`接收取消和 workspace；它从不读取环境中的取消状态。

`OpenHumanHostBundleFactory` 是 B1 宿主组装点。它从相同的会话/运行时输入构建上下文、定义、安全、模型、记忆、预算、进度、工具结果和体验适配器。OpenHuman 保留所有策略决策；TinyAgents 目前只接收由此产生的规范运行上下文。将完整的宿主能力捆绑注入每次调用仍是后续切换步骤。

## 自愈回顾

主循环之上还叠着几个小型自适应系统：

* 工具输出摘要熔断器：某线程连续三次摘要失败后，TinyJuice 会停止在该线程里请求摘要，因此结果会回退到压缩和截断。
* 本地与远程重试分诊：先用本地 LLM；解析失败时回退远程。
* 未知工具和参数格式错误恢复：中间件把无效的模型工具调用重写成可恢复的结果，而不是中止运行。

这些都不会改变循环的形态，只是让常见失败模式可以在无需用户介入的情况下恢复。

## 代码中哪里看

harness 外壳位于 `crates/openhuman-core/src/agent/`，tinyagents 适配器接缝在 `crates/openhuman-core/src/agent/tinyagents/` ，原型定义在 `crates/openhuman-core/src/agent_registry/`。 `crates/openhuman-core/src/agent/` 中的 README 列出了公共表面；最关键的文件（相对于 `crates/openhuman-core/src/agent/` 路径，除非前缀另有说明）是：

| 文件 / 目录                                | 那里包含什么                                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `session_host/runtime_session.rs`      | `Agent::turn`：上面描述的生命周期；通过 `tinyagents/turn_exec.rs`.                                            |
| `tinyagents/mod.rs`                    | `TurnSpec::run`：共享的 tinyagents harness 组装（实时循环）。                                                 |
| `tinyagents/middleware.rs`             | 命名的 OpenHuman 中间件栈（审批/安全、工具策略、恢复、预算、熔断器）。                                                        |
| `../channels/runtime/turn_driver.rs`   | 通道/CLI 总线回合驱动器，与通道运行时放在一起并路由到 tinyagents 运行器。                                                    |
| `subagent_host/`                       | 直接的 OpenHuman 规划器/执行器/持久化适配器，用于 `tinyagents-orchestration::subagent`；产品提示词/工具/模型/安全/工件/进度行为仍在这里。 |
| `orchestration/subagent_sessions/`     | 持久可复用的子代理身份、兼容性匹配、持久化状态/历史。                                                                      |
| `../agent_registry/host_definition.rs` | 关于 `tinyagents_definition::AgentDefinition`.                                                     |
| `subagent_host/ops/runner.rs`          | 集成工具排名和宿主执行叶子；通用生命周期仍留在 `tinyagents-orchestration`.                                              |
| `tinyagents/payload_summarizer.rs`     | TinyJuice 那个超大工具结果摘要背后的模型调用。                                                                     |
| `session_host/tool_progress.rs`        | 幸存下来的 OpenHuman 接缝： `TurnProgress`.                                                              |
| `message_convert.rs`                   | 围绕规范工具调用方言 API 的具体持久化/提供方转换。                                                                     |
| `triage/`                              | 外部触发分类 + 升级。                                                                                     |
| `../agent_registry/agents/`            | 内置原型：每个代理一个子目录。                                                                                  |
| `hooks.rs` / `stop_hooks.rs`           | 回合后和回合中钩子接口。                                                                                     |
| `cost.rs`                              | 按回合的美元/Token 计费。                                                                                 |
| `progress.rs`                          | 向 UI 实时推送进度事件。                                                                                   |

## TinyAgents 上的代理引擎与编排

每个代理回合都使用 TinyAgents。聊天和 channel/CLI 调用方会调用 session/turn host，而子代理调用则进入 `agent/subagent_host` 及其直接 `tinyagents-orchestration::subagent` 生命周期。没有 `harness/subagent_runner` 兼容路径。周边的 OpenHuman 接缝：

| 文件（`crates/openhuman-core/src/agent/tinyagents/`) | 角色                                                                                                                         |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `mod.rs`                                          | 运行器（`TurnSpec::run`）：安装原生 `ChatModel`、宿主工具适配器以及中间件到一个 `AgentHarness`；运行一个回合；通过原生 `MaxTokensModel`；镜像进度；转发引导；并在模型调用上限处优雅暂停。 |
| `mod.rs` / `model.rs` / `tools.rs` / `convert.rs` | `RunPolicy` / `ChatModel` / `Tool` / 消息适配器（包括未知工具策略和带外推理转发）。                                                               |
| `observability.rs`                                | Harness `AgentEvent` → `AgentProgress` + 成本； `GraphTracingSink` 用于图事件。                                                     |
| `orchestration.rs`                                | 重新导出 `graph::orchestration` task-store 类型；map-reduce 扇出直接使用 TinyAgents SDK 接口。                                             |
| `orchestration/README.md`                         | OpenHuman 的子代理适配器和面向产品的持久投影；通用任务执行属于 TinyAgents。                                                                           |

基于图的编排（`crates/openhuman-core/src/agent/orchestration/`):

* 工作流执行使用 TinyFlows 图执行；OpenHuman 保留工作流定义、控制器接口和持久运行记录。
* 代理团队执行和通用委派属于 TinyAgents；OpenHuman 保留产品策略、适配器和面向 UI 的投影。
* 脱离式子代理（`running_subagents.rs`）使用 TinyAgents `DetachedTaskRegistry` 用于具备所有权感知的快照、等待/超时、引导查找、协作式取消、强制中止和终结清理。OpenHuman 保留持久 task-store 投影、产品/会话元数据、RPC 和传递语义，以及 `RunQueue` 兼容回退。

故意不放入该 crate 的基础原语中（这些是设计决定，不是缺失）：

* 子代理宿主策略（`subagent_host/`）仍归 OpenHuman 所有：定义解析、原型工具过滤、提供方解析、窄提示构建、记忆上下文、工作线程镜像、交接配置以及产品检查点/会话投影。它直接实现 TinyAgents 的 planner/executor/persistence traits。中立驱动器，而不是宿主本地运行器，负责生命周期排序和任务键合并。
* 持久运行账本（`workflow_runs`, `agent_teams`, `command_center`, `subagent_sessions`）继续保留在 openhuman SQLite/JSON 中，直到其控制器投影和重启语义映射到 TinyAgents 的 task/status/journal 记录上。该 `agent_teams` 具备竞争安全的 SQL compare-and-swap 任务领取仍归 OpenHuman 所有。

> TinyAgents 提供 harness store/cache/session 原语（`harness::store` 带 JSONL 追加存储， `harness::cache`, `harness::subagent`、具备血缘感知状态）、图任务存储、脱离式运行时注册表以及一致性契约。会话壳层和产品特定的子代理构建/交付流水线仍归 OpenHuman 所有。

## 可靠性：熔断器、回传和分类失败

三个协同机制防止运行走偏或静默失败：

无进展熔断器（`RepeatedToolFailureMiddleware`, `crates/openhuman-core/src/agent/tinyagents/middleware.rs`）是该 crate 中 `NoProgressTracker`的一个薄驱动层。它为每次工具调用的参数生成指纹，并将结果输入升级阶梯： `继续` → `轻推` （通过以下方式注入的结构化“自步骤 X 以来无进展”纠正信息： `SteeringCommand::InjectMessage`，在交互式回合中是安全的）→ `停止` （将根因摘要记录到 `HaltSummarySlot`，并通过引导句柄暂停）。使用相同参数重试会计入触发条件（阈值为连续 3 次相同失败）； *可恢复的* 失败（超时、连接重置、速率限制、5xx）会使用扩展余量阶梯，而不是固定的 crate 阈值。

子代理回传（`subagent_host/`）：子代理运行会解析为以下三种状态之一：

* `已完成`：干净的最终响应。
* `AwaitingUser { question, options }`：子进程调用了 `ask_user_clarification`；宿主会在完整的 TinyAgents 任务键下投影其完整检查点（历史、问题、选项和覆盖项） `（root_run_id, parent_run_id, thread_id, task_id）`。恢复时会找回原始键，而不是从新的父回合派生新键。
* `Incomplete { reason }`：子进程被熔断器停止或达到模型调用上限。委派父进程会转发阻塞原因，而不是把被停止的子进程当作已完成答案，或重新发起相同的委派。

顶层的熔断停止同样绝不会静默结束，而且熔断器的根因摘要也不会原样展示给用户：它是为模型措辞的（“将此反馈回来，而不是重试”）。 `hit_cap` / `breaker_halt` 会在回合结果中显现，而聊天回合会以与关闭未产生最终文本的工具回合相同的方式关闭已停止的运行（`session_host/driver/grounded_close.rs`):

1. 一个禁用工具的收尾调用，其指令会重述该回合的工具记录，并包含每次失败各自的信息，同时将熔断摘要作为停止说明传入，用于解释而不是重复。
2. 一个单独的检查调用，只看到请求、记录和候选回复。它会拒绝仅叙述意图、与记录相矛盾，或遗漏解释未完成请求的失败信息的回复。
3. 针对空回复、调用工具的回复、被拒绝的回复或未验证回复（检查失败或未给出裁定）的确定性回退。它会引用每个工具结果和停止说明。

只有在检查通过后才会流式输出被接受的文本，因此被拒绝的回复不会渲染。

分类工具失败（`tinytools::status`，作为 vendored 代码位于 `vendor/tinyagents/vendor/tinytools`）：每次失败的工具调用都会被分类为一种与传输无关的 `ClassifiedFailure { class, category, cause_plain, next_action, recoverable }`。类别包括 `MissingPermission`, `MissingApp`, `ServiceUnavailable`, `BadCredentials`, `BlockedByPolicy`, `ModelConnection`, `Timeout`, `Denied`, `ApprovalExpired`；类别与 UI 状态一一对应： *可恢复的* （安全自动重试）， *被策略阻止* （更改设置）， *需要用户确认* （登录 / 安装 / 授权）， *用户拒绝* （绝不自动重试）。该分类会随 `AgentProgress::ToolCallCompleted.failure` （包括子代理调用）进入聊天时间线。

## 日志、重放和迁移阴影

每次运行都会追加到持久事件日志（`tinyagents/journal.rs`）： `StoreEventJournal` 基于位于以下路径的 JSONL 追加存储上的 `{workspace}/tinyagents_store/journal`，由以下组成： `FanOutSink` （实时桥接 + 日志）→ `RedactingSink` （持久化前进行凭据遮蔽），并使用重启稳定的事件 ID（`{run_id}-evt-{offset}`）。即使未被观察到的后台回合，事后也能重建。三个只读 RPC 会暴露它： `agent_run_events` （分页、后附加重放，按 `run_id`/`offset`/`limit`), `agent_run_status` （最新 harness 状态），以及 `agent_runs_active` （活跃运行，可按线程或根运行过滤）。

剩余的存储切换在阴影脚手架上运行。产品行为保持不变，差异会被记录：

* 会话存放在存储后端中（`DriverSessionStores`）；文件时代的双写和阴影读取已被移除，而且 `config.session_dual_write` / `session_shadow_reads` 不再产生任何影响。

目标和待办已完全由 crate 支撑，不再有阴影：线程目标位于 crate 的 `graph.goals` KV 存储（`agent/goals/store.rs`），而会话待办列表位于进程内 crate 的 `graph.todos` 存储（`agent/todos/ops.rs`）；参见 [目标和待办](/openhuman/zh/gong-neng/goals-and-todos.md).

## 工作负载路由和 burst 层

`tinyagents/routes.rs` 是声明式的 TinyAgents `ModelRouter` 用于 OpenHuman 工作负载角色 `chat`, `reasoning`, `agentic`, `coding`, `burst`, `summarization`过滤父级的工具注册表， `vision`，以它们的 `hint:*` 别名为键。它负责回退链和能力门控； `inference::provider::factory` 将每个所选角色解析到其配置的原生 `ChatModel`，在托管后端上，则解析到固定的默认模型（`openrouter/deepseek/deepseek-v4-flash` ，除非在 设置 → 路由 → 默认模型 下更改）；没有按角色划分的 tier 端点。该 `burst` 角色服务于低上下文、高扇出的 worker。

## 另请参见

* [架构概览](/openhuman/zh/kai-fa/architecture.md)：harness 在整体架构中的位置。
* [记忆](/openhuman/zh/gong-neng/memory.md) ，并且 [记忆架构](/openhuman/zh/kai-fa/architecture/memory.md)： `记忆` 工具、按回合的记忆生命周期和对话日志。
* [自动模型路由](/openhuman/zh/gong-neng/model-routing.md)：如何 `model: "hint:reasoning"` 解析为具体的 provider+model。
* [原生工具：代理协调](/openhuman/zh/gong-neng/native-tools/agent-coordination.md)：面向用户的 `spawn_subagent`, `delegate_*`, `待办`.

[^1]: 技能通配符条目（`{ skills = "*" }`）被豁免，因为它们没有指定任何代理：它们会扩展为连接的 Composio 动作为 `延迟的` 工具，代理是通过 `tool_search`接触到这些工具的，而不是生成一个子代理。
