fix(quickjs): persist top-level JS declarations across evals (#4147)

Updates `langchain-quickjs` to require `quickjs-rs` 0.2.3 so the REPL
can use the upstream runtime-level source transform for top-level
declarations. The QuickJS runtime now enables
`SourceTransform.TOP_LEVEL_CONST_TO_VAR`, keeping top-level `const` and
`let` bindings visible across consecutive eval calls and matching the
persistence behavior promised to agents.

Changes:
- Bumps `quickjs-rs` to 0.2.3 in `langchain-quickjs` and refreshes
dependent lockfiles for the code and evals packages.
- Configures `_Registry`-owned `Runtime` instances with
`SourceTransform.TOP_LEVEL_CONST_TO_VAR`.
- Adds sync and async regression coverage for same-turn eval persistence
of top-level declarations.
This commit is contained in:
Hunter Lovell
2026-06-22 14:23:01 -07:00
committed by GitHub
parent 151beaef2e
commit 7574fea88b
4 changed files with 79 additions and 6 deletions
@@ -27,6 +27,7 @@ from quickjs_rs import (
MemoryLimitError,
Runtime,
Snapshot,
SourceTransform,
ThreadWorker,
)
from quickjs_rs import (
@@ -986,7 +987,10 @@ class _Registry:
slot.worker.close()
async def _acreate_runtime(self) -> Runtime:
return Runtime(memory_limit=self.memory_limit)
return Runtime(
memory_limit=self.memory_limit,
transform_flags=SourceTransform.TOP_LEVEL_CONST_TO_VAR,
)
def close(self) -> None:
with self._lock:
+1 -1
View File
@@ -24,7 +24,7 @@ keywords = ["deepagents", "langchain", "langgraph", "repl", "javascript", "quick
dependencies = [
"deepagents>=0.6.8,<0.7.0",
"quickjs-rs>=0.2.0,<0.3.0",
"quickjs-rs>=0.2.3,<0.3.0",
"langchain>=1.3.9,<2.0.0",
"langchain-core>=1.4.7,<2.0.0",
"langgraph>=1.2.5,<2.0.0",
@@ -233,3 +233,72 @@ async def test_repl_snapshot_persists_top_level_await_binding_between_turns(
second_eval = _eval_tool_message(second)
assert "<error" not in second_eval.content, second_eval.content
assert "<result>hi</result>" in second_eval.content, second_eval.content
@pytest.mark.parametrize(
"invoke_mode",
["invoke", "ainvoke"],
ids=["sync_invoke", "async_ainvoke"],
)
async def test_repl_top_level_const_let_persist_across_evals_same_turn(
invoke_mode: InvokeMode,
) -> None:
"""Top-level ``const``/``let`` bindings persist between evals.
The runtime is configured with
``SourceTransform.TOP_LEVEL_CONST_TO_VAR`` so top-level lexical
declarations are rewritten to ``var`` and survive on the global object.
Without it, a second eval on the same live context would raise
``ReferenceError`` because lexical bindings are dropped between evals.
This exercises the in-turn path (two evals, one context, no snapshot
round-trip in between), which is what the bare-``const`` REPL
persistence model promises.
"""
script = [
AIMessage(
content="",
tool_calls=[
{
"name": "eval",
"args": {"code": "const greeting = 'hello'; let count = 41;"},
"id": "call_1",
"type": "tool_call",
},
],
),
AIMessage(
content="",
tool_calls=[
{
"name": "eval",
"args": {"code": "greeting + (count + 1)"},
"id": "call_2",
"type": "tool_call",
},
],
),
AIMessage(content="done"),
]
agent = create_deep_agent(
model=FakeChatModel(messages=iter(script)),
middleware=[CodeInterpreterMiddleware()],
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "quickjs-const-let-thread"}}
result = await _invoke_agent(
agent,
{"messages": [HumanMessage(content="declare then read const/let")]},
config,
invoke_mode,
)
eval_messages = [
m for m in result["messages"] if isinstance(m, ToolMessage) and m.name == "eval"
]
assert len(eval_messages) == 2, eval_messages
for msg in eval_messages:
assert "<error" not in msg.content, msg.content
assert "<result>hello42</result>" in eval_messages[-1].content, eval_messages[
-1
].content
+4 -4
View File
@@ -975,7 +975,7 @@ requires-dist = [
{ name = "langchain", specifier = ">=1.3.9,<2.0.0" },
{ name = "langchain-core", specifier = ">=1.4.7,<2.0.0" },
{ name = "langgraph", specifier = ">=1.2.5,<2.0.0" },
{ name = "quickjs-rs", specifier = ">=0.2.0,<0.3.0" },
{ name = "quickjs-rs", specifier = ">=0.2.3,<0.3.0" },
]
[package.metadata.requires-dev]
@@ -1779,14 +1779,14 @@ wheels = [
[[package]]
name = "quickjs-rs"
version = "0.2.0"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wasmtime" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3f/59/263e19bcff54b7e7993de3f79da25c23b7d7296d84c13d31a3e38ea0ec7f/quickjs_rs-0.2.0.tar.gz", hash = "sha256:db1a8d72ddfc0e766772622adb510f91aa8626d87afd843893c1e2a360ee1ce8", size = 483392, upload-time = "2026-06-18T00:08:53.928Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/d5/3657636d9cdff1b2e76e17c210a52704158f13149e7f8175cb533bb64ca2/quickjs_rs-0.2.3.tar.gz", hash = "sha256:5715bb3b6c7583999a4f053a4547fb149c70a9ef0962aa93884cbfb89fb5a471", size = 829047, upload-time = "2026-06-22T20:40:56.372Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/22/7caf4552a7880f205c95099c3c3e6d36f146af4e34a0b3baa12d49c47ea1/quickjs_rs-0.2.0-py3-none-any.whl", hash = "sha256:cb661d40ee3ba6d3673a75729223315fd4f0b22087373156425fa4227fa2adc6", size = 454517, upload-time = "2026-06-18T00:08:52.445Z" },
{ url = "https://files.pythonhosted.org/packages/b7/fe/637b4128ee76febedc18350329099a764024736eafa79b72635b928a7b80/quickjs_rs-0.2.3-py3-none-any.whl", hash = "sha256:52879678d62dea10f7f30c5821f217a75a2c6d51a3f4c3059d0eacf319b9a889", size = 799751, upload-time = "2026-06-22T20:40:54.697Z" },
]
[[package]]