@langchain/quickjs 0.2.6 → 0.4.0

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.
package/dist/index.cjs CHANGED
@@ -24,18 +24,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let langchain = require("langchain");
25
25
  let zod_v4 = require("zod/v4");
26
26
  let dedent = require("dedent");
27
- dedent = __toESM(dedent);
27
+ dedent = __toESM(dedent, 1);
28
28
  let _langchain_langgraph = require("@langchain/langgraph");
29
29
  let deepagents = require("deepagents");
30
30
  let quickjs_emscripten = require("quickjs-emscripten");
31
31
  let quickjs_emscripten_core = require("quickjs-emscripten-core");
32
32
  let node_path_posix = require("node:path/posix");
33
- node_path_posix = __toESM(node_path_posix);
33
+ node_path_posix = __toESM(node_path_posix, 1);
34
34
  let acorn = require("acorn");
35
35
  let _sveltejs_acorn_typescript = require("@sveltejs/acorn-typescript");
36
36
  let estree_walker = require("estree-walker");
37
37
  let magic_string = require("magic-string");
38
- magic_string = __toESM(magic_string);
38
+ magic_string = __toESM(magic_string, 1);
39
39
  let json_schema_to_typescript = require("json-schema-to-typescript");
40
40
  let _langchain_core_utils_json_schema = require("@langchain/core/utils/json_schema");
41
41
  //#region src/transform.ts
@@ -453,6 +453,38 @@ function formatSkillNotAvailable(missing) {
453
453
  return `Skills unavailable: ${[...missing].sort().join(", ")}`;
454
454
  }
455
455
  //#endregion
456
+ //#region src/eval-queue.ts
457
+ /**
458
+ * Serializes async operations on a shared WASM module.
459
+ *
460
+ * The quickjs-emscripten asyncify variant allows only one concurrent
461
+ * async call per module instance. This queue enforces that constraint
462
+ * by chaining operations into a promise queue — each caller waits for
463
+ * the previous one to finish before executing.
464
+ */
465
+ var AsyncEvalQueue = class {
466
+ tail = Promise.resolve();
467
+ /**
468
+ * Enqueue an async operation. The operation will not start until all
469
+ * previously enqueued operations have completed.
470
+ */
471
+ async enqueue(fn) {
472
+ let release;
473
+ const gate = new Promise((r) => {
474
+ release = r;
475
+ });
476
+ const prev = this.tail;
477
+ this.tail = gate;
478
+ return prev.then(async () => {
479
+ try {
480
+ return await fn();
481
+ } finally {
482
+ release();
483
+ }
484
+ });
485
+ }
486
+ };
487
+ //#endregion
456
488
  //#region src/session.ts
457
489
  /**
458
490
  * Core REPL engine built on quickjs-emscripten (asyncify variant).
@@ -472,15 +504,41 @@ function formatSkillNotAvailable(missing) {
472
504
  * runtime is lazily started on the first `.eval()` call, making the session
473
505
  * safe across graph interrupts and checkpointing.
474
506
  */
475
- const DEFAULT_MEMORY_LIMIT = 50 * 1024 * 1024;
507
+ const DEFAULT_MEMORY_LIMIT = 64 * 1024 * 1024;
476
508
  const DEFAULT_MAX_STACK_SIZE = 320 * 1024;
477
- const DEFAULT_EXECUTION_TIMEOUT = 3e4;
509
+ const DEFAULT_EXECUTION_TIMEOUT = 5e3;
478
510
  const DEFAULT_MAX_PTC_CALLS = 256;
479
511
  const DEFAULT_MAX_RESULTS_CHARS = 4e3;
480
512
  const variantImport = import("@jitl/quickjs-ng-wasmfile-release-asyncify");
