OpenHuman `agent/` 向 TinyAgents 迁移的技术方案:trait 注入驱动的通用 Agent 运行时下沉
OpenHuman 的 Agent 核心模块(src/openhuman/agent/,约 152 个文件、70,530 行)承担着会话生命周期、turn 编排、工具调用、子 Agent 调度等职责,但它以直接 import 的方式耦合了 45 个 OpenHuman 领域模块。本文档是 2026-07-28 由维护者拍板方向、以"如何迁移"而非"是否迁移"为目标的 spec + plan:它规划如何把这个运行时以 对宿主状态类型泛型化(generic over State) 的方式迁入 vendor/tinyagents 通用 Agent 运行时 crate,同时把 OpenHuman 特有的产品逻辑全部收敛为 约 10 个宿主能力 trait 的实现。读完本文,你将理解这套"运行时下沉 + trait 注入反耦合"迁移方案的完整骨架:耦合全景、处置边界、两个门控决策、Phase 0–7 分阶段计划,以及每一阶段在仓库中的真实落地痕迹与风险底线。
1. 背景:一次被拍板、而非被讨论的架构搬迁
本方案源自 OpenHuman 自研"TinyAgents"通用 Agent 运行时的长期战略。在此之前,仓库中已有两份先行文档从不同角度处理过 agent/ 的去向:
- docs/specs/2026-07-28-agent-session-transcript-to-tinyagents-design.md 中将
builder/、turn/、runtime.rs、types.rs定为"永久驻留宿主(permanent host)"; - docs/tinyagents-migration-plan-2026-07-22.md §7 中
agent/ remainder — STAYS行也把agent/主体留在宿主侧。
docs/specs/plan-agents.md 推翻并重新开启 上述结论:它要求把 src/openhuman/agent/ 整体迁入 vendor/tinyagents 作为通用运行时,OpenHuman 与它的全部耦合改由 trait 注入表达。方案特别注明,被取代的 ledger 行必须同步更新,避免两份文档"各说各话"。
1.1 可行性的事实基础:crate 已经对宿主状态泛型化
早期反对意见的核心是依赖方向:agent/ 触达 OpenHuman 45 个领域模块,迁移似乎会迫使一个按 GPL 再分发的 crate 去 import Composio、SecurityPolicy、memory_store 等宿主专属实现。
该反对意见假设代码"原样搬走"——但事实并非如此,该 crate 早已对宿主提供的状态类型泛型化,文档给出的四个核心抽象是:
pub struct AgentHarness<State: Send + Sync, Ctx: Send + Sync = ()> { … }
pub trait Tool<State: Send + Sync>: Send + Sync { … }
pub trait ChatModel<State: Send + Sync>: Send + Sync { … }
pub trait Middleware<State: Send + Sync, Ctx: Send + Sync = ()>: Send + Sync { … }
这里 State 就是注入载体(injection vehicle):迁出的运行时并不 import crate::openhuman::memory,它只对一个"提供了记忆能力"的 State 泛型化,而真正提供记忆实现的是宿主侧(OpenHuman)。当前 crate 已经按此模式内置了 18 个扩展 trait,包括 ChatModel、Tool、ChatHistory、Store、AppendStore、Summarizer、EmbeddingModel、VectorStore、ResponseCache、WorkspaceIsolation、HarnessEventJournal、HarnessStatusStore、EventListener、Middleware、ModelMiddleware、ToolMiddleware、ModelBaseCall、ToolBaseCall。本次迁移只是再新增 约 10 个同类 trait——它不是一套新架构,而是对已有架构的"更多应用"。
GPL / crates.io 约束也随之收窄:禁止发布的是 OpenHuman 产品逻辑,而不是任何 Agent 运行时。trait 与泛型主循环是可发布的;provider_role_for 的 subconscious 路由、integrations_agent 分发器覆盖等则是不可发布的宿主实现,它们留在宿主侧作为 trait 实现即可。
仓库现状印证:宿主侧实现已真实存在。见 src/openhuman/agent/tinyagents/host/mod.rs 的模块文档,其开篇即声明:"Each module here adapts one crate trait onto the OpenHuman domains that actually provide the behaviour. This is
docs/specs/plan-agents.mdPhase 4"——即本目录的十个模块就是方案 Phase 4 的产物。
2. 需要反转的耦合全景(Ground Truth)
方案先把耦合切成两半分别计量,因为入站与出站的处置方式完全不同。
2.1 出站:agent/ import 了什么(45 个领域)
按引用次数排序,出站依赖实际只穿过少数几条"概念接缝"(conceptual seams),因此可用约 10 个新 trait 覆盖 45 个领域:
| Refs | 领域 | 处置方式 |
|---|---|---|
| 195 | config(Config 53、AgentConfig 45、MemoryConfig 28、ContextConfig 24 …) |
crate 自持配置结构体,宿主侧负责填充——最大阻塞点(见 §4.1) |
| 91 | tinyagents(接缝本身) |
溶解,迁移后变为内部依赖 |
| 84 | tools |
现有 Tool<State> + SharedToolAdapter |
| 72 | inference |
现有 ChatModel<State> + 新增 ModelResolver trait |
| 57+41+16+11+5+3 | memory、memory_store、memory_tree、agent_memory、memory_tools、memory_conversations |
新增 MemoryProvider trait |
| 52 | context |
新增 ContextComposer trait |
| 34+22 | profiles、agent_registry |
新增 DefinitionRegistry trait |
| 28 | composio |
宿主 Tool 实现,无需新 trait |
| 25+10+6+4+2 | security、approval、agent_tool_policy、sandbox、prompt_injection |
新增 SecurityGate trait |
| 22+1 | skills、skill_runtime |
宿主 Tool 实现 |
| 21 | todos |
已是 crate graph::todos(父规格 DS-1) |
| 19+7+4 | tokenjuice、cost、scheduler_gate |
新增 BudgetGate trait |
| 11 | learning |
新增 LearningSink trait |
| 8 | subconscious |
LearningSink 背后的宿主实现 |
| 6+4 | web_chat、channels |
新增 ProgressSink trait |
| 5 | tool_status |
新增 ToolOutcomeClassifier trait |
| 5 | thread_goals |
ContextComposer 背后的宿主实现 |
| 5 | embeddings |
现有 EmbeddingModel |
| 5 | agent_orchestration |
现有 graph::orchestration |
| 3 | agent_experience |
新增 ExperienceStore trait |
| 其余 | util、app_state、session_db、file_state、task_sources、mcp_registry、threads、session_import、migrations、tinycortex、tool_timeout(各 ≤4 引用) |
宿主实现或内联泛型 |
2.2 入站:什么 import 了 agent/(48 个领域)——真正更大的风险
此前分析低估的一半。按符号计量:
| Refs | 符号 | 备注 |
|---|---|---|
| 275 | agent::harness::* |
主体,随迁下沉 |
| 58 | agent::turn_origin |
产品枚举——留在宿主 |
| 43 | agent::messages(ChatMessage) |
持久化 DTO——留在宿主(WP-1) |
| 24 | agent::triage |
产品逻辑——留在宿主 |
| 22 | agent::progress(AgentProgress) |
UI 契约——留在宿主,经 ProgressSink 产出 |
| 14 | agent::prompts |
SOUL.md/IDENTITY.md——留在宿主 |
| 14 | agent::host_runtime |
按定义留在宿主 |
| 14 | agent::bus |
事件总线胶水——留在宿主 |
| 12 | agent::task_board |
已是 crate graph::todos |
| 12 | agent::message_convert |
边界适配——留在宿主 |
| 11 | agent::hooks |
trait 定义迁移;实现留在宿主 |
| 8+8+7+7+7+6+4 | error、cost、tool_policy、progress_tracing、pformat、task_dispatcher、stop_hooks |
混合,见 §3 |
结论:agent/ 不会搬空。 约 20–25k 行将作为宿主适配层留下(ChatMessage、AgentProgress、turn_origin、prompts、triage、bus、host_runtime、message_convert 以及全部 trait 实现)。交付物的本质是"运行时下沉 + OpenHuman 保留适配层",而不是"该目录消失"。
2.3 诚实的成本核算
45 个出站领域待反转、48 个入站消费者待改指、约 29k 行测试需要迁移或重新归属、需要跨两个 Cargo 世界做变更,同时存在用户依赖的磁盘/行为表面(transcript 格式、progress 事件、成本核算)。这是一个跨多个季度的工程计划,不是一次重构。 §5 的分阶段设计保证了:每个阶段独立有价值,且可以在任意阶段边界停止而不会让代码树处于损坏状态。
3. 处置方案(Disposition):谁迁走、谁留下
3.1 迁入 tinyagents(对 State 泛型化)的部分
| 宿主区域 | 生产代码行 | 迁入后位置 |
|---|---|---|
harness/session/{runtime,types,builder} — 会话生命周期与装配 |
~3,700 | harness::session — Session<State> + 基于能力 trait 的 builder |
harness/session/turn/* — turn 编排外壳 |
~4,476 | harness::session::turn — 泛型循环 + TurnPreparation 流水线 |
harness/subagent_runner/ |
~5,541 | 并入现有 harness::subagent + graph::orchestration |
harness/session/transcript.rs + turn_checkpoint.rs |
~2,100 | crate 的 Store/AppendStore 会话日志({workspace}/tinyagents_store/),经进行中的 #4249 迁移——不是 JsonlChatHistory(2026-08-03 修正,见 §5 Phase 2) |
harness/{parse,definition,definition_loader,tool_filter,required_output,graph,agent_graph,fork_context}.rs |
~3,300 | harness::{tool_calling, definition, graph} — 与 #55/#57 合并 |
harness/artifact_offload/、tool_result_artifacts/ |
~1,400 | harness::artifacts — artifact_offload 已落地(tinyagents#101);tool_result_artifacts/ 待办 |
harness/run_queue/、harness/memory_context*.rs |
~1,000 | harness::runtime,置于 MemoryProvider 之后 |
task_dispatcher/、dispatcher.rs(解析半)、pformat.rs、stop_hooks.rs、hooks.rs(trait 定义) |
~3,000 | harness::{tool_calling, hooks} |
progress_tracing/ |
~3,186 | 删除而非迁移——crate 可观测性已覆盖(父规格 DS-5) |
multimodal.rs(解析半) |
~1,300 | harness::multimodal,位于 multimodal cargo feature 之后——已落地(见 §5 Phase 5) |
3.2 留在 OpenHuman 的适配层
messages.rs(ChatMessage)、message_convert.rs、progress.rs(AgentProgress)、turn_origin.rs、prompts/、triage/、bus.rs、host_runtime.rs、error.rs、cost.rs、tool_policy.rs、agent/tools/、archivist/、schemas.rs,以及约 10 个新 trait 的全部实现,估计含测试约 20–25k 行。
关于
multimodal.rs的修正(2026-08-21)。 直到 2026-08-21,该行仍写"multimodal.rs迁走"。维护者随后反转了该行并将其拆分:宿主侧ChatMessage适配器、配置映射、代理客户端、PDF 抽取器与磁盘附件暂存仍然保留;而 marker 解析、data:URI 解码、MIME 检测、限额钳制与渲染 payload 对任何宿主都相同,它们现在位于harness::multimodal。宿主文件因此轻了约 1,300 行,剩下的正是文档所称"不可再减的剩余部分(irreducible remainder)"。
4. 两个门控一切的关键决策
4.1 配置(195 处引用——真正的阻塞点)
agent/ 直接读取 Config、AgentConfig(742 行 schema)、MemoryConfig、ContextConfig。通用运行时不可能 import OpenHuman 的配置 schema,三个候选方案:
- 方案 A — crate 自持配置结构体:crate 定义
SessionConfig、TurnConfig、ToolConfig;OpenHuman 在构建期把自己的 schema 映射进它们。显式、可版本化,镜像了 TinyCortex 对MemoryConfig的派生方式(tinycortex/config.rs::memory_config_from)。文档推荐。 - 方案 B —
ConfigProvidertrait(约 40 个 getter):省掉映射层,但把每次配置读取变成虚调用,且 trait 容易沦为"杂物倾倒场"。 - 方案 C — 泛型
State携带配置:代码最少,可发现性最差,crate 侧每次读取都需要加 bound。
最终选定 方案 A——这是组织内已在"隔壁 crate"成功使用的模式。
4.2 ChatMessage 与 transcript 格式(唯一有真实用户可见风险的变更)
把会话运行时下沉,意味着持久化会话记录必须变为 crate 所有。ChatMessage 的持久化字段必须以 crate Message + raw 透传的方式存活(沿用 ToolResult::raw 先例)。现有安装存在活跃 transcript,升级后 resume 必须继续工作——Phase 2 的存在意义就是为它单独去风险。
2026-08-03 修正。 本节此前声称选择"transcript 规格的 Option B:
session_rawJSONL 格式成为 crate 公有 API"。实际并非如此。进行中的 #4249 迁移(src/openhuman/agent/session_import/)收敛到 crate 的Store/AppendStorejournal,而非把遗留 JSONL 布局升级为 crate API。遗留session_raw/*.jsonl格式始终是宿主实现细节,待读取方迁移完成后退役;它永远不会成为 crate 公有表面。
5. 分阶段计划(Phase 0 – Phase 7)
每一阶段独立交付价值且保持代码树绿色,"任何阶段边界可停止"是硬性要求而非锦上添花。
Phase 0 — Ledger + trait 目录(无代码),进行中(2026-08-02)
重开被取代的 ledger 行;把约 10 个 trait 签名作为上游 RFC 写入 vendor/tinyagents/docs/。在 trait 目录被上游接受前不迁移任何代码——否则第一个动手的人会"意外地"定义接缝。
退出条件:RFC 被接受;ledger 行已重开。
草案已落地:tinyagents 仓库的 docs/spec/host-capability-traits-rfc.md(vendored 于 vendor/tinyagents/,因链接检查器不检出 submodule 而以 URL 引用)——全部十个签名,均以实测引用数为依据。尚未被接受,携带四个阻塞 Phase 1 的开放问题,最棘手的是一处命名冲突:tinyflows 0.5.1 发布了它自己、与本次无关的 MemoryProvider trait,而两个 crate 都在 OpenHuman 的依赖图中。
Phase 1 — 上游落地 trait,空实现
把 trait + no-op/内存内默认实现加入 crate,宿主零改动。
退出条件:crate cargo test --all-features 全绿;版本号 bump;两份 lockfile 同步。
Phase 2 — Transcript 迁至 crate 会话存储(soak 已启动,2026-08-03)
这是唯一存在磁盘数据风险的阶段,因此必须尽早且单独推进。要求一个发布周期的 shadow-read 对等性:差异只记录日志、绝不 panic,遗留 DDMMYYYY/ 与 read_transcript_legacy_md 路径均需覆盖。
退出条件:真实 workspace 升级后 resume 正常;对等性 soak 干净。
该阶段曾被错误规格化,且大部分已提前建成。 启动时发现两项修正:
1. 目标不是
JsonlChatHistory。 原标题为"Transcript 迁至 crateJsonlChatHistory(transcript 规格 Option B)",实际并不存在该收敛。真正目标——已在 #4249 下选定并完成约一半——是 crate 的Store/AppendStorejournal(位于{workspace}/tinyagents_store/{kv,journal})。若新建JsonlChatHistory等于在遗留 JSONL 与迁移目标之外引入第三个存储。
2. 本阶段开启前已完成约 2/3。 src/openhuman/agent/session_import/(约 2,452 行)已实现:
切片 状态 Phase 1 — 导入器(遗留 JSONL → store) 完成 04.1 — 实时双写( session_dual_write)完成,默认开启 shadow-read 对比 + ShadowReadOutcome完成,原默认关闭 04.2 — 读取方切换到 store 未开始
遗留
session_raw/*.jsonl仍是权威读写方;store 仅为镜像。这是正确的顺序且早已正确——本阶段的工作是收尾而非重启。
本轮已完成项:
- 补齐本阶段退出标准点名的两处遗留形态覆盖缺口(此前均无测试,见 session_import/live_tests.rs):
- 按日期分组的
session_raw/DDMMYYYY/与扁平 transcript 解析到同一 store 流——会话 key 是文件名 stem,外围目录不得改变它;若改变,所有迁移前会话都会读成Unavailable,soak 看似干净实则什么都没覆盖; - 遗留
.md会话应读为Unavailable而非Divergence——它们先于 store 存在、本无对应流,报 Divergence 会让磁盘上每个旧 transcript 都给 soak 灌入假阳性。
- 按日期分组的
session_shadow_reads现默认开启,正式启动对等性 soak。可安全默认开启的理由:纯观测无副作用——遗留侧保持权威、探针按 resume(而非每 turn)在后台任务跑一次、store 读失败降级为Unavailable、OPENHUMAN_SESSION_SHADOW_READS=0是随时可用的总开关。最坏情况只是日志噪音,不会破坏 resume。
仓库印证:开关的默认值真实存在于 src/openhuman/config/schema/agent.rs 的 default_session_shadow_reads()(及配套 enable_session_shadow_reads 迁移),对应对等性测试覆盖了 live_dual_write_matches_legacy_jsonl_render、shadow_read_roundtrip_matches_legacy、shadow_read_unavailable_and_divergence、config_flag_and_env_kill_switch 等场景(见 session_import/live_tests.rs)。
Phase 2 剩余工作(以及为何尚未完成):
- Soak:收集一个发布周期内真实 workspace 的
[session_shadow_read]差异率。目前尚无数据,因此后续无从论证。 - 04.2 — 切换读取方,同开关门控,仅当 soak 干净后。
- 读取方已在 store 上运行一个发布周期后,退役遗留 writer。
切勿跳到第 2 步。 本阶段的整体设计就是"读取方切换要用证据来购买",而证据在探针随一个发布版本上线之前并不存在。
Phase 3 — 配置映射(§4.1 方案 A),进行中(2026-08-02)
引入 crate 配置结构体 + 宿主 session_config_from(&Config) 映射器,先在原位置把 agent/ 内部改指到 crate 结构体,再谈迁移。
退出条件:待迁代码内对 crate::openhuman::config:: 的引用归零。
目前落地(仅地基,尚未改指任何代码):
tinyagents::harness::config—SessionConfig、TurnConfig、ToolConfig、MemoryLimits、RequiredOutput、ToolDispatcher。惰性(仅 serde + std),默认值钉死为 OpenHuman 当前值。- src/openhuman/agent/tinyagents/config.rs —
session_config_from以及apply_team_models/apply_delegate,遵循tinycortex::config::memory_config_from先例。之所以拆成三块,是因为 OpenHuman 的模型 pin 不是全局的:Config::teams按 team 键控、Config::agents按 delegate 键控,单一扁平映射器必须凭空为一个会话发明模型。 - 两侧合计 23 个测试。承重测试是
default_config_maps_to_the_crate_defaults(见 config_tests.rs),一旦两套默认值漂移即失败。
ToolDispatcher 的拼写坑:crate 侧是枚举,宿主侧却是 String。四种被接受的拼写是 auto / native / xml / pformat——不是一般人猜的 auto/native/parsed 三元组。未知值映射为 Auto 并告警、而非报错:因为宿主自身 schema 允许拼写错误通过校验,拒绝构建会话等于把"一个装饰性配置错误"升级成"Agent 无法运行"。
改指:"37 个文件"实际分解为什么
"37 个文件"统计的是每个含限定 config:: 路径的 agent/ 生产文件。其中只有约 19 个属于待迁集合——其余(host_runtime、bus、triage/、schemas、multimodal、prompts/、agent/tools/、archivist/、progress_tracing/)按 §3 留在宿主侧,本就应该继续读 Config,它们是映射器的调用方,改指反而错误。待迁集合内有 41 处限定引用,拆成三个性质完全不同的问题:
1. 环境配置装载——11 处(不是改指,是签名重构;2026-08-02 完成:11 处 → 2 处真问题 + 1 处边界快照)。
Config::load_or_init().await 在待迁代码中出现 11 次。通用运行时没有配置文件、没有 load_or_init,因此这些调用无法指向某个结构体——配置必须由调用方穿线传入。
load_or_init 没有缓存:它每次调用都会重新解析配置目录并重读 config.toml。run_typed_mode 调用它六次,意味着一个子 Agent 生成会命中磁盘六次,并且可能在一个生成中途观察到六个不同的配置。run_subagent 现在改为获取单次快照(LoadedConfig = Result<Arc<Config>, String>)并向下传递。
之所以用 Result<_, String> 而非 Option:integrations_agent 路径会把装载错误报告给其调用方,而其余五处静默降级——保留两种形态让每个站点保持原始失败行为。快照在 tier_gate_decision 之后 获取:load_or_init 首次运行会初始化配置,而被 tier gate 拒绝的生成不应产生这个副作用。
子 Agent 图也做了同样处理:build_subagent_context_mw 现在接收 Option<&Config>(且不再是 async),经 run_subagent_via_graph 与新的 AgentTurnRequest::config 字段贯穿,让自定义图路径保留其 [context] 旋钮而不静默回退到默认值。四个图测试传 None,因此它们是封闭的(hermetic)——此前它们读的是开发者机器上随便什么 config.toml。
范围修正:task_dispatcher/ 不在待迁集合内。 §3 把它与 dispatcher.rs 并排列出、都映射到 harness::{tool_calling, hooks},实则混同了两个无关模块。dispatcher.rs 从模型输出解析工具调用,是真正通用的;task_dispatcher/ 是一个任务看板分发器,触达 task_sources、threads、web_chat、todos、profiles、scheduler_gate——留宿主侧的产品逻辑,其三处 load_or_init 是边界代码、现状即正确。§3 的行应拆分。
待迁文件中仍剩两处装载,均为将被抽取而非移动的宿主边界代码:
session/turn/tools.rs的 Composio 集成获取——配置已通过会话的runtime_config(在factory.rs设定)穿线,此处仅是 setter 路径构建会话时的回退。Composio 是宿主产品逻辑、将在 Phase 4 成为Tool实现,故保留回退以免 setter 构建的会话静默丢失集成获取。harness/definition.rs的load_for_default_workspace()——唯一调用方是 src/core/agent_cli.rs,CLI 边界辅助函数,definition.rs迁走时留在宿主侧。
2. 被 Phase 2 阻塞——会话暂时不能放弃 AgentConfig。
会话只读取 9 个不同的 AgentConfig 字段,其中 7 个 crate 结构体已覆盖。未覆盖的两个——session_dual_write 与 session_shadow_reads(见 session/turn/session_io.rs)——是 transcript 实时存储迁移标志,在 Phase 2 决定 transcript 归属前在 crate 中没有家,所以 Agent.config: AgentConfig 暂时必须保留。
曾考虑在它旁边新增 crate SessionConfig 但被否决:session/runtime.rs 在构建后还会修改 self.config.max_tool_iterations(迭代上限覆盖),两个配置会在"被读得最多的字段"上静默分叉。要么单一事实来源,要么什么都没有。
3. 被 Phase 4 阻塞——builder/factory.rs。
factory.rs 触达 21 个领域,将被 拆分 成宿主 trait 实现而非原样搬移。现在改指它的 Config 用法是返工。
本轮已完成的其余工作
RequiredOutputContract → crate RequiredOutput,这是唯一可用的干净类型交换:harness/required_output.rs(纯逻辑、无宿主领域)与 session/turn/session_io.rs,在 session/turn/core.rs 的读取点经 tinyagents::config::required_output_from 转换。crate 类型新增 all_keys(),语义与宿主完全一致,包括那个微妙点——空白 block_key 会让契约失效,即使 required_keys 列出了兄弟项。现有 12 个 required_output 测试对 crate 类型原样通过,证明交换是行为保持的。
映射器也拆成了分节函数(turn_config_from、tool_config_from、memory_limits_from、apply_agent_config),因为会话 builder 接收逐 Agent 的 AgentConfig 覆盖——只从全局 Config 映射会丢弃它,让所有 Agent 都跑在全局限额上。
修正后的剩余工作: ① 环境装载站点的配置穿线——2026-08-02 完成;② Phase 2 后用 crate 配置替换 Agent.config,迁移两处真实的外部 agent_config() 消费者(agent_orchestration/parent_context/、subconscious/session.rs);③ Phase 4 后处理 factory.rs 与 session/turn/tools.rs 的 Composio 获取;④ 拆分 §3 中 task_dispatcher/ + dispatcher.rs 行——只有后者迁移。
测试备注:
openhuman::agent::需要RUST_MIN_STACK=16777216,否则session::tests::turn_dispatches_spawn_subagent_through_full_path会栈溢出(§6 已标记)。设置后全套 1080 通过 / 1 失败——builder_tests::profile_allowed_tools_restrict_shared_session_builder即使在干净树上随全套跑也会失败、单独跑则通过,属既有顺序依赖,不是 Phase 3 的连带问题。
Phase 4 — 在宿主侧原位置实现 trait(适配器已落地 2026-08-03;调用点尚未改指)
AgentMemory、ContextComposer、SecurityGate、BudgetGate、DefinitionRegistry、ExperienceStore、LearningSink、ProgressSink、ToolOutcomeClassifier、ModelResolver——agent/ 改为调用它们,而非直接探入领域模块。本阶段以零搬迁风险交付了绝大部分架构价值——完成之后,agent/ 的出站耦合是约 10 个 trait 而非 45 个领域,程序在此停下也是正当的。
退出条件:grep -c "crate::openhuman::" src/openhuman/agent/harness/session/ 从基线降至仅剩适配层。
退出标准基线修正。 原措辞"约 2,000 处引用"。实测:
session/生产代码 295 处(含测试 548 处)。更大的数字统计了更宽的树。295 才是要压下去的数字。
全部十个适配器已落地于 src/openhuman/agent/tinyagents/host/(约 6,000 行、140 个测试),每个将一条 crate trait 接到真实 OpenHuman 领域,策略在适配器侧强制。agent/ 尚未调用它们,因此退出标准仍停在 295——"写适配器"与"改指调用点"是两件独立工作,目前只完成前者。
集成时发现并修复了两个缺陷,都属于"编译干净、静默失败"的类型:
security_gate:channel 的RequireApproval判决曾解析为Allow。 映射层基于"调用会落入 approval park"的理论返回"无判决"——但 park 只从shell与外部效应分支可达,于是任何普通工具都被放行且无人询问。agent_tool_policy::engine把RequireApproval与Deny一起归档到blocked_tool_names下,因此这颠倒了宿主自身语义。之所以只是潜在问题,是因为build_session当前不产生RequireApproval——即一个"为开启者上膛的陷阱"。现已路由到 park,并在没有审批门时拒绝(这是该适配器唯一一处比遗留中间件更严的拒绝,见模块头论证)。由require_approval_never_silently_allows_a_plain_tool钉住。experience_store:跨 Agent 记录碰撞。 领域层的stable_experience_id_for_profile对 task + tool 序列 + outcome + profile 做哈希并刻意排除agent_id;原生捕获钩子只是碰巧受保护(总提供真实 tool 序列)。该适配器无可供提供者,于是两个 Agent 记录相同任务与相同 outcome 时碰撞到同一 id,putupsert——第二个写入者静默销毁第一个的记录。现在 Agent id 被折进哈希的 tool 序列槽位。
Phase 4 剩余工作——改指。2026-08-04 调研结论:被阻塞,而非仅仅困难。 四项实测发现:
1. 退出标准统计了错误的代码。 session/ 中 118 处非适配层引用里约 2/3 根本不是"消费":
| 位置 | Refs | 本质 |
|---|---|---|
builder/(factory 29、setters 12、mod 3、helpers 2) |
46 | 装配——正是将变为 trait 实现 的部分(§3) |
types.rs |
16 | 字段类型标注——注入点本身 |
runtime.rs |
13 | 会话状态管理(如 rebuild_tool_policy_session) |
turn/ |
40 | 唯一真正的运行时消费 |
| misc | 3 |
因此"把 session/ 压到仅剩适配层"靠改指无法达成——那些引用中大部分就是适配层。诚实的度量是 turn/ 的约 40 处。
2. 会话分发句柄多于消费句柄。 self.memory 的 11 处使用全部把 Arc<dyn Memory> 交给协作者(memory loader、context loader、AgentExperienceStore)而非直接调用 recall/store。把字段换成 Arc<dyn AgentMemory> 会破坏这些需要完整领域接口的协作者。记忆接缝在协作者也迁到 trait 之后之前无法改指。
3. 没有能力 trait 与现有调用形态 1:1 吻合。 十个全部查过。ContextComposer::compose_system_prompt 返回 String,但 turn 需要结构化的 LearnedContextData 去喂它自己的 SystemPromptBuilder,采纳它意味着把整个提示词装配搬进适配器——那是 Phase 5 的工作而非改指。ToolOutcomeClassifier 唯一的宿主消费者是 progress_tracing/,而后者按 §3 是删除而非迁移。session/ 中的 subconscious 只是 factory.rs 里的类型标注。
4. 四个适配器无法从会话状态构造。
| 适配器 | 构造所需 | 会话拥有 |
|---|---|---|
AgentMemory、ExperienceStore |
Arc<dyn Memory> |
有(memory_arc()) |
ToolOutcomeClassifier |
无 | 有 |
ProgressSink |
Sender<AgentProgress> |
有 |
LearningSink |
Vec<Arc<dyn PostTurnHook>> |
有 |
BudgetGate、ContextComposer、ModelResolver |
Arc<Config> |
有——会话携带 runtime_config: Option<Arc<Config>> |
SecurityGate |
Arc<SecurityPolicy> + 工具集 |
工具集有,策略无 |
曾居于阻塞链之首的 Arc<Config> 缺口已关闭:会话字段在 #5396 中从 integration_runtime_config: Option<Config> 提升为 runtime_config: Option<Arc<Config>>,同一改动把子 Agent 生成路径的七处环境 Config::load_or_init() 调用折叠成一处。因此 BudgetGate、ContextComposer、ModelResolver 今天就可构造。
剩余阻塞比之前更窄:
Phase 4 的改指仍需要 Phase 2 为
session_dual_write/session_shadow_reads重新安家,那需要对等性 soak,而 soak 需要已发布的版本来产出数据。因此 §5 中"Phase 4 以零搬迁风险交付绝大部分架构价值"对适配器成立——它们已完成。改指这一半被"流逝的时间"而非"工作量"门控。
#5396 review 又暴露两个 crate 侧阻塞点,均已上报上游,改指不应越过它们推进:
tinyagents#88—ProgressEvent缺少 tool 完成里程碑,宿主无法上报工具结果;工具行将永远停在running,伪造success: true会向时间线与 trace 导出器灌入错误数据。tinyagents#89—ModelResolveRequest不携带模型 pin,definition 的精确模型 id 无法到达解析器;逐 Agent pin 会静默解析到工作负载默认值。
两者已在上游
tinyagents#100修复、待合并:ProgressEvent新增ToolCallFinished { run, call, success, output },ModelResolveRequest新增model_pin: Option<String>。给改指执行者的两条备注:
- 两种类型都尚未被运行时发出。 PR 只是让接缝可表达;agent 主循环仍要产出
ToolCallFinished,subagent runner 仍要从AgentDefinition.model填充model_pin。合并 #100 解锁的是适配器,不是接线。model_pin设计上就是咨询性的。 是否履行 pin 由宿主决定,因为运行时看不到凭据或 provider 健康度。OpenHumanModelResolver因此应把 id 与已配置 provider 校验,而不是盲目透传——这正是 §4 适配器备注一直想要却没有通道的行为。
适配器中仍保留 21 个 TODO(phase4) 标记,每个都指名一块当时不可达的领域表面。它们是诚实的缺口而非假装工作的桩,值得注意的有 AgentMemory::thread_summary(不存在宿主撰写的逐线程散文汇总)与 SecurityGate::screen_input 永不返回 Redacted(OpenHuman 能检测 PII 但没有公开的文本改写辅助函数)。
Phase 5 — 逐模块家族搬迁(7 个家族中 3 个已落地)
按入站耦合从低到高排序:artifact_offload → run_queue → multimodal → parse/tool_calling(与 DS-5b 合并)→ subagent_runner → session/turn → session/{builder,runtime,types}。每个家族:迁入 vendor/tinyagents/src/harness/,经宿主适配层 re-export 一个发布周期,再改指消费者。
每家族退出条件:crate 测试全绿;宿主 cargo check 双世界通过;家族测试移居上游。
| 家族 | 状态 |
|---|---|
artifact_offload |
已落地 — crate harness::artifacts(tinyagents#101),宿主保留提示词契约 + 两个策略适配器 |
run_queue |
已落地 — crate harness::run_queue 拥有 FIFO 机制;宿主包装保留 QueueMode::{Interrupt,Parallel} 与 QueuedMessage payload |
multimodal |
已落地 — crate harness::multimodal 拥有 marker 解析、data: URI、MIME 检测、限额与 payload 渲染;宿主保留 ChatMessage 适配器、配置映射、代理客户端、documents PDF 抽取器与附件暂存 |
parse/tool_calling |
未开始 |
subagent_runner |
未开始 |
session/turn |
被 Phase 2 soak 阻塞 |
session/{builder,runtime,types} |
被 Phase 2 soak 阻塞 |
两个已落地家族收敛出的形态(也是其余家族的模板):crate 拥有机制,宿主保留一个恰好含产品特定部分的薄包装。
run_queue保留两个枚举变体与一个 payload 结构体;artifact_offload保留提示词文本与两个策略适配器。两者都不是 re-export 垫片——每个都是不可再减的剩余部分,这正是"只保留接线"的实践含义。
artifact_offload搬迁确立的两条规则(都会再次出现):
- 提示词文本永不迁移。 §6 将 OpenHuman 提示词文本列为不可发布物,且提示词会指名宿主工具 id。因此 crate 的指针渲染器把读取工具名作为参数而非常量——在再分发 crate 里硬编码工具名,等于把"其他宿主没有的工具"塞进它的提示词。
- 宿主策略变成 trait 对,而非 import。
SecurityPolicy与sanitize_text变为ArtifactPathPolicy/ArtifactRedactor。宿主构造函数每次都同时安装两者,因为 crate 允许各取None,而 OpenHuman 两边都不想要——经由一个辅助函数路由构造,才能阻止某个调用点因疏漏造出不受防护的写入器。
Phase 6 — 折叠接缝与适配器
src/openhuman/agent/tinyagents/ 溶解进宿主适配层,删除兼容性 re-export。
退出条件:agent/ 仅剩适配层;父规格 DS-0 的 re-export 门控 allowlist 无接缝。
Phase 7 — 退出门
完整跑通 scripts/test-rust-with-mock.sh;两个 vendored crate 各自 cargo test --all-features;精简禁用构建 与 cargo test --lib --no-default-features core::;pnpm rust:check;删除 ledger 总额对账;重写架构文档。
6. 风险清单
- 入站耦合才是真实成本,而非出站。 48 个领域 import
agent::。Phase 5 的逐家族 re-export 窗口是让这件事可驾驭的关键;跳过它会迫使每次家族搬迁都变成 48 领域原子提交。 - 磁盘 transcript 风险(Phase 2) 是全计划唯一用户可见的数据风险,被刻意隔离并排在最先。
AgentProgress是 UI 契约。 它留在宿主侧并经ProgressSink产出。若漂移进 crate,前端时间线、成本页脚、引文 chip 会以单测抓不到的方式损坏。- trait 爆炸。 十个 trait 是预算。若 Phase 4 需要第十五个,那是"某条接缝错了"的信号——应重开 RFC 而非加 trait。
- GPL / crates.io。 可发布:trait、泛型主循环、工具调用线上格式。不可发布:
provider_role_for的subconscious路由、integrations_agent覆盖、OpenHuman 提示词文本、后端措辞、密钥材料。每个被搬迁文件都要过这道检查。 - ≥ 80% diff-coverage 门 作用于这种体量的计划——Phase 4 与 5 会碰触数百文件。按切片检查
diff-cover。 - 两个 Cargo 世界——每次 crate bump 都要重新生成根与
app/src-tauri两份 lockfile(#3877)。 RUST_MIN_STACK=16777216——subagent runner 的大 future 在 Apple Silicon 上已溢出默认栈;Phase 5 的 subagent 搬迁正是它再次爆发的点。- 本地根 crate 构建需
GGML_NATIVE=OFF。
7. 总结
| 项目 | 结论 |
|---|---|
| 决策 | 把 agent/ 迁入 tinyagents(维护者拍板,2026-07-28) |
| 使能因素 | crate 已对 State 泛型化;18 个扩展 trait 今天就在用此模式 |
| 反转 | 45 个出站领域 → 约 10 个能力 trait |
| 现实核对 | agent/ 不会搬空——约 20–25k 行以宿主适配器留下(ChatMessage、AgentProgress、prompts、triage、bus、trait 实现) |
| 门控决策 | 配置映射(§4.1 → 方案 A);持久会话记录收敛于 crate store 而非 crate 自有 JSONL(§4.2,2026-08-03 修正) |
| 最高价值/最低风险阶段 | Phase 4——原位置 trait 注入:不搬任何文件就把耦合从 45 压到 10,是正当的停止点 |
| 最高风险阶段 | Phase 2——磁盘 transcript 格式,被隔离并排在最先;曾被错误规格化且已在 #4249 下完成约 2/3;对等性 soak 于 2026-08-03 启动 |
| 诚实的成本 | 跨多季度计划;每个阶段都保持代码树绿色可发布 |
8. 与当前仓库的对账
截至本文整理时,方案在仓库中的可验证痕迹如下:
- 宿主适配层(Phase 4 产物):src/openhuman/agent/tinyagents/host/ 下十个适配器(
agent_memory、budget_gate、context_composer、definition_registry、experience_store、learning_sink、model_resolver、progress_sink、security_gate、tool_outcome_classifier)各配独立测试文件。 - 配置映射(Phase 3 产物):config.rs 的
session_config_from/turn_config_from/tool_config_from/memory_limits_from/apply_team_models/apply_delegate/required_output_from,以及 config_tests.rs 的承重测试default_config_maps_to_the_crate_defaults。 - 会话导入与 shadow-read(Phase 2 产物):src/openhuman/agent/session_import/ 的实现与 live_tests.rs 的对等性测试;开关默认值位于 src/openhuman/config/schema/agent.rs。
- 待迁与宿主代码的物理结构:src/openhuman/agent/harness/(session、subagent_runner、run_queue、artifact_offload、multimodal、required_output 等)与 src/openhuman/agent/ 下被判定留驻宿主侧的 messages、progress、turn_origin、triage、prompts、bus、host_runtime、message_convert 等模块均可按 §3/§5 逐行对照。
- 目标 crate:vendor/tinyagents/ 以 git submodule 方式 vendored(见 .gitmodules),模块尚未在默认检出中展开时,其上游代码随 submodule 拉取后即可查阅。
- 相关规划文档:docs/specs/2026-07-28-agent-session-transcript-to-tinyagents-design.md、docs/tinyagents-migration-plan-2026-07-22.md、docs/tinyagents-phase3-router-registry-design.md 共同构成该迁移决策的前后文。
这份方案的可贵之处在于它的"反夸大"姿态:不把搬迁描述为一次可一蹴而就的重构,而是明说它是多季度工程;不回避"295 处改指计数错位"、ToolDispatcher 拼写歧义、progress_tracing 应删而非搬这类细节代价;用"每一阶段都可停止、代码树始终绿色"来管理长期工程风险。对于任何想要理解"如何把一个产品深度耦合的 Agent 模块,反耦合成一个可再分发的通用运行时"的工程师,Phase 0–7 与那 10 个能力 trait 的清单本身就是一份高密度、可执行的架构样例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00