Skip to content

Run the langchain-quickjs code interpreter in-workflow - #1874

Closed
DABH wants to merge 5 commits into
mainfrom
deepagents-quickjs
Closed

DABH wants to merge 5 commits into
mainfrom
deepagents-quickjs

Conversation

@DABH

@DABH DABH commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

TLDR: Makes Deep Agents' QuickJS code interpreter (langchain-quickjs) work under DeepAgentsPlugin, with sub-agents dispatched from JavaScript getting their own deepagents.invoke_model Activities. Stacked on #1873 (deepagents 0.7 bump), which langchain-quickjs requires. langchain-quickjs stays optional — nothing is imported unless the workflow imports it.

Why two pieces

Sandbox passthrough alone would make things worse: today the sandbox fails loudly at middleware construction (threading.Lock restricted); with passthrough but no executor change the workflow hangs silently after the first model call. The middleware hosts its VM on a dedicated thread and wakes the caller via call_soon_threadsafe, which the deterministic workflow loop cannot service and which never runs outside an activation. So this PR ships both:

  1. Inline executor (_quickjs.py) — while workflow.in_workflow(), quickjs_rs.threading.ThreadWorker's run_sync/run_async run the coroutine on the calling loop and no thread is started. Outside workflows upstream behavior is untouched; when quickjs_rs isn't installed the patch is a no-op. Installed/uninstalled with the plugin's other seams in run_context.
  2. Passthrough for langchain_quickjs, quickjs_rs, wasmtime, bsdiff4 — a name-only allowlist checked at import time, so it costs nothing for users who don't use the interpreter.

What you get

JavaScript runs as workflow code and replays; task(...) from JavaScript dispatches the sub-agent in-workflow (model calls → invoke_model Activities); PTC tools.<name>(...) follows the tool's own wrapping (tool_as_activity → invoke_tool Activity). README documents the rules for the JavaScript (no Date.now()/Math.random() — both verified nondeterministic in the VM; generous wall-clock timeout; prefer mode="turn" since mode="thread" snapshots aren't carried across continue-as-new).

Verification

  • New tests/contrib/deepagents/test_quickjs_interpreter.py: one eval that calls a PTC tool and then a task() sub-agent → invoke_model == 3, invoke_tool == 1, result carries the sub-agent's answer, history replays deterministically — under the default sandbox, and again with max_cached_workflows=0. Plus a test that the patch is inert outside workflows.
  • Full deepagents suite and lint gate green. langchain-quickjs added to the dev group only (0.3.5 is the newest inside the two-week exclude-newer window).

Not in this PR (follow-ups)

Determinism guards inside the VM (host functions for Date.now/Math.random backed by workflow time/random), and an upstream ask to LangChain for an executor hook so the ThreadWorker patch can go away.

…kend

The deepagents extra now requires deepagents>=0.7.12,<0.8 (0.7.14 exists
but is inside the two-week exclude-newer window). deepagents 0.7 needs
langsmith>=0.11.2, so the langsmith extra allows langsmith<0.13; the
RunTree.patch overrides follow langsmith 0.12's Optional exclude_inputs
so LangSmith's own default applies on every supported version.

deepagents 0.7 made delete an optional protocol op and checks support on
the wrapper's class (type(backend).delete against the protocol default),
which raised AttributeError for TemporalBackend's __getattr__-based ops.
The I/O ops are now real class-level methods, and the wrapper picks a
subclass mirroring the inner backend's delete support, so deepagents
offers the delete tool exactly when the unwrapped backend would.
langchain_quickjs.CodeInterpreterMiddleware hosts its QuickJS VM on a
dedicated thread and wakes the caller through call_soon_threadsafe, which
the deterministic workflow loop cannot service: an unmodified eval parked
the workflow forever with no failure and no deadlock report. The plugin's
run context now makes quickjs_rs's worker hops inline while in a workflow
(inert elsewhere, a no-op when quickjs-rs is absent) and passes the
interpreter's modules through the sandbox. JavaScript is then workflow
code, a task() sub-agent runs in-workflow with its own invoke_model
activities, and a PTC tool follows its own tool_as_activity wrapping.
Verified end to end with replay, also under cache eviction. langchain-quickjs
stays optional; it is only a dev dependency for the tests.
… default

Per review. deepagents decides execution support with a nominal
isinstance check against SandboxBackendProtocol (an ABC subclass, not a
structural Protocol), so no wrapper was ever mistaken for shell-capable;
the real gap ran the other way: a wrapped LocalShellBackend lost its
shell tool. The mirror class now derives from SandboxBackendProtocol only
when deepagents reports the inner backend as execution-capable, with
execute/aexecute dispatchers and id forwarded; plain backends stay
non-sandbox classes. Mirroring also applies to user subclasses of
TemporalBackend (generated as a subclass of theirs; ops they define are
left alone) instead of being skipped. The langsmith RunTree.patch tests
now pin that a no-argument call forwards exclude_inputs=None so
LangSmith's own default applies.
@DABH

DABH commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Going to close this for now and re-open once lang* plugins are migrated over to ai-integrations

@DABH DABH closed this Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant