> 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 负责提供方实现以及规范化的类型化通道和 web 事件契约。

## 将 OpenHuman 作为库嵌入

`openhuman_embed` 提供一个两步 API。 `Runtime::builder()` 在每个进程中启动一个进程内 core、后台服务、已注册的领域家族、后端 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` 封装，其中内部的 `arguments` 对象没有任何提供方能够对解码进行校验或约束。

它接收的是工厂，而不是工具带。 `Agent` 是 `Clone` ，并且 `Box<dyn Tool>` 不是，而 spec 背后的会话会从 `Config` 在每次轮次中重建，因此持有 `dyn Tool` 的任何内容都无法在轮次之间存活。因而该闭包每轮运行一次，这也意味着，工具属于比代理更短命之物（一个 episode、一个 room、一次 assignment）的宿主，可以每次返回不同的工具带，而不是注册第二个代理。

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

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

按代理隔离的是一个上下文，而不是第二个 core。 `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()` 调用；不要为每个代理各建一个 core。每次调用都会拥有一个独立会话，除非提供了先前的 session id。

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

`Workspace::Inherit` 并配合 `Provider::inherit()` ；故意不使用 API key 的推理不会走库路由。它借用了已安装的 OpenHuman 配置，因此保留已安装应用的会话检查。在不进行应用登录的嵌入场景中，请显式提供 provider 或 API key。

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

每个代理轮次都通过公开的 [`tinyagents`](https://crates.io/crates/tinyagents) harness 运行。有三个入口点： `Agent::turn`、通道和 CLI 总线路径，以及 `run_subagent`。它们都通过 [`crates/openhuman-core/src/agent/tinyagents/`](https://github.com/tinyhumansai/openhuman/tree/main/crates/openhuman-core/src/agent/tinyagents/README.md) (`中的适配器接缝进入`）。上下文裁剪由 `MessageTrimMiddleware`负责，而取消则走 tinyagents 的 steering channel。策略停止钩子（预算、线程目标和迭代上限）使用上游的 `tinyagents-hooks` `StopHookMiddleware` ，并在第一次停止投票时暂停运行。网页聊天接入和队列机制使用 TinyAgents 的 `TurnRunner`；OpenHuman 的 `web_chat` 宿主提供入口接入、会话检出、审批、持久化和终端事件策略。通道提供方传输以及类型化 web 事件 schema/投影由 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)当前宿主表面在 [orchestration 所有权图](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 的投影记录在 [orchestration 所有权图](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`.
* 脱离子代理注册表由一个类型化的 `TaskStore` 生命周期账本支撑（Pending、Running，然后是 Completed、Failed 或 Cancelled）。

## TinyAgents crate：特性与兼容性

OpenHuman 通过路径依赖到 vendored git 子模块 `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 拥有的产品策略。每条在线路径都是一个 `Arc<dyn ChatModel<()>>`：TinyAgents 的 OpenAI 兼容客户端覆盖线级等价的托管、本地和 BYOK 路径，而宿主 `ChatModel` 实现覆盖 Claude SDK/Code 和 Codex 特定传输。OpenHuman 仍负责凭据解析、OAuth、访问门禁、端点选择、外发披露、计费元数据和错误分类。
* 一条原生 SQLite 链服务于 OpenHuman 的持久化。OpenHuman 的 root 世界和 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 模块通过类型化总线操作和共享 wire 类型准备。OpenHuman 施加账户授权和宿主策略，然后提供 TinyAgents 的会话与转录机制所使用的持久化端口。TinyAgents 负责通用的轮次执行、会话身份、转录重放和可复用的轮次运行器。提供方传输、归一化和规范化 web 输出投影留在 TinyChannels 中；OpenHuman 负责账户授权、审批、会话与交付策略，以及宿主适配器和持久化实现。

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

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

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

## 会话与 `Agent::turn`

会话是一个正在运行的实例对应的实时对话。 `Agent` 实例正在运行。该 `Agent` 结构体拥有：

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

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

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

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

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

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

* 工具块也算数，而且它 *排在最前面*。生产中的每个前缀缓存都会在对话前渲染工具目录，聊天模板必须把它放在模型在首轮用户轮次之前会读取到的位置。OpenAI 的自动缓存、Anthropic 的 `cache_control` （tools → system → messages）、DeepSeek 的上下文缓存以及任何 vLLM/SGLang radix 缓存都是这样工作的。所以一个在轮次之间变化的 `tools` 数组也会使冻结的系统提示失效，而请求体的 JSON 键顺序（它把 `messages` 放在 `tools`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 会拒绝未知的 content-part 字段。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` 本身，始终是 `无` ，因此该标志仅用于诊断，任何标记或键都从未传到线上。 `[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`。两个层级只在构建系统提示词时加载一次（每轮不会重新读取，因此 frozen-prefix / KV-cache 契约成立）：

