自定义工具
你现在index.ts是手工制作工具列表。read、grep、bash、task、askUser、todo、loadSkill。这对课程里的七个人来说是可以的。当有人想添加自己的deploy工具,或者用项目特定的安全命令包裹bash而不分叉Harness时,这种方法就不起作用。
一个工具注册表是接缝。新工具通过它注册。现有工具可以组合成新的工具。核心Harness保持不变。
学习成果
src/registry.ts暴露了一个工具注册表,包含register、get、list和一个小型wrapTool辅助。Agent的工具集是基于注册表构建的,而不是硬编码在index.ts中。
快速路径
- 定义一个包含
register(name, tool)、get(name)、list()的ToolRegistry - 把内置工具线路移到
registerBuiltins(registry, sandbox)辅助器里 - 添加
wrapTool(base, { beforeExecute, afterExecute })用于合成 - 用
registry.list()而不是内嵌对象来构建Agent的工具集
动手练习 11.2
构建注册表并迁移现有工具到那里。
要求:
src/registry.ts导出时createRegistry()返回一个对象,包含register、get、list和entries- 内置工具通过
registerBuiltins(registry, sandbox)注册。和之前一样的套装 wrapTool(base, hooks)还回来了一个新的工具,可以beforeExecute运行,afterExecute``base.executeindex.ts中,Agent的工具来自Object.fromEntries(registry.entries())而非内联对象- 演示一个定制套位(
deploy工具)以证明接缝有效
实现提示:
- 注册表是一个
Map<string, Tool>加上三到四个方法名称。别建得太过头 wrapTool是一个薄函数,返回一个新工具。基础工具保持不变entries()退回[name, tool][]让Object.fromEntries干净利落地工作
注册表
import type { Tool } from "ai";
export interface ToolRegistry {
register(name: string, tool: Tool): void;
get(name: string): Tool | undefined;
list(): string[];
entries(): [string, Tool][];
}
export function createRegistry(): ToolRegistry {
const tools = new Map<string, Tool>();
return {
register: (name, tool) => {
tools.set(name, tool);
},
get: (name) => tools.get(name),
list: () => [...tools.keys()],
entries: () => [...tools.entries()],
};
}注册表不拥有任何策略。它是一个带有打字界面的映射。谁调用 register谁决定什么能进去。
内置辅助器
import type { Sandbox } from "./sandbox";
import { createReadTool, createGrepTool, createBashTool, createTaskTool, createAskUserTool, createTodoTool, createLoadSkillTool } from "./tools";
import { createApproval } from "./approval";
import type { Skill } from "./skills";
export function registerBuiltins(
registry: ToolRegistry,
sandbox: Sandbox,
skills: Skill[],
) {
registry.register("read", createReadTool(sandbox));
registry.register("grep", createGrepTool(sandbox));
registry.register(
"bash",
createBashTool(sandbox, createApproval({ mode: "interactive" })),
);
registry.register(
"task",
createTaskTool(sandbox, {
read: registry.get("read")!,
grep: registry.get("grep")!,
}),
);
registry.register("askUser", createAskUserTool());
registry.register("todo", createTodoTool());
registry.register("loadSkill", createLoadSkillTool(skills));
}顺序在这里很重要。task需要read和grep已经存在于注册表中,因为它会生成使用它们的子 Agent。如果顺序错误,你会注册task引用未明确。
包装器
import { tool, type Tool } from "ai";
interface WrapHooks {
beforeExecute?: (input: any) => any | Promise<any>;
afterExecute?: (result: any) => any | Promise<any>;
}
export function wrapTool(base: Tool, hooks: WrapHooks): Tool {
return tool({
description: base.description,
inputSchema: base.inputSchema,
execute: async (input) => {
const transformed = hooks.beforeExecute ? await hooks.beforeExecute(input) : input;
const result = await base.execute(transformed);
return hooks.afterExecute ? await hooks.afterExecute(result) : result;
},
});
}beforeExecute可以重写输入。afterExecute可以对输出进行后处理。这两种方式都是可选的。基础工具保持不变,因此项目可以包裹内置功能而不破坏其他用户的内置功能。
接入 CLI
import { createRegistry, registerBuiltins } from "./src/registry";
const registry = createRegistry();
registerBuiltins(registry, sandbox, skills);
registry.register("deploy", tool({
description: `Deploy the project to a target environment.
WHEN TO USE: pushing changes to staging or production.
WHEN NOT TO USE: testing changes (use bash with the test runner instead).`,
inputSchema: z.object({
environment: z.enum(["staging", "production"]),
}),
execute: async ({ environment }) => {
const { stdout } = await sandbox.exec(`vercel deploy --${environment}`);
return stdout;
},
}));
const agent = new ToolLoopAgent({
// ...
tools: Object.fromEntries(registry.entries()),
instructions: buildSystemPrompt({
// ...
toolNames: registry.list(),
}),
});Agent的tools场现在由注册表推导出来。添加新工具只需一个registry.register(...) 调用,Agent就建好了。移除工具只是一条线。
为项目包装bash
这就是wrapTool赢得其地位的原因:
const baseBash = registry.get("bash")!;
registry.register("bash", wrapTool(baseBash, {
beforeExecute: (input) => {
if (input.command.startsWith("bun test")) {
return { ...input, command: input.command + " --reporter=spec" };
}
return input;
},
}));基础bash仍在记忆中;注册表现在收录了一个包装版本,增加了针对项目的调整。Agent从未见过未包装的那张。Harness核心从未改变。
**注意:延伸曲面表**
| 表面 | 你可以自定义的内容 | 怎么会这样 |
|---|---|---|
| 工具 | 添加、移除、包裹 | 注册表加wrapTool |
| 技能 | 增加专业知识 | skills/目录加loadSkill |
| 沙箱 | 自定义后端 | createSandbox工厂 |
| 审批 | 自定义策略 | 配置加事件(下一课) |
| 系统提示词 | 自定义部分 | PromptContext加buildSystemPrompt |
| 模型 | 每个榜样 | 子 Agent定义 |
每一行都映射到你已经写好的代码。注册表是第一排的入口。其余的也是伸展曲面,每个都有自己的约定。
动手试试
添加一个专门针对项目的小工具来确认接缝:
registry.register("now", tool({
description: "Return the current timestamp",
inputSchema: z.object({}),
execute: async () => new Date().toISOString(),
}));运行一个使用它(Nits)的任务:
bun run index.ts . "What's the current timestamp? Use the now tool."Agent应该调用 now并返回时间戳。移除register线,运行同一个提示词,Agent应该会报告now不存在。
npx tsc --noEmit提交
git add src/registry.ts index.ts
git commit -m "feat(registry): tool registry with wrapTool composition"完成标准
- [ ]
createRegistry退还了一台工作用注册表 - [ ]
registerBuiltins线连接七个内置工具 - [ ]
wrapTool在基础工具周围构建钩子 - [ ] Agent的
tools场地建于registry.entries() - [ ]
registerBuiltins之后注册的自定义工具可被Agent呼叫 - [ ]
npx tsc --noEmit
**注意:替换,不要包裹**
包裹工具可以保持底座不变。更换一个后,它会完全消失。添加registry.unregister(name)和一个模式,项目可以将内置bash替换为带有更小安全前缀列表的项目专用版本。替换需要在自助顺序中发生在哪里?如果Agent建好后才更换,什么会坏?
参考实现
请参见上方完整代码块。