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%
3.4 KB · 53 lines typescript
Raw Blame History
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