Agent Harness 学习
← 返回对照矩阵

tool definition

工具

工具定义方式

新增一个工具要写什么?谁负责校验参数和生命周期?

横向读这一行

两种风格:openJiuwen 和 Codex 用「注册表 + 抽象基类/trait」把生命周期收进框架,DeepSeek 和 Pi 用「工厂函数 + schema 声明」把工具做成可独立测试的值。值得注意的是 Codex 与 DeepSeek 都另外实现了 code mode——让模型写代码调工具,而不是逐个发起工具调用。

对比
openJiuwen带代码

抽象基类 + ToolCard 元数据,元类织入生命周期

工具是一个类:继承 Tool,用 ToolCard 声明元数据,实现 invoke 与 stream 两个抽象方法。关键在 _ToolMeta 这个元类——它在实例化时把 invoke/stream 包进 _lifecycle_invoke / _lifecycle_stream,于是权限检查、日志、异常处理这些横切关注点由框架统一织入,工具作者写不到也绕不过。另有 exposure.py 控制工具是否向模型暴露。

class Tool(metaclass=_ToolMeta):
    """tool class that defined the data types and content for LLM modules"""

    def __init__(self, card: ToolCard):
        ...

    @abstractmethod
    async def invoke(self, inputs: Input, **kwargs) -> Output:
        """Execute the tool with provided inputs and return final result."""

    @abstractmethod
    async def stream(self, inputs: Input, **kwargs) -> AsyncIterator[Output]:
        ...

# _ToolMeta.__call__ 在实例化时把 invoke/stream 包进 _lifecycle_invoke /
# _lifecycle_stream(同文件 76-158 行),生命周期由框架织入而非工具自理。
openjiuwen/core/foundation/tool/base.py:160-210@ fd6c47854201
Codex带代码

trait 对象注册表,显式处理重名冲突

工具实现 CoreToolRuntime trait,以 Arc<dyn> 形式注册进 ToolRegistry。用 IndexMap 保序,并单独记一个 first_collision 字段——工具来源包括内置、MCP 服务器、插件,重名是必然要面对的问题,所以它被提升为注册表的一等状态而非报错了事。同目录下还有 approvals.rs、parallel.rs、lifecycle.rs 与 code_mode/,说明审批、并行、生命周期都在工具层而非循环层解决。

pub struct ToolRegistry {
    tools: IndexMap<ToolName, RegisteredTool>,
    first_collision: Option<ToolName>,
}

