How to Give an AutoGen Agent a Code Execution Tool
AutoGen has taken code execution seriously from early on, which is more than most agent frameworks can say. It ships a local command-line executor and a Docker-backed one, and the documentation is refreshingly blunt that the local one runs generated code on your machine and you should not do that with anything you have not read.
The Docker executor is the usual next step, and it is a real improvement. It is also a container: your agent's code and your host share one kernel. That is fine for a demo on a laptop you can rebuild, and it is not what you want when the same agent is running against real data in production, with code no human reviewed. This guide wires AutoGen to a microVM instead — a sandbox with its own guest kernel — and covers the multi-agent details that turn out to matter more than the executor choice.
The shape of the integration
In current AutoGen, a tool is a Python function with type hints and a docstring; you pass it to an agent and the framework handles the schema. So the integration is small: create a sandbox, wrap 'run this code in it' in a function, hand that function to the agent, and clean up when the conversation ends. The interesting decisions are not in the wiring — they are in lifetime and sharing.
import asyncio
from pandastack import Sandbox
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
class SandboxTool:
"""A microVM that stays alive for the whole conversation."""
def __init__(self, ttl_seconds: int = 1800):
# 'base' is a general Ubuntu image; 'code-interpreter' comes with the
# scientific Python stack already installed.
self.sb = Sandbox.create(template="code-interpreter",
ttl_seconds=ttl_seconds)
def run_python(self, code: str) -> str:
"""Run Python code in an isolated sandbox and return its output.
Use this for any calculation, data analysis, file manipulation, or
verification the task needs. State persists between calls: packages you
install and files you write are still there on the next call.
"""
self.sb.filesystem.write("/tmp/cell.py", code)
res = self.sb.exec("cd /tmp && python3 cell.py", timeout_seconds=120)
out = (res.stdout or "") + (res.stderr or "")
if res.exit_code != 0:
return f"exit code {res.exit_code}\n{out}"
return out or "(no output)"
def close(self) -> None:
self.sb.kill()Read the docstring again, because it is doing more work than the code. AutoGen passes it to the model as the tool description, and the difference between 'runs Python' and a description that names the situations — any calculation, any analysis, and the fact that state persists — is the difference between a model that reaches for the tool and one that guesses in prose and gets arithmetic wrong.
Wiring it to an agent
async def main() -> None:
tool = SandboxTool()
try:
model = OpenAIChatCompletionClient(model="gpt-4o")
analyst = AssistantAgent(
name="analyst",
model_client=model,
tools=[tool.run_python],
system_message=(
"You are a data analyst. Run code to verify every numeric "
"claim before you state it. Do not estimate what you can compute."
),
)
result = await analyst.run(
task="Compute the 90th percentile of /data/latencies.csv and explain the shape."
)
print(result.messages[-1].content)
finally:
tool.close()
asyncio.run(main())The try/finally is the part people skip and regret. Without it, an exception on turn three leaves a VM running, and you find out at the end of the month. The TTL you set at creation is the backstop that caps the damage; the explicit close is the mechanism you actually rely on.
The multi-agent decision: one sandbox or several?
This is the AutoGen-specific question, and getting it wrong produces bugs that look like model failures. In a team — a round-robin group, a selector group, any multi-agent chat — do the agents share one sandbox or get one each?
Share one when the agents are collaborating on the same artefact. A coder that writes a script and a reviewer that runs it must see the same filesystem, or the reviewer is testing a file that does not exist. This is the common case, and it is why passing the same tool instance to several agents is usually correct.
Give each its own when the work is genuinely independent — three agents exploring three approaches, or a critic that must not be able to alter what it is judging. The failure mode of sharing here is subtle and infuriating: agent B installs a package or overwrites a variable, agent A's next cell behaves differently, and the transcript gives you no hint that anything crossed over.
The other route: a custom code executor
AutoGen also has a code-executor abstraction — the thing its local and Docker executors implement — and you can write your own that targets a remote sandbox. The contract is small: receive a list of code blocks, execute them in order, return an exit code and combined output. Wiring that up means AutoGen's built-in code-executing agents work against your sandbox with no other changes.
Prefer this when you are already using AutoGen's code-executor agents and want to swap the backend without touching your agent definitions. Prefer the plain function tool when you are building agents from scratch — it is fewer moving parts and far less exposed to AutoGen's interface churn between versions, which has been real. Check the executor protocol against the version you have pinned before you write against it.
The defaults that keep this boring
- Always set a TTL. The sandbox should die on its own even if your process is killed mid-conversation. This is the single control that turns a leak into a bounded cost.
- Always set a per-call timeout. An agent writing an accidental infinite loop is not an edge case — it is a Tuesday. Without a timeout, one bad cell holds the conversation open until the TTL fires.
- Return errors to the model rather than raising. Handing back 'exit code 1' plus the traceback lets the agent fix its own mistake, which it usually can. Raising ends the run and wastes the whole trajectory.
- Cap what you return. A cell that prints a hundred thousand rows will blow your context window and cost real money. Truncate the output and say that you did.
- Turn off network access unless the task needs it. Most analysis tasks do not, and an agent that cannot reach the internet cannot exfiltrate anything or pull a package you did not expect.
Why the boundary is worth the trouble
A container isolates processes; it does not isolate kernels. Your agent's code and your host talk to the same one, so a kernel bug reachable from inside the container is a host problem. For code a human wrote and reviewed, that risk is usually acceptable. For code a language model generated in response to input you do not control, it is a different conversation — and prompt injection makes 'input you do not control' a much larger category than most teams assume.
A microVM gives each sandbox its own guest kernel, isolated by hardware virtualisation, so guest code never touches the host kernel at all. The historic objection was start-up cost. On PandaStack there is no warm pool of idle VMs — every create restores a baked Firecracker snapshot, which lands around 179ms p50 and 203ms p99. That is fast enough that you can create one per conversation, or one per agent, without the latency showing up in the transcript.
Frequently asked questions
Is AutoGen's Docker code executor safe enough for production?
It is a meaningful improvement over the local executor and the right default for local development. Whether it is enough in production depends on what the agent's input looks like. Containers share the host kernel, so a kernel-level escape is a host compromise, and your agent is running code generated from text that may be attacker-influenced. If a security reviewer would ask 'what stops generated code from reaching the host,' 'a container' is a weaker answer than 'a separate kernel behind hardware virtualisation.'
Should every agent in a team get its own sandbox?
Only when their work is genuinely independent. Agents collaborating on the same code must share a filesystem or the collaboration is fiction — a reviewer cannot run a script it cannot see. Give separate sandboxes to agents exploring separate approaches, or to a critic that should not be able to modify what it evaluates. When you share, put it in the system message and give each agent its own working directory, because silent cross-contamination is much harder to debug than a missing file.
How do I get data into the sandbox for the agent to work on?
Upload it yourself before the conversation starts, using the SDK's filesystem API, and name the path in the task. Asking the agent to download it costs tokens, fails at least once on an authentication redirect, and makes the run harder to reproduce. The same applies to outputs: have the agent write to a known path and read the file back after the run, rather than trying to extract a large result from the final message.
What happens if the agent's code runs forever?
Whatever your timeout says, which is why the parameter is not optional. Set a per-call timeout so a runaway cell returns a timeout the model can react to, and a TTL on the sandbox so the VM is reaped even if your process dies. Returning the timeout as a normal tool result rather than an exception matters too: an agent told 'that took too long' will usually try a cheaper approach, whereas an exception ends the run and you pay for the whole trajectory with nothing to show.
Keep reading
- Sandboxes for AI agents — microVM isolation, ~179ms create
- The same tool, wired into CrewAI
- The same tool, in LangChain
- Sandboxing LLM tool calls
- From prompt injection to RCE
49ms p50 cold start. Fork, snapshot, and scale to zero.