langchain-quickjs 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ """QuickJS integration package for Deep Agents."""
2
+
3
+ from langchain_quickjs.middleware import QuickJSMiddleware
4
+
5
+ __version__ = "0.0.1"
6
+
7
+ __all__ = ["QuickJSMiddleware", "__version__"]
@@ -0,0 +1,447 @@
1
+ """Render compact prompt-facing documentation for QuickJS foreign functions.
2
+
3
+ QuickJS foreign functions are Python callables or LangChain tools exposed inside
4
+ JavaScript. The model needs a concise description of their names, argument
5
+ shapes, return types, and any referenced structured types. This module converts
6
+ Python signatures and docstrings into small TypeScript-like stubs and prompt
7
+ sections that are easy for the model to consume.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import inspect
14
+ from typing import TYPE_CHECKING, Any, get_args, get_origin, get_type_hints
15
+
16
+ from langchain_core.tools import BaseTool
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable
20
+
21
+ _ELLIPSIS_TUPLE_ARG_COUNT = 2
22
+
23
+
24
+ def _format_basic_annotation(annotation: Any) -> str | None:
25
+ """Render simple built-in annotations without container introspection."""
26
+ basic_types = {
27
+ Any: "any",
28
+ inspect.Signature.empty: "any",
29
+ str: "string",
30
+ bool: "boolean",
31
+ type(None): "null",
32
+ dict: "Record<string, any>",
33
+ }
34
+ if annotation in basic_types:
35
+ return basic_types[annotation]
36
+ if annotation in (int, float):
37
+ return "number"
38
+ if isinstance(annotation, type):
39
+ return annotation.__name__
40
+ return None
41
+
42
+
43
+ def _format_collection_annotation(origin: Any, args: tuple[Any, ...]) -> str | None:
44
+ """Render collection-like generic annotations."""
45
+ if origin in (list, set, frozenset):
46
+ item = _format_annotation(args[0]) if args else "any"
47
+ return f"{item}[]"
48
+ if origin is tuple:
49
+ if len(args) == _ELLIPSIS_TUPLE_ARG_COUNT and args[1] is Ellipsis:
50
+ return f"{_format_annotation(args[0])}[]"
51
+ return f"[{', '.join(_format_annotation(arg) for arg in args)}]"
52
+ if origin is dict:
53
+ key_type = _format_annotation(args[0]) if args else "string"
54
+ value_type = _format_annotation(args[1]) if len(args) > 1 else "any"
55
+ return f"Record<{key_type}, {value_type}>"
56
+ return None
57
+
58
+
59
+ def _format_generic_annotation(origin: Any, args: tuple[Any, ...]) -> str | None:
60
+ """Render non-collection generic annotations."""
61
+ if origin is type:
62
+ inner = _format_annotation(args[0]) if args else "any"
63
+ return f"new (...args: any[]) => {inner}"
64
+ origin_name = getattr(origin, "__name__", None)
65
+ if origin_name in {"Union", "UnionType"}:
66
+ return " | ".join(_format_annotation(arg) for arg in args)
67
+ if origin_name:
68
+ formatted_args = ", ".join(_format_annotation(arg) for arg in args)
69
+ return f"{origin_name}<{formatted_args}>"
70
+ return None
71
+
72
+
73
+ def _format_stringified_annotation(annotation: Any) -> str:
74
+ """Render fallback annotations from their string representation."""
75
+ rendered = str(annotation).replace("typing.", "").replace("'", "")
76
+ if rendered.endswith(" | None"):
77
+ return f"{rendered.removesuffix(' | None')} | null"
78
+ if "." in rendered and "[" not in rendered:
79
+ return rendered.rsplit(".", maxsplit=1)[-1]
80
+ return rendered
81
+
82
+
83
+ def _format_annotation(annotation: Any) -> str:
84
+ """Render one Python type annotation in a TypeScript-like form.
85
+
86
+ Args:
87
+ annotation: The Python annotation to render.
88
+
89
+ Returns:
90
+ A compact string suitable for prompt-facing function signatures.
91
+ """
92
+ basic = _format_basic_annotation(annotation)
93
+ if basic is not None:
94
+ return basic
95
+
96
+ origin = get_origin(annotation)
97
+ if origin is None:
98
+ return _format_stringified_annotation(annotation)
99
+
100
+ args = get_args(annotation)
101
+ collection = _format_collection_annotation(origin, args)
102
+ if collection is not None:
103
+ return collection
104
+
105
+ generic = _format_generic_annotation(origin, args)
106
+ if generic is not None:
107
+ return generic
108
+
109
+ return _format_stringified_annotation(annotation)
110
+
111
+
112
+ def _unwrap_typed_dict_annotation(annotation: Any) -> tuple[Any, str]:
113
+ """Extract a TypedDict annotation from supported container types."""
114
+ container_prefix = ""
115
+ origin = get_origin(annotation)
116
+ if origin not in (list, tuple, set, frozenset):
117
+ return annotation, container_prefix
118
+
119
+ args = get_args(annotation)
120
+ if not args:
121
+ return annotation, container_prefix
122
+
123
+ unwrapped = args[0]
124
+ container_prefix = f"Contained `{unwrapped.__name__}` structure:\n"
125
+ return unwrapped, container_prefix
126
+
127
+
128
+ def _is_not_required_annotation(annotation: Any) -> bool:
129
+ """Return whether a TypedDict field annotation marks an optional key."""
130
+ origin = get_origin(annotation)
131
+ if getattr(origin, "__name__", None) == "NotRequired":
132
+ return True
133
+ forward_arg = getattr(annotation, "__forward_arg__", None)
134
+ return isinstance(forward_arg, str) and forward_arg.startswith("NotRequired[")
135
+
136
+
137
+ def _typed_dict_key_sets(
138
+ annotation: type[Any],
139
+ ) -> tuple[frozenset[str], frozenset[str]]:
140
+ """Return required and optional TypedDict keys, including postponed annotations."""
141
+ required_keys = getattr(annotation, "__required_keys__", frozenset())
142
+ optional_keys = getattr(annotation, "__optional_keys__", frozenset())
143
+ if optional_keys:
144
+ return required_keys, optional_keys
145
+
146
+ raw_annotations = getattr(annotation, "__annotations__", {})
147
+ inferred_optional = {
148
+ key
149
+ for key, value in raw_annotations.items()
150
+ if _is_not_required_annotation(value)
151
+ }
152
+ if not inferred_optional:
153
+ return required_keys, optional_keys
154
+ inferred_required = frozenset(set(raw_annotations) - inferred_optional)
155
+ return inferred_required, frozenset(inferred_optional)
156
+
157
+
158
+ def _render_typed_dict_fields(
159
+ annotation: type[Any], field_types: dict[str, Any]
160
+ ) -> str | None:
161
+ """Render field lines for a TypedDict annotation."""
162
+ if not field_types:
163
+ return None
164
+
165
+ required_keys, optional_keys = _typed_dict_key_sets(annotation)
166
+ lines = [f"Return structure `{annotation.__name__}`:"]
167
+ for key, value in field_types.items():
168
+ marker = "required" if key in required_keys else "optional"
169
+ if key not in required_keys and key not in optional_keys:
170
+ marker = "field"
171
+ field_name = f"{key}?" if key in optional_keys else key
172
+ lines.append(f"- {field_name}: {_format_annotation(value)} ({marker})")
173
+ return "\n".join(lines)
174
+
175
+
176
+ def _format_typed_dict_structure(annotation: Any) -> str | None:
177
+ """Render a compact field listing for a TypedDict annotation."""
178
+ annotation, container_prefix = _unwrap_typed_dict_annotation(annotation)
179
+ if not isinstance(annotation, type):
180
+ return None
181
+ if not hasattr(annotation, "__annotations__") or not hasattr(
182
+ annotation, "__required_keys__"
183
+ ):
184
+ return None
185
+
186
+ field_types = getattr(annotation, "__annotations__", {})
187
+ with contextlib.suppress(TypeError, NameError):
188
+ field_types = get_type_hints(annotation)
189
+
190
+ rendered_fields = _render_typed_dict_fields(annotation, field_types)
191
+ if rendered_fields is None:
192
+ return None
193
+ return container_prefix + rendered_fields
194
+
195
+
196
+ def render_external_functions_section(
197
+ implementations: dict[str, Callable[..., Any] | BaseTool], *, add_docs: bool
198
+ ) -> str:
199
+ """Build the optional prompt section describing foreign functions."""
200
+ if not implementations:
201
+ return ""
202
+
203
+ if not add_docs:
204
+ formatted_functions = "\n".join(f"- {name}" for name in implementations)
205
+ return f"\n\nAvailable foreign functions:\n{formatted_functions}"
206
+
207
+ return f"\n\n{render_foreign_function_section(implementations)}"
208
+
209
+
210
+ def _get_tool_doc_target(tool: BaseTool) -> Callable[..., Any] | None:
211
+ """Choose the underlying callable that best represents a LangChain tool.
212
+
213
+ Args:
214
+ tool: The LangChain tool being documented.
215
+
216
+ Returns:
217
+ The sync function or coroutine that provides the most useful signature
218
+ and docstring for prompt generation, or `None` if neither is available.
219
+ """
220
+ target = getattr(tool, "func", None)
221
+ if callable(target):
222
+ return target
223
+ target = getattr(tool, "coroutine", None)
224
+ if callable(target):
225
+ return target
226
+ return None
227
+
228
+
229
+ def _get_foreign_function_mode(implementation: Callable[..., Any] | BaseTool) -> str:
230
+ """Return whether a foreign function should be treated as sync or async."""
231
+ if isinstance(implementation, BaseTool):
232
+ coroutine = getattr(implementation, "coroutine", None)
233
+ return "async" if callable(coroutine) else "sync"
234
+ return "async" if inspect.iscoroutinefunction(implementation) else "sync"
235
+
236
+
237
+ def _get_return_annotation(target: Callable[..., Any]) -> Any:
238
+ """Resolve the return annotation for a callable, if present."""
239
+ with contextlib.suppress(TypeError, ValueError, NameError):
240
+ inspected_signature = inspect.signature(target)
241
+ resolved_hints = get_type_hints(target)
242
+ return resolved_hints.get("return", inspected_signature.return_annotation)
243
+ return inspect.Signature.empty
244
+
245
+
246
+ def _render_jsdoc(doc: str) -> str:
247
+ """Convert a Python docstring into a compact JSDoc block."""
248
+ lines = inspect.cleandoc(doc).splitlines()
249
+ summary: list[str] = []
250
+ params: list[tuple[str, str]] = []
251
+ in_args = False
252
+ for line in lines:
253
+ stripped = line.strip()
254
+ if stripped == "Args:":
255
+ in_args = True
256
+ continue
257
+ if in_args:
258
+ if not stripped:
259
+ continue
260
+ if line.startswith(" ") and ":" in stripped:
261
+ name, description = stripped.split(":", maxsplit=1)
262
+ params.append((name.strip(), description.strip()))
263
+ continue
264
+ in_args = False
265
+ if stripped:
266
+ summary.append(stripped)
267
+
268
+ rendered = ["/**"]
269
+ rendered.extend(f" * {line}" for line in summary)
270
+ if summary and params:
271
+ rendered.append(" *")
272
+ for name, description in params:
273
+ rendered.append(f" * @param {name} {description}")
274
+ rendered.append(" */")
275
+ return "\n".join(rendered)
276
+
277
+
278
+ def _render_function_stub(
279
+ name: str, implementation: Callable[..., Any] | BaseTool
280
+ ) -> str:
281
+ """Render one prompt-facing function declaration for a foreign function.
282
+
283
+ Args:
284
+ name: The JavaScript-visible foreign function name.
285
+ implementation: The Python callable or LangChain tool backing that name.
286
+
287
+ Returns:
288
+ A TypeScript-like function declaration, optionally prefixed with a small
289
+ JSDoc block derived from the Python docstring.
290
+ """
291
+ function_mode = _get_foreign_function_mode(implementation)
292
+ target = (
293
+ _get_tool_doc_target(implementation)
294
+ if isinstance(implementation, BaseTool)
295
+ else implementation
296
+ )
297
+ if target is None:
298
+ prefix = "async function" if function_mode == "async" else "function"
299
+ return f"{prefix} {name}(...args: any[]): any"
300
+
301
+ signature = "(...args: any[])"
302
+ return_annotation = inspect.Signature.empty
303
+ with contextlib.suppress(TypeError, ValueError, NameError):
304
+ inspected_signature = inspect.signature(target)
305
+ resolved_hints = get_type_hints(target)
306
+ parameter_parts = [
307
+ (
308
+ f"{param.name}: "
309
+ + _format_annotation(resolved_hints.get(param.name, param.annotation))
310
+ )
311
+ if param.annotation is not inspect.Signature.empty
312
+ or param.name in resolved_hints
313
+ else f"{param.name}: any"
314
+ for param in inspected_signature.parameters.values()
315
+ ]
316
+ signature = f"({', '.join(parameter_parts)})"
317
+ return_annotation = resolved_hints.get(
318
+ "return", inspected_signature.return_annotation
319
+ )
320
+
321
+ rendered_return = (
322
+ _format_annotation(return_annotation)
323
+ if return_annotation is not inspect.Signature.empty
324
+ else "any"
325
+ )
326
+ if function_mode == "async":
327
+ rendered_return = f"Promise<{rendered_return}>"
328
+ prefix = "async function" if function_mode == "async" else "function"
329
+ declaration = f"{prefix} {name}{signature}: {rendered_return}"
330
+ doc = inspect.getdoc(target) or inspect.getdoc(implementation)
331
+ if not doc:
332
+ return declaration
333
+ return f"{_render_jsdoc(doc)}\n{declaration}"
334
+
335
+
336
+ def _collect_referenced_types(
337
+ implementations: dict[str, Callable[..., Any] | BaseTool],
338
+ ) -> list[type[Any]]:
339
+ """Collect unique structured return types that should be documented.
340
+
341
+ Args:
342
+ implementations: Mapping of JavaScript-visible function names to Python
343
+ callables or LangChain tools.
344
+
345
+ Returns:
346
+ A list of TypedDict-like annotations referenced by foreign function
347
+ return types, preserving first-seen order.
348
+ """
349
+ collected: list[type[Any]] = []
350
+ seen: set[type[Any]] = set()
351
+ for implementation in implementations.values():
352
+ target = (
353
+ _get_tool_doc_target(implementation)
354
+ if isinstance(implementation, BaseTool)
355
+ else implementation
356
+ )
357
+ if target is None:
358
+ continue
359
+ annotation = _get_return_annotation(target)
360
+ origin = get_origin(annotation)
361
+ if origin in (list, tuple, set, frozenset):
362
+ args = get_args(annotation)
363
+ if args:
364
+ annotation = args[0]
365
+ if not isinstance(annotation, type):
366
+ continue
367
+ if not hasattr(annotation, "__annotations__") or not hasattr(
368
+ annotation, "__required_keys__"
369
+ ):
370
+ continue
371
+ if annotation not in seen:
372
+ seen.add(annotation)
373
+ collected.append(annotation)
374
+ return collected
375
+
376
+
377
+ def _render_typed_dict_definition(annotation: type[Any]) -> str:
378
+ """Render a TypeScript-like type definition for a TypedDict."""
379
+ _, optional_keys = _typed_dict_key_sets(annotation)
380
+ with contextlib.suppress(TypeError, NameError):
381
+ field_types = get_type_hints(annotation)
382
+ lines = [f"type {annotation.__name__} = {{"]
383
+ for key, value in field_types.items():
384
+ field_name = f"{key}?" if key in optional_keys else key
385
+ lines.append(f" {field_name}: {_format_annotation(value)}")
386
+ lines.append("}")
387
+ return "\n".join(lines)
388
+
389
+ field_types = getattr(annotation, "__annotations__", {})
390
+ lines = [f"type {annotation.__name__} = {{"]
391
+ for key, value in field_types.items():
392
+ field_name = f"{key}?" if key in optional_keys else key
393
+ lines.append(f" {field_name}: {_format_annotation(value)}")
394
+ lines.append("}")
395
+ return "\n".join(lines)
396
+
397
+
398
+ def render_foreign_function_section(
399
+ implementations: dict[str, Callable[..., Any] | BaseTool],
400
+ ) -> str:
401
+ """Render the complete prompt section for available foreign functions."""
402
+ function_blocks = [
403
+ _render_function_stub(name, implementation)
404
+ for name, implementation in implementations.items()
405
+ ]
406
+ sections = [
407
+ "Available foreign functions:\n",
408
+ (
409
+ "These are JavaScript-callable foreign functions exposed inside QuickJS. "
410
+ "The TypeScript-style signatures below document argument and return shapes."
411
+ ),
412
+ "",
413
+ "```ts",
414
+ "\n\n".join(function_blocks),
415
+ "```",
416
+ ]
417
+
418
+ referenced_types = _collect_referenced_types(implementations)
419
+ if referenced_types:
420
+ type_blocks = [
421
+ _render_typed_dict_definition(annotation) for annotation in referenced_types
422
+ ]
423
+ sections.extend(
424
+ [
425
+ "",
426
+ "Referenced types:",
427
+ "```ts",
428
+ "\n\n".join(type_blocks),
429
+ "```",
430
+ ]
431
+ )
432
+ return "\n".join(sections)
433
+
434
+
435
+ def format_foreign_function_docs(
436
+ name: str,
437
+ implementation: Callable[..., Any] | BaseTool,
438
+ ) -> str:
439
+ """Render a compact signature and docstring block for a foreign function."""
440
+ return _render_function_stub(name, implementation)
441
+
442
+
443
+ __all__ = [
444
+ "format_foreign_function_docs",
445
+ "render_external_functions_section",
446
+ "render_foreign_function_section",
447
+ ]
@@ -0,0 +1,257 @@
1
+ """Bridge Python foreign functions into QuickJS with transparent JSON round-tripping.
2
+
3
+ The QuickJS Python binding can pass primitive return values directly, but complex
4
+ Python values like lists and dicts do not automatically become JavaScript arrays
5
+ or objects. This module adds a small bridge layer that JSON-encodes complex
6
+ Python results on the way out and parses them back inside QuickJS so foreign
7
+ functions behave more naturally from JavaScript.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import inspect
14
+ import json
15
+ import threading
16
+ from typing import TYPE_CHECKING, Any, Literal
17
+
18
+ from langchain_core.tools import BaseTool
19
+ from langchain_core.tools.base import (
20
+ _is_injected_arg_type,
21
+ get_all_basemodel_annotations,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable, Coroutine
26
+ from concurrent.futures import Future
27
+
28
+ import quickjs
29
+ from langchain.tools import ToolRuntime
30
+
31
+
32
+ class _AsyncLoopThread:
33
+ """Run coroutines on a dedicated daemon-thread event loop."""
34
+
35
+ def __init__(self) -> None:
36
+ self._ready = threading.Event()
37
+ self._loop: asyncio.AbstractEventLoop | None = None
38
+ self._thread = threading.Thread(target=self._run, daemon=True)
39
+ self._thread.start()
40
+ self._ready.wait()
41
+
42
+ def _run(self) -> None:
43
+ loop = asyncio.new_event_loop()
44
+ asyncio.set_event_loop(loop)
45
+ self._loop = loop
46
+ self._ready.set()
47
+ loop.run_forever()
48
+
49
+ def submit(self, coroutine: Coroutine[Any, Any, Any]) -> Future[Any]:
50
+ loop = self._loop
51
+ if loop is None:
52
+ msg = "Async loop thread was not initialized."
53
+ raise RuntimeError(msg)
54
+ return asyncio.run_coroutine_threadsafe(coroutine, loop)
55
+
56
+
57
+ _ASYNC_LOOP_THREAD = _AsyncLoopThread()
58
+
59
+
60
+ def _await_if_needed(value: Any) -> Any:
61
+ """Resolve awaitable results on the background event loop when needed."""
62
+ if inspect.isawaitable(value):
63
+ return _ASYNC_LOOP_THREAD.submit(value).result()
64
+ return value
65
+
66
+
67
+ def _invoke_tool(
68
+ tool: BaseTool,
69
+ payload: str | dict[str, Any],
70
+ *,
71
+ prefer_async: bool = False,
72
+ ) -> Any:
73
+ """Invoke a tool through its sync or async entrypoint as appropriate."""
74
+ has_async = getattr(tool, "coroutine", None) is not None or (
75
+ tool.__class__._arun is not BaseTool._arun # noqa: SLF001
76
+ )
77
+ has_sync = getattr(tool, "func", None) is not None or (
78
+ tool.__class__._run is not BaseTool._run # noqa: SLF001
79
+ )
80
+ if has_async and (prefer_async or not has_sync):
81
+ return _await_if_needed(tool.ainvoke(payload))
82
+ return _await_if_needed(tool.invoke(payload))
83
+
84
+
85
+ def get_ptc_implementations(
86
+ ptc: list[Callable[..., Any] | BaseTool] | None,
87
+ ) -> dict[str, Callable[..., Any] | BaseTool]:
88
+ """Return configured PTC implementations keyed by exported function name."""
89
+ implementations: dict[str, Callable[..., Any] | BaseTool] = {}
90
+ for implementation in ptc or []:
91
+ if isinstance(implementation, BaseTool):
92
+ implementations[implementation.name] = implementation
93
+ continue
94
+ name = getattr(implementation, "__name__", None)
95
+ if isinstance(name, str):
96
+ implementations[name] = implementation
97
+ return implementations
98
+
99
+
100
+ def _get_runtime_arg_name(tool: BaseTool) -> str | None:
101
+ """Return the injected runtime parameter name for a tool, if any."""
102
+ for name, type_ in get_all_basemodel_annotations(tool.get_input_schema()).items():
103
+ if name == "runtime" and _is_injected_arg_type(type_):
104
+ return name
105
+ return None
106
+
107
+
108
+ def _build_tool_payload(
109
+ tool: BaseTool,
110
+ args: tuple[Any, ...],
111
+ kwargs: dict[str, Any],
112
+ *,
113
+ runtime: ToolRuntime | None = None,
114
+ ) -> str | dict[str, Any]:
115
+ """Convert QuickJS call arguments into a LangChain tool payload."""
116
+ input_schema = tool.get_input_schema()
117
+ schema_annotations = getattr(input_schema, "__annotations__", {})
118
+ fields = [
119
+ name
120
+ for name, type_ in schema_annotations.items()
121
+ if not _is_injected_arg_type(type_)
122
+ ]
123
+ runtime_arg_name = _get_runtime_arg_name(tool)
124
+
125
+ if kwargs:
126
+ payload: str | dict[str, Any] = kwargs
127
+ elif (
128
+ len(args) == 1 and isinstance(args[0], (str, dict)) and runtime_arg_name is None
129
+ ):
130
+ payload = args[0]
131
+ elif len(args) == 1 and len(fields) == 1:
132
+ payload = {fields[0]: args[0]}
133
+ elif len(args) == len(fields) and fields:
134
+ payload = dict(zip(fields, args, strict=False))
135
+ else:
136
+ payload = {"args": list(args)}
137
+
138
+ if (
139
+ runtime is not None
140
+ and runtime_arg_name is not None
141
+ and isinstance(payload, dict)
142
+ ):
143
+ return {**payload, runtime_arg_name: runtime}
144
+ return payload
145
+
146
+
147
+ def _wrap_tool_for_js(
148
+ tool: BaseTool,
149
+ *,
150
+ prefer_async: bool = False,
151
+ runtime: ToolRuntime | None = None,
152
+ ) -> Callable[..., Any]:
153
+ """Adapt a LangChain tool into a plain sync callable for QuickJS."""
154
+
155
+ def tool_wrapper(*args: Any, **kwargs: Any) -> Any:
156
+ payload = _build_tool_payload(tool, args, kwargs, runtime=runtime)
157
+ return _invoke_tool(tool, payload, prefer_async=prefer_async)
158
+
159
+ return tool_wrapper
160
+
161
+
162
+ def _serialize_for_js(value: Any) -> Any:
163
+ """Convert Python return values into primitives the bridge can round-trip."""
164
+ if value is None or isinstance(value, (str, int, float, bool)):
165
+ return value
166
+ return json.dumps(value)
167
+
168
+
169
+ def _wrap_function_for_js(implementation: Callable[..., Any]) -> Callable[..., Any]:
170
+ """Wrap a Python callable so complex return values are JSON-encoded."""
171
+
172
+ def function_wrapper(*args: Any, **kwargs: Any) -> Any:
173
+ return _serialize_for_js(_await_if_needed(implementation(*args, **kwargs)))
174
+
175
+ return function_wrapper
176
+
177
+
178
+ def _raw_function_name(name: str) -> str:
179
+ """Build the hidden Python callable name used by the JS bridge shim."""
180
+ return f"__python_{name}"
181
+
182
+
183
+ def _build_external_functions(
184
+ implementations: dict[str, Callable[..., Any] | BaseTool] | None,
185
+ *,
186
+ prefer_async: bool = False,
187
+ runtime: ToolRuntime | None = None,
188
+ ) -> dict[str, Callable[..., Any]]:
189
+ """Normalize foreign implementations into QuickJS-registerable callables."""
190
+ external_functions: dict[str, Callable[..., Any]] = {}
191
+ for name, implementation in (implementations or {}).items():
192
+ callable_implementation = (
193
+ _wrap_tool_for_js(
194
+ implementation,
195
+ prefer_async=prefer_async,
196
+ runtime=runtime,
197
+ )
198
+ if isinstance(implementation, BaseTool)
199
+ else implementation
200
+ )
201
+ external_functions[_raw_function_name(name)] = _wrap_function_for_js(
202
+ callable_implementation
203
+ )
204
+ return external_functions
205
+
206
+
207
+ _EXTERNAL_FUNCTION_SHIM_TEMPLATE = """
208
+ globalThis[{name}] = (...args) => {{
209
+ const value = globalThis[{raw_name}](...args);
210
+ if (typeof value !== \"string\") {{ return value; }}
211
+ const trimmed = value.trim();
212
+ if (!trimmed) {{ return value; }}
213
+ const first = trimmed[0];
214
+ if (first !== \"[\" && first !== \"{{\") {{ return value; }}
215
+ return JSON.parse(value);
216
+ }};
217
+ """
218
+
219
+
220
+ def inject_external_function_shims(
221
+ context: quickjs.Context, external_functions: list[str] | None
222
+ ) -> None:
223
+ """Install JavaScript shims for foreign functions inside a QuickJS context."""
224
+ if not external_functions:
225
+ return
226
+
227
+ shim_lines = []
228
+ for name in external_functions:
229
+ raw_name = _raw_function_name(name)
230
+ shim_lines.append(
231
+ _EXTERNAL_FUNCTION_SHIM_TEMPLATE.format(
232
+ name=json.dumps(name),
233
+ raw_name=json.dumps(raw_name),
234
+ )
235
+ )
236
+ context.eval("".join(shim_lines))
237
+
238
+
239
+ def install_external_functions(
240
+ context: quickjs.Context,
241
+ implementations: dict[str, Callable[..., Any] | BaseTool] | None,
242
+ *,
243
+ execution_mode: Literal["sync", "async"] = "sync",
244
+ runtime: ToolRuntime | None = None,
245
+ ) -> None:
246
+ """Install foreign functions and JavaScript shims into a QuickJS context."""
247
+ external_functions = _build_external_functions(
248
+ implementations,
249
+ prefer_async=execution_mode == "async",
250
+ runtime=runtime,
251
+ )
252
+ for name, implementation in external_functions.items():
253
+ context.add_callable(name, implementation)
254
+ inject_external_function_shims(context, list(implementations or {}))
255
+
256
+
257
+ __all__ = ["get_ptc_implementations", "install_external_functions"]
@@ -0,0 +1,235 @@
1
+ """Middleware for providing a QuickJS-backed repl tool to an agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Annotated, Any
6
+
7
+ import quickjs
8
+ from deepagents.middleware._utils import append_to_system_message
9
+ from langchain.agents.middleware.types import (
10
+ AgentMiddleware,
11
+ AgentState,
12
+ ContextT,
13
+ ModelRequest,
14
+ ModelResponse,
15
+ ResponseT,
16
+ )
17
+ from langchain.tools import ToolRuntime
18
+ from langchain_core.tools import BaseTool, StructuredTool
19
+
20
+ from langchain_quickjs._foreign_function_docs import render_external_functions_section
21
+ from langchain_quickjs._foreign_functions import (
22
+ get_ptc_implementations,
23
+ install_external_functions,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Awaitable, Callable
28
+
29
+ from langchain.tools import ToolRuntime
30
+
31
+
32
+ REPL_TOOL_DESCRIPTION = """Evaluates code using a QuickJS-backed JavaScript REPL.
33
+
34
+ CRITICAL: The REPL does NOT retain state between calls. Each `repl` invocation is evaluated from scratch.
35
+ Do NOT assume variables, functions, imports, or helper objects from prior `repl` calls are available.
36
+
37
+ Capabilities and limitations:
38
+ - Executes JavaScript with QuickJS.
39
+ - Use `print(...)` to emit output. The tool returns printed lines joined with newlines.
40
+ - The final expression value is returned only if nothing was printed.
41
+ - There is no filesystem or network access unless you expose Python callables as foreign functions.
42
+ {external_functions_section}
43
+ """ # noqa: E501 # preserve prompt text formatting exactly for the model
44
+
45
+ REPL_SYSTEM_PROMPT = """## REPL tool
46
+
47
+ You have access to a `repl` tool.
48
+
49
+ CRITICAL: The REPL does NOT retain state between calls. Each `repl` invocation is evaluated from scratch.
50
+ Do NOT assume variables, functions, imports, or helper objects from prior `repl` calls are available.
51
+
52
+ - The REPL executes JavaScript with QuickJS.
53
+ - Use `print(...)` to emit output. The tool returns printed lines joined with newlines.
54
+ - The final expression value is returned only if nothing was printed.
55
+ - There is no filesystem or network access unless equivalent foreign functions have been provided.
56
+ - Use it for small computations, control flow, JSON manipulation, and calling externally registered foreign functions.
57
+ {external_functions_section}
58
+ """ # noqa: E501 # preserve prompt text formatting exactly for the model
59
+
60
+
61
+ class QuickJSMiddleware(AgentMiddleware[AgentState[Any], ContextT, ResponseT]):
62
+ """Provide a QuickJS-backed `repl` tool to an agent."""
63
+
64
+ def __init__(
65
+ self,
66
+ *,
67
+ ptc: list[Callable[..., Any] | BaseTool] | None = None,
68
+ add_ptc_docs: bool = False,
69
+ timeout: int | None = None,
70
+ memory_limit: int | None = None,
71
+ ) -> None:
72
+ """Initialize the middleware.
73
+
74
+ Args:
75
+ ptc: Functions or LangChain tools to expose inside the REPL.
76
+ add_ptc_docs: Whether to add signatures and docstrings for exposed PTC
77
+ functions to the system prompt.
78
+ timeout: Optional timeout in seconds for each evaluation.
79
+ memory_limit: Optional memory limit in bytes for each evaluation.
80
+ """
81
+ self._ptc = ptc or []
82
+ self._add_ptc_docs = add_ptc_docs
83
+ self._timeout = timeout
84
+ self._memory_limit = memory_limit
85
+ self.tools = [self._create_repl_tool()]
86
+
87
+ def _format_repl_system_prompt(self) -> str:
88
+ """Build the system prompt fragment describing the repl tool."""
89
+ external_functions_section = render_external_functions_section(
90
+ get_ptc_implementations(self._ptc),
91
+ add_docs=self._add_ptc_docs,
92
+ )
93
+ return REPL_SYSTEM_PROMPT.format(
94
+ external_functions_section=external_functions_section
95
+ )
96
+
97
+ def modify_request(self, request: ModelRequest[ContextT]) -> ModelRequest[ContextT]:
98
+ """Inject REPL usage instructions into the system message."""
99
+ repl_prompt = self._format_repl_system_prompt()
100
+ new_system_message = append_to_system_message(
101
+ request.system_message, repl_prompt
102
+ )
103
+ return request.override(system_message=new_system_message)
104
+
105
+ def wrap_model_call(
106
+ self,
107
+ request: ModelRequest[ContextT],
108
+ handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
109
+ ) -> ModelResponse[ResponseT]:
110
+ """Wrap model call to inject REPL instructions into system prompt."""
111
+ modified_request = self.modify_request(request)
112
+ return handler(modified_request)
113
+
114
+ async def awrap_model_call(
115
+ self,
116
+ request: ModelRequest[ContextT],
117
+ handler: Callable[
118
+ [ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]
119
+ ],
120
+ ) -> ModelResponse[ResponseT]:
121
+ """Async wrap model call to inject REPL instructions into system prompt."""
122
+ modified_request = self.modify_request(request)
123
+ return await handler(modified_request)
124
+
125
+ def _create_context(
126
+ self,
127
+ timeout: int | None,
128
+ printed_lines: list[str],
129
+ *,
130
+ prefer_async: bool = False,
131
+ runtime: ToolRuntime | None = None,
132
+ ) -> quickjs.Context:
133
+ """Create a configured QuickJS context for a single evaluation."""
134
+ context = quickjs.Context()
135
+ effective_timeout = timeout if timeout is not None else self._timeout
136
+ if effective_timeout is not None:
137
+ if effective_timeout <= 0:
138
+ msg = f"timeout must be positive, got {effective_timeout}."
139
+ raise ValueError(msg)
140
+ context.set_time_limit(effective_timeout)
141
+ if self._memory_limit is not None:
142
+ if self._memory_limit <= 0:
143
+ msg = f"memory_limit must be positive, got {self._memory_limit}."
144
+ raise ValueError(msg)
145
+ context.set_memory_limit(self._memory_limit)
146
+
147
+ def _print(*args: Any) -> None:
148
+ printed_lines.append(" ".join(map(str, args)))
149
+
150
+ context.add_callable("print", _print)
151
+ install_external_functions(
152
+ context,
153
+ get_ptc_implementations(self._ptc),
154
+ execution_mode="async" if prefer_async else "sync",
155
+ runtime=runtime,
156
+ )
157
+ return context
158
+
159
+ def _evaluate(
160
+ self,
161
+ code: str,
162
+ *,
163
+ timeout: int | None,
164
+ prefer_async: bool = False,
165
+ runtime: ToolRuntime | None = None,
166
+ ) -> str:
167
+ """Execute JavaScript and return printed output or final value."""
168
+ printed_lines: list[str] = []
169
+ try:
170
+ context = self._create_context(
171
+ timeout,
172
+ printed_lines,
173
+ prefer_async=prefer_async,
174
+ runtime=runtime,
175
+ )
176
+ except ValueError as exc:
177
+ return f"Error: {exc}"
178
+
179
+ try:
180
+ value = context.eval(code)
181
+ except quickjs.JSException as exc:
182
+ return str(exc)
183
+
184
+ if printed_lines:
185
+ return "\n".join(printed_lines).rstrip()
186
+ if value is None:
187
+ return ""
188
+ return str(value)
189
+
190
+ def _create_repl_tool(self) -> BaseTool:
191
+ """Create the LangChain tool wrapper around QuickJS execution."""
192
+
193
+ def _sync_quickjs(
194
+ code: Annotated[str, "Code string to evaluate in QuickJS."],
195
+ runtime: ToolRuntime,
196
+ timeout: Annotated[
197
+ int | None, "Optional timeout in seconds for this evaluation."
198
+ ] = None,
199
+ ) -> str:
200
+ """Execute a single QuickJS program and return captured stdout."""
201
+ return self._evaluate(
202
+ code,
203
+ timeout=timeout,
204
+ prefer_async=False,
205
+ runtime=runtime,
206
+ )
207
+
208
+ async def _async_quickjs(
209
+ code: Annotated[str, "Code string to evaluate in QuickJS."],
210
+ runtime: ToolRuntime,
211
+ timeout: Annotated[
212
+ int | None, "Optional timeout in seconds for this evaluation."
213
+ ] = None,
214
+ ) -> str:
215
+ """Execute a single QuickJS program in the async tool path."""
216
+ return self._evaluate(
217
+ code,
218
+ timeout=timeout,
219
+ prefer_async=True,
220
+ runtime=runtime,
221
+ )
222
+
223
+ tool_description = REPL_TOOL_DESCRIPTION.format(
224
+ external_functions_section=render_external_functions_section(
225
+ get_ptc_implementations(self._ptc),
226
+ add_docs=self._add_ptc_docs,
227
+ )
228
+ )
229
+
230
+ return StructuredTool.from_function(
231
+ name="repl",
232
+ description=tool_description,
233
+ func=_sync_quickjs,
234
+ coroutine=_async_quickjs,
235
+ )
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-quickjs
3
+ Version: 0.0.1
4
+ Summary: QuickJS integration package for Deep Agents
5
+ Project-URL: Homepage, https://github.com/langchain-ai/deepagents/tree/main/libs/partners/quickjs
6
+ Project-URL: Repository, https://github.com/langchain-ai/deepagents
7
+ Project-URL: Documentation, https://github.com/langchain-ai/deepagents/tree/main/libs/partners/quickjs
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: <4.0,>=3.11
19
+ Requires-Dist: deepagents
20
+ Requires-Dist: quickjs<2,>=1.19.4
21
+ Description-Content-Type: text/markdown
22
+
23
+ # langchain-quickjs
24
+
25
+ `langchain-quickjs` provides a QuickJS-backed REPL middleware for Deep Agents. It adds a `repl` tool that can evaluate small JavaScript snippets for computation, control flow, JSON manipulation, and calls to exposed Python foreign functions.
26
+
27
+ ## Basic usage
28
+
29
+ ```python
30
+ from deepagents import create_deep_agent
31
+ from langchain_quickjs import QuickJSMiddleware
32
+
33
+
34
+ def normalize_name(name: str) -> str:
35
+ return name.strip().lower()
36
+
37
+
38
+ agent = create_deep_agent(
39
+ model="openai:gpt-4.1",
40
+ tools=[],
41
+ middleware=[
42
+ QuickJSMiddleware(
43
+ ptc=[normalize_name],
44
+ add_ptc_docs=True,
45
+ )
46
+ ],
47
+ )
48
+ ```
49
+
50
+ With this middleware installed, the agent receives a `repl` tool that runs each JavaScript evaluation in a fresh QuickJS context. If you expose Python callables through `ptc`, they are available inside the REPL as foreign functions.
51
+
52
+ ## REPL behavior
53
+
54
+ - The REPL is stateless. Each call starts from a fresh QuickJS context, so variables, functions, and other values defined in one `repl` call are not available in the next one.
55
+ - Execution uses [QuickJS](https://bellard.org/quickjs/), so JavaScript support is limited to what QuickJS provides. It is good for small computations, control flow, JSON manipulation, and calling exposed foreign functions, but it is not a browser or Node.js runtime and does not provide their APIs.
56
+ - Foreign functions support passing primitive values between JavaScript and Python, including `int`, `float`, `bool`, `str`, and `None`. Lists and dictionaries returned from Python are also supported and are bridged back into JavaScript arrays and objects.
57
+ - Async foreign functions are supported. Because QuickJS callbacks are synchronous, awaitables are delegated to a dedicated daemon-thread event loop and their resolved results are returned back into the REPL call.
58
+
59
+ ## Current limitations
60
+
61
+ - Does not work with HIL in the REPL.
62
+ - Does not support `ToolRuntime` yet.
@@ -0,0 +1,8 @@
1
+ langchain_quickjs/__init__.py,sha256=XJLajb7cQ2pYcFL2B5W_QqLJ47glRvge2SF9Z4e3IxQ,182
2
+ langchain_quickjs/_foreign_function_docs.py,sha256=2mkq4dUhXDuGKOHL5NArcbc7HSI6uzmH0ZMinaHUxeM,16049
3
+ langchain_quickjs/_foreign_functions.py,sha256=qwK0XTpTwb6SWXZ-Wx4groSnZXVnwV6ez3pOaOwZxdc,8506
4
+ langchain_quickjs/middleware.py,sha256=54SXMF2Ojw5pXLLz7UtPSie87PtIYOYKChAiy8--4hY,8717
5
+ langchain_quickjs-0.0.1.dist-info/METADATA,sha256=0WMdwzpjZIZl_7O-NNxZoECEObNoZ8Es1S9IUv5SZbs,2921
6
+ langchain_quickjs-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ langchain_quickjs-0.0.1.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
8
+ langchain_quickjs-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) LangChain, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.