* 全局， `<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 个字符），并带有 `[... truncated]` 标记，这样大文件就不会挤占提示词空间。

加载器是 `agent::prompts::agents_md` （返回预加载字符串的纯函数，在读取时就已设定边界，这样异常的多 MB 文件也不会在渲染时上限生效前耗尽内存）； `PromptContext` (`agents_md_global` / `agents_md_local`） `并由`AgentsInstructionsSection`PromptSource::Dynamic`）通过 `render_*` 辅助函数组装自己的正文，因此同样的 `并由` 会由 `SystemPromptBuilder::from_dynamic`居中注入，附加在代理自身正文之后、中央 grounding 合约之前，而不是由每个 `agents/<id>/prompt.rs` 构建器负责。该功能由 `agent.agents_md_enabled` （默认开启）；关闭后，不会加载或注入任何 AGENTS.md 内容。

## 工具调用循环

在 `Agent::turn`中，工具调用循环是内部引擎。它是已发布的 tinyagents crate 的 `AgentHarness` 循环，由每一轮通过 `中的适配器接缝进入` ([`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` 以及其 `checkpoint` 文本：

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

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

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

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

`agent.tool_dispatcher` （可通过一次启动覆盖，使用 `OPENHUMAN_TOOL_DISPATCHER`）来选择工具如何与模型对话。 `auto` （默认）使用原生工具调用，通过 `ChatModel` 适配器传递结构化工具规范并接收结构化回调，只要提供方配置文件支持；对于本地 Ollama 之类的提示词引导型提供方，则回退为 tag 内 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 = "Only the GitHub MCP server is approved."

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

规则如何生效：

* **优先级。** `拒绝` 在同一层内优先。 `隐藏` 让工具仍可调用，但不显示在列表中。 `require_approval` 会像声明了 `approval_required`; `auto_approve` 的工具一样延后调用，但会豁免该声明。
* **模式。** Glob 支持 `*` ，并且 `?` 并忽略 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 工具中间件强制执行。超出的部分会被硬截断，并附带说明性标记，以便模型知道自己没有看到完整输出。
* Microcompact / autocompact：当总历史逐渐逼近上下文窗口时，tinyagents 中间件（消息裁剪 + 安装在 `tinyagents/middleware.rs` ，并且 `tinyagents/context_wiring.rs`中的压缩钩子，阶梯顺序和大小由 `tinyagents-harness` `ContextLadder`）会在下一次提供方调用之前，把较早的轮次压缩成摘要。压缩后的历史会保持系统提示词和最近的轮次不变（KV-cache 稳定性），并重写中间部分。摘要由该轮自己的模型生成，除非 `[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 提取，以及静态工具声明和 wire DTO，都属于 TinyJuice 和 `tinyjuice-bus`。OpenHuman 的工具是适配器：它们把类型化请求发送给模块，并将文件系统授权保留在宿主侧。句柄查询针对模块的 CCR 存储运行；artifact 查询把经授权的内容而不是路径发送给同一个模块操作。宿主没有并行的 CCR 存储，也没有本地算法回退。如果模块不可用，必需查询和提取会报告该操作不可用；可选的工具输出压缩则保持其文档所述的透传行为。即使当前设置不再创建新的句柄，恢复的线程也会保留其记录的 Juice 声明和 schema；当前策略和模块可用性仍然决定执行。

调用方焦点。工具可以选择启用一个可选的 `summary_focus` 参数，只需在其 schema 中添加 `tokenjuice::focus::summary_focus_property()` 。 `web_fetch` ，并且 `web_search_tool` 会这么做。模型用它来说明自己从结果中需要什么，例如“速率限制”。 `before_tool` 会在验证之前将该参数从调用中移除，因此工具永远看不到它。只有 schema 中携带该精确属性的工具才会失去它；任何其他具有同名参数的工具都会保留它。它会随结果一起到达 TinyJuice，在那里它会引导摘要、为缓存加键，并为确定性压缩器对文本排序。像 `web_fetch`file\_write

### 文件系统卸载： `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` 的子代理会在其系统提示词中获得一份长期 Artifact 卸载契约：超过大约 2 000 个 token 的结果会写入位于以下路径下的文件： `outputs/`，回复则是该相对路径加上一个简短摘要。这个门控是有意设计的：提示词只能列出代理确实能调用的工具，否则模型会发出失败的调用。经过技能过滤的专门代理不会获得契约文本，专门的守卫会断言它们的提示词绝不会提到文件系统工具。它们由下面的 harness 部分覆盖，后者无需模型配合。 `规划器` 提示明确说明该约定对其自身工作的含义；规划器被告知在 DAG 节点之间引用工件路径，而不是把有效载荷继续向前粘贴。
* 运行框架。 `卸载超大结果` 适用于每个子代理结果，因此即使工作线程已经将其内联，超大结果也会被卸载。它在定义的 `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"}
注：完整结果已写入动作工作区，而不是以内联方式返回。……

[摘要]
标题级发现……
```

`SubagentRunOutcome.artifact_paths` 在结构上携带相同的路径，这些路径是从 `[artifact]` 输出中的指针，因此无论运行框架是否卸载了结果，或工作线程是否自己写入了文件，交接都会携带路径。父级接收的任何路径都可以通过普通的 `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 → 保留变更块，折叠未变更上下文，总结锁文件块。
* 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`），它是一个有能力的默认代理，能够直接回答并完成普通工作，包括“检查 → 编辑 → 验证”的编码循环。当并行、深度推理或某种专门能力能显著提升效果时，它会生成专门的子代理。

### 为什么采用多代理

一个什么都知道的单一代理，其系统提示词也会大到像一本小书。把工作拆分给专门角色意味着：

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

### 内置原型

每个原型位于 `agents/<name>/` ，并带有一个 `agent.toml` （元数据、工具范围、模型提示）和一个提示词。编排器的聊天委派来自其 `[subagents]` 白名单：

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

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

| 原型                                   | 谁运行它                                                                         |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| `规划器`, `critic`                      | 只有 `parallel_research_cross_check` 工作流运行模板（分解和研究 =  `规划器`，交叉检查 =  `critic`). |
| `总结器`                                | 运行框架，用于压缩过大的工具结果；也用于该工作流运行模板的综合阶段。                                           |
| `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` 引用。
* 如果兼容的工作线程处于空闲或暂停状态且历史可复用，运行框架会为同一个持久的 `subagent_session_id` 启动一个新的临时运行，并将保存的子历史通过 `SubagentRunOptions.initial_history`传入，同时将新指令附加为用户可见的后续内容。
* 如果形态不兼容、工作线程已关闭， `fresh: true` 被传入，或者不存在会话，则运行框架会创建一个新的持久会话和工作线程。

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