481
- async function newAsyncModule() {
482
- const variant = await variantImport;
483
- return (0, quickjs_emscripten_core.newQuickJSAsyncWASMModuleFromVariant)(variant.default ?? variant);
513
+ /**
514
+ * Process-global eval queue. Serializes all evalCodeAsync calls across
515
+ * sessions to enforce the asyncify one-at-a-time constraint.
516
+ */
517
+ const sharedEvalQueue = new AsyncEvalQueue();
518
+ /**
519
+ * Process-global WASM module shared by all sessions.
520
+ *
521
+ * Each session creates its own runtime and context on this module,
522
+ * providing full isolation for globals, heap, and stack. The module
523
+ * itself is stateless between runtimes — only the compiled WASM code
524
+ * and Emscripten infrastructure are shared.
525
+ *
526
+ * This is safe because:
527
+ * - The module loader is synchronous (preloaded skill cache), so
528
+ * imports don't cause asyncify suspensions.
529
+ * - Tool injection uses the promise-based pattern (newFunction +
530
+ * newPromise), not newAsyncifiedFunction, so tool calls don't
531
+ * cause asyncify suspensions.
532
+ * - The eval queue serializes evalCodeAsync calls to satisfy the
533
+ * one-concurrent-async-call-per-module constraint.
534
+ */
535
+ let sharedModulePromise;
536
+ function getSharedModule() {
537
+ if (!sharedModulePromise) sharedModulePromise = (async () => {
538
+ const variant = await variantImport;
539
+ return (0, quickjs_emscripten_core.newQuickJSAsyncWASMModuleFromVariant)(variant.default ?? variant);
540
+ })();
541
+ return sharedModulePromise;
484
542
  }
485
543
  function makeErrorSource(message) {
486
544
  return `throw { name: "Error", message: ${JSON.stringify(message)} };`;
@@ -601,6 +659,16 @@ var ReplSession = class ReplSession {
601
659
  skillsFailed = /* @__PURE__ */ new Map();
602
660
  maxPtcCalls;
603
661
  ptcCallsRemaining = null;
662
+ /**
663
+ * Reset the shared WASM module. Forces the next session to instantiate
664
+ * a fresh module. Only needed in tests where module state must be
665
+ * isolated between test files.
666
+ *
667
+ * @internal
668
+ */
669
+ static resetSharedModule() {
670
+ sharedModulePromise = void 0;
671
+ }
604
672
  constructor(id, options = {}) {
605
673
  this.id = id;
606
674
  this.options = options;
@@ -608,15 +676,15 @@ var ReplSession = class ReplSession {
608
676
  }
609
677
  async ensureStarted() {
610
678
  if (this.runtime) return;
611
- const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, skillsEnabled = false, maxResultChars = DEFAULT_MAX_RESULTS_CHARS } = this.options;
612
- const runtime = (await newAsyncModule()).newRuntime();
679
+ const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, skillsEnabled = false, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, captureConsole = true } = this.options;
680
+ const runtime = (await getSharedModule()).newRuntime();
613
681
  runtime.setMemoryLimit(memoryLimitBytes);
614
682
  runtime.setMaxStackSize(maxStackSizeBytes);
615
683
  const context = runtime.newContext();
616
684
  this.runtime = runtime;
617
685
  this.context = context;
618
686
  this.consoleBuffer = new ConsoleBuffer(maxResultChars);
619
- this.setupConsole();
687
+ if (captureConsole) this.setupConsole();
620
688
  if (tools !== void 0 && tools.length > 0) this.injectTools(tools);
621
689
  if (skillsEnabled) this.installModuleLoader();
622
690
  }
@@ -641,21 +709,45 @@ var ReplSession = class ReplSession {
641
709
  throw err;
642
710
  }
643
711
  }
644
- async resolveSpecifier(specifier) {
712
+ /**
713
+ * Pre-load all skills referenced in source code into the in-memory
714
+ * cache. Must be called before `evalCodeAsync` so the module loader
715
+ * can resolve synchronously. An async loader would cause asyncify
716
+ * suspensions on each import, which is incompatible with the shared
717
+ * WASM module used by all sessions.
718
+ */
719
+ async preloadReferencedSkills(code) {
720
+ const refs = scanSkillReferences(code);
721
+ for (const name of refs) {
722
+ if (this.skillsLoaded.has(name) || this.skillsFailed.has(name)) continue;
723
+ try {
724
+ await this.ensureSkillLoaded(name);
725
+ } catch (err) {
726
+ if (!this.skillsFailed.has(name)) this.skillsFailed.set(name, err);
727
+ }
728
+ }
729
+ }
730
+ /**
731
+ * Resolve a module specifier to source code. Strictly synchronous —
732
+ * only reads from the in-memory skill cache populated by
733
+ * `preloadReferencedSkills`. Returns error source (not a thrown
734
+ * exception) for missing or failed skills so QuickJS reports the
735
+ * error inside the VM.
736
+ */
737
+ resolveSpecifier(specifier) {
645
738
  const parsed = parseSkillSpecifier(specifier);
646
739
  if (parsed === void 0) return makeErrorSource(`Module not found: ${specifier}`);
647
- let loaded;
648
- try {
649
- loaded = await this.ensureSkillLoaded(parsed.name);
650
- } catch (err) {
651
- return makeErrorSource(err.message ?? String(err));
652
- }
740
+ const cachedError = this.skillsFailed.get(parsed.name);
741
+ if (cachedError !== void 0) return makeErrorSource(cachedError.message ?? String(cachedError));
742
+ const loaded = this.skillsLoaded.get(parsed.name);
743
+ if (loaded === void 0) return makeErrorSource(`Skill '${parsed.name}' was not preloaded. Ensure the import specifier is a static string literal (dynamic specifiers like \`import("@/skills/" + name)\` are not supported).`);
653
744
  if (parsed.rel === void 0) {
654
745
  const source = loaded.files.get(loaded.entryRel);
655
746
  if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': entrypoint '${loaded.entryRel}' missing from bundle`);
656
747
  return source;
657
748
  }
658
- const source = loaded.files.get(parsed.rel);
749
+ let source = loaded.files.get(parsed.rel);
750
+ if (source === void 0 && parsed.rel.endsWith(".js")) source = loaded.files.get(parsed.rel.slice(0, -3) + ".ts");
659
751
  if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': '${parsed.rel}' not found in bundle`);
660
752
  return source;
661
753
  }
@@ -675,10 +767,16 @@ var ReplSession = class ReplSession {
675
767
  }
676
768
  /**
677
769
  * Wire the QuickJS module loader and normalizer on this session's runtime.
770
+ *
771
+ * The loader is strictly synchronous — it reads from the in-memory skill
772
+ * cache populated by `preloadReferencedSkills`. This is critical: an async
773
+ * module loader causes asyncify suspensions on each import, and disposing
774
+ * a runtime after multi-file imports corrupts the shared module's asyncify
775
+ * state, silently breaking the loader for all subsequent sessions.
678
776
  */
679
777
  installModuleLoader() {
680
778
  if (this.runtime === null) return;
681
- this.runtime.setModuleLoader(async (specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
779
+ this.runtime.setModuleLoader((specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
682
780
  }
683
781
  /**
684
782
  * Initialise the per-eval PTC counter. Called at the top of every `eval()`.
@@ -742,7 +840,7 @@ var ReplSession = class ReplSession {
742
840
  }
743
841
  /**
744
842
  * Push the current skills metadata + backend into the session.
745
- * Called by the middleware once per `js_eval` invocation, before eval runs.
843
+ * Called by the middleware once per `eval` invocation, before eval runs.
746
844
  * Pass `undefined` to clear the context (no skill imports will resolve).
747
845
  */
748
846
  setSkillsContext(ctx) {
@@ -761,6 +859,7 @@ var ReplSession = class ReplSession {
761
859
  await this.ensureStarted();
762
860
  const runtime = this.runtime;
763
861
  const context = this.context;
862
+ await this.preloadReferencedSkills(code);
764
863
  const drainLogs = () => {
765
864
  const [raw, dropped] = this.consoleBuffer.drain();
766
865
  return {
@@ -773,7 +872,7 @@ var ReplSession = class ReplSession {
773
872
  if (timeoutMs >= 0) runtime.setInterruptHandler((0, quickjs_emscripten.shouldInterruptAfterDeadline)(Date.now() + timeoutMs));
774
873
  else runtime.setInterruptHandler(() => false);
775
874
  const transformed = transformForEval(code);
776
- const result = await context.evalCodeAsync(transformed);
875
+ const result = await sharedEvalQueue.enqueue(() => context.evalCodeAsync(transformed));
777
876
  if (result.error) {
778
877
  const error = context.dump(result.error);
779
878
  result.error.dispose();
@@ -932,33 +1031,26 @@ var ReplSession = class ReplSession {
932
1031
  //#endregion
933
1032
  //#region src/middleware.ts
934
1033
  /**
935
- * QuickJS REPL middleware for deepagents.
1034
+ * Code Interpreter middleware for deepagents.
936
1035
  *
937
- * Provides a `js_eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
1036
+ * Provides an `eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
938
1037
  * interpreter. Supports:
939
1038
  * - Persistent state across evaluations (true REPL)
940
1039
  * - Programmatic tool calling (PTC) — expose agent or custom tools inside the REPL
941
1040
  */
942
- const REPL_SYSTEM_PROMPT = dedent.default`
943
- ## TypeScript/JavaScript REPL (\`js_eval\`)
944
-
945
- You have access to a sandboxed TypeScript/JavaScript REPL running in an isolated interpreter.
946
- TypeScript syntax (type annotations, interfaces, generics, \`as\` casts) is supported and stripped at evaluation time.
947
- Variables, functions, and closures persist across calls within the same session.
948
-
949
- ### Hard rules
950
-
951
- - **No network, no direct filesystem** — only through tools provided in the \`tools\` namespace below.
952
- - **Cite your sources** — when reporting values from files, include the path and key/index so the user can verify.
953
- - **Use console.log()** for output — it is captured and returned. \`console.warn()\` and \`console.error()\` are also available.
954
- - **Reuse state from previous cells** — variables, functions, and results from earlier \`js_eval\` calls persist across calls. Reference them by name in follow-up cells instead of re-embedding data as inline JSON literals.
955
-
956
- ### Limitations
1041
+ const DEFAULT_TOOL_NAME = "eval";
1042
+ function renderReplSystemPrompt(opts) {
1043
+ return dedent.default`
1044
+ ### Interpreter
957
1045
 
958
- - ES2023+ syntax with TypeScript support. No Node.js APIs, no \`require\`, no \`import\`.
959
- - Output is truncated beyond a fixed character limit be selective about what you log.
960
- - Execution timeout per call (default 30 s).
961
- `;
1046
+ An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
1047
+ - State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
1048
+ - Top-level \`await\` works; Promises resolve before the call returns.
1049
+ - Sandboxed: no filesystem, no stdlib, no network, no real clock, no \`fetch\`, no \`require\`.
1050
+ - Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
1051
+ - \`console.log\` output is captured and returned alongside the result.
1052
+ `;
1053
+ }
962
1054
  /**
963
1055
  * Generate the PTC API Reference section for the system prompt.
964
1056
  */
@@ -1035,21 +1127,25 @@ async function prepareSkillsForEval(session, skillsBackend, code) {
1035
1127
  });
1036
1128
  }
1037
1129
  /**
1038
- * Create the QuickJS REPL middleware.
1130
+ * Create the Code Interpreter middleware.
1039
1131
  */
1040
- function createQuickJSMiddleware(options = {}) {
1041
- const { ptc, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null, skillsBackend, maxPtcCalls = 256, maxResultChars = DEFAULT_MAX_RESULTS_CHARS } = options;
1132
+ function createCodeInterpreterMiddleware(options = {}) {
1133
+ const { ptc, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null, skillsBackend, maxPtcCalls = 256, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, toolName = DEFAULT_TOOL_NAME, captureConsole = true } = options;
1042
1134
  if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
1043
- const baseSystemPrompt = customSystemPrompt || REPL_SYSTEM_PROMPT;
1135
+ const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
1136
+ toolName,
1137
+ timeout: executionTimeoutMs / 1e3,
1138
+ memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024))
1139
+ });
1044
1140
  const middlewareId = crypto.randomUUID();
1045
1141
  let cachedPtcPrompt = null;
1046
1142
  let ptcTools = [];
1047
1143
  function filterToolsForPtc(allTools) {
1048
1144
  if (!ptc) return [];
1049
- return resolveToolList(ptc, allTools.filter((t) => t.name !== "js_eval"));
1145
+ return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
1050
1146
  }
1051
1147
  return (0, langchain.createMiddleware)({
1052
- name: "QuickJSMiddleware",
1148
+ name: "CodeInterpreterMiddleware",
1053
1149
  tools: [(0, langchain.tool)(async (input, config) => {
1054
1150
  const sessionKey = `${config.configurable?.thread_id || "__default__"}:${middlewareId}`;
1055
1151
  const session = ReplSession.getOrCreate(sessionKey, {
@@ -1058,7 +1154,8 @@ function createQuickJSMiddleware(options = {}) {
1058
1154
  maxPtcCalls,
1059
1155
  tools: ptcTools,
1060
1156
  skillsEnabled: skillsBackend !== void 0,
1061
- maxResultChars
1157
+ maxResultChars,
1158
+ captureConsole
1062
1159
  });
1063
1160
  if (skillsBackend !== void 0) {
1064
1161
  const setupError = await prepareSkillsForEval(session, skillsBackend, input.code);
@@ -1066,7 +1163,7 @@ function createQuickJSMiddleware(options = {}) {
1066
1163
  }
1067
1164
  return formatReplResult(await session.eval(input.code, executionTimeoutMs));
1068
1165
  }, {
1069
- name: "js_eval",
1166
+ name: toolName,
1070
1167
  description: dedent.default`
1071
1168
  Evaluate TypeScript/JavaScript code in a sandboxed REPL. State persists across calls.
1072
1169
  Use console.log() for output. Returns the result of the last expression.
@@ -1100,7 +1197,7 @@ exports.MAX_SKILL_BUNDLE_BYTES = MAX_SKILL_BUNDLE_BYTES;
1100
1197
  exports.PTCCallBudgetExceededError = PTCCallBudgetExceededError;
1101
1198
  exports.ReplSession = ReplSession;
1102
1199
  exports.SKILL_MODULE_EXTENSIONS = SKILL_MODULE_EXTENSIONS;
1103
- exports.createQuickJSMiddleware = createQuickJSMiddleware;
1200
+ exports.createCodeInterpreterMiddleware = createCodeInterpreterMiddleware;
1104
1201
  exports.formatReplResult = formatReplResult;
1105
1202
  exports.formatSkillNotAvailable = formatSkillNotAvailable;
1106
1203
  exports.loadSkill = loadSkill;