# Sandboxed code execution as a model tool The most common sandbox integration: give a model a `run_python` tool whose implementation writes the generated code into an isolated VM, runs it, and returns the output. The model gets a real computer to verify its own answers on; your process never executes model-generated code. > **The shape is always the same** > > Whatever the provider or framework: define a tool with a `code` parameter, implement it as `files.write` + `exec` against a sandbox, loop until the model stops calling tools. Below is that pattern for the Anthropic API, the OpenAI API, and the Vercel AI SDK. ## The tool definition A single required `code` string. The description matters - it is what tells the model *when* to reach for the tool. **Python:** ```python RUN_PYTHON_TOOL = { "name": "run_python", "description": ( "Execute Python code in an isolated sandbox and return stdout, " "stderr and the exit code. Use this whenever the answer requires " "computation, data processing, or verifying code actually runs." ), "input_schema": { "type": "object", "properties": { "code": {"type": "string", "description": "Python source to execute"} }, "required": ["code"], }, } ``` **JavaScript:** ```javascript const RUN_PYTHON_TOOL = { name: "run_python", description: "Execute Python code in an isolated sandbox and return stdout, " + "stderr and the exit code. Use this whenever the answer requires " + "computation, data processing, or verifying code actually runs.", input_schema: { type: "object", properties: { code: { type: "string", description: "Python source to execute" }, }, required: ["code"], }, }; ``` ## The executor Create one sandbox per conversation on the [code-interpreter](https://orkestr.eu/docs/sandboxes/code-interpreter) template and reuse it across tool calls: every call runs in the same persistent interpreter, so the dataframe the model loaded in call one is still there in call five. Return the exception when the code raises - models fix their own bugs when they can see the traceback - and remember the kernel keeps its state through an exception. **Python:** ```python from orkestr import Sandbox # One sandbox per conversation, on the code-interpreter template: every # tool call runs in the SAME Python interpreter, so variables, imports # and dataframes persist across calls - a real REPL for the model. sbx = Sandbox.create(template="code-interpreter", network="restricted") def run_python(code: str) -> str: execution = sbx.run_code(code, timeout_seconds=60) if execution.error: # the code raised; state up to the raise is kept return f"{execution.error.name}: {execution.error.value}\n" + \ "\n".join(execution.error.traceback[-5:]) parts = [execution.stdout, execution.text or ""] return "\n".join(p for p in parts if p) or "(no output)" ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // One sandbox per conversation, on the code-interpreter template: every // tool call runs in the SAME Python interpreter, so variables, imports // and dataframes persist across calls - a real REPL for the model. const sbx = await Sandbox.create({ template: "code-interpreter", network: "restricted", }); async function runPython(code) { const execution = await sbx.runCode(code, { timeoutSeconds: 60 }); if (execution.error) { // the code raised; state up to the raise is kept return `${execution.error.name}: ${execution.error.value}\n` + execution.error.traceback.slice(-5).join("\n"); } const text = execution.results.at(-1)?.text ?? ""; return [execution.stdout, text].filter(Boolean).join("\n") || "(no output)"; } ``` ## Anthropic API Claude returns `tool_use` blocks and `stop_reason: "tool_use"`; you run the code and reply with a `tool_result` for each block, all in a single user message. **Python:** ```python import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY messages = [{"role": "user", "content": "What is the 1000th prime number? Verify by computing it."}] while True: response = client.messages.create( model="claude-opus-4-8", max_tokens=16000, tools=[RUN_PYTHON_TOOL], messages=messages, ) if response.stop_reason != "tool_use": break # Echo the assistant turn, run each requested tool, send results back. messages.append({"role": "assistant", "content": response.content}) results = [] for block in response.content: if block.type == "tool_use" and block.name == "run_python": results.append({ "type": "tool_result", "tool_use_id": block.id, "content": run_python(block.input["code"]), }) messages.append({"role": "user", "content": results}) print(next(b.text for b in response.content if b.type == "text")) sbx.terminate() ``` **JavaScript:** ```javascript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); // reads ANTHROPIC_API_KEY const messages = [ { role: "user", content: "What is the 1000th prime number? Verify by computing it.", }, ]; let response; while (true) { response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, tools: [RUN_PYTHON_TOOL], messages, }); if (response.stop_reason !== "tool_use") break; // Echo the assistant turn, run each requested tool, send results back. messages.push({ role: "assistant", content: response.content }); const results = []; for (const block of response.content) { if (block.type === "tool_use" && block.name === "run_python") { results.push({ type: "tool_result", tool_use_id: block.id, content: await runPython(block.input.code), }); } } messages.push({ role: "user", content: results }); } console.log(response.content.find((b) => b.type === "text").text); await sbx.terminate(); ``` ## OpenAI API Same loop, different envelope: `tool_calls` on the assistant message, results as `role: "tool"` messages. **Python:** ```python import json from openai import OpenAI client = OpenAI() # reads OPENAI_API_KEY tools = [{ "type": "function", "function": { "name": "run_python", "description": RUN_PYTHON_TOOL["description"], "parameters": RUN_PYTHON_TOOL["input_schema"], }, }] messages = [{"role": "user", "content": "What is the 1000th prime number? Verify by computing it."}] while True: response = client.chat.completions.create( model="gpt-4o", tools=tools, messages=messages, ) msg = response.choices[0].message if not msg.tool_calls: break messages.append(msg) for call in msg.tool_calls: code = json.loads(call.function.arguments)["code"] messages.append({ "role": "tool", "tool_call_id": call.id, "content": run_python(code), }) print(msg.content) sbx.terminate() ``` **JavaScript:** ```javascript import OpenAI from "openai"; const client = new OpenAI(); // reads OPENAI_API_KEY const tools = [ { type: "function", function: { name: "run_python", description: RUN_PYTHON_TOOL.description, parameters: RUN_PYTHON_TOOL.input_schema, }, }, ]; const messages = [ { role: "user", content: "What is the 1000th prime number? Verify by computing it.", }, ]; let msg; while (true) { const response = await client.chat.completions.create({ model: "gpt-4o", tools, messages, }); msg = response.choices[0].message; if (!msg.tool_calls?.length) break; messages.push(msg); for (const call of msg.tool_calls) { const { code } = JSON.parse(call.function.arguments); messages.push({ role: "tool", tool_call_id: call.id, content: await runPython(code), }); } } console.log(msg.content); await sbx.terminate(); ``` ## Vercel AI SDK The framework runs the loop for you - hand it the executor and a step limit. **JavaScript:** ```javascript import { generateText, tool } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; import { z } from "zod"; import { Sandbox } from "orkestr"; const sbx = await Sandbox.create({ template: "python-3.12" }); const { text } = await generateText({ model: anthropic("claude-opus-4-8"), tools: { run_python: tool({ description: "Execute Python code in an isolated sandbox and return the output.", parameters: z.object({ code: z.string() }), execute: async ({ code }) => { await sbx.files.write("/workspace/tool_call.py", code); const r = await sbx.exec("python /workspace/tool_call.py", { timeoutSeconds: 60, }); return `exit_code: ${r.exitCode}\n${r.stdout}${r.stderr}`; }, }), }, maxSteps: 8, prompt: "What is the 1000th prime number? Verify by computing it.", }); console.log(text); await sbx.terminate(); ``` ## LangChain Wrap the executor in a `@tool` (Python) or `tool()` (JS) and hand it to any LangChain or LangGraph agent - the docstring/description does the same job the tool definition does above. **Python:** ```python from langchain_core.tools import tool from orkestr import Sandbox sbx = Sandbox.create(template="code-interpreter", network="restricted") @tool def run_python(code: str) -> str: """Execute Python in a persistent sandboxed interpreter. Variables and imports persist across calls. Returns stdout and the value of the final expression; on an exception, returns the traceback.""" execution = sbx.run_code(code, timeout_seconds=60) if execution.error: return f"{execution.error.name}: {execution.error.value}" return (execution.stdout + (execution.text or "")) or "(no output)" # Works with any LangChain/LangGraph agent: # from langgraph.prebuilt import create_react_agent # agent = create_react_agent(model, tools=[run_python]) ``` **JavaScript:** ```javascript import { tool } from "@langchain/core/tools"; import { z } from "zod"; import { Sandbox } from "orkestr"; const sbx = await Sandbox.create({ template: "code-interpreter", network: "restricted", }); const runPython = tool( async ({ code }) => { const execution = await sbx.runCode(code, { timeoutSeconds: 60 }); if (execution.error) { return `${execution.error.name}: ${execution.error.value}`; } const text = execution.results.at(-1)?.text ?? ""; return (execution.stdout + text) || "(no output)"; }, { name: "run_python", description: "Execute Python in a persistent sandboxed interpreter. Variables " + "and imports persist across calls. Returns stdout and the value " + "of the final expression; on an exception, returns the traceback.", schema: z.object({ code: z.string() }), }, ); // Works with any LangChain/LangGraph agent: // const agent = createReactAgent({ llm: model, tools: [runPython] }); ``` ## Hardening checklist - **Network off unless the code needs it.** The default `network="off"` is right for pure computation; use `restricted` when the model should be able to `pip install`. See [Networking & egress](https://orkestr.eu/docs/sandboxes/networking). - **Cap every call.** Model-written code loops forever more often than yours does - keep per-call `timeout_seconds` tight. On the interpreter a timeout interrupts the cell but keeps the kernel's variables, so the model can retry without losing its state. - **Truncate huge outputs before they reach the model.** A print in a loop can produce megabytes; clip the tool result to what the model needs. - **Terminate in a finally block** (or use the context manager / `withTemp`) so a crashed loop doesn't leave a metered sandbox running. - **Preinstall heavy stacks.** The `code-interpreter` template ships pandas, NumPy and matplotlib already; for anything beyond that, bake it into a [custom template](https://orkestr.eu/docs/sandboxes/templates) rather than pip-installing on every conversation.