首页
/ OpenHuman `agent/` 向 TinyAgents 迁移的技术方案:trait 注入驱动的通用 Agent 运行时下沉

OpenHuman `agent/` 向 TinyAgents 迁移的技术方案:trait 注入驱动的通用 Agent 运行时下沉

2026-09-08 11:18:06作者:翟萌耘Ralph

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/plan-agents.md 推翻并重新开启 上述结论:它要求把 src/openhuman/agent/ 整体迁入 vendor/tinyagents 作为通用运行时,OpenHuman 与它的全部耦合改由 trait 注入表达。方案特别注明,被取代的 ledger 行必须同步更新,避免两份文档"各说各话"。

1.1 可行性的事实基础:crate 已经对宿主状态泛型化

早期反对意见的核心是依赖方向agent/ 触达 OpenHuman 45 个领域模块,迁移似乎会迫使一个按 GPL 再分发的 crate 去 import Composio、SecurityPolicymemory_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,包括 ChatModelToolChatHistoryStoreAppendStoreSummarizerEmbeddingModelVectorStoreResponseCacheWorkspaceIsolationHarnessEventJournalHarnessStatusStoreEventListenerMiddlewareModelMiddlewareToolMiddlewareModelBaseCallToolBaseCall。本次迁移只是再新增 约 10 个同类 trait——它不是一套新架构,而是对已有架构的"更多应用"。

GPL / crates.io 约束也随之收窄:禁止发布的是 OpenHuman 产品逻辑,而不是任何 Agent 运行时。trait 与泛型主循环是可发布的;provider_role_forsubconscious 路由、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.md Phase 4"——即本目录的十个模块就是方案 Phase 4 的产物。


2. 需要反转的耦合全景(Ground Truth)

方案先把耦合切成两半分别计量,因为入站与出站的处置方式完全不同

2.1 出站:agent/ import 了什么(45 个领域)

按引用次数排序,出站依赖实际只穿过少数几条"概念接缝"(conceptual seams),因此可用约 10 个新 trait 覆盖 45 个领域:

Refs 领域 处置方式
195 configConfig 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 memorymemory_storememory_treeagent_memorymemory_toolsmemory_conversations 新增 MemoryProvider trait
52 context 新增 ContextComposer trait
34+22 profilesagent_registry 新增 DefinitionRegistry trait
28 composio 宿主 Tool 实现,无需新 trait
25+10+6+4+2 securityapprovalagent_tool_policysandboxprompt_injection 新增 SecurityGate trait
22+1 skillsskill_runtime 宿主 Tool 实现
21 todos 已是 crate graph::todos(父规格 DS-1)
19+7+4 tokenjuicecostscheduler_gate 新增 BudgetGate trait
11 learning 新增 LearningSink trait
8 subconscious LearningSink 背后的宿主实现
6+4 web_chatchannels 新增 ProgressSink trait
5 tool_status 新增 ToolOutcomeClassifier trait
5 thread_goals ContextComposer 背后的宿主实现
5 embeddings 现有 EmbeddingModel
5 agent_orchestration 现有 graph::orchestration
3 agent_experience 新增 ExperienceStore trait
其余 utilapp_statesession_dbfile_statetask_sourcesmcp_registrythreadssession_importmigrationstinycortextool_timeout(各 ≤4 引用) 宿主实现或内联泛型

2.2 入站:什么 import 了 agent/(48 个领域)——真正更大的风险

此前分析低估的一半。按符号计量:

Refs 符号 备注
275 agent::harness::* 主体,随迁下沉
58 agent::turn_origin 产品枚举——留在宿主
43 agent::messagesChatMessage 持久化 DTO——留在宿主(WP-1)
24 agent::triage 产品逻辑——留在宿主
22 agent::progressAgentProgress 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 errorcosttool_policyprogress_tracingpformattask_dispatcherstop_hooks 混合,见 §3

