Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Runtime 组合契约(提案)

状态: 提案阶段,尚未实现。本页是 #10993 要求在任何调用方迁移之前提交的组合 API 提案。它交付了该 issue 的第一个验收项,以及 #7432 R1 的设计部分。随此提案附带的唯一代码是 crates/zeroclaw-runtime/src/composition.rs 中的类型骨架,目前尚无任何代码使用它。

这是 RFC #5574 为第二阶段 D1 设定的目标:构建一个智能体运行时,嵌入方可以使用其自行提供的能力来运行它;该运行时“对 Telegram、Discord、Anthropic 或任何具体的工具实现一无所知”,并且二进制程序变为一个轻量的装配层。

当前运行时的状态

在 master 分支提交 c44909b1ed 上,使用 cargo metadata 并通过一个仅统计生产代码的 grep 进行测量,该 grep 会排除 #[cfg(test)] 模块和测试文件。

Crate 边

|箱||Depends on (workspace crates)

Translation: 依赖(工作区 crate)| |-|––––––––––––––––––––––––––––| |zeroclaw-runtime|api, 命令, 配置, 基础设施, 日志, 宏, 记忆, 提供方, relay-proto, sop-graph, 派生, TLS, tool-call-parser, 工具; 插件(可选)| |zeroclaw-channels|api, 配置, 基础设施, 日志, 内存, 提供方, 运行时, spawn, tool-call-parser; 工具(可选)| |zeroclaw-gateway|api、通道、配置、硬件、基础设施、日志、记忆、提供商、运行时、生成、TLS、工具;插件(可选)|

运行时无条件链接每一个具体的提供方(provider)、记忆后端(memory backend)和可选工具。channels crate 位于运行时之上,这正是 #6864 所描述的依赖倒置问题。

在何处构造具体能力

在运行时内部,所有能力(capability)的构建都汇集到少数几个入口函数中:

函数构建
agent::loop_::run|provider, memory, observer, tool registry

Wait, I need to translate this. Here is the translation: 提供方, 记忆, 观察者, 工具注册表| |-|| |agent::loop_::process_message_shared||provider, memory, observer, tool registry

Wait, I need to translate this. Here is the translation: 提供方, 记忆, 观察者, 工具注册表| |-|| |Agent::from_config_with_session_cwd_and_mcp_approval_mode|提供商、记忆、观察者| |Agent::try_apply_model_switch, build_session_model_provider|提供商| |agent::turn::assemble_owned_execution|内存,工具注册表| |tools::delegate (build_target_provider, memory_for_target_agent, independent_agentic_tools_for_target)||provider、memory、tool registry 是委派目标的组成部分

Wait, that’s not a faithful translation fragment. Correct output:

提供方、记忆、工具注册表(用于委派目标)| |daemon::run, daemon::run_heartbeat_worker||memory, observer 内存、观察者| |-|| |cron::scheduler::run_agent_job|记忆| |tools::all_tools_with_runtime 和 default_tools|工具注册表本身|

能力生产施工站点Files
提供者136
内存148
Observer84
工具注册表组装106
zeroclaw_tools:: 使用98(tools/mod.rs 中为 78)10

Construction 也会在运行时之外、与运行时并行地发生。这不仅是依赖问题,还是第二个事实来源(source of truth)。

  • Gateway(zeroclaw-gateway/src/lib.rs)会自行构建具备容错能力的 provider、memory 后端、observer 以及两个工具注册表,然后调用 agent::process_message。
  • Channels 编排器会为每个发送者维护一个提供方缓存(get_or_create_provider),该缓存通过 create_resilient_model_provider_from_ref 构建,并直接调用 all_tools_with_runtime。
  • ACP 服务器通过 Agent::from_config_with_session_cwd_and_mcp_backchannel* 构建代理。
  • src/main.rs 为多个命令构造提供方(providers),并为 SOP 引擎构造记忆(memory),然后调用 agent::run 和 daemon::run。

DaemonRegistry 已有的功能