1. 从其类型化运行上下文接收父级取消和工作区；其余遗留的 provider、sandbox 和 transcript 输入会保持在其作用域内，直到它们的显式迁移到位。
2. 解析子代理的模型：内联 `model` 覆盖优先，然后是配置级固定项（`[orchestrator].model`, `[teams.*].lead_model`, `[teams.*].agent_model`），然后是原型提示或继承的父级模型。
3. 按定义的 `tools`, `disallowed_tools`，以及 `skill_filter`过滤父级的工具注册表。 `在` 模式下，父级的完整注册表会原样继承。
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_completions`/`background_delivery`。其背后的队列是运行框架的 `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` 追加会折叠到同一行（会话存储对于这些确定性的 `agent:`-前缀 ID 是幂等的；其他所有 ID 都是 UUID 新鲜生成，并保留常数时间追加路径），而不是再次持久化已投递的结果。交互式轮次遵循相同契约： `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"`），前端在加载线程时会将其重新水合为提案卡片。

### 生成层级与等级

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

```
Primary     (可直接执行，`coding` 提示下的主代理)
  │
  ├─► Worker      ◄─── 快速路径：一次委派，叶子节点完成工作
  │
  └─► Reasoning   (慢速、深度思考，例如 `reasoning` 提示下的规划器)
        │
        └─► Worker  ◄─── 深路径：推理负责分解，工作线程负责执行
```

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

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

规则的原因。

