把 Agent 运行时做成插件系统:agent-harness 的 Rust 实践
一个 Agent 系统真正难的,不是把模型调用跑通,而是让模型、工具、会话、工作流、安全策略和外部基础设施能够被组合、替换、回滚,并且在失败时留下可解释的证据。
agent-harness正是在解决这个问题:它用 Rust 构建一个插件化、可观测、可逆的 Agent Runtime,并通过契约、事件、服务注册和 Profile 组合能力支撑不同的 Agent 应用。
先说结论:它不是一个"更大的 Agent Loop"
很多 Agent 框架的核心抽象是一个循环:
text
用户输入 → 模型 → 工具 → 工具结果 → 模型 → 最终回答
这个抽象适合做 Demo,但当系统进入真实生产环境后,问题会迅速增加:
- 模型 provider 如何替换,而不修改 Agent Loop?
- 工具调用前如何统一接入权限、路径、预算和安全策略?
- 工作流、子代理、团队运行时如何共享会话和取消语义?
- 插件挂载失败时,已经注册的服务和监听器如何回滚?
- 开发环境可以使用 mock,但生产环境如何保证不会误用 mock?
- 一个模型请求到底看到了什么,系统崩溃后能否重建?
- "测试通过"究竟证明了实现、生产路径,还是仅仅证明了某个内存 mock?
agent-harness 的选择不是继续向核心循环里添加更多分支,而是把能力拆成可组合的插件,并让内核只负责机制,不负责业务。
openJiuwen、agent-core 与 agent-harness 的关系
理解这个项目,首先要区分三个名字:
- openJiuwen:上游开源项目与产品生态名称,包含 Agent 能力、团队协作、工作流、工具和演进能力等更大的技术背景;
- agent-core:openJiuwen 体系中的历史 Python Agent Runtime。它提供了原始的公开行为参考,包括输入输出、状态迁移、错误、取消、超时、恢复、持久化和外部协议语义;
- agent-harness:本仓库中的 Rust 实现。它以插件化 Runtime 的方式,重新组织并实现 openJiuwen agent-core 的公开行为目标。
可以用下面的关系理解:
text
openJiuwen
└── agent-core(历史 Python 行为规格)
↓ 公开行为、fixture、协议和状态语义
agent-harness(Rust 插件化实现)
因此,agent-harness 不是一个与 openJiuwen 无关的通用 Agent Demo,也不是把 Python 项目简单翻译成 Rust。它的目标是:在不依赖 Python Runtime 的前提下,使用 Rust 重新实现可验证的公开行为,并用更清晰的契约、插件和生命周期边界承载这些能力。
哪些内容属于迁移目标
迁移目标覆盖 openJiuwen agent-core 公开能力中的多个领域:
- core:application、agent loop、workflow、controller、session;
- harness:tools、rails、security、subagents、CLI;
- agent teams:团队任务板、消息、调度和多 Agent 协作;
- agent evolving:轨迹、评估、经验和优化;
- RSI:数据集生成、执行、评估、提示精化和 checkpoint;
- extensions:Redis、向量检索、消息队列、遥测、传输和远程 sandbox 等外围能力。
这些能力在 Rust 中不是集中放进一个巨大模块,而是分别落在 ah-contracts、ah-hub 和多个 ah-plugins-* crate 中。这样做是为了让每一项迁移都能单独定义契约、选择 provider、验证生产路径,并记录自己的完成状态。
哪些内容不属于兼容目标
项目明确排除了三类兼容:
- 不提供
openjiuwen.*的 Python import 路径; - 不复制 Python 的继承、metaclass、协程对象身份等对象模型;
- 产品实现、默认测试和生产 Profile 不依赖 Python。
Python agent-core 在本项目中是历史行为规格参考,而不是构建依赖或运行时后端。需要执行用户提交的 Python 代码时,那属于单独声明的 code 能力边界,不意味着整个 Runtime 依赖 Python。
这一区分很重要:项目追求的是行为层面的可验证重实现,不是 Python 包级别的 API 兼容。对于可以放在同一抽象层比较的能力,仓库使用 Rust contract fixture、golden fixture 和有限的 Python/Rust differential 进行验证;不能公平比较的 Python 宿主细节,则明确记录为不适用或未完成,而不是制造一个虚假的 parity 结论。
项目结构:四层,单向依赖
仓库的核心分层很简单:

