// Generic Anthropic tool loop with the official Node SDK (@anthropic-ai/sdk 0.126). // STATUS: LIVE_VERIFIED 2026-09-18 — `node --env-file=.env --experimental-strip-types examples/shared/tool-loop/anthropic_tool_loop.ts` // (claude-haiku-4-5-20251001, two custom tools, ~1.5k tokens ≈ $0.002). Mirrors anthropic_tool_loop.py — see docs/tools/anthropic/tool-use-loop.md. import Anthropic from "@anthropic-ai/sdk"; import { logRequest } from "./log.ts"; const client = new Anthropic(); const MODEL = "claude-haiku-4-5-20251001"; type Handler = (input: any) => string | Anthropic.Messages.ToolResultBlockParam["content"]; function pendingServerCalls(content: any[]): any[] { const answered = new Set(content.filter((b) => typeof b.type === "string" && b.type.endsWith("_tool_result")).map((b) => b.tool_use_id)); return content.filter((b) => (b.type === "server_tool_use" || b.type === "mcp_tool_use") && !answered.has(b.id)); } export async function runToolLoop(messages: Anthropic.MessageParam[], tools: any[], handlers: Record, opts: { model?: string; maxTokens?: number; maxTurns?: number; betas?: string[] } = {}): Promise { let container: string | undefined; let last!: Anthropic.Message; for (let turn = 0; turn < (opts.maxTurns ?? 8); turn++) { const params: any = { model: opts.model ?? MODEL, max_tokens: opts.maxTokens ?? 300, tools, messages, ...(container ? { container } : {}) }; // beta features (computer_2025*, advisor, mcp_servers) go through client.beta.messages with `betas` const resp: any = opts.betas ? await client.beta.messages.create({ ...params, betas: opts.betas }) : await client.messages.create(params); logRequest("anthropic", "POST", "/v1/messages", 200, 0.001, `anthropic_tool_loop.ts turn ${turn}`); last = resp; if (resp.container?.id) container = resp.container.id; messages.push({ role: "assistant", content: resp.content }); if (resp.stop_reason === "pause_turn") continue; // re-send as-is if (resp.stop_reason !== "tool_use") return resp; const results: any[] = []; let programmatic = false; for (const block of resp.content) { if (block.type !== "tool_use") continue; programmatic ||= (block.caller?.type ?? "direct") !== "direct"; const result: any = { type: "tool_result", tool_use_id: block.id }; if (block.toolset_name) result.toolset_name = block.toolset_name; // computer / browser members try { const h = handlers[block.name]; if (!h) throw new Error(`no handler for tool ${block.name}`); result.content = h(block.input); } catch (err) { result.content = `Error: ${(err as Error).message}`; result.is_error = true; } results.push(result); } if (results.length === 0) continue; // only pending server tools -> re-send as-is // tool_result blocks only (mandatory when a server/programmatic call is pending); same tools array on the next turn void pendingServerCalls(resp.content); void programmatic; messages.push({ role: "user", content: results }); } return last; } if (import.meta.url === `file://${process.argv[1]}`) { const tools = [ { name: "get_weather", description: "Get the current weather for a city.", input_schema: { type: "object", properties: { location: { type: "string" } }, required: ["location"] } }, { name: "get_time", description: "Get the current local time in a city.", input_schema: { type: "object", properties: { location: { type: "string" } }, required: ["location"] } }, ]; const handlers: Record = { get_weather: (i) => `${i.location}: 18 C, light rain`, get_time: (i) => `${i.location}: 14:05` }; const messages: Anthropic.MessageParam[] = [{ role: "user", content: "Weather and local time in Tokyo? Use both tools, then answer in one short sentence." }]; const final = await runToolLoop(messages, tools, handlers, { maxTokens: 200 }); console.log(final.stop_reason, final.content.filter((b: any) => b.type === "text").map((b: any) => b.text)); }