* *聊天 → 聊天是没有意义的。* 聊天层级存在是为了提供迅捷的用户体验。聊天代理生成另一个聊天代理只会让 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` 字段，以及运行时深度计数器任务本地变量都已启用。深度同时受静态加载器契约和运行时 `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` 机制。

## 钩子：可观测性和策略控制杆

两个钩子入口在循环两端包裹着它：

### 停止钩子（轮次中途）

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

* 预算停止钩子：使用每次迭代的成本累加器，以美元为单位限制累计轮次成本。
* 每次调用支出预算：每个提供方调用在发送前都会为本轮共享账本预留一个保守上限，并根据实际用量结算。它适用于附件渲染之下，因此照片、贴纸或文档轮次会基于其渲染后的文本获准进入，而图像读数会计入同一账本。当预算处于活动状态时，模型接收的是图像读数和提取出的文档文本，而不是原生媒体。
* 最大迭代次数停止钩子：在代理持久配置之外限制迭代次数。
* 动作预算策略： `SecurityPolicy` 强制执行 `config.autonomy.max_actions_per_hour` 用于具有副作用的工具操作。用户可在“设置 > 高级 > 代理自主性”中调节它，运维人员也可以用以下项覆盖： `OPENHUMAN_MAX_ACTIONS_PER_HOUR`.

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

### 轮次后钩子

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

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

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

钩子通过 `tokio::spawn`运行，因此用户会在它们完成之前先收到答案。

## 中断：优雅取消

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

* 每个正在运行的子代理共享同一取消范围，并会在下一个检查点退出。
* 进行中的提供方流会被丢弃。
* 缓冲的对话轮次仍会保存（空闲刷新，或退出时的退出刷新）。

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

## 成本核算

每个提供方响应都携带一个 `UsageInfo` 块、输入词元、输出词元、缓存输入词元，以及权威的 `charged_amount_usd` 由 OpenHuman 后端填充。 `TurnCost` 将一次轮次中的每次提供方调用累加起来，以便 harness 可以：

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

当后端没有暴露 charged amount（较旧的构建、未通过它计费的提供方）时，一个小型的按层级费率表会提供词元费率下限估算。只要后端可用，直接成本始终优先。

## Web 通道事件和 RPC（assistant-ui elements 通道）

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

新增/扩展的 socket 事件（从 `DomainEvent` 到 `WebChannelEvent` 通过 `web_chat::event_bus` ，并且 `openhuman_rpc::server::socketio`):

* `时间戳` （纪元毫秒）由以下函数为每个事件加盖时间戳： `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`：根代理最终模型调用的输入加输出词元。它为上下文窗口指标供数，并排除更早的调用和子代理； `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` （提示注入/安全护栏）。其他所有 `start_chat` 拒绝都保持为 `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:` 模式一致）。提示注入筛查拦下的轮次，在其接受后（会话守卫或托管门禁拒绝提示或工具输出，包括安全模块无法回答时）是 `start_chat` 提示注入筛查拦下的轮次，在其接受后（会话守卫或托管门禁拒绝提示或工具输出，包括安全模块无法回答时）是 `chat_error{error_type:"prompt_screened", error_source:"security", copy_key:"chat_error.prompt_screened"}`，可重试。
* `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"` 在决策通道被丢弃时， `无` 对于普通用户决策，在线上传输为现有的 `cancel_reason` 字段）。仅当原始挂起同时具有 `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` 调用的每个 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` 因此前端可以将工件卡片绑定到生成它的确切轮次（`无` （适用于没有聊天上下文的 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`：直接读取线程当前的目标/待办状态。
* `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`）。参见 `threads::ops::edit`模块文档，了解完整的 UI 消息 ID -> 转录切点映射。
* `channel.web_queue_remove { client_id, thread_id, item_id }`，从线程的运行队列中移除一个特定的排队项。
* `agent.context_breakdown { agent_id?, thread_id? }`，是面向 UI 的代理渲染后提示大小视图（系统/工具/历史分拆），用于 composer 的上下文使用指示器。
* `commands.list`：将内置命令与以下内容合并： `skills.list` ，并且 `flows.list` ，合并为一个斜杠命令目录。

`plan_exit` （将线程从 Plan 切回 Build 的工具）只有在该线程上没有仍在挂起的计划审查时才会切换模式（`plan_review::gate::PlanReviewGate::parked_review_for_thread`），审查会在 `request_plan_review` 将控制权返回给代理之前完成，因此这只在一个时机不对/并发的 `plan_exit` 调用与仍待处理的审查竞速时才重要；在用户实际上尚未批准任何内容之前，绝不能解锁所有工具。

## 显式运行上下文和主机能力

`OpenHumanRunContext` 是位于共享聊天、通道和子代理轮次接缝处的实时载体。根会快照其当前作用域原点、进度、停止钩子、调度状态、线程、路由槽和工作区授权，然后在将上下文传递给运行器之前拥有或显式接收一个取消令牌；递归子代理会用 `child()`对其进行分叉，在隔离路由观察和用量核算的同时保留共享的取消/策略句柄。共享运行器会创建 `RunContext<OpenHumanRunContext>` 实现 `into_tinyagents`，而 OpenHuman 组装和中间件注册表会直接消费该类型化上下文。路由和线程作用域仍只包裹着面向类型化 harness 边界之外的旧式工具/模型 API。递归扩展会从其类型化父级接收取消和工作区 `RunContext` 实现 `ToolDispatch`；它从不读取环境中的取消状态。