impl ToolRegistry {
    pub(crate) fn from_tools(tools: impl IntoIterator<Item = Arc<dyn CoreToolRuntime>>) -> Self {
        let mut registry = Self::default();

        for runtime in tools {
            registry.register_trusted(runtime);
        }
        ...
codex-rs/core/src/tools/registry.rs:271-284@ 4beea50e26dd

defineTool 接口,强制声明规范化输出

ToolDefinition 里 output 是「强制的规范输出声明」——工具不能只说自己接受什么,还必须声明返回什么形状。execute 收到的参数是无损快照且被冻结,必须观察或转发 exec.signal 才能正确响应取消;注释诚实地写明注册表保留了调用方的取消语义,但无法硬杀同进程代码。另有 code-mode.ts 与 py-types.ts,支持模型写代码批量调工具。

export interface ToolDefinition extends ToolSchema {
  /** Mandatory canonical output declaration. */
  readonly output: ToolOutputDefinition
  /**
   * Run one accepted call and return only its canonical lossless-JSON value.
   * Async work must observe or forward exec.signal and settle only after its
   * owned work reaches quiescence. The registry preserves caller cancellation
   * through around-dispatch signal replacement and does not abandon this
   * promise, but it cannot hard-kill same-process code.
   */
  execute(args: unknown, exec: ToolRunContext): Promise<unknown>
packages/core/tools/src/index.ts:222-235@ b150a551b8d4
Pi带代码

TypeBox schema + 工厂函数,内核只有四个工具

工具就是一个值:用 TypeBox 声明 schema,用 createXxxTool 工厂函数造出来,类型由 Static<typeof schema> 自动推导。整个内核工具集只导出 createBashTool / createReadTool / createWriteTool / createEditTool 四个——MCP、子代理、沙箱统统不在内核。工厂带 TContext 泛型,宿主自己决定执行上下文,这是它把隔离外推的又一处体现。

const bashSchema = Type.Object({
	command: Type.String({ description: "Bash command to execute" }),
	timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
});

export type BashToolInput = Static<typeof bashSchema>;

export function createBashTool<TContext extends ExecutionToolContext = ExecutionToolContext>(
	...
)

// harness/tools/index.ts 的全部导出:createBashTool / createReadTool /
// createWriteTool / createEditTool —— 内核工具就这四个。
packages/agent/src/harness/tools/bash.ts:11-51@ bfb004d4418f

design space / 设计空间分析

四家把「谁对工具的哪部分负责」分给了不同的角色:openJiuwen 和 codex 把结构性义务压给框架自身,deepseek harness 把对称义务压给工具作者,pi 把执行上下文这一项显式甩给宿主。

openJiuwen 用元类把义务焊死在框架里:Tool 类以 metaclass=_ToolMeta 声明,作者写 ToolCard 元数据并实现 invoke / stream 两个抽象方法,但 _ToolMeta 在实例化时把这两个方法包进 _lifecycle_invoke / _lifecycle_stream——权限检查、日志、异常处理这些横切逻辑由框架统一织入,工具作者写不到也绕不开;另有 exposure.py 单独控制工具是否向模型暴露,是与生命周期分开的一道闸。

codex 把工具来源的多样性正面接住:工具实现 CoreToolRuntime trait,以 Arc<dyn> 注册进 ToolRegistry;注册表用 IndexMap 保序,并单独记一个 first_collision 字段——因为工具来源包括内置、MCP 服务器、插件,重名是必然要面对的问题,所以被提升为注册表的一等状态而不是直接报错。同目录下的 approvals.rs、parallel.rs、lifecycle.rs、code_mode/ 说明审批、并行、生命周期这些问题也都在工具层解决,而不是像有些框架那样堆进循环层。

deepseek harness 让工具作者承担对称的声明义务:ToolDefinition 里 output 是「Mandatory canonical output declaration」——工具不能只声明接受什么,还必须声明返回什么形状。execute 收到的参数是冻结的无损快照,必须观察或转发 exec.signal 才能对取消正确响应;源码注释也诚实地写明了义务的边界:注册表保留了调用方的取消语义,但无法硬杀同进程代码。另有 code-mode.ts 与 py-types.ts,支持模型写代码批量调工具。

pi 把工具定义压到最小、把执行上下文甩给宿主:工具是一个值,用 TypeBox 声明 schema,用 createXxxTool 工厂函数造出来,类型由 Static<typeof schema> 自动推导。内核工具集只有 createBashTool / createReadTool / createWriteTool / createEditTool 四个,MCP、子代理、沙箱都不在内核。工厂函数带 TContext 泛型,执行上下文由宿主自己决定——格子原文把这称为「隔离外推的又一处体现」,与它在其它维度上的选择是同一套作风。

另有一条不属于本维度格子内容、但做四方横向对比时值得记住的背景:deepseek harness 官方带 packages/llm/llm-pi-ai(https://github.com/deepseek-ai/deepseek-harness/blob/b150a551b8d465e31e418e1b2eaf5e79bbb7d28e/packages/llm/llm-pi-ai/README.md),其 README 自述是基于 pi 的 @earendil-works/pi-ai 的多厂商 LLM 适配器——四家里有两家在模型调用层直接相连,并非完全独立的四份实现。这条与本维度描述的工具定义结构本身无直接关系,只是横向对比时的一个提醒。

编辑判断

把四种分工方式排在一起看:openJiuwen 和 codex 都选择让框架/注册表兜底(前者靠元类强制织入生命周期,后者靠注册表把冲突和审批变成显式状态),换来的是工具作者不用操心横切逻辑,代价是作者对自己工具的控制权更少;deepseek harness 走反方向,把对称声明和取消语义的责任压给作者本人,换来的是每个工具的行为更透明,代价是作者要多写更多样板;pi 干脆把「执行上下文该是什么」这个问题都不在内核层回答。这段是本站的编辑判断。

尚未收敛:四种义务分配方式在实际工具生态规模扩大后(尤其是 MCP/插件大量涌入时)分别会在哪里出现摩擦或漏洞,本站目前没有可比的一手数据;llm-pi-ai 这一层适配具体在多大程度上影响 deepseek harness 与 pi 各自工具定义层的独立可比性,本站也没有读过这份适配器的源码,不下结论。

back to course / 回到课程

矩阵展示的是「各家怎么做」。这个问题本身为什么存在、有哪些经典权衡,在课程里讲:

5

工具调用:MCP 与 Skills

把世界暴露给模型:协议、技能与渐进式披露