daemon::DaemonRegistry(已合入 #7430)负责注册子系统启动器:网关、通道编排器、本地 socket、WSS、中继、注册(enrollment)以及 MQTT。它同时持有共享的 SOP 引擎和审计日志记录器。它不注册 providers、memory、tools 或 observers。它解决的是进程组合问题,而非能力组合问题。

提案

两个注册表,因为存在两种生命周期

RFC #5574 提出了一个统一的 Registry,包含 register_channel、register_tool、set_provider、set_memory 和 set_observer。当前代码中有两个生命周期不同的组件,若将它们合并到同一个类型中,这种差异会被掩盖:

功能Subsystems
什么提供商、记忆、工具、外发通道、观察器网关、渠道编排器、套接字、WSS、中继、注册、MQTT
类型RuntimeCapabilities(新增)DaemonRegistry(已存在)
生命周期一次配置生成一次守护进程运行或重载迭代
Consumed byEvery agent turndaemon::run 监督

运行时同时持有这两者。子系统启动器接收当前代际的能力对象,而不是自行构造,这正是消除 channels 编排器中并行构造的做法。gateway 是例外:它的启动器会在 v0.9.0 的 gateway 拆分中被删除,因此它不会迁移到这一契约上(见第 4 步)。

Source 与实例

The RFC 草稿传入的是已完成的实例(set_provider(Arc<dyn Provider>))。这无法满足当前运行时的需求:

  • Providers 和 memory 在多智能体模型(ADR-011)下按每个智能体分别解析。
  • A session 可以在对话中途切换模型(try_apply_model_switch),而委托任务会解析其他智能体的提供方和记忆。
  • A live 配置应用会生成一个新的 generation(参见 ADR-012),而已经运行中的 turn 必须保留其启动时所具备的能力。

运行时向各个源请求其所需的内容,并指明智能体与代次。源决定如何构建与缓存。运行时决定何时发起请求、为哪个智能体发起,以及依据哪个已解析的策略发起。

公共类型

这些内容位于骨架中,zeroclaw_runtime::composition:

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct RuntimeCapabilities {
    pub providers: Arc<dyn ProviderSource>,
    pub memory: Arc<dyn MemorySource>,
    pub tools: Arc<dyn ToolSource>,
    pub channels: Arc<dyn ChannelSource>,
    pub observer: Arc<dyn Observer>,
}

pub trait ProviderSource: Send + Sync {
    fn model_provider(&self, request: &ProviderRequest<'_>)
        -> anyhow::Result<Arc<dyn ModelProvider>>;
}
pub trait MemorySource: Send + Sync {
    fn memory(&self, request: &MemoryRequest<'_>) -> anyhow::Result<Arc<dyn Memory>>;
}
pub trait ToolSource: Send + Sync {
    fn tools(&self, request: &ToolRequest<'_>) -> anyhow::Result<Vec<Box<dyn Tool>>>;
}
pub trait ChannelSource: Send + Sync {
    fn channel(&self, alias: &str) -> Option<Arc<dyn Channel>>;
}
}

ProviderRequest 包含配置、代理别名以及一个可选的显式提供方引用。MemoryRequest 包含配置和代理别名。ToolRequest 包含配置、代理别名、已解析的 SecurityPolicy、所选的 RuntimeAdapter,以及该代理的记忆句柄。

我们计划由以下运行时入口点接收这些输入。它不在骨架中,因为没有实现就无法存在:

#![allow(unused)]
fn main() {
pub struct Runtime { /* config, Arc<RuntimeCapabilities>, DaemonRegistry */ }

impl Runtime {
    pub fn new(config: Config, capabilities: RuntimeCapabilities) -> Self;
    pub fn with_daemon(self, subsystems: DaemonRegistry) -> Self;
    /// 无守护进程时的单次智能体回合:独立消费者路径。
    pub async fn run_turn(&self, agent_alias: &str, message: &str) -> anyhow::Result<String>;
}

/// 守护进程模式:监管已注册的子系统。
pub async fn run(runtime: Runtime) -> anyhow::Result<DaemonExit>;
}

run_turn 的设计刻意保持精简。完整的入站命令路径,包括会话序列化、准入控制、取消和审批,属于 #11012 中跟踪的 RuntimeIngress 合约。run_turn 的存在是为了在该合约落地之前,为独立消费者验收标准提供一个稳定的接口面。待该合约落地后,run_turn 应改为 RuntimeIngress 的轻量客户端。

所有权与生命周期规则

  1. 运行时负责解析;来源负责构造。 运行时决定请求对应哪个智能体、哪个提供方引用以及哪一代。来源从不通过读取环境状态来做此决定。
  2. 能力在每一代中不可变。 修改接线的配置应用会构建一个新的 RuntimeCapabilities。一个回合会一直持有它开始时所用的 Arc,直到结束,因此重新加载永远不会在回合执行中途替换掉某个提供方。
  3. 返回的句柄按作用域借用。 provider 或内存句柄属于请求它的会话或轮次。来源可以缓存并共享实例;调用方不得将其保存到所属会话之外。
  4. 每个智能体在每一代中只对应一个存储。 同一智能体在同一代中的两次内存请求,必须访问同一个底层存储。当前代码通过从同一配置构造来隐式保证这一点;该约定则将其变为数据源的一项强制义务。
  5. 观察器是按代的进程级对象。 它是一个实例,而不是一个来源,因为它不是按智能体逐个解析的。
  6. 仅出站。 ChannelSource 涵盖运行时通过其发送消息的渠道:按别名回复、定时投递以及审批提示。入站对话轮次通过 RuntimeIngress 到达,不经过此契约。

安全策略由运行时统一管理

该问题要求已解析的安全策略在边界两侧保持显式。规则如下:

  • 运行时会解析每个 agent 的 SecurityPolicy 和 RuntimeAdapter,并将它们_传入_ ToolRequest。原生来源是受信任的实现,必须基于这些提供的值来构建工具。将它们传入 Rust trait 并不能机械地限制进程内代码:来源可以返回任意原生工具,因此运行时准入、策略过滤,以及逐次调用的门控和审批,仍然是各自独立的要求。
  • 构建针对某项策略的工具并不会授予任何权限。运行时仍会在构造之后像现在一样对每次调用进行门控和审批。
  • 运行时可能会添加核心工具,也可能移除策略排除的工具。它绝不会因为某个来源返回了某工具就添加策略禁止的该工具。

在第一个实现切片中,奇偶校验测试必须表明:对于代表性配置中的每个 agent,其有效工具集和审批行为均保持不变。

DaemonRegistry 的定位

DaemonRegistry 是被合并(incorporated)而非被替换:

  • 它保留其角色及当前的注册方式。
  • 渠道、cron、SOP 和 RpcContext 启动器将获得本次生成的 RuntimeCapabilities,因此不再自行构建 provider、memory 和 tools。该签名变更单独作为一个切片(下文第 4 步)处理。gateway 启动器不接收任何 capabilities:它将在 v0.9.0 切割点被删除,其路由迁移到 RPC 后,它自身的构造也随之消失。
  • 共享的 SOP 引擎和当前附带的审计日志记录器属于能力形态的状态。本提案中它们保持原位不动,并列为后续事项,待迁移至能力生成机制之后。

与其他工作的关系

  • #6864 已于 2026-08-23 缩小范围,仅保留机械式依赖门控。它最初提出的编排器迁移工作现已归入运行时负责的会话工作。本提案与两者都不重复,而是提供迁移后的编排器将要使用的能力契约。
  • #11012 及其 RFC #9487 负责入站命令路径(RuntimeIngress / InboundTurn)。本提案负责定义一个回合由_哪些部分构建而成_。两者在 Runtime 句柄处交汇,入站处理程序持有该句柄。
  • #10998(默认运行时仅包含核心工具)使用 ToolSource:可选工具被移到应用程序的 ToolSource 之后,运行时只保留其核心工具。

契约所在位置

需要 Config 和 SecurityPolicy,它们位于 zeroclaw-config 中。这排除了 zeroclaw-api,因为该包按设计没有任何工作区依赖。其余可选位置如下:

选项对于反对
A. zeroclaw-runtime(骨架当前的放置位置)它_就是_运行时的公共接口;没有新增 crate;调用方已经依赖它该 crate 是过渡性的占位 crate。在其中放置新的公共 API 需要根据 ADR-016 获得核心团队的例外批准,本提案无法自行授予该例外。
B. 一个新的 contract crate,依赖 api 和 configClean boundary; application wiring can depend on it without the runtimeA 新 crate 位于计划中的内核抽离之前;ADR-016 记录中已指出过早确立边界的风险
C. 计划中的 zeroclaw-kernel目标位置,即两个活动运行时异常已指定的那个不存在;若先提取它,将阻塞当前 agent-loop 提取工作。

决定:选项 A。 该契约位于 zeroclaw-runtime 中,依据单独记录于 #11092 的有界临时容纳 crate 例外,该例外已获得核心团队批准,并于 2026-09-28 合并。范围涵盖 src/composition.rs、在 src/agent/{loop_.rs,agent.rs,turn/mod.rs} 中消费它的 *_with_capabilities 入口适配器,以及 src/daemon/{registry.rs,mod.rs} 中携带能力的 DaemonRegistry 启动器签名,范围限定于 #10993 组合 API 及本页的迁移步骤。不包括将具体的 provider、memory 或 tool 构造迁移到本 crate 中。

预定的去向是计划中的 zeroclaw-kernel 抽取中的组合所有者。该例外将在智能体循环抽取设计评审时提交评审,或在契约新增提供者、记忆、工具、出站通道和观察者之外的能力类型之前提交评审。放置前提条件在该已记录范围内已满足;它并不批准更广泛的运行时新增内容,也不构成实现验收。

迁移调用方的顺序

每个步骤都保留现有配置和有效权限。旧入口点在最后一个调用方迁移之前,作为兼容适配器保留,这是该 issue 风险部分的要求。

步骤更改需要提供证据
0该提案及其骨架。无调用方。API 边界评审
1DefaultCapabilities::from_config:封装当前 create_* 函数且保持不变的来源Provider、记忆与工具集按 Agent 解析的一致性测试
2运行时入口新增 *_with_capabilities 形式:agent::loop_::run、process_message_shared、Agent::from_config*、assemble_owned_execution。旧签名通过 DefaultCapabilities 委托调用。现有的 agent 测试无需修改即可通过;策略传播回归测试
3src/main.rs 只构建一次 DefaultCapabilities,并为 agent 和 daemon 调用新的形式。启动、取消与关闭的回归测试覆盖(CLI 与守护进程)
4channels starter、cron 调度器、SOP 驱动程序和 RpcContext 接收该代的 RuntimeCapabilities。gateway starter 在 v0.9.0 切割时被删除,不接收任何能力;随着其路由迁移到 RPC,其自身的构造也随之消失。每个已迁移的 starter 都须与原实现保持对等,且已迁移的 starter 中不再保留任何 create_* 调用。gateway 不做原地迁移:防止其重新耦合的检查是第 7 步中的依赖棘轮(dependency ratchet),而不是一条规则。
5Channels 编排器的按发送者划分的 provider 缓存成为 ProviderSource 的一个实现;ACP 迁移到新的形式。与 #11012 按顺序配合进行。zeroclaw-channels 中已没有剩余的 create_* 调用
6内部调用方:delegate、cron、heartbeat、subagents、SOP 工具规范运行时中除 DefaultCapabilities 之外,已不再有 create_* 调用
7DefaultCapabilities 和第三层(可选)的工具组装将移至应用层,由其 ToolSource 统一管理。第一层(核心)和第二层(与宿主耦合)的工具仍由运行时构造。运行时将保留 zeroclaw-tools 作为有说明的依赖,直至 v0.9.0(见下文);移除该依赖属于 v1.0.0 的工作。工具按三个粗粒度的 Cargo 特性分组:tools-core(始终启用)、tools-host 和 tools-extra(两者默认启用),并为体积较大的依赖提供按集成划分的独立特性(#10998)。基于 cargo metadata 的依赖棘轮测试,包含三条规则:运行时(需说明其 zeroclaw-tools 依赖边的原因);网关(不得依赖 runtime、channels、tools、providers、memory、plugins、config、hardware 或 sop-graph);rpc-proto(仅允许依赖 api、config 和 sop-graph,不得包含 tokio I/O)。一个独立消费者通过 Runtime::run_turn 运行真实的 agent 轮次,其 ToolSource 返回空的第 3 层。针对每个目标,对比 --no-default-features --features agent-runtime,tools-core 与 dist 的体积证据(#10998 AC5)。
8移除兼容性适配器移除操作在不再有调用方后即可机械完成。

剩余的依赖项及其原因

该 issue 要求:对于保留的任何依赖项,都应加以说明,而不是隐藏在未使用的功能之后。预期的最终状态为:

  • zeroclaw-tools:保留至 v0.9.0,作为一条已说明的依赖边。 目前一级核心工具分布在两个 crate 中:shell 和 file_read 位于运行时中;file_write、file_edit、glob_search、content_search、记忆工具、git_operations 和 web_fetch 位于 zeroclaw-tools 中。二级工具需要运行时内部组件(调度器、canvas 存储、ask-user 句柄、SOP 引擎、会话后端和实时配置),而 ToolRequest 并不携带这些内容。若将任一层迁移到应用层 ToolSource 之后,就会把这些内部组件泄漏到公共契约中。因此第 7 步只迁移三级工具,棘轮机制会将这条依赖边记录在案。移除该依赖属于 v1.0.0 的工作。
  • zeroclaw-providers 和 zeroclaw-memory:目前预计保留,原因与轮次组装无关:doctor、quickstart、migration 以及视觉路由都直接构造 provider 或 memory,且目前仍托管在此 crate 中。根据 holding-crate 计划,每一项都是单独的抽离候选。第 7 步的 ratchet 应仅允许这些依赖边并逐一列明,以便新增的使用方式导致检查失败。

验收映射

#10993 验收项Delivered by
为公共组合 API、所有权与生命周期规则以及 DaemonRegistry 编写文档,涵盖 #6864 的相关部分该页面(第 0 步)
独立的使用方在运行真实的智能体回合时提供所需能力,而无需在运行时内部具体构建 provider、channel 或 tool。根据拟议的第 7 步,此项判定为部分满足:Runtime::run_turn 将会使用所提供的能力,但一级核心工具和二级宿主耦合工具仍将由运行时构造。这一分阶段的保留工具设计并未完全满足该验收标准。在验收完成之前,必须实现并证明剩余的构造边界,或在 #10993 中明确调整该标准。
依赖检查展示了边界;保留的依赖已说明步骤 7 的棘轮及上方的章节
CLI 与守护进程共用同一份契约,并覆盖策略、启动、取消和关闭等场景步骤 2 和 3
Implementation PR 和证据链接见 #7432 R1Each step’s PR

已确定的决策

这些问题在本页面最初提出时仍未解决。v0.9.0 架构评审已对这两项作出决议。

  1. run_turn与等待RuntimeIngress的对比。 run_turn保持最小化并随之发布,它是与session/prompt所使用的同一会话Actor的客户端。它不等待#11012。
  2. 是否 ProviderSource 会接收发起请求的主体。 会,从第一个实现切片开始:当具备能力的构造函数落地时,ProviderRequest 会新增 principal: Option<&PrincipalId> 字段。之后再为嵌入方实现的请求类型添加字段将构成破坏性变更。本页的骨架尚未包含该字段。