Agents: OpenAI
Give GPT models secure code execution via function calling. The same tool registry that powers the Anthropic pack maps directly onto OpenAI's tool format.
Install
pnpm add openai @tangle-network/sandboxMinimal example
import OpenAI from "openai";
import { Sandbox } from "@tangle-network/sandbox";
import { openaiTools } from "@tangle-network/sandbox/agent";
const openai = new OpenAI();
const sandbox = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
const box = await sandbox.create({ publicTemplateId: "python-data-science" });
const { tools, handleAssistantMessage } = openaiTools(box, { sessionId: "s1" });
const messages: any[] = [
{ role: "user", content: "Plot sin(x) for x in [0, 2π]." },
];
for (let step = 0; step < 6; step++) {
const r = await openai.chat.completions.create({
model: "gpt-5.4",
tools,
messages,
});
const m = r.choices[0].message;
messages.push(m);
if (!m.tool_calls?.length) break;
messages.push(...(await handleAssistantMessage(m)));
}handleAssistantMessage dispatches every tool_call on the assistant message in parallel and returns a role: "tool" message per call. Spread them into the conversation before the next turn.
Tool format
Tools are emitted as:
[
{
type: "function",
function: {
name: "sandbox_run_code",
description: "Execute code in a persistent language kernel ...",
parameters: { type: "object", properties: { ... }, required: ["language", "source"] },
},
},
// ...sandbox_exec, sandbox_read, sandbox_write, sandbox_list, sandbox_search
]Restricting tools
openaiTools(box, { allow: ["sandbox_run_code"] });Argument parsing
OpenAI sends function.arguments as a JSON string. The dispatcher accepts JSON objects, including {} for tools without arguments.
It returns a tool error for blank, malformed, scalar, array, or null input before calling a handler.
Send {} when a tool needs no arguments.
Responses API
The same tool definitions also work with the Responses API. Wire handleToolCall (singular) directly into your Response item loop: its return type is a chat-completions tool message but the tool_call_id + content shape carries over.
Type reference
import type {
OpenAITool,
OpenAIToolCall,
OpenAIToolPack,
OpenAIToolPackOptions,
OpenAIToolResultMessage,
CodeExecutionResult,
CodeResultPart,
} from "@tangle-network/sandbox/agent";
// OpenAIToolCall: what a model emits inside choices[0].message.tool_calls:
// { id: string, type?: "function", function: { name: string, arguments: string } }
//
// OpenAIToolResultMessage: what you append before the next chat completion:
// { role: "tool", tool_call_id: string, content: string }
//
// CodeResultPart: discriminated union inside `CodeExecutionResult.results`:
// text | json | image (base64) | html | dataframe | errorDurability & resume
Don't reinvent stream durability. The SDK already buffers every event for every session server-side; if you're sketching a Durable Object / KV / Redis ring buffer to "survive a reload," stop and use the primitives below.
The sessionId passed to openaiTools(box, { sessionId }) scopes the code-execution kernel: variables persist across calls. To make the outer tool-call loop survive a Worker restart, browser reload, or driver crash, wrap it in box.dispatchPrompt + box.session(id).events({ since }). Pass a stable turnId alongside sessionId: a retry from a fresh process then hits the completed-turn cache and the agent never re-runs. sessionId on its own only stops a second agent while the first is still in flight — once the turn finishes it is no longer a retry key.
// In your /chat handler: survives Worker isolate eviction and browser reload.
const turnId = req.headers.get("x-turn-id") ?? crypto.randomUUID();
const sessionId = turnId;
const lastEventId = req.headers.get("last-event-id") ?? undefined;
// turnId is the retry key: a completed turn is a cache lookup, not a second
// agent run. sessionId alone only blocks a concurrent dispatch.
await box.dispatchPrompt(userMessage, {
sessionId,
turnId,
metadata: { framework: "openai-tool-use" },
});
for await (const event of box.session(sessionId).events({ since: lastEventId })) {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
}For browser-direct streaming, use SessionGatewayClient from @tangle-network/sandbox/session-gateway: auto-reconnect, sequence-gap replay, lastEventId persistence across reloads, no Durable Object needed. See the SDK's session resume guide and examples/cf-worker-chat.ts.
Idempotency & retry
Pass idempotencyKey to dedup a single runCode call within a 15-minute window. Concurrent retries with the same (sessionId, idempotencyKey) collapse onto one execution; sequential retries return the cached result without re-running. Thrown infra errors are not cached.
const result = await box.runCode("python", source, {
sessionId: "s1",
idempotencyKey: req.headers.get("idempotency-key") ?? undefined,
});For exactly-once across the outer tool-use loop, wrap with box.dispatchPrompt({ sessionId, turnId }): that dedups the entire conversation including every runCode inside it.