结论:agent/ 不会搬空。20–25k 行将作为宿主适配层留下ChatMessageAgentProgressturn_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::sessionSession<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::artifactsartifact_offload 已落地(tinyagents#101);tool_result_artifacts/ 待办
harness/run_queue/harness/memory_context*.rs ~1,000 harness::runtime,置于 MemoryProvider 之后
task_dispatcher/dispatcher.rs(解析半)、pformat.rsstop_hooks.rshooks.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.rsChatMessage)、message_convert.rsprogress.rsAgentProgress)、turn_origin.rsprompts/triage/bus.rshost_runtime.rserror.rscost.rstool_policy.rsagent/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/ 直接读取 ConfigAgentConfig742 行 schema)、MemoryConfigContextConfig。通用运行时不可能 import OpenHuman 的配置 schema,三个候选方案:

  • 方案 A — crate 自持配置结构体:crate 定义 SessionConfigTurnConfigToolConfig;OpenHuman 在构建期把自己的 schema 映射进它们。显式、可版本化,镜像了 TinyCortex 对 MemoryConfig 的派生方式(tinycortex/config.rs::memory_config_from)。文档推荐。
  • 方案 B — ConfigProvider trait(约 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_raw JSONL 格式成为 crate 公有 API"。实际并非如此。进行中的 #4249 迁移(src/openhuman/agent/session_import/)收敛到 crate 的 Store/AppendStore journal,而非把遗留 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 迁至 crate JsonlChatHistory(transcript 规格 Option B)",实际并不存在该收敛。真正目标——已在 #4249 下选定并完成约一半——是 crate 的 Store/AppendStore journal(位于 {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 读失败降级为 UnavailableOPENHUMAN_SESSION_SHADOW_READS=0 是随时可用的总开关。最坏情况只是日志噪音,不会破坏 resume。

仓库印证:开关的默认值真实存在于 src/openhuman/config/schema/agent.rsdefault_session_shadow_reads()(及配套 enable_session_shadow_reads 迁移),对应对等性测试覆盖了 live_dual_write_matches_legacy_jsonl_rendershadow_read_roundtrip_matches_legacyshadow_read_unavailable_and_divergenceconfig_flag_and_env_kill_switch 等场景(见 session_import/live_tests.rs)。

Phase 2 剩余工作(以及为何尚未完成):

  1. Soak:收集一个发布周期内真实 workspace 的 [session_shadow_read] 差异率。目前尚无数据,因此后续无从论证。
  2. 04.2 — 切换读取方,同开关门控,仅当 soak 干净后。
  3. 读取方已在 store 上运行一个发布周期后,退役遗留 writer。

切勿跳到第 2 步。 本阶段的整体设计就是"读取方切换要用证据来购买",而证据在探针随一个发布版本上线之前并不存在。

Phase 3 — 配置映射(§4.1 方案 A),进行中(2026-08-02)

引入 crate 配置结构体 + 宿主 session_config_from(&Config) 映射器,先在原位置把 agent/ 内部改指到 crate 结构体,再谈迁移。 退出条件:待迁代码内对 crate::openhuman::config:: 的引用归零。

目前落地(仅地基,尚未改指任何代码):

  • tinyagents::harness::configSessionConfigTurnConfigToolConfigMemoryLimitsRequiredOutputToolDispatcher。惰性(仅 serde + std),默认值钉死为 OpenHuman 当前值。
  • src/openhuman/agent/tinyagents/config.rssession_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_runtimebustriage/schemasmultimodalprompts/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.tomlrun_typed_mode 调用它六次,意味着一个子 Agent 生成会命中磁盘六次,并且可能在一个生成中途观察到六个不同的配置。run_subagent 现在改为获取单次快照(LoadedConfig = Result<Arc<Config>, String>)并向下传递。

之所以用 Result<_, String> 而非 Optionintegrations_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_sourcesthreadsweb_chattodosprofilesscheduler_gate——留宿主侧的产品逻辑,其三处 load_or_init 是边界代码、现状即正确。§3 的行应拆分。

待迁文件中仍剩两处装载,均为将被抽取而非移动的宿主边界代码:

  • session/turn/tools.rs 的 Composio 集成获取——配置已通过会话的 runtime_config(在 factory.rs 设定)穿线,此处仅是 setter 路径构建会话时的回退。Composio 是宿主产品逻辑、将在 Phase 4 成为 Tool 实现,故保留回退以免 setter 构建的会话静默丢失集成获取。
  • harness/definition.rsload_for_default_workspace()——唯一调用方是 src/core/agent_cli.rs,CLI 边界辅助函数,definition.rs 迁走时留在宿主侧。

2. 被 Phase 2 阻塞——会话暂时不能放弃 AgentConfig

会话只读取 9 个不同的 AgentConfig 字段,其中 7 个 crate 结构体已覆盖。未覆盖的两个——session_dual_writesession_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_fromtool_config_frommemory_limits_fromapply_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.rssession/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;调用点尚未改指)

