Skip to main content

JavaScript REPL middleware for deepagents

Project description

langchain-quickjs

A deepagents middleware that gives an agent a persistent, sandboxed JavaScript REPL tool, backed by quickjs-rs (QuickJS embedded via PyO3 + rquickjs).

Instead of issuing N serial tool calls, the model can write one block of JavaScript that orchestrates work in-loop — variables and functions defined in one call survive into the next, Promise.all runs concurrent work, and (opt-in) agent tools are callable from inside the REPL as await tools.<name>(...).

from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    middleware=[CodeInterpreterMiddleware()],
)

Why

Tool calling is fine for small, discrete requests. It falls apart when the model needs to:

  • loop over a list and call a tool per item
  • run two independent tool calls concurrently
  • compute something between calls (aggregate, filter, dedupe, format)
  • reuse intermediate state across several turns

Each of those currently costs one round-trip to the model per step. With a REPL, all of it happens in one eval call. This enables programmatic tool calling where the model writes JavaScript that invokes the agent's own tools.

Install

uv add langchain-quickjs

langchain-quickjs depends on quickjs-rs, a PyO3 extension module that ships prebuilt wheels for macOS, Linux, and Windows on CPython 3.11+.

Quick start

from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    middleware=[CodeInterpreterMiddleware()],
)

# Use `ainvoke` — PTC bridges register as async QuickJS host functions,
# and sync `invoke` on a REPL with async bridges raises ConcurrentEvalError.
result = await agent.ainvoke({"messages": [{"role": "user", "content": "..."}]})

The middleware:

  1. registers an eval tool (configurable name) that runs JS in a persistent context;
  2. appends a short system-prompt snippet explaining the tool's semantics (sandbox, timeout, memory limit);
  3. gives every LangGraph thread_id its own QuickJS Runtime, so two conversations can't see each other's globals.

What the REPL is

Persistence

The REPL is module-flavoured: top-level let/const/function persist across eval calls in the same thread. By default (mode="thread"), state also persists across turns in the same LangGraph thread_id by snapshotting after each run and restoring before the next.

Set mode="call" to reset REPL state on every eval tool call. Set mode="turn" to persist state only within each turn. Snapshot payloads are capped by max_snapshot_bytes (defaults to memory_limit); oversized snapshots are dropped instead of persisted.

// call 1
const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));

// call 2
fib(10)  // 55

Sandbox

The REPL runs in a QuickJS context with no ambient capabilities. There is no filesystem, no network, no fetch, no require, no real clock (Date.now() is whatever QuickJS provides, not wall-clock for security-sensitive uses), no process, no import of anything you didn't explicitly install.

Capabilities can be explicitly added using PTC to call into the agent's own tools (see below).

Console capture

console.log / console.warn / console.error are captured by default and returned as a <stdout> block alongside the result, separately truncated. Disable with capture_console=False if you'd rather the guest see no console at all.

console.log("hi", 2);
1 + 1
<stdout>
hi 2
</stdout>
<result>2</result>

Timeouts and memory

Each call has a per-call wall-clock timeout (default 5 s). Breaching it produces:

<error type="Timeout">...</error>

The runtime has a shared memory limit across every context under it (default 64 MiB). OOM surfaces as:

<error type="OutOfMemory">...</error>

PTC host-function calls are also budgeted per eval call (default 256 tools.* invocations). Exceeding the budget surfaces as:

<error type="PTCCallBudgetExceeded">...</error>

Set max_ptc_calls=None only in trusted environments. Disabling the budget allows unbounded PTC-call loops and increases DoS risk.

Top-level await works on the async path — the promise settles before the call returns. An un-resolvable top-level promise (no host work in flight, no resolver) surfaces as <error type="Deadlock">.

Result formatting

Every eval renders into one wire format consumed by the model:

Outcome Rendered as
Marshalable value <result>{json-ish}</result>
Function or unmarshalable <result kind="handle">[Function] arity=2</result>
JS-level throw <error type="TypeError">{message}\n{stack}</error>
Timeout / deadlock / OOM <error type="Timeout" | "Deadlock" | "OutOfMemory">...</error>
console.* output separate <stdout>...</stdout> block

Results and stdout are independently truncated to max_result_chars (default 4000) before being sent back to the model.

Numeric rendering follows Node's REPL convention — whole-valued floats (42.0) render as integers (42) so the model isn't confused by JS's single numeric type.

Programmatic tool calling (PTC)

PTC is the reason to use this middleware over a plain code-interpreter tool. When configured, each exposed tool is available inside the REPL as:

