Python 88.3%
TypeScript 7.6%
Shell 4.1%
1// Generic Anthropic tool loop with the official Node SDK (@anthropic-ai/sdk 0.126).2// STATUS: LIVE_VERIFIED 2026-09-18 — `node --env-file=.env --experimental-strip-types examples/shared/tool-loop/anthropic_tool_loop.ts`3// (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.4import Anthropic from "@anthropic-ai/sdk";5import { logRequest } from "./log.ts";67const client = new Anthropic();8const MODEL = "claude-haiku-4-5-20251001";9type Handler = (input: any) => string | Anthropic.Messages.ToolResultBlockParam["content"];1011function pendingServerCalls(content: any[]): any[] {12 const answered = new Set(content.filter((b) => typeof b.type === "string" && b.type.endsWith("_tool_result")).map((b) => b.tool_use_id));13 return content.filter((b) => (b.type === "server_tool_use" || b.type === "mcp_tool_use") && !answered.has(b.id));14}1516export async function runToolLoop(messages: Anthropic.MessageParam[], tools: any[], handlers: Record<string, Handler>,17 opts: { model?: string; maxTokens?: number; maxTurns?: number; betas?: string[] } = {}): Promise<Anthropic.Message> {18 let container: string | undefined;19 let last!: Anthropic.Message;20 for (let turn = 0; turn < (opts.maxTurns ?? 8); turn++) {21 const params: any = { model: opts.model ?? MODEL, max_tokens: opts.maxTokens ?? 300, tools, messages, ...(container ? { container } : {}) };22 // beta features (computer_2025*, advisor, mcp_servers) go through client.beta.messages with `betas`23 const resp: any = opts.betas ? await client.beta.messages.create({ ...params, betas: opts.betas }) : await client.messages.create(params);24 logRequest("anthropic", "POST", "/v1/messages", 200, 0.001, `anthropic_tool_loop.ts turn ${turn}`);25 last = resp;26 if (resp.container?.id) container = resp.container.id;27 messages.push({ role: "assistant", content: resp.content });28 if (resp.stop_reason === "pause_turn") continue; // re-send as-is29 if (resp.stop_reason !== "tool_use") return resp;30 const results: any[] = [];31 let programmatic = false;32 for (const block of resp.content) {33 if (block.type !== "tool_use") continue;34 programmatic ||= (block.caller?.type ?? "direct") !== "direct";35 const result: any = { type: "tool_result", tool_use_id: block.id };36 if (block.toolset_name) result.toolset_name = block.toolset_name; // computer / browser members37 try {38 const h = handlers[block.name];39 if (!h) throw new Error(`no handler for tool ${block.name}`);40 result.content = h(block.input);41 } catch (err) {42 result.content = `Error: ${(err as Error).message}`;43 result.is_error = true;44 }45 results.push(result);46 }47 if (results.length === 0) continue; // only pending server tools -> re-send as-is48 // tool_result blocks only (mandatory when a server/programmatic call is pending); same tools array on the next turn49 void pendingServerCalls(resp.content);50 void programmatic;51 messages.push({ role: "user", content: results });52 }53 return last;54}5556if (import.meta.url === `file://${process.argv[1]}`) {57 const tools = [58 { name: "get_weather", description: "Get the current weather for a city.", input_schema: { type: "object", properties: { location: { type: "string" } }, required: ["location"] } },59 { name: "get_time", description: "Get the current local time in a city.", input_schema: { type: "object", properties: { location: { type: "string" } }, required: ["location"] } },60 ];61 const handlers: Record<string, Handler> = { get_weather: (i) => `${i.location}: 18 C, light rain`, get_time: (i) => `${i.location}: 14:05` };62 const messages: Anthropic.MessageParam[] = [{ role: "user", content: "Weather and local time in Tokyo? Use both tools, then answer in one short sentence." }];63 const final = await runToolLoop(messages, tools, handlers, { maxTokens: 200 });64 console.log(final.stop_reason, final.content.filter((b: any) => b.type === "text").map((b: any) => b.text));65}66