PandaStack

Code Execution for AI Agents: Any Framework

Wire code execution for AI agents into any framework — LangGraph, CrewAI, the Vercel AI SDK, MCP — with one Firecracker sandbox pattern.

Every LLM agent that computes — parses a CSV, fits a model, plots a trend, calls an API and reshapes the response — has to run that code somewhere. Running it on your host is a liability: one hallucinated os.system call is your problem. The fix is the same regardless of which framework drives the loop: give the agent a tool whose body runs untrusted code inside an isolated Firecracker microVM and hands the model back a result — a number, a traceback, or a chart as an object.

This guide frames that universal pattern once, then routes you to a copy-paste recipe for your exact framework. If you're new to the sandbox primitives, start with the code interpreter guide; if you want the broader "why sandbox an agent at all" framing, see AI agents.

The universal pattern

Whether you're on LangGraph, CrewAI, the Vercel AI SDK, or a bare MCP server, the shape never changes. You wire a single tool into the agent loop, and that tool's body talks to a sandbox:

  1. Create a sandbox on the code-interpreter template — a microVM that boots in ~180ms from a snapshot.
  2. Open a persistent code context — a long-lived Jupyter-style kernel. State survives across tool calls, so the agent can define a DataFrame in one step and plot it in the next.
  3. The tool body calls run_code with whatever the model emitted, and returns the text or the formatted traceback to the model — both are signal the agent can reason about.
  4. Surface rich results. A matplotlib/plotly chart comes back as a base64 PNG on ex.png; a DataFrame comes back as an HTML table on ex.results[].html. No save-to-disk, no second download — hand it straight to the model or render it in your UI.

The win is persistence. Create one context per agent session and reuse it across every tool call, instead of spinning up a fresh process and losing every variable between turns.

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 calls

def run_python(code: str) -> str:
    """The body of your framework's tool. Returns signal to the model."""
    ex = ctx.run_code(code)          # -> Execution
    if ex.error:
        return ex.error              # formatted traceback; the model can fix it
    # ex.png is a base64 PNG when the cell drew a chart — hand it to your UI/message.
    return ex.text or ex.stdout      # last text repr, or captured stdout
import { Sandbox } from "@pandastack/sdk";

// One sandbox + one persistent kernel for the whole agent session.
const sandbox = await Sandbox.create({ template: "code-interpreter", ttlSeconds: 3600 });
const ctx = await sandbox.createCodeContext(); // state persists across runCode calls

async function runPython(code: string): Promise<string> {
  const ex = await ctx.runCode(code);          // -> Execution
  if (ex.error) return ex.error;               // formatted traceback; the model can fix it
  // ex.png is a base64 PNG when the cell drew a chart — hand it to your UI/message.
  return ex.text ?? ex.stdout;                 // last text repr, or captured stdout
}

That run_python function is the integration. Every framework recipe below differs only in how you register it as a tool — @tool for LangGraph, tool() for the Vercel AI SDK, a @function_tool decorator for the OpenAI Agents SDK — and how the agent loop hands you the model's generated code string. The sandbox half is identical.

What the tool returns to the model

You returnWhenThe model sees
ex.text / ex.stdoutnormal outputthe result it asked for
ex.errorthe cell raiseda real traceback it can debug and retry against
ex.png (out of band)a chart was producedrender it in your UI or attach to the message
ex.results[].html / .jsona DataFrame / structured valuea table or JSON it can summarize

Returning the traceback (not swallowing it) is what makes agents self-correct: the model reads NameError: name 'pd' is not defined, prepends import pandas as pd, and tries again — no special handling on your side.

Pick your framework

Each recipe below is a complete, runnable agent: install, wire the tool, run the loop. They all share the pattern above — pick the one that matches your stack and copy it.

Python frameworks

  • LangGraph — a ReAct agent with a run_python tool over a persistent kernel; charts surface via a post-run hop.
  • LangChain — the classic agent-executor wiring with a sandboxed code tool.
  • CrewAI — give a crew member a code-execution tool so the agent can compute, not just plan.
  • AutoGen — replace AutoGen's local code executor with a remote Firecracker one.
  • LlamaIndex — a FunctionTool backed by a sandbox for query-time computation.
  • Pydantic AI — a typed tool whose body runs code in the sandbox.
  • smolagents — sandbox smolagents' code-first agent loop so generated Python runs off-host.

