Agents

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.

tool
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 and reuse it across tool calls - installed packages and files persist, so the model can build on its own previous steps. Return stderr and the exit code too: models fix their own bugs when they can see the traceback.

executor
from orkestr import Sandbox

# One sandbox for the whole conversation: pip installs and files
# persist across tool calls, and state accumulates like a REPL's.
sbx = Sandbox.create(template="python-3.12", network="restricted")

def run_python(code: str) -> str:
    sbx.files.write("/workspace/tool_call.py", code)
    r = sbx.exec("python /workspace/tool_call.py", timeout_seconds=60)
    return f"exit_code: {r.exit_code}\nstdout:\n{r.stdout}\nstderr:\n{r.stderr}"

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.

anthropic
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()

OpenAI API

Same loop, different envelope: tool_calls on the assistant message, results as role: "tool" messages.

openai
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()

Vercel AI SDK

The framework runs the loop for you - hand it the executor and a step limit.

vercel-ai
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();

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.
  • Cap every exec. Model-written code loops forever more often than yours does - keep per-call timeout_seconds tight and catch ExecTimeout.
  • 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 with a custom template - a data-analysis tool that pip-installs pandas on every conversation wastes the first tool call.
Reading with an agent? This page is also plain markdown at /docs/sandboxes/llm-tools.md, and the full docs index lives at /docs/llms.txt.