async tools.<camelCaseName>(input: {...}): Promise<string>

So an agent with a search_web tool and a summarize tool can do:

const results = await Promise.all([
  tools.searchWeb({ query: "deepagents" }),
  tools.searchWeb({ query: "quickjs" }),
]);

await tools.summarize({ text: results.join("\n\n") })

...in one eval call — three tool invocations, zero round-trips to the model between them.

Enabling it

CodeInterpreterMiddleware()                              # disabled (default)
CodeInterpreterMiddleware(ptc=["search_web"])            # explicit allowlist
CodeInterpreterMiddleware(ptc=[search_tool])             # explicit tool object allowlist

The REPL's own tool is always excluded from PTC; tools.eval("tools.eval(...)") would be pointless recursion, and if the model wants nested code it can just write nested code in one call.

What the model sees

When PTC is on, the system-prompt snippet grows an API Reference — tools namespace section listing every exposed tool as a TypeScript-ish signature derived from the tool's args schema:

/** Search the web for the given query. */
async tools.searchWeb(input: {
  /** The query string. */
  query: string;
  /** Max results. */
  limit?: number;
}): Promise<string>

Enums, anyOf unions, nested objects, and arrays are all supported by the schema renderer. Opaque types fall back to Record<string, unknown> — the description is usually enough.

How it works (so you can debug it)

  • Each PTC-exposed tool gets a QuickJS host-function bridge registered under a generated __tools_* global symbol. The bridge is async, so the guest sees tools.x(...) as returning a Promise.
  • globalThis.tools is rebuilt every turn from the currently-exposed name set. So if an upstream middleware filters tools on a per-turn basis, the tools namespace follows along.
  • When the bridge invokes a tool, it forwards the ToolRuntime captured from the outer eval call — so subagent tools like task see graph state, store, context, and a synthesised child tool_call_id.
  • Tool return values are coerced to strings: strings pass through, ToolMessages get unwrapped, a Command has its last-message content extracted, everything else gets json.dumps'd.

Configuration reference

CodeInterpreterMiddleware(
    memory_limit=64 * 1024 * 1024,  # bytes, shared across contexts
    timeout=5.0,                     # per-call seconds
    max_ptc_calls=256,     # per-eval `tools.*` bridge calls, None disables (DoS risk)
    tool_name="eval",                # what the model calls it
    max_result_chars=4000,           # result/stdout truncation, each
    capture_console=True,            # install console.log/warn/error bridge
    mode="thread",                   # "thread" | "turn" | "call"
    max_snapshot_bytes=None,         # defaults to `memory_limit`; larger snapshots are dropped
    ptc=None,                        # None | list[str] | list[BaseTool]
)

Errors the model can see

Type Cause
SyntaxError, TypeError, ReferenceError, ... User-code error. Re-surfaces the JS error name verbatim.
Timeout Call exceeded timeout=.
OutOfMemory Runtime hit memory_limit=.
PTCCallBudgetExceeded Uncaught tools.* call-budget overflow in one eval (max_ptc_calls=).
Deadlock Top-level promise never resolved with no async host work in flight.
ConcurrentEval Shouldn't happen under locks; defensive mapping for QuickJS ConcurrentEvalError.

asyncio.CancelledError propagates out cleanly when JS declines to catch a HostCancellationError — so LangGraph cancellation semantics work end-to-end.

License

MIT. See LICENSE

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

langchain_quickjs-0.2.0.tar.gz (214.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langchain_quickjs-0.2.0-py3-none-any.whl (41.4 kB view details)

Uploaded Python 3

File details

Details for the file langchain_quickjs-0.2.0.tar.gz.

File metadata

  • Download URL: langchain_quickjs-0.2.0.tar.gz
  • Upload date:
  • Size: 214.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for langchain_quickjs-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e10c43ef2e06c99800277554351ff2f939940f3e9131ae3377214773acadb9ae
MD5 c2d7fb7e819749a9e4437bad52acf0c9
BLAKE2b-256 6c2d9c6a945da2524790a4d3e43f6dd262a497c5709a22ba4ae73c179e2e3baa

See more details on using hashes here.

File details

Details for the file langchain_quickjs-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for langchain_quickjs-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2510a8c584dcb27bcdb1cf7a849004e8cc189023a079ab71a8cb0a58c4c1008c
MD5 cda4f65cf0f16ad062ff8e8d2e9f8f15
BLAKE2b-256 a058325bb4910082752a86ab15424a59f8c4008f454d324a0ca49e617d5990e1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page