图 1:ah-app 负责组装,插件负责能力,ah-hub 负责机制,ah-contracts 负责边界。
#mermaid-svg-saBkWnIMNaM6ysNH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-saBkWnIMNaM6ysNH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-saBkWnIMNaM6ysNH .error-icon{fill:#552222;}#mermaid-svg-saBkWnIMNaM6ysNH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-saBkWnIMNaM6ysNH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-saBkWnIMNaM6ysNH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-saBkWnIMNaM6ysNH .marker.cross{stroke:#333333;}#mermaid-svg-saBkWnIMNaM6ysNH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-saBkWnIMNaM6ysNH p{margin:0;}#mermaid-svg-saBkWnIMNaM6ysNH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-saBkWnIMNaM6ysNH .cluster-label text{fill:#333;}#mermaid-svg-saBkWnIMNaM6ysNH .cluster-label span{color:#333;}#mermaid-svg-saBkWnIMNaM6ysNH .cluster-label span p{background-color:transparent;}#mermaid-svg-saBkWnIMNaM6ysNH .label text,#mermaid-svg-saBkWnIMNaM6ysNH span{fill:#333;color:#333;}#mermaid-svg-saBkWnIMNaM6ysNH .node rect,#mermaid-svg-saBkWnIMNaM6ysNH .node circle,#mermaid-svg-saBkWnIMNaM6ysNH .node ellipse,#mermaid-svg-saBkWnIMNaM6ysNH .node polygon,#mermaid-svg-saBkWnIMNaM6ysNH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-saBkWnIMNaM6ysNH .rough-node .label text,#mermaid-svg-saBkWnIMNaM6ysNH .node .label text,#mermaid-svg-saBkWnIMNaM6ysNH .image-shape .label,#mermaid-svg-saBkWnIMNaM6ysNH .icon-shape .label{text-anchor:middle;}#mermaid-svg-saBkWnIMNaM6ysNH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-saBkWnIMNaM6ysNH .rough-node .label,#mermaid-svg-saBkWnIMNaM6ysNH .node .label,#mermaid-svg-saBkWnIMNaM6ysNH .image-shape .label,#mermaid-svg-saBkWnIMNaM6ysNH .icon-shape .label{text-align:center;}#mermaid-svg-saBkWnIMNaM6ysNH .node.clickable{cursor:pointer;}#mermaid-svg-saBkWnIMNaM6ysNH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-saBkWnIMNaM6ysNH .arrowheadPath{fill:#333333;}#mermaid-svg-saBkWnIMNaM6ysNH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-saBkWnIMNaM6ysNH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-saBkWnIMNaM6ysNH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-saBkWnIMNaM6ysNH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-saBkWnIMNaM6ysNH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-saBkWnIMNaM6ysNH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-saBkWnIMNaM6ysNH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-saBkWnIMNaM6ysNH .cluster text{fill:#333;}#mermaid-svg-saBkWnIMNaM6ysNH .cluster span{color:#333;}#mermaid-svg-saBkWnIMNaM6ysNH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-saBkWnIMNaM6ysNH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-saBkWnIMNaM6ysNH rect.text{fill:none;stroke-width:0;}#mermaid-svg-saBkWnIMNaM6ysNH .icon-shape,#mermaid-svg-saBkWnIMNaM6ysNH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-saBkWnIMNaM6ysNH .icon-shape p,#mermaid-svg-saBkWnIMNaM6ysNH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-saBkWnIMNaM6ysNH .icon-shape .label rect,#mermaid-svg-saBkWnIMNaM6ysNH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-saBkWnIMNaM6ysNH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-saBkWnIMNaM6ysNH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-saBkWnIMNaM6ysNH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ah-app
组装与启动
ah-hub
注册表、事件总线、插件、Profile
ah-plugins-*
模型、工具、会话、工作流等
ah-contracts
Seam、Event、纯类型
ah-contracts:只定义 Seam trait、事件、ServiceKey 和纯数据类型,不放 provider 实现。ah-hub:提供ServiceRegistry、EventBus、Plugin、Effect、Profile 和依赖拓扑挂载。ah-plugins-*:实现模型、工具、工作流、会话、记忆、团队、遥测、传输等能力。ah-app:读取 Profile,解析插件目录,挂载插件,再通过 Seam 调用服务。
依赖方向是硬规则:
text
ah-app → ah-hub → ah-contracts
ah-plugins-* → ah-hub + ah-contracts
除了组装层 ah-app,生产插件不能直接依赖另一个插件的具体类型。插件之间只能通过 ah-contracts 中声明的契约协作。
这条规则看起来严格,但它解决了一个长期问题:当一个 provider 被替换时,consumer 不应该因为 import 路径变化而一起修改。
Seam:把一个能力拆成三角形
项目把可替换能力称为 Seam。一个完整 Seam 不是一个 trait,而是三个部分:
| 部分 | 含义 | 所在位置 |
|---|---|---|
| Service Definition | 能力契约,例如 ModelProvider |
ah-contracts |
| Service Provider | 契约的具体实现 | ah-plugins-* |
| Consumer | 通过契约使用能力的组件 | 任意插件或应用 |
例如,模型能力不要求 Agent Loop 知道 OpenAI、Anthropic 或 mock provider 的具体类型。Agent Loop 只查找稳定的服务键,并请求 dyn ModelProvider:
rust
let model = ctx
.service::<dyn ModelProvider>(&LLM)
.ok_or(AppError::MissingService(LLM))?;
let response = model.chat(request).await?;
Provider 则在插件挂载时注册自己:
rust
impl Plugin for MyModelPlugin {
fn name(&self) -> &'static str {
"ah-plugins-my-model"
}
fn provides(&self) -> Vec<ServiceKey> {
vec![LLM]
}
fn inject(&self) -> Vec<ServiceKey> {
vec![CREDENTIALS]
}
fn apply(&self, ctx: &Context) -> Result<Vec<Effect>, PluginError> {
let provider: Arc<dyn ModelProvider> =
Arc::new(MyModelProvider::new(/* config */));
Ok(vec![ctx.register(LLM, provider)])
}
}
这里有三个重要约束:
- Provider 明确声明自己提供哪些服务。
- Provider 明确声明自己依赖哪些服务。
apply返回Effect,而不是悄悄修改全局状态。
因此,插件的依赖关系可以被检查、排序和诊断,而不必依赖一长串手写的启动顺序。
无特权内核:核心只提供机制
ah-hub 不内置模型、工具、会话或 Agent Loop。它只提供组合能力:
- 服务注册和查找;
- 事件监听和分发;
- 插件挂载;
- 依赖拓扑排序;
- 重复 provider、缺失依赖和循环依赖检测;
- 可逆注册;
- Profile 解析。
Context::mount_all 的语义大致是:
- 收集每个插件的
provides,拒绝重复 provider; - 根据
inject和provides建立依赖图; - 拓扑排序,依赖先于消费者挂载;
- 检测循环依赖和缺失依赖;
- 按顺序执行
apply; - 任一插件失败时,释放此前已经产生的
Effect。
这样做的直接收益是:失败不会留下半挂载的 Context。

