Python 88.3%
TypeScript 7.6%
Shell 4.1%
1// Generic OpenAI Responses API tool loop (TypeScript, openai SDK).2// STATUS: LIVE_VERIFIED 2026-09-18 — `node --env-file=.env --experimental-strip-types examples/shared/tool-loop/openai_tool_loop.ts`3// (gpt-5.4-nano, one get_weather round trip). See docs/openai/tool-loop.md for the patterns.4import OpenAI from "openai";5import { logRequest } from "./log.ts";67const client = new OpenAI();8const MODEL = "gpt-5.4-nano";9type Handler = (args: Record<string, unknown>) => unknown;1011export async function runToolLoop(input: string, tools: any[], handlers: Record<string, Handler>, maxTurns = 6): Promise<string> {12 let req: any = { model: MODEL, tools, input, max_output_tokens: 256, reasoning: { effort: "low" } };13 for (let turn = 0; turn < maxTurns; turn++) {14 const resp: any = await client.responses.create(req);15 logRequest("openai", "POST", "/v1/responses", 200, 0, `ts tool loop turn ${turn}`);16 const outputs: any[] = [];17 for (const item of resp.output) {18 if (item.type === "function_call") {19 let result: unknown;20 try {21 const fn = handlers[item.name];22 result = fn ? fn(JSON.parse(item.arguments || "{}")) : { error: `unknown tool ${item.name}` };23 } catch (e) { result = { error: String(e).slice(0, 500) }; }24 outputs.push({ type: "function_call_output", call_id: item.call_id, output: JSON.stringify(result) });25 } else if (item.type === "custom_tool_call") {26 const fn = handlers[item.name];27 outputs.push({ type: "custom_tool_call_output", call_id: item.call_id, output: JSON.stringify(fn ? fn({ input: item.input }) : { error: "unknown" }) });28 } else if (item.type === "mcp_approval_request") {29 outputs.push({ type: "mcp_approval_response", approval_request_id: item.id, approve: false, reason: "denied by policy" });30 } else if (item.type === "shell_call" && (!item.environment || item.environment.type === "local")) {31 outputs.push({ type: "shell_call_output", call_id: item.call_id, output: item.action.commands.map((c: string) => ({32 stdout: c.trim() === "echo OK" ? "OK\n" : "", stderr: c.trim() === "echo OK" ? "" : "blocked by policy",33 outcome: { type: "exit", exit_code: c.trim() === "echo OK" ? 0 : 126 } })) });34 } else if (item.type === "apply_patch_call") {35 const bad = item.operation.path.includes("..") || item.operation.path.startsWith("/");36 outputs.push({ type: "apply_patch_call_output", call_id: item.call_id, status: bad ? "failed" : "completed", output: bad ? "path rejected" : "applied (dry run)" });37 }38 }39 console.log(`turn ${turn}: ${resp.output.map((i: any) => i.type).join(", ")}`);40 if (outputs.length === 0) return resp.output_text;41 req = { model: MODEL, tools, previous_response_id: resp.id, input: outputs, max_output_tokens: 256, reasoning: { effort: "low" } };42 }43 throw new Error("tool loop did not converge");44}4546if (import.meta.url === `file://${process.argv[1]}`) {47 const weather = { type: "function", name: "get_weather", description: "Current weather for a city.", strict: true,48 parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"], additionalProperties: false } };49 const text = await runToolLoop("What's the weather in Paris? Answer in 5 words.", [weather],50 { get_weather: (a) => ({ city: a.city, temp_c: 18, sky: "clear" }) });51 console.log("final:", text);52}53