DEV Community

Syeed Talha
Syeed Talha

Posted on

Understanding Interpreters in LangChain Deep Agents

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:

  1. Model calls get_score("alpha")
  2. Result comes back, gets added to the conversation
  3. Model calls get_score("beta")
  4. Result comes back, added again
  5. Model calls get_score("gamma")
  6. Result comes back, added again
  7. 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:

  1. Model calls eval once, with a short script
  2. Inside that script, all three get_score calls happen, along with the sort and the subtraction
  3. Only the final sentence comes back out of eval
  4. 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
Enter fullscreen mode Exit fullscreen mode
from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware

agent = create_deep_agent(
    model="openai:gpt-5.4",
    middleware=[CodeInterpreterMiddleware()],
)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. The user message goes to the model.
  2. The model decides this needs code, not three separate tool calls, and calls eval with a script.
  3. Inside that script, tools.getScore runs three times, once per team, all inside the sandboxed JS environment.
  4. The script sorts the results and builds a sentence naming the winner and the margin.
  5. That sentence is the only thing eval returns.
  6. 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}`;
Enter fullscreen mode Exit fullscreen mode

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 in ptc=[...] 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 expects tools.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 eval instead 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 await is 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)