If you've built even a basic agent, you know the rhythm. Model wants to do something, it calls a tool, the result comes back, the model reads it and decides what's next. Every single one of those round trips goes through the model, and every result lands in the conversation history whether the model needed to see the raw data or not.
That's fine for one or two tool calls. It gets clunky fast once the agent needs to loop over ten items, combine a few results, retry something that failed, or do plain computation like sorting a list. Doing that one call at a time is slow, and it stuffs the model's context with numbers and text nobody actually needed to read.
This is the problem interpreters in Deep Agents are meant to fix.
What an interpreter actually is
An interpreter gives the agent a small programmable workspace, a bit like a scratchpad, where it can write and run real JavaScript. Under the hood this runs on QuickJS, a lightweight JS engine embedded directly in the same Python process. Nothing gets written to disk, no separate process gets spawned. The code lives in memory, runs in memory, and only the final result comes back to the model.
That last detail is the whole point. Instead of the model calling a tool, waiting, reading the result, deciding on the next step, and repeating that a few times, it writes one piece of code that does the looping, sorting, and combining by itself. The model only sees the answer.
One way to keep it straight: a sandbox lets an agent touch the outside world, running shell commands, editing real files. An interpreter lets an agent work inside its own reasoning, keeping the messy intermediate steps out of the context window entirely.
How the flow actually changes
It helps to see the two approaches side by side, step by step, for something as simple as checking three scores and finding the winner.
Without an interpreter, one tool call at a time:
- Model calls
get_score("alpha") - Result comes back, gets added to the conversation
- Model calls
get_score("beta") - Result comes back, added again
- Model calls
get_score("gamma") - Result comes back, added again
- Model reads all three, does the comparison itself, writes the answer
That's seven steps, three of which are pure round trips through the model just to fetch numbers it isn't going to reason about deeply, it's just going to compare them.
With an interpreter:
- Model calls
evalonce, with a short script - Inside that script, all three
get_scorecalls happen, along with the sort and the subtraction - Only the final sentence comes back out of
eval - Model reads that sentence and replies
Same task, one tool call from the model's point of view instead of three, and none of the raw scores ever touch the conversation history. The looping and comparing happen where they belong, in code, not in the model's head.
Adding one to an agent
Getting an interpreter attached to an agent takes very little setup. Install the package, then add the middleware when you create the agent.
pip install deepagents langchain-quickjs
from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware
agent = create_deep_agent(
model="openai:gpt-5.4",
middleware=[CodeInterpreterMiddleware()],
)
That single line of middleware adds one new tool called eval to the agent. You never call it yourself, the model calls it when it decides writing code is the better way to handle whatever you asked for.
Letting the interpreter use your tools
On its own, an interpreter can do math, sort arrays, and reshape data that's already in front of it, but nothing else. Most real agents need to reach outside for that, checking a database or hitting an API. This is what programmatic tool calling, PTC for short, is for.
PTC lets you pick specific tools the interpreter is allowed to call directly from its code, instead of only through the model. Here's a small working example.
from langchain.tools import tool
from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware
from dotenv import load_dotenv
load_dotenv()
@tool
def get_score(team: str) -> str:
"""Get the score for a team."""
scores = {"alpha": 42, "beta": 78, "gamma": 55}
return str(scores.get(team.lower(), 0))
agent = create_deep_agent(
model="openrouter:nvidia/nemotron-3-super-120b-a12b",
tools=[get_score],
middleware=[
CodeInterpreterMiddleware(ptc=["get_score"])
],
system_prompt=(
"Use the eval tool to write JavaScript. "
"Call tools.getScore({team}) with await when you need a score."
),
)
result = agent.invoke(
{"messages": [("user",
"Check scores for alpha, beta, and gamma. "
"Tell me the winner and the margin."
)]},
config={"configurable": {"thread_id": "demo-1"}},
)
for m in result["messages"]:
if m.type == "ai" and m.content:
print(m.content)
A couple of details worth pointing out. ptc=["get_score"] is an allowlist, it tells the interpreter exactly which tools it's permitted to touch from JavaScript and nothing beyond that. Once a tool is on the list, its name gets converted to camel case automatically for use in code, so get_score in Python becomes tools.getScore in JavaScript.
What actually happens when you run this
Walking through it in order helps make the invisible part visible:
- The user message goes to the model.
- The model decides this needs code, not three separate tool calls, and calls
evalwith a script. - Inside that script,
tools.getScoreruns three times, once per team, all inside the sandboxed JS environment. - The script sorts the results and builds a sentence naming the winner and the margin.
- That sentence is the only thing
evalreturns. - The model receives that single string and turns it into a reply.
The generated script looks something like this:
const teams = ["alpha", "beta", "gamma"];
const scores = await Promise.all(
teams.map(async t => ({
team: t,
score: parseInt(await tools.getScore({ team: t }))
}))
);
scores.sort((a, b) => b.score - a.score);
`${scores[0].team} wins by ${scores[0].score - scores[1].score}`;
All three lookups, the sort, and the subtraction happen inside that one call. None of it shows up in the conversation history, only the final sentence does.
A checklist before you wire this into your own agent
If you're trying this for the first time, a few things are worth checking before you assume something's broken:
- Confirm the tool you want to expose is passed both to
tools=[...]on the agent and listed inptc=[...]on the middleware. Missing either one means the interpreter won't see it. - Remember the camelCase conversion. If your Python tool is
get_user_orders, the interpreter expectstools.getUserOrders, not the snake_case version. - Mention the tool names in your system prompt, in camelCase, the way the example does. The model doesn't automatically know it should reach for
evalinstead of calling the tool directly. - If a script fails, check whether the tool call inside it was awaited. Tool calls from PTC are asynchronous, forgetting
awaitis the most common reason results come back as pending promises instead of values. - Keep the scripts small and single-purpose at first. It's easier to debug a ten-line script that does one job than a fifty-line one that tries to do everything.
Why bother with this at all
The benefit is obvious once you picture the alternative. Without an interpreter, checking three scores and comparing them takes three tool calls, each needing its own round trip through the model before the next step can start. With the interpreter, it's one call to eval, with three calls tucked away inside the code it runs. Fewer round trips, less clutter in the context window, and the model spends its attention on the actual answer instead of bookkeeping.
This matters more as the task grows. Looping over fifty records, running several searches at once, retrying a failed call, keeping a running total across many steps, these are all awkward to do one tool call at a time but completely natural as a short program.
A common misunderstanding worth clearing up
It's easy to assume that because the interpreter runs code, it must also be able to save files, generate images, or produce something like a PDF. It can't, and that's intentional. By default an interpreter has no filesystem access and no network access. It's a sandboxed space for computation and for calling a short, explicit list of tools, nothing more. If a task needs to produce a real file someone can open, that's a job for a sandbox backend, a separate part of the Deep Agents toolkit built specifically for touching real files or running real commands.
The one line summary
An interpreter is a private scratchpad the model can write code into. It lets an agent think in loops, conditions, and function calls instead of one slow tool call at a time, and it keeps the intermediate mess out of the conversation so only the answer that matters comes back to you.
Top comments (0)