Effect AI SDK 修复实录:Anthropic 客户端执行工具(Memory / Text Editor / Computer Use / Bash)线上不可用的三项根因与修复
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本篇文章以@effect/ai-anthropic的一则 patch 级 changeset(.repos/effect-smol/.changeset/pre/fix-anthropic-memory-tool.md)为主线,完整还原 Anthropic 提供商工具在传输(wire)层面不可用的三个根因及其修复方案。读完本文,你将理解 Effect AI SDK 中"provider 定义工具"(provider-defined tool)在请求/响应两端的名称映射机制、命令载荷 Schema 的字段要求,以及Schema.optional与Schema.optionalKey在 Anthropic 编解码器中的关键差异,并能在自己的工具开发中直接复用这些修复经验。
一、问题背景:为什么"客户端执行工具"在线上不可用
该 changeset 针对@effect/ai-anthropic包发布了一个patch级修复,改动点概括如下:
Fix client-executed provider tools (Memory, Text Editor, Computer Use, Bash) which were unusable on the wire.
所谓"客户端执行工具"(client-executed provider tools),是指由 Anthropic 服务端预定义、但由客户端代码实际执行(requiresHandler: true)的工具,包括:
| 工具 | 提供方 wire 名称 | SDK 自定义名称 | 用途 |
|---|---|---|---|
| Memory | memory | AnthropicMemory | 跨会话的持久化文件操作(创建、查看、编辑、重命名、删除) |
| Text Editor | text_editor | AnthropicTextEditor | 文件内容读写与目录列举 |
| Computer Use | computer | AnthropicComputerUse | 鼠标/键盘等屏幕操作 |
| Bash | bash | AnthropicBash | 沙箱内执行 shell 命令 |
以 Memory 工具为例,其在 AnthropicTool.ts 中的定义如下:
export const Memory_20250818 = Tool.providerDefined({ id: "anthropic.memory_20250818", customName: "AnthropicMemory", providerName: "memory", requiresHandler: true, parameters: Memory_20250818_Commands, success: Schema.String })注意customName("AnthropicMemory")与providerName("memory")并不相同。在 Effect AI SDK 中,自定义名称用于解决"同一套 toolkit 中包含来自多个 provider 的同名工具"的命名冲突(例如多个 provider 都有web_search),因此providerDefined工具会同时维护两个名字。这恰恰是本轮修复第一项问题的导火索。
二、根因一:wire 名称与自定义名称映射断裂,导致ToolNotFoundError
2.1 问题现象
当 Anthropic 在响应中返回一个客户端执行工具的调用时,其tool_use内容块里的name字段是服务端 wire 名称(如"memory"),而不是 SDK 层 toolkit 所注册的自定义名称(如"AnthropicMemory")。
在修复之前,makeResponse(以及流式场景对应的makeStreamResponse)在处理tool_use时会直接拿 wire 名称去 toolkit 中查找工具,结果自然找不到——因为 toolkit 的键是自定义名称——最终抛出ToolNotFoundError,导致 Memory、Text Editor、Computer Use、Bash 这四个客户端执行工具在真实链路上全部不可用。
2.2 修复方案:引入toolNameMapper双向映射
修复后的makeResponse在tool_use分支中,先通过toolNameMapper.getCustomName(part.name)把 wire 名称反解回自定义名称,再以该名称执行参数转换与回调分发,见 AnthropicLanguageModel.ts:
case "tool_use": { // ... // Map the provider wire name (e.g. "memory") back to the tool's // custom name (e.g. "AnthropicMemory") that the toolkit is keyed by const toolName = toolNameMapper.getCustomName(part.name) const params = yield* transformToolCallParams(options.tools, toolName, part.input) parts.push({ type: "tool-call", id: part.id, name: toolName, params, // ... }) break }toolNameMapper是在请求入口处基于用户传入的工具列表构造的(AnthropicLanguageModel.ts):
const toolNameMapper = new Tool.NameMapper(options.tools) const request = yield* makeRequest({ config, options, toolNameMapper }) return yield* makeResponse({ options, rawResponse, response, toolNameMapper })2.3NameMapper的底层实现
NameMapper定义在核心库 Tool.ts,内部维护两张互逆的映射表:
export class NameMapper<Tools extends ReadonlyArray<Any>> { readonly #customToProvider: Map<string, string> = new Map() readonly #providerToCustom: Map<string, string> = new Map() constructor(tools: Tools) { for (const tool of tools) { if (isProviderDefined(tool)) { this.#customToProvider.set(tool.name, tool.providerName) this.#providerToCustom.set(tool.providerName, tool.name) } } } getCustomName(providerName: string): string { return this.#providerToCustom.get(providerName) ?? providerName } getProviderName(customName: string): string { return this.#customToProvider.get(customName) ?? customName } }getCustomName(providerName):wire 名称 → 自定义名称(响应方向,makeResponse使用);getProviderName(customName):自定义名称 → wire 名称(请求方向,makeRequest在组装工具定义与消息时使用,见 AnthropicLanguageModel.ts 与 L1137);- 两个 getter 均为"查不到就原样返回"的兜底策略,保证非 provider 定义工具的兼容性。
getCustomName的映射在流式路径同样生效:makeStreamResponse中所有tool_use、server_tool_use、web_fetch、web_search、code_execution等分支均通过toolNameMapper.getCustomName(...)归一化工具名(AnthropicLanguageModel.ts、L2247 等),这正是 changeset 中"and the streaming equivalents"所指的改动。
2.4 修复后的错误路径仍然存在
值得注意的是,映射修复后ToolNotFoundError并未从代码库中消失,而是退回到真正的"异常兜底"职责:在 transformToolCallParams 中,若映射后仍未在用户工具列表中找到对应工具,仍会抛出带availableTools清单的ToolNotFoundError;随后使用Schema.decodeEffect对tool_use载荷做参数校验,校验失败则包装为ToolParameterValidationError。这说明修复目标是"可用的工具不再被误判为未找到",而不是吞掉真实的错误。
三、根因二:MemoryCreateCommand缺失file_text,创建文件时正文被丢弃
3.1 问题现象
Memory 工具的create命令用于在模型的内存空间创建新文件。修复前,MemoryCreateCommand的 Schema 中file_text被声明为可选:
file_text: Schema.optional(Schema.NullOr(Schema.String)) // 修复前效果是:当模型想"创建文件并写入正文"时,payload 中的file_text会被当作可选字段处理,实际发送给客户端执行器的create命令丢掉了文件正文,导致创建出的文件内容为空。
3.2 修复方案:file_text升级为必填字段
修复后,MemoryCreateCommand 将file_text改为必填的Schema.String:
export const MemoryCreateCommand = Schema.Struct({ command: Schema.Literal("create"), /** * The path to the file that should be created. */ path: Schema.String, /** * The content to write to the file. */ file_text: Schema.String })command用Schema.Literal("create")作为判别字段,与delete、insert、rename、str_replace、view等命令通过Schema.Union组合成Memory_20250818_Commands(AnthropicTool.ts)。字段从可选变为必填后,create命令的载荷在构造、传输与解码三个环节都能保证file_text存在,文件正文不再丢失。
这一改动同时印证了 Anthropic 服务端对该命令的协议要求:create语义上"创建即写内容",正文属于命令的组成部分而非可选项。
四、根因三:Schema.optional与Schema.optionalKey的编解码差异——"Unsupported AST Undefined"
4.1 问题现象
changeset 明确记录了一行异常信息:
which the Anthropic codec rejected with "Unsupported AST Undefined"
客户端执行工具中有一批可选参数,包括:
| 工具 | 可选参数 | 类型 |
|---|---|---|
| Memory / Text Editor | view_range | [start, end]行号元组(1 起始,-1表示读到文件末尾) |
| Computer Use | coordinate | [x, y]像素坐标(缺省时使用当前鼠标位置) |
| Bash | restart | boolean |
修复前它们使用Schema.optional(...)声明,例如 Bash 工具(AnthropicTool.ts):
parameters: Schema.Struct({ command: Schema.String, restart: Schema.optional(Schema.Boolean) // 修复前:Anthropic codec 拒绝 })当 SDK 把工具定义编码为 Anthropic 的 JSON Schema(wire format)时,Schema.optional会生成包含"Undefined"的 AST,Anthropic 编解码器无法理解该 AST 节点,直接报出"Unsupported AST Undefined",使得带可选参数的工具整体无法注册/使用。
4.2 修复方案:统一改用Schema.optionalKey
修复后,所有受影响的可选参数统一改用Schema.optionalKey:
- Memory 的
MemoryViewCommand(AnthropicTool.ts):
export const MemoryViewCommand = Schema.Struct({ command: Schema.Literal("view"), path: Schema.String, view_range: Schema.optionalKey(ViewRange) })- Text Editor 的
TextEditorViewCommand(AnthropicTool.ts):
view_range: Schema.optionalKey(ViewRange)- Bash 的
restart(AnthropicTool.ts):
restart: Schema.optionalKey(Schema.Boolean)- Computer Use 各动作中的
coordinate(如左键点击 AnthropicTool.ts、双击 L832、拖拽 L958、滚轮 L1102 等)也全部为Schema.optionalKey(Coordinate)。
两者的差异在于:Schema.optional允许字段值本身为undefined(在 AST 中引入Undefined节点),而Schema.optionalKey表示"键可缺省",缺省时整个字段从对象中移除,从而在编码为 Anthropic JSON Schema 时不产生UndefinedAST,规避了编解码器的拒绝行为。这是使用外部 LLM provider 时非常典型的"SDK 内部 Schema 语义"与"服务端协议"之间的适配问题。
五、修复的完整落地与验证
本轮修复的三项改动共同作用于四个工具,最终由@effect/ai-anthropic以patch版本发布(对应 issue #2615)。相关的回归保障体现在:
- 单测覆盖:AnthropicLanguageModel.test.ts 与 AnthropicClient.test.ts 覆盖了
makeResponse对tool_use的解析、toolNameMapper映射以及参数编解码路径; - 同源修复:同样的
NameMapper机制在 OpenAiLanguageModel.ts 与 OpenAiLanguageModel.ts (openai-compat) 中被一致使用,说明"wire 名称 ↔ 自定义名称"双向映射是 Effect AI SDK 各 provider 适配器的通用设计。
在 t3code 仓库中,effect-smol 作为受控第三方仓库被归档在.repos/effect-smol下,根工作区通过 patches/effect@4.0.0-rc.112.patch 对 Effect 生态进行补丁管理。因此,当你升级依赖时,可以重点关注@effect/ai-anthropic的 patch 版本变更记录(CHANGELOG.md),确认是否包含本 changeset 描述的修复。
六、对工具开发者的实践启示
- provider 定义工具的命名是"双轨制":
customName是 SDK/toolkit 层的键,providerName是服务端 wire 名称。凡是处理工具调用回包的地方,都应通过NameMapper(或等价的getCustomName映射)归一化名称,切勿直接假定 wire 名称与本地工具名一致。 - 命令类工具的载荷字段"宁可必填":
create类命令一旦语义上要求携带正文,就应当把正文字段声明为必填,避免可选字段在编码/传输环节被静默丢弃。 - 面向外部编解码器的可选字段优先用
optionalKey:当 Schema 会被编码为第三方(如 Anthropic)的 JSON Schema 时,Schema.optional产生的UndefinedAST 可能不被外部编解码器接受;Schema.optionalKey通过"键缺省"表达可选性,兼容性更好。这也是本次"Unsupported AST Undefined"报错给出的最直接经验。 - changeset 是问题定位的浓缩档案:一条 patch changeset 往往包含问题现象、根因、修复位置与关联 issue,是理解开源 SDK 演进脉络的高性价比入口——本次三个根因恰好对应"名称映射、字段必填、Schema 语义"三类高频 bug 类别。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考