TypeScript frameworks

  • Vercel AI SDK — a tool() with a Zod schema that runs code in a sandbox and streams the result.
  • Mastra — wire a code-execution tool into a Mastra agent.

Protocol & application patterns

  • MCP code-execution server — expose a run_code tool over the Model Context Protocol so any MCP client (Claude Desktop, your own host) gets sandboxed execution. See also the MCP concept.
  • Data-analyst agent — an end-to-end agent that ingests a CSV, computes, and returns charts; the canonical "agent that needs a kernel" build.
  • Persistent agent sessions — keep one kernel alive across a multi-turn conversation so the agent's working set survives.

The building blocks

Most of what the recipes do reduces to four primitives. Here's where each is documented in depth.

Run a one-off API call

When the agent just needs to hit an HTTP endpoint and reshape the JSON, you don't always need a stateful kernel — a single run_code does it. The execute a Python API call recipe shows the minimal version.

ex = ctx.run_code("""
import urllib.request, json
data = json.load(urllib.request.urlopen("https://api.github.com/repos/pandastack-io/pandastack-ai"))
print(data["stargazers_count"])
""")
print(ex.text)

Install packages on demand

The code-interpreter template bakes IPython, matplotlib, plotly, kaleido, and pandas. For anything else, install at runtime — pip/uv are available, and the kernel honors IPython's !pip / %pip:

ctx.run_code("!pip install yfinance")          # IPython magic inside the kernel
ex = ctx.run_code("import yfinance; print(yfinance.__version__)")
await sandbox.exec("pip install yfinance");     // or from a shell, outside the kernel
const ex = await ctx.runCode("import yfinance; print(yfinance.__version__)");

The install packages in a sandbox recipe covers exec vs. !pip, pinning, and pre-baking into a custom template.

Stream long-running output

When a build, a training run, or a slow script would blow past a single response, stream it. exec_stream / execStream delivers output via callbacks as it's produced and returns the exit code:

code = sandbox.exec_stream(
    "bash -lc 'pip install torch && python train.py'",
    on_stdout=lambda line: print(line, end=""),
)
print("exited", code)
const code = await sandbox.execStream(
  "bash -lc 'pip install torch && python train.py'",
  { onStdout: (line) => process.stdout.write(line) },
);
console.log("exited", code);

See stream code output for surfacing live output to a user mid-tool-call.

Snapshot & fork between turns

For long-lived agents, you don't want to re-run setup every turn. snapshot() captures memory + rootfs to an id; create(from_snapshot=id) resumes with the kernel and its loaded state intact. fork() makes a same-host copy-on-write clone in ~400ms — ideal for an agent exploring several branches in parallel.

snap_id = sandbox.snapshot()                          # capture warm kernel
# ...later, a new turn...
resumed = Sandbox.create(from_snapshot=snap_id)       # imports + data still loaded

child = sandbox.fork()                                 # CoW clone for a speculative branch
const { id } = await sandbox.snapshot();              // capture warm kernel
const resumed = await Sandbox.create({ fromSnapshot: id });

const child = await sandbox.fork();                    // CoW clone for a speculative branch

The snapshot between agent turns recipe shows the full save/resume loop for a stateful conversational agent.

Auth & cleanup

Set your key once in the environment — never hardcode it. The SDK's default client reads PANDASTACK_API_KEY:

export PANDASTACK_API_KEY=pds_...

Always tear down sandboxes you own. Close the context when a session ends and kill the sandbox; idle kernels are reaped automatically, but explicit cleanup keeps your footprint tight:

ctx.close()
sandbox.kill()
await ctx.close();
await sandbox.kill();

Tag agent sandboxes with metadata on create (e.g. {"agent": "researcher", "session": session_id}) so you can list and reap them later, and set a ttl_seconds so a crashed loop can't leak a VM indefinitely.

Next steps

  • Code interpreter — the full code-context API, rich results, and file uploads.
  • AI agents — security model, one-sandbox-per-turn, and fork-on-explore patterns.
  • Snapshots and forks — how warm-state resume and CoW cloning work.
  • Templates — bake your agent's dependencies into a custom image.
  • MCP server — expose code execution to any MCP client.

On this page