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