SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
4.0 KB · 66 lines typescript
Raw Blame History
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