AgentMemoryContextComposerSecurityGateBudgetGateDefinitionRegistryExperienceStoreLearningSinkProgressSinkToolOutcomeClassifierModelResolver——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::engineRequireApprovalDeny 一起归档到 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,put upsert——第二个写入者静默销毁第一个的记录。现在 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. 四个适配器无法从会话状态构造。

适配器 构造所需 会话拥有
AgentMemoryExperienceStore Arc<dyn Memory> 有(memory_arc()
ToolOutcomeClassifier
ProgressSink Sender<AgentProgress>
LearningSink Vec<Arc<dyn PostTurnHook>>
BudgetGateContextComposerModelResolver 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() 调用折叠成一处。因此 BudgetGateContextComposerModelResolver 今天就可构造。

剩余阻塞比之前更窄:

Phase 4 的改指仍需要 Phase 2 为 session_dual_write/session_shadow_reads 重新安家,那需要对等性 soak,而 soak 需要已发布的版本来产出数据。

因此 §5 中"Phase 4 以零搬迁风险交付绝大部分架构价值"对适配器成立——它们已完成。改指这一半被"流逝的时间"而非"工作量"门控。

#5396 review 又暴露两个 crate 侧阻塞点,均已上报上游,改指不应越过它们推进:

  • tinyagents#88ProgressEvent 缺少 tool 完成里程碑,宿主无法上报工具结果;工具行将永远停在 running,伪造 success: true 会向时间线与 trace 导出器灌入错误数据。
  • tinyagents#89ModelResolveRequest 不携带模型 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_offloadrun_queuemultimodalparse/tool_calling(与 DS-5b 合并)→ subagent_runnersession/turnsession/{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 搬迁确立的两条规则(都会再次出现):

  1. 提示词文本永不迁移。 §6 将 OpenHuman 提示词文本列为不可发布物,且提示词会指名宿主工具 id。因此 crate 的指针渲染器把读取工具名作为参数而非常量——在再分发 crate 里硬编码工具名,等于把"其他宿主没有的工具"塞进它的提示词。
  2. 宿主策略变成 trait 对,而非 import。 SecurityPolicysanitize_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_forsubconscious 路由、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 行以宿主适配器留下(ChatMessageAgentProgress、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. 与当前仓库的对账

截至本文整理时,方案在仓库中的可验证痕迹如下:

这份方案的可贵之处在于它的"反夸大"姿态:不把搬迁描述为一次可一蹴而就的重构,而是明说它是多季度工程;不回避"295 处改指计数错位"、ToolDispatcher 拼写歧义、progress_tracing 应删而非搬这类细节代价;用"每一阶段都可停止、代码树始终绿色"来管理长期工程风险。对于任何想要理解"如何把一个产品深度耦合的 Agent 模块,反耦合成一个可再分发的通用运行时"的工程师,Phase 0–7 与那 10 个能力 trait 的清单本身就是一份高密度、可执行的架构样例。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389