`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/`。其中的 README 位于 `crates/openhuman-core/src/agent/` 列举了公开表面；最关键的文件（相对于 `crates/openhuman-core/src/agent/` ，除非有前缀）如下：

| 文件/目录                                  | 其中包含什么                                                                                             |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `session_host/runtime_session.rs`      | `Agent::turn`：即上述生命周期；通过以下项路由进入 tinyagents 运行器： `tinyagents/turn_exec.rs`.                         |
| `tinyagents/mod.rs`                    | `中的适配器接缝进入`：共享的 tinyagents harness 组装（实时循环）。                                                       |
| `tinyagents/middleware.rs`             | 具名的 OpenHuman 中间件栈（审批/安全、工具策略、恢复、预算、断路器）。                                                          |
| `../channels/runtime/turn_driver.rs`   | channel/CLI 总线轮次驱动器，放置在 channel runtime 中并路由到 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 的具体持久化/提供方转换。                                                                      |
| `分诊/`                                  | 外部触发分类 + 升级处理。                                                                                     |
| `../agent_registry/agents/`            | 内置原型：每个代理一个子目录。                                                                                    |
| `hooks.rs` / `stop_hooks.rs`           | 轮后和轮中钩子接口。                                                                                         |
| `cost.rs`                              | 按轮进行的美元/令牌计费。                                                                                      |
| `progress.rs`                          | 向 UI 推送实时进度事件。                                                                                     |

## tinyagents 上的代理引擎和编排

每个代理轮次都使用 TinyAgents。聊天和 channel/CLI 调用者会调用会话/轮次宿主，而子代理调用则进入 `agent/subagent_host` 及其直接 `tinyagents-orchestration::subagent` 生命周期。不存在 `harness/subagent_runner` 兼容路径。周围的 OpenHuman 接口缝合层：

| 文件（`crates/openhuman-core/src/agent/tinyagents/`) | 角色                                                                                                                              |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `mod.rs`                                          | 运行器（`中的适配器接缝进入`）：安装原生 `ChatModel`、宿主工具适配器以及中间件到一个 `AgentHarness`；运行一个轮次；通过原生 `MaxTokensModel`；镜像进度；转发 steering；并在达到模型调用上限时优雅暂停。 |
| `mod.rs` / `model.rs` / `tools.rs` / `convert.rs` | `RunPolicy` / `ChatModel` / `Tool` / 消息适配器（包括未知工具策略和带外推理转发）。                                                                    |
| `observability.rs`                                | Harness `AgentEvent` → `AgentProgress` + 费用； `GraphTracingSink` 用于图事件。                                                          |
| `orchestration.rs`                                | 重新导出 `graph::orchestration` 任务存储类型；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` 用于感知所有权的快照、等待/超时、steering 查找、协作式取消、强制中止以及终端清理。OpenHuman 保留持久化任务存储投影、产品/会话元数据、RPC 和传递语义，以及 `RunQueue` 兼容性回退。

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

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

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

## 可靠性：中断器、回传和已分类故障

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

无进展熔断器（`RepeatedToolFailureMiddleware`, `crates/openhuman-core/src/agent/tinyagents/middleware.rs`）是对该 crate 的 `NoProgressTracker`的轻量驱动。它会为每次工具调用的参数生成指纹，并将结果输入升级阶梯： `继续` → `提醒` （通过 `SteeringCommand::InjectMessage`注入的结构化“自步骤 X 以来无进展”纠正信息，在交互式轮次中是安全的）→ `停止` （将根因摘要记录到 `HaltSummarySlot`，并通过 steering 句柄暂停）。重复重试相同参数会计入触发条件（阈值为连续 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` ，基于位于 `{workspace}/tinyagents_store/journal`的 JSONL 追加存储，并组合为 `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` ，除非在 Settings → Routing → Default model 中更改）；没有按角色划分的 tier 端点。 `burst` 该角色服务于低上下文、高扇出的工作器。

## 另请参见

* [架构概览](/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"` 解析为具体的提供方+模型。
* [原生工具：代理协同](/openhuman/zh/gong-neng/native-tools/agent-coordination.md)：面向用户的接口，用于 `spawn_subagent`, `delegate_*`, `待办`.

[^1]: 技能通配条目（`{ skills = "*" }`）被豁免，因为它们没有指定代理：它们会展开为连接的 Composio 操作，作为 `延后` 代理通过……访问的工具 `tool_search`，而不是生成。
