OpenAI Agents SDK: A Custom Code Sandbox
Build an OpenAI Agents SDK code execution sandbox in Python — a custom function_tool that runs code in an isolated PandaStack microVM.
The OpenAI Agents SDK (openai-agents) lets a model call tools you define. The cleanest way to give an agent code execution is a custom @function_tool that runs the code inside an isolated Firecracker microVM and returns the output — so a hallucinated os.system call lands in a throwaway VM, not on your host.
The SDK also ships a first-class sandbox layer with named hosted providers (E2B, Modal, Daytona, and others). PandaStack is not one of those listed named providers. The portable, version-stable path documented here is a custom tool: it works against a plain Agent, needs no buy-in to the sandbox-layer API, and is the same shape used across the agent frameworks hub.
Setup
export PANDASTACK_API_KEY=pds_...
pip install pandastack openai-agentsThe PandaStack SDK reads PANDASTACK_API_KEY from the environment — never hardcode it. The OpenAI SDK reads OPENAI_API_KEY the same way. Note the import root: pip install openai-agents exposes from agents import ....
The tool: one persistent kernel, reused across calls
The win is persistence. Instead of a fresh sandbox per tool call (losing every variable between them), create one CodeContext up front — a long-lived Jupyter-style kernel — and reuse it. The agent's imports, DataFrames, and intermediate results carry over from one run_python call to the next.
from pandastack import Sandbox
# One sandbox + one persistent kernel for the whole agent session.
sandbox = Sandbox.create(template="code-interpreter", ttl_seconds=3600)
ctx = sandbox.create_code_context() # state persists across run_code callsNow wire the tool. @function_tool turns a plain Python function into a FunctionTool: it builds the JSON args schema from the type annotations and parses the docstring (Google/Sphinx/NumPy style) for the tool and argument descriptions. The docstring is how the model learns what the tool does — write it for the model.
from agents import function_tool
@function_tool
def run_python(code: str) -> str:
"""Execute Python in an isolated sandbox kernel and return its output.
State (variables, imports, DataFrames) persists across calls within
this session. Use matplotlib/plotly normally — charts are captured
automatically; you do not need to savefig or write files.
Args:
code: The Python source to execute.
"""
ex = ctx.run_code(code)
if ex.error:
return f"Error:\n{ex.error}"
return ex.stdout or ex.text or "(no output)"The function can be sync or async — @function_tool handles both. The body just calls ctx.run_code(...) and returns the Execution's text, or its formatted traceback so the model can self-correct.
The PandaStack surface is symmetric, so the same tool body in TypeScript (the OpenAI Agents SDK itself is Python-first):
import { Sandbox } from "@pandastack/sdk";
const sandbox = await Sandbox.create({ template: "code-interpreter", ttlSeconds: 3600 });
const ctx = await sandbox.createCodeContext();
// Inside a tool body:
// const ex = await ctx.runCode(code);
// return ex.error ?? ex.stdout ?? ex.text;Build the agent
Pass the tool in Agent(tools=[...]). The model is a plain string on the agent.
from agents import Agent
agent = Agent(
name="Code Runner",
instructions=(
"You are a Python data analyst. Use run_python to compute answers "
"and to plot results. The kernel keeps state between calls, so build "
"your analysis up incrementally. Prefer matplotlib for charts."
),
model="gpt-4o",
tools=[run_python],
)| Field | What it is |
|---|---|
name | label for the agent, used in tracing |
instructions | the system prompt the model runs under |
model | model id as a string (e.g. "gpt-4o") |
tools | list of FunctionTools the model may call |
Run it
Runner exposes three classmethods: Runner.run_sync(...) (blocking, no await), Runner.run(...) (async, awaitable), and Runner.run_streamed(...) (streams events). For a script, run_sync is the simplest — it runs .run() under the hood. The final answer is on result.final_output.
from agents import Runner
result = Runner.run_sync(
agent,
"Compute the first 15 Fibonacci numbers, then plot them on a "
"log-scale line chart. Tell me the 15th value.",
)
print(result.final_output)Behind the scenes the model emits a run_python tool call, the SDK invokes your function (which runs the code in the microVM), feeds the kernel's output back as the tool result, and the model reasons over it. Because the kernel is persistent, a follow-up like "now zoom into the last 5 points" reuses the already-computed fib list — no recomputation.
The async form is equivalent:
import asyncio
from agents import Runner
async def main():
result = await Runner.run(agent, "Run print('hello') in python.")
print(result.final_output)
asyncio.run(main())Rich results, not just stdout
The code-interpreter template bakes IPython, matplotlib, plotly, kaleido, and pandas, so a plt.show() or a bare DataFrame as the last expression is captured as a rich result — no savefig, no temp file. Every Execution exposes .results, a list of typed Result objects with .png / .jpeg / .svg, .html (DataFrames render as HTML tables), .json, .markdown, and .text.
To hand the model a table to reason over instead of a flattened repr, return ex.results[0].html from the tool. To surface a chart in your UI, read ex.png (a base64 PNG) and render it outside the agent loop.
To install a package the model needs at runtime, run it through the kernel — IPython %pip/!pip works on the code-interpreter template:
ctx.run_code("!pip install yfinance")Cleanup
The kernel and its microVM are reaped when the ttl_seconds budget expires, but close them explicitly when the session ends:
ctx.close()
sandbox.kill()To make agent sessions resumable across process restarts, snapshot the sandbox instead of killing it and resume with Sandbox.create(from_snapshot=...) on the next turn — the kernel's memory state comes back with it.
Next steps
- Agent frameworks — the hub: the same code-execution wedge wired into every major framework.
- Run code from a LangGraph agent — the same pattern with a LangGraph ReAct agent, including surfacing charts back to the model.
- Code interpreter — the pillar guide: code contexts, rich results, one-shot exec, and file uploads.
- Snapshots and forks — persist a kernel between agent turns or fork it to explore branches in parallel.