smolagents secure code execution with PandaStack
Give a Hugging Face smolagents agent a run_python tool for secure code execution in an isolated PandaStack sandbox microVM.
smolagents agents are good at writing Python — but that code has to run somewhere. Run it on your host and a hallucinated os.system call is now your problem. This recipe gives a smolagents agent a run_python tool backed by a PandaStack code interpreter microVM, so the smolagents Hugging Face code execution happens inside a secure sandbox — an isolated Firecracker VM — and the agent gets a chart object back, not stdout it has to parse.
Tool, not a custom executor
A quick clarification, because it's the first thing you'll reach for. smolagents has an executor_type knob on CodeAgent, but it is a closed enum — "local", "e2b", "docker", "blaxel", "modal", "wasm" — with no public registration hook. There is no supported executor_type="pandastack"; the executor is private internals where smolagents runs the agent's own generated action loop.
The documented, stable extension point is a Tool. So we surface PandaStack as a tool the agent calls: the smolagents reasoning loop runs wherever you like (locally is fine), and any code it wants to actually execute goes to an isolated microVM. Clean separation — reasoning here, untrusted execution over there.
smolagents executor_type | A PandaStack @tool (this recipe) | |
|---|---|---|
| Public extension point? | No — closed enum, no registration API | Yes — @tool / Tool are first-class |
| What it runs | The agent's own generated action loop | Code the agent explicitly hands you |
| Isolation | Depends on the enum value | Firecracker microVM, netns-isolated egress |
Install
The PandaStack SDK reads PANDASTACK_API_KEY from the environment — never hardcode it in the tool.
export PANDASTACK_API_KEY=pds_... # your API key, pds_ prefix
pip install pandastack 'smolagents[toolkit]'The [toolkit] extra ships smolagents' default tools (web search, etc.). For OpenAI/Anthropic models via LiteLLM, also pip install 'smolagents[litellm]'.
The tool: one persistent kernel, reused across calls
The win is persistence. Instead of a fresh sandbox per call (losing every variable between them), create one CodeContext — a long-lived Jupyter-style kernel inside the microVM — and reuse it. The agent's imports, DataFrames, and intermediate results carry over from one run_python call to the next, exactly like a notebook.
A smolagents @tool has two load-bearing requirements: type hints on every parameter and the return, and a docstring with an Args: section describing each argument. Both get baked into the system prompt; missing either raises at decoration time. Create the kernel lazily so merely importing the module doesn't spin up a live VM.
from smolagents import tool
from pandastack import Sandbox, CodeContext
_ctx: CodeContext | None = None
def _kernel() -> CodeContext:
"""Lazily create one persistent sandbox kernel for the whole session."""
global _ctx
if _ctx is None:
sandbox = Sandbox.create(
template="code-interpreter",
ttl_seconds=3600,
metadata={"tool": "smolagents-run-python"},
)
_ctx = sandbox.create_code_context(language="python")
return _ctx
@tool
def run_python(code: str) -> str:
"""
Execute Python in a secure, isolated sandbox kernel and return its output.
State (variables, imports, DataFrames) persists across calls within this
session, so build up your analysis incrementally. Use matplotlib/plotly
normally — charts are captured automatically; do not savefig or write files.
Args:
code: The Python source to execute in the sandbox.
"""
ex = _kernel().run_code(code)
# Hand the agent everything it needs to self-correct.
if ex.error:
return f"ERROR:\n{ex.error}\n\nstdout:\n{ex.stdout}".strip()
parts = []
if ex.stdout:
parts.append(ex.stdout.rstrip())
if ex.stderr:
parts.append(f"[stderr]\n{ex.stderr.rstrip()}")
if ex.text and ex.text not in (ex.stdout or ""):
parts.append(f"[result] {ex.text}")
if ex.png:
# A chart came back as a base64 PNG object — not stdout to parse.
parts.append("[chart] PNG produced (available as ex.png on the Execution)")
return "\n\n".join(parts) or "(no output)"Returning stderr and the formatted traceback alongside stdout is deliberate: the agent reads its own failure and fixes the next turn. That self-correction loop is the whole point of giving a model an executor.
Why a code context, not exec
create_code_context() is a persistent kernel; sandbox.exec() is a one-shot shell command with no state between calls. For an agent that iterates — define a DataFrame in one step, plot it in the next — you want the kernel. And on the code-interpreter template, which bakes IPython, matplotlib, plotly, kaleido, and pandas, rich outputs are captured automatically: a plt.show() or a bare DataFrame as the last expression comes back as ex.png / ex.results[0].html objects — no savefig, no save-to-disk, no second download.
Execution field | What the agent gets |
|---|---|
stdout / stderr | captured streams (feed both back so it can self-correct) |
error | formatted traceback if the cell raised |
text | convenience: the last text repr (a bare expression result) |
png | convenience: first chart as a base64 PNG object |
results | full list of rich Result outputs (.html, .json, .svg, .markdown, …) |
Wire it into an agent
Two choices. A CodeAgent writes Python to orchestrate its tools and would run that in its own executor — useful, but here the actual computation already lives in the sandbox, so a ToolCallingAgent (which emits plain JSON tool calls, no second interpreter) is the leaner fit. Both take the same tools=[...] and model=.
from smolagents import ToolCallingAgent, InferenceClientModel
# from smolagents import LiteLLMModel # for OpenAI/Anthropic via LiteLLM
model = InferenceClientModel() # Hugging Face Inference Providers default
# model = LiteLLMModel(model_id="anthropic/claude-3-5-sonnet-latest") # needs ANTHROPIC_API_KEY
agent = ToolCallingAgent(
tools=[run_python],
model=model,
)InferenceClientModel() is the current default model class (it's not HfApiModel — that older name is gone). A CodeAgent is the alternative: it runs the agent's own generated orchestration code in its executor_type (the closed enum above), with run_python still handing the actual computation to the microVM. Note that the tool already sandboxes the code it executes — the executor_type only governs the agent's own action loop, not your tool, so there's no need to stack a second sandbox under it. additional_authorized_imports=[...] is a CodeAgent-only kwarg and has no effect on ToolCallingAgent.
Run it
result = agent.run(
"Generate 1,000 samples from a normal distribution (mean 50, std 8) with "
"numpy. Report the sample mean and the 95th percentile, then plot a "
"histogram of the samples with matplotlib (call plt.show())."
)
print(result)The agent emits a run_python tool call, smolagents executes it (the numpy code runs in the microVM), and the tool's return string — the computed mean and percentile — goes back into the agent's context. Because the kernel is persistent, a follow-up like "now overlay the mean as a vertical line" reuses the already-generated samples array; no recomputation. If the agent writes broken code, the ERROR: prefix and traceback land back in its context and it retries.
To inspect what happened step by step, agent.logs holds the structured trace and agent.write_memory_to_messages() renders it as a message list — handy for surfacing the agent's reasoning in a UI.
Surfacing the chart back to the model
This is the wedge. Because a chart comes back as ex.png — a base64 PNG object, captured automatically — you hold the image directly, with no savefig and no save-to-disk round trip. Run chart code in the kernel and the PNG is right there on the Execution:
ex = _kernel().run_code(
"import matplotlib.pyplot as plt; plt.plot([1, 2, 3]); plt.show()"
)
chart_b64 = ex.png # base64 PNG object — no savefig, no file writtenFrom here chart_b64 is yours to render: write it to a file, embed it in a UI, or pass it to whatever multimodal model you use, following that model's own image API. (The run_python tool above already signals the agent when a chart is available; this snippet shows where the bytes live.)
For tables, look at ex.results for the result whose .html is set — that renders a DataFrame as an HTML table you can return from the tool when you want the model to reason over structured data instead of a flattened repr. See the code interpreter guide for the full result model.
Cleanup
The kernel and its microVM are reaped when ttl_seconds expires, but close them explicitly when a session ends:
if _ctx is not None:
_ctx.close() # tears down the lazily-created kernel held by the _ctx globalTo make agent sessions resumable across process restarts, snapshot the sandbox instead of killing it and restore on the next turn — the kernel state comes back with it. To run many agents in parallel without sharing one kernel, give each its own tool instance (one CodeContext per agent), or fork a warm kernel so each branch starts from the same loaded state.
TypeScript note. smolagents is Python-only, so there's no TS agent wiring. The underlying PandaStack call maps one-to-one if you build the same tool pattern in a TS framework:
const ex = await ctx.runCode(code)returns{ stdout, stderr, error?, text?, png? }, mirroring the PythonExecution.
Next steps
- Code interpreter — the pillar guide: code contexts, rich results (charts, DataFrames, JSON), one-shot exec, and file uploads.
- Run code from a LangGraph agent — the same
run_pythonwedge wired into a LangChain/LangGraph runtime. - CrewAI code execution — the sandbox tool pattern in a CrewAI crew.
- Build a data-analyst agent — upload a CSV, let the agent explore it across many tool calls, and stream charts to a UI.
Pydantic AI code execution with PandaStack
Give a Pydantic AI agent a run_python tool backed by a PandaStack code execution sandbox in Python — the model writes code, the microVM runs it, and charts come back as objects.
Vercel AI SDK code execution tool (TypeScript)
Build a Vercel AI SDK code execution sandbox in TypeScript — a tool() that runs model-written code inside a self-hosted Firecracker microVM and hands back charts as objects.