图 2:插件从 Profile 进入 Context,退出时由 Effect 统一回滚。
从零跑通一次开发流程
下面是一条不需要云端模型凭据的本地路径:
sh
cargo build --workspace
cargo test --workspace
cargo run --offline -p ah-app --bin ah-app -- profiles/dev.toml
cargo run -p ah-code-cli -- --workspace .
dev.toml 使用确定性的 mock LLM,但工具链、会话日志、工作流、记忆和安全 rail 仍然走真实插件。它适合验证挂载、解析、调用、事件广播和终端渲染,不代表生产 provider 已验证。
用一个最小插件接入新的 provider
新增 provider 时,核心工作不是修改 Agent Loop,而是实现一个 Seam provider:
rust
impl Plugin for MyProviderPlugin {
fn name(&self) -> &'static str { "ah-plugins-my-provider" }
fn provides(&self) -> Vec<ServiceKey> { vec![LLM] }
fn inject(&self) -> Vec<ServiceKey> { vec![CREDENTIALS] }
fn apply(&self, ctx: &Context) -> Result<Vec<Effect>, PluginError> {
let provider: Arc<dyn ModelProvider> =
Arc::new(MyProvider::from_credentials(ctx)?);
Ok(vec![ctx.register(LLM, provider)])
}
}
接线步骤:在 ah-contracts 定义或复用 trait 和 ServiceKey,在独立的 ah-plugins-* crate 实现 provider,在 ah-app catalog 注册名称,在 Profile 中加入插件,最后补齐 mount、resolve、invoke、unmount 和失败路径测试。
生产模式与开发模式
真实 provider 使用 env 文件显式提供配置:
sh
OPENAI_API_KEY=replace-me
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini
REDIS_URL=redis://127.0.0.1:6379/
sh
AH_ENV_FILE=/secure/path/agent-harness.env \
cargo run --offline -p ah-app --bin ah-app -- profiles/prod.toml
真实 provider 请求失败时会显式报错,不会回退到 mock。不要把真实密钥提交到仓库;缺失 env 文件时启动也应失败。
观察会话事件与拦截工具调用
只观察事件时使用 emit:
rust
let session_events = ctx.on::<SessionEvent>(|event| {
println!("session event: {event:?}");
});
run_application().await?;
drop(session_events);
工具执行前需要安全决策时使用 waterfall;允许继续必须调用 next():
rust
let guard = ctx.on_waterfall::<ToolDecision, ToolDecision, _, _>(
|_event, decision, next| async move {
if decision.path_escapes_workspace() {
ToolDecision::Deny
} else {
next.next(decision).await
}
},
);
这几个例子体现了项目的核心生命周期规则:观察、决策和资源注册都必须可追踪、可撤销。
Effect:所有注册都必须可逆
在项目中,服务注册和事件监听都返回一个 RAII guard:Effect。
rust
let service_effect = ctx.register(LLM, provider);
let listener_effect = ctx.on::<AgentStep>(|event| {
record(event);
});
// Effect 存活期间注册有效。
// 离开作用域或显式 drop 后,注册被撤销。
drop(listener_effect);
drop(service_effect);
这使插件卸载变成普通的资源生命周期管理,而不是额外设计一套"反注册 API"。服务、监听器以及插件创建的后台资源,都应该和插件生命周期绑定。
这里有一个容易踩的 Rust 陷阱:
rust
let _ = ctx.register(LLM, provider);
这段代码会立即丢弃 Effect,注册也会立即回滚。需要使用具名绑定持有 guard:
rust
let _effect = ctx.register(LLM, provider);
可逆注册的价值不仅在正常关闭流程。它还保证了批量挂载的原子性:如果后续插件 apply 失败,之前的注册可以通过 Effect 统一撤销。
类型化事件:四种分发语义,而不是一个 EventBus 方法
Agent 系统中的事件并不只有一种语义。ah-hub 明确区分四种分发模式:
| 模式 | 语义 | 适用场景 |
|---|---|---|
emit |
同步、按注册顺序通知 | 日志、观察、轻量遥测 |
serial |
异步、按顺序逐个等待 | 审计链、顺序副作用 |
parallel |
异步并发执行全部监听器 | 扇出通知、独立遥测 |
waterfall |
通过 next() 形成异步委托链 |
rail、审批、策略决策 |
普通事件监听器同样返回 Effect:
rust
let effect = ctx.on::<SessionEvent>(|event| {
println!("{event:?}");
});
最有意思的是 waterfall:
rust
let effect = ctx.on_waterfall::<ToolDecision, ToolDecision, _, _>(
|_event, decision, next| async move {
if should_deny(&decision) {
return ToolDecision::Deny;
}
next.next(decision).await
},
);
它不是简单的广播。监听器调用 next(),才会把结果交给下游;不调用 next() 就代表短路。于是工具执行前可以形成这样的决策链:
text
工具请求
→ PathGuard
→ ShellGuard
→ ToolBudget
→ PermissionApproval
→ SandboxPolicy
→ 真正执行
其中任意一层都可以拒绝或改写决策,而且消费方不需要 import 每个策略插件的具体类型。
当前契约级事件包括 agent/step、session/event、tools/pre-execute、tools/post-execute、teams/swarm 和 teams/task。事件目录同时记录生产者、消费者和分发模式,避免事件变成没有所有权的全局广播。
Profile:用组合配置选择环境
插件并不在代码里硬编码一套固定组合,而是通过 Profile 组织:
toml
name = "dev"
[[bundles]]
id = "mock"
plugins = ["ah-plugins-mock"]
启动过程是:
text
Profile
→ ah-app plugin catalog
→ 解析插件名称
→ 依赖拓扑排序
→ Context::mount_all
→ 解析 Seam
→ 调用 ApplicationRuntime
开发 Profile 可以使用确定性的 mock provider;生产 Profile 必须通过 mock gate,不能包含 ah-plugins-mock。
更重要的是,Profile 只决定组合,不改变 consumer 的代码。开发环境和生产环境可以使用不同 provider,但 Agent Loop 仍然只依赖 ModelProvider。
项目也明确禁止静默 fallback:真实 provider 失败时必须显式返回错误,不能悄悄切换到本地 mock,再把这次运行报告成生产验证。
日志即真相:模型看到什么,日志就记录什么
会话系统采用 append-only JSONL 事件日志。基本原则是:
任何到达模型请求的输入,都必须能够从会话事件日志重建。
这意味着用户输入、助手消息、工具调用、工具结果、Agent step 和系统事件不是散落在多个内存对象里,而是有顺序、有序列号的会话事件。
它带来几个实际能力:
- 会话历史可以回放;
- 崩溃后可以恢复;
- checkpoint、fork、restore 有明确的持久化边界;
- CLI 可以在事件落盘后实时渲染,而不必等整轮 Agent 执行结束;
- 工具恢复可以根据稳定的
tool_call_id判断是否允许重新执行。
这也是为什么项目不把日志只当作 observability 输出。对于 Agent Runtime,日志本身是状态的一部分。
一个可运行的例子:ah-code-cli
仓库中的 example/ah-code-cli 是 Claude Code 风格的开发工具示例,使用主项目的真实插件链:
- Agent Loop;
- ToolRegistry;
- 受限文件系统和 Shell;
- Session JSONL;
- Todo 工具;
- 流式终端渲染;
- 工具权限确认;
- Mock/OpenAI-compatible Provider。
启动:
sh
cargo run -p ah-code-cli -- --workspace .
无交互冒烟:
sh
cargo run -p ah-code-cli -- \
--workspace . \
--once "inspect this workspace"
典型输出结构类似:
text
● LS(.)
⎿ Found 2 entries
● mock final answer ...
↳ completed · iterations=1 · tools=1
这里的终端输出不是直接打印原始 JSON,而是消费结构化会话事件。模型流式增量、工具调用和工具结果可以实时渲染;内部记账事件不会被渲染成噪声行。
权限也不是简单的命令白名单。工作区内的读、写、编辑和查找可以直接放行,run_shell 进入 ask 层,通过 permission-approval Seam 请求用户确认。管道、CI 和 --once 等非交互宿主无法询问用户时,会显式拒绝,而不是静默放行。
工程验证:测试通过不等于完成
这个项目最值得借鉴的地方,不是插件数量,而是它对"完成"的定义比较严格。
项目区分至少三类证据:
- Implementation:代码是否存在,接口是否能调用;
- Production verification:真实 Profile、真实协议和真实外部依赖是否跑过;
- Parity:与历史行为规格的差异是否经过契约或差分验证。
因此,下面这些结论不能混为一谈:
- mock provider 能启动;
- Rust 单元测试通过;
- production profile 使用真实 Redis 和模型 provider 执行;
- 与历史行为规格的差分契约通过。
CI 中对应有多层门禁:
cargo fmt --all --check;- workspace Clippy,warnings 视为错误;
- workspace 测试;
- workspace line coverage 不低于 80%;
- production profile mock gate;
- 插件生产依赖隔离测试;
- audit ledger 生成结果的新鲜度检查;
- 独立的 LLM Rails differential job;
- Redis 和真实协议参与的 production smoke。
项目还禁止在生产路径中使用 todo!()、unimplemented!() 或静默 fallback。无法支持的能力必须显式返回错误,并在能力账本中标为未完成或不适用。
当前规模与边界
按当前工作树实测:
cargo metadata --no-deps:114 个 workspace package;- 其中 109 个主仓库
ah-plugins-*package,另有 1 个示例插件; crates/与example/中约 125,469 行 Rust 源码,包含测试;- 源码中有约 1,510 处
#[test]或#[tokio::test]标注; - git 历史自 2026-08-14 起已有 320 次提交。
这些数字只描述规模,不代表行为对等完成度。项目文档也明确区分了工作包账本和能力地图:审计账本中的工作包完成,不等于所有细粒度能力都已经具备完整的历史行为对等。
当前正式排除的范围包括:
openjiuwen.*Python import 路径兼容;- Python 对象模型兼容;
- 产品运行时依赖 Python;
- 运行中的热插拔和动态 ABI。
仍需要谨慎解读的部分包括:Agent Loop 的部分长尾语义、复杂 workflow 行为、部分外部 provider 的真实部署验证,以及厂商特定能力的 live smoke。当前审计记录中,DashScope 原生 embedding/rerank 的 live smoke 被明确豁免,不能写成已验证的真实厂商结果。
为什么选择这种设计
可以把 agent-harness 的设计原则压缩成三句话:
1. 核心不拥有业务,业务通过插件出现
模型、工具、会话、团队和遥测都不是内核特权。内核越小,组合和替换越容易审计。
2. 默认可逆,而不是事后补偿
服务注册、事件监听、插件挂载和资源释放都围绕 Effect 建模。失败路径不是异常情况,而是正常生命周期的一部分。
3. 默认可观察,而不是事后猜测
类型化事件、append-only 会话日志、结构化错误、版本化 checkpoint 和审计 ledger,让系统能够解释"发生了什么"。
这三点比单纯增加一个模型 provider 或一个工具更基础,但也更决定一个 Agent Runtime 能否长期演进。
快速开始
开发环境可以直接使用 mock LLM:
sh
cargo build --workspace
cargo test --workspace
cargo run --offline -p ah-app --bin ah-app -- profiles/dev.toml
运行示例 CLI:
sh
cargo run -p ah-code-cli -- --workspace .
生产 Profile 需要按照项目的 env 文件规则配置 OpenAI-compatible provider、Redis 以及相应外部服务。缺失配置时,启动应该失败并给出明确错误,而不是自动降级到 mock。
结语
Agent 工程的下一个阶段,不只是让模型"会调用工具",而是让整个运行时具备可替换、可恢复、可验证和可审计的结构。
agent-harness 的答案是:
- 用 Seam 隔离能力边界;
- 用插件实现 provider 和 consumer;
- 用类型化事件表达扩展点;
- 用
Effect保证注册可逆; - 用 Profile 组合环境;
- 用 append-only 日志保存模型可见状态;
- 用 production smoke、contract fixture 和审计账本区分"实现了"与"证明了"。
这套设计不承诺所有能力已经完成,也不把一次 mock 测试包装成生产可用。它更像一套面向长期演进的工程约束:先把边界、生命周期和证据体系建立起来,再让 Agent 能力持续增长。