@langchain/quickjs 0.3.0 → 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.d.cts CHANGED
@@ -6,9 +6,9 @@ import { AnyBackendProtocol, BackendFactory, SkillMetadata } from "deepagents";
6
6
 
7
7
  //#region src/types.d.ts
8
8
  /**
9
- * Configuration options for the REPL middleware.
9
+ * Configuration options for the Code Interpreter middleware.
10
10
  */
11
- interface REPLMiddlewareOptions {
11
+ interface CodeInterpreterMiddlewareOptions {
12
12
  /**
13
13
  * Enable programmatic tool calling from within the REPL.
14
14
  *
@@ -121,9 +121,9 @@ interface SkillsContext {
121
121
  //#endregion
122
122
  //#region src/middleware.d.ts
123
123
  /**
124
- * Create the REPL middleware.
124
+ * Create the Code Interpreter middleware.
125
125
  */
126
- declare function createREPLMiddleware(options?: REPLMiddlewareOptions): AgentMiddleware<undefined, undefined, unknown, readonly [_$langchain.DynamicStructuredTool<z.ZodObject<{
126
+ declare function createCodeInterpreterMiddleware(options?: CodeInterpreterMiddlewareOptions): AgentMiddleware<undefined, undefined, unknown, readonly [_$langchain.DynamicStructuredTool<z.ZodObject<{
127
127
  code: z.ZodString;
128
128
  }, z.core.$strip>, {
129
129
  code: string;
@@ -184,12 +184,35 @@ declare class ReplSession {
184
184
  private skillsFailed;
185
185
  private readonly maxPtcCalls;
186
186
  private ptcCallsRemaining;
187
+ /**
188
+ * Reset the shared WASM module. Forces the next session to instantiate
189
+ * a fresh module. Only needed in tests where module state must be
190
+ * isolated between test files.
191
+ *
192
+ * @internal
193
+ */
194
+ static resetSharedModule(): void;
187
195
  constructor(id: string, options?: ReplSessionOptions);
188
196
  private ensureStarted;
189
197
  /**
190
198
  * Load the skill into cache on first access and replay cached errors.
191
199
  */
192
200
  private ensureSkillLoaded;
201
+ /**
202
+ * Pre-load all skills referenced in source code into the in-memory
203
+ * cache. Must be called before `evalCodeAsync` so the module loader
204
+ * can resolve synchronously. An async loader would cause asyncify
205
+ * suspensions on each import, which is incompatible with the shared
206
+ * WASM module used by all sessions.
207
+ */
208
+ preloadReferencedSkills(code: string): Promise<void>;
209
+ /**
210
+ * Resolve a module specifier to source code. Strictly synchronous —
211
+ * only reads from the in-memory skill cache populated by
212
+ * `preloadReferencedSkills`. Returns error source (not a thrown
213
+ * exception) for missing or failed skills so QuickJS reports the
214
+ * error inside the VM.
215
+ */
193
216
  private resolveSpecifier;
194
217
  /**
195
218
  * Canonicalize an `import` specifier. Bare specifiers pass through;
@@ -199,6 +222,12 @@ declare class ReplSession {
199
222
  private normalizeSpecifier;
200
223
  /**
201
224
  * Wire the QuickJS module loader and normalizer on this session's runtime.
225
+ *
226
+ * The loader is strictly synchronous — it reads from the in-memory skill
227
+ * cache populated by `preloadReferencedSkills`. This is critical: an async
228
+ * module loader causes asyncify suspensions on each import, and disposing
229
+ * a runtime after multi-file imports corrupts the shared module's asyncify
230
+ * state, silently breaking the loader for all subsequent sessions.
202
231
  */
203
232
  private installModuleLoader;
204
233
  /**
@@ -356,5 +385,5 @@ declare function loadSkill(metadata: SkillMetadata, backend: AnyBackendProtocol)
356
385
  */
357
386
  declare function scanSkillReferences(source: string): Set<string>;
358
387
  //#endregion
359
- export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, type LoadedSkill, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, type REPLMiddlewareOptions, type ReplResult, ReplSession, type ReplSessionOptions, SKILL_MODULE_EXTENSIONS, createREPLMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
388
+ export { type CodeInterpreterMiddlewareOptions, DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, type LoadedSkill, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, type ReplResult, ReplSession, type ReplSessionOptions, SKILL_MODULE_EXTENSIONS, createCodeInterpreterMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
360
389
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -6,9 +6,9 @@ import { StructuredToolInterface } from "@langchain/core/tools";
6
6
 
7
7
  //#region src/types.d.ts
8
8
  /**
9
- * Configuration options for the REPL middleware.
9
+ * Configuration options for the Code Interpreter middleware.
10
10
  */
11
- interface REPLMiddlewareOptions {
11
+ interface CodeInterpreterMiddlewareOptions {
12
12
  /**
13
13
  * Enable programmatic tool calling from within the REPL.
14
14
  *
@@ -121,9 +121,9 @@ interface SkillsContext {
121
121
  //#endregion
122
122
  //#region src/middleware.d.ts
123
123
  /**
124
- * Create the REPL middleware.
124
+ * Create the Code Interpreter middleware.
125
125
  */
126
- declare function createREPLMiddleware(options?: REPLMiddlewareOptions): AgentMiddleware<undefined, undefined, unknown, readonly [_$langchain.DynamicStructuredTool<z.ZodObject<{
126
+ declare function createCodeInterpreterMiddleware(options?: CodeInterpreterMiddlewareOptions): AgentMiddleware<undefined, undefined, unknown, readonly [_$langchain.DynamicStructuredTool<z.ZodObject<{
127
127
  code: z.ZodString;
128
128
  }, z.core.$strip>, {
129
129
  code: string;
@@ -184,12 +184,35 @@ declare class ReplSession {
184
184
  private skillsFailed;
185
185
  private readonly maxPtcCalls;
186
186
  private ptcCallsRemaining;
187
+ /**
188
+ * Reset the shared WASM module. Forces the next session to instantiate
189
+ * a fresh module. Only needed in tests where module state must be
190
+ * isolated between test files.
191
+ *
192
+ * @internal
193
+ */
194
+ static resetSharedModule(): void;
187
195
  constructor(id: string, options?: ReplSessionOptions);
188
196
  private ensureStarted;
189
197
  /**
190
198
  * Load the skill into cache on first access and replay cached errors.
191
199
  */
192
200
  private ensureSkillLoaded;
201
+ /**
202
+ * Pre-load all skills referenced in source code into the in-memory
203
+ * cache. Must be called before `evalCodeAsync` so the module loader
204
+ * can resolve synchronously. An async loader would cause asyncify
205
+ * suspensions on each import, which is incompatible with the shared
206
+ * WASM module used by all sessions.
207
+ */
208
+ preloadReferencedSkills(code: string): Promise<void>;
209
+ /**
210
+ * Resolve a module specifier to source code. Strictly synchronous —
211
+ * only reads from the in-memory skill cache populated by
212
+ * `preloadReferencedSkills`. Returns error source (not a thrown
213
+ * exception) for missing or failed skills so QuickJS reports the
214
+ * error inside the VM.
215
+ */
193
216
  private resolveSpecifier;
194
217
  /**
195
218
  * Canonicalize an `import` specifier. Bare specifiers pass through;
@@ -199,6 +222,12 @@ declare class ReplSession {
199
222
  private normalizeSpecifier;
200
223
  /**
201
224
  * Wire the QuickJS module loader and normalizer on this session's runtime.
225
+ *
226
+ * The loader is strictly synchronous — it reads from the in-memory skill
227
+ * cache populated by `preloadReferencedSkills`. This is critical: an async
228
+ * module loader causes asyncify suspensions on each import, and disposing
229
+ * a runtime after multi-file imports corrupts the shared module's asyncify
230
+ * state, silently breaking the loader for all subsequent sessions.
202
231
  */
203
232
  private installModuleLoader;
204
233
  /**
@@ -356,5 +385,5 @@ declare function loadSkill(metadata: SkillMetadata, backend: AnyBackendProtocol)
356
385
  */
357
386
  declare function scanSkillReferences(source: string): Set<string>;
358
387
  //#endregion
359
- export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, type LoadedSkill, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, type REPLMiddlewareOptions, type ReplResult, ReplSession, type ReplSessionOptions, SKILL_MODULE_EXTENSIONS, createREPLMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
388
+ export { type CodeInterpreterMiddlewareOptions, DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, type LoadedSkill, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, type ReplResult, ReplSession, type ReplSessionOptions, SKILL_MODULE_EXTENSIONS, createCodeInterpreterMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
360
389
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -427,6 +427,38 @@ function formatSkillNotAvailable(missing) {
427
427
  return `Skills unavailable: ${[...missing].sort().join(", ")}`;
428
428
  }
429
429
  //#endregion
430
+ //#region src/eval-queue.ts
431
+ /**
432
+ * Serializes async operations on a shared WASM module.
433
+ *
434
+ * The quickjs-emscripten asyncify variant allows only one concurrent
435
+ * async call per module instance. This queue enforces that constraint
436
+ * by chaining operations into a promise queue — each caller waits for
437
+ * the previous one to finish before executing.
438
+ */
439
+ var AsyncEvalQueue = class {
440
+ tail = Promise.resolve();
441
+ /**
442
+ * Enqueue an async operation. The operation will not start until all
443
+ * previously enqueued operations have completed.
444
+ */
445
+ async enqueue(fn) {
446
+ let release;
447
+ const gate = new Promise((r) => {
448
+ release = r;
449
+ });
450
+ const prev = this.tail;
451
+ this.tail = gate;
452
+ return prev.then(async () => {
453
+ try {
454
+ return await fn();
455
+ } finally {
456
+ release();
457
+ }
458
+ });
459
+ }
460
+ };
461
+ //#endregion
430
462
  //#region src/session.ts
431
463
  /**
432
464
  * Core REPL engine built on quickjs-emscripten (asyncify variant).
@@ -452,9 +484,35 @@ const DEFAULT_EXECUTION_TIMEOUT = 5e3;
452
484
  const DEFAULT_MAX_PTC_CALLS = 256;
453
485
  const DEFAULT_MAX_RESULTS_CHARS = 4e3;
454
486
  const variantImport = import("@jitl/quickjs-ng-wasmfile-release-asyncify");
455
- async function newAsyncModule() {
456
- const variant = await variantImport;
457
- return newQuickJSAsyncWASMModuleFromVariant(variant.default ?? variant);
487
+ /**
488
+ * Process-global eval queue. Serializes all evalCodeAsync calls across
489
+ * sessions to enforce the asyncify one-at-a-time constraint.
490
+ */
491
+ const sharedEvalQueue = new AsyncEvalQueue();
492
+ /**
493
+ * Process-global WASM module shared by all sessions.
494
+ *
495
+ * Each session creates its own runtime and context on this module,
496
+ * providing full isolation for globals, heap, and stack. The module
497
+ * itself is stateless between runtimes — only the compiled WASM code
498
+ * and Emscripten infrastructure are shared.
499
+ *
500
+ * This is safe because:
501
+ * - The module loader is synchronous (preloaded skill cache), so
502
+ * imports don't cause asyncify suspensions.
503
+ * - Tool injection uses the promise-based pattern (newFunction +
504
+ * newPromise), not newAsyncifiedFunction, so tool calls don't
505
+ * cause asyncify suspensions.
506
+ * - The eval queue serializes evalCodeAsync calls to satisfy the
507
+ * one-concurrent-async-call-per-module constraint.
508
+ */
509
+ let sharedModulePromise;
510
+ function getSharedModule() {
511
+ if (!sharedModulePromise) sharedModulePromise = (async () => {
512
+ const variant = await variantImport;
513
+ return newQuickJSAsyncWASMModuleFromVariant(variant.default ?? variant);
514
+ })();
515
+ return sharedModulePromise;
458
516
  }
459
517
  function makeErrorSource(message) {
460
518
  return `throw { name: "Error", message: ${JSON.stringify(message)} };`;
@@ -575,6 +633,16 @@ var ReplSession = class ReplSession {
575
633
  skillsFailed = /* @__PURE__ */ new Map();
576
634
  maxPtcCalls;
577
635
  ptcCallsRemaining = null;
636
+ /**
637
+ * Reset the shared WASM module. Forces the next session to instantiate
638
+ * a fresh module. Only needed in tests where module state must be
639
+ * isolated between test files.
640
+ *
641
+ * @internal
642
+ */
643
+ static resetSharedModule() {
644
+ sharedModulePromise = void 0;
645
+ }
578
646
  constructor(id, options = {}) {
579
647
  this.id = id;
580
648
  this.options = options;
@@ -583,7 +651,7 @@ var ReplSession = class ReplSession {
583
651
  async ensureStarted() {
584
652
  if (this.runtime) return;
585
653
  const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, skillsEnabled = false, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, captureConsole = true } = this.options;
586
- const runtime = (await newAsyncModule()).newRuntime();
654
+ const runtime = (await getSharedModule()).newRuntime();
587
655
  runtime.setMemoryLimit(memoryLimitBytes);
588
656
  runtime.setMaxStackSize(maxStackSizeBytes);
589
657
  const context = runtime.newContext();
@@ -615,21 +683,45 @@ var ReplSession = class ReplSession {
615
683
  throw err;
616
684
  }
617
685
  }
618
- async resolveSpecifier(specifier) {
686
+ /**
687
+ * Pre-load all skills referenced in source code into the in-memory
688
+ * cache. Must be called before `evalCodeAsync` so the module loader
689
+ * can resolve synchronously. An async loader would cause asyncify
690
+ * suspensions on each import, which is incompatible with the shared
691
+ * WASM module used by all sessions.
692
+ */
693
+ async preloadReferencedSkills(code) {
694
+ const refs = scanSkillReferences(code);
695
+ for (const name of refs) {
696
+ if (this.skillsLoaded.has(name) || this.skillsFailed.has(name)) continue;
697
+ try {
698
+ await this.ensureSkillLoaded(name);
699
+ } catch (err) {
700
+ if (!this.skillsFailed.has(name)) this.skillsFailed.set(name, err);
701
+ }
702
+ }
703
+ }
704
+ /**
705
+ * Resolve a module specifier to source code. Strictly synchronous —
706
+ * only reads from the in-memory skill cache populated by
707
+ * `preloadReferencedSkills`. Returns error source (not a thrown
708
+ * exception) for missing or failed skills so QuickJS reports the
709
+ * error inside the VM.
710
+ */
711
+ resolveSpecifier(specifier) {
619
712
  const parsed = parseSkillSpecifier(specifier);
620
713
  if (parsed === void 0) return makeErrorSource(`Module not found: ${specifier}`);
621
- let loaded;
622
- try {
623
- loaded = await this.ensureSkillLoaded(parsed.name);
624
- } catch (err) {
625
- return makeErrorSource(err.message ?? String(err));
626
- }
714
+ const cachedError = this.skillsFailed.get(parsed.name);
715
+ if (cachedError !== void 0) return makeErrorSource(cachedError.message ?? String(cachedError));
716
+ const loaded = this.skillsLoaded.get(parsed.name);
717
+ 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).`);
627
718
  if (parsed.rel === void 0) {
628
719
  const source = loaded.files.get(loaded.entryRel);
629
720
  if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': entrypoint '${loaded.entryRel}' missing from bundle`);
630
721
  return source;
631
722
  }
632
- const source = loaded.files.get(parsed.rel);
723
+ let source = loaded.files.get(parsed.rel);
724
+ if (source === void 0 && parsed.rel.endsWith(".js")) source = loaded.files.get(parsed.rel.slice(0, -3) + ".ts");
633
725
  if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': '${parsed.rel}' not found in bundle`);
634
726
  return source;
635
727
  }
@@ -649,10 +741,16 @@ var ReplSession = class ReplSession {
649
741
  }
650
742
  /**
651
743
  * Wire the QuickJS module loader and normalizer on this session's runtime.
744
+ *
745
+ * The loader is strictly synchronous — it reads from the in-memory skill
746
+ * cache populated by `preloadReferencedSkills`. This is critical: an async
747
+ * module loader causes asyncify suspensions on each import, and disposing
748
+ * a runtime after multi-file imports corrupts the shared module's asyncify
749
+ * state, silently breaking the loader for all subsequent sessions.
652
750
  */
653
751
  installModuleLoader() {
654
752
  if (this.runtime === null) return;
655
- this.runtime.setModuleLoader(async (specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
753
+ this.runtime.setModuleLoader((specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
656
754
  }
657
755
  /**
658
756
  * Initialise the per-eval PTC counter. Called at the top of every `eval()`.
@@ -735,6 +833,7 @@ var ReplSession = class ReplSession {
735
833
  await this.ensureStarted();
736
834
  const runtime = this.runtime;
737
835
  const context = this.context;
836
+ await this.preloadReferencedSkills(code);
738
837
  const drainLogs = () => {
739
838
  const [raw, dropped] = this.consoleBuffer.drain();
740
839
  return {
@@ -747,7 +846,7 @@ var ReplSession = class ReplSession {
747
846
  if (timeoutMs >= 0) runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + timeoutMs));
748
847
  else runtime.setInterruptHandler(() => false);
749
848
  const transformed = transformForEval(code);
750
- const result = await context.evalCodeAsync(transformed);
849
+ const result = await sharedEvalQueue.enqueue(() => context.evalCodeAsync(transformed));
751
850
  if (result.error) {
752
851
  const error = context.dump(result.error);
753
852
  result.error.dispose();
@@ -906,7 +1005,7 @@ var ReplSession = class ReplSession {
906
1005
  //#endregion
907
1006
  //#region src/middleware.ts
908
1007
  /**
909
- * REPL middleware for deepagents.
1008
+ * Code Interpreter middleware for deepagents.
910
1009
  *
911
1010
  * Provides an `eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
912
1011
  * interpreter. Supports:
@@ -1002,9 +1101,9 @@ async function prepareSkillsForEval(session, skillsBackend, code) {
1002
1101
  });
1003
1102
  }
1004
1103
  /**
1005
- * Create the REPL middleware.
1104
+ * Create the Code Interpreter middleware.
1006
1105
  */
1007
- function createREPLMiddleware(options = {}) {
1106
+ function createCodeInterpreterMiddleware(options = {}) {
1008
1107
  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;
1009
1108
  if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
1010
1109
  const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
@@ -1020,7 +1119,7 @@ function createREPLMiddleware(options = {}) {
1020
1119
  return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
1021
1120
  }
1022
1121
  return createMiddleware({
1023
- name: "REPLMiddleware",
1122
+ name: "CodeInterpreterMiddleware",
1024
1123
  tools: [tool(async (input, config) => {
1025
1124
  const sessionKey = `${config.configurable?.thread_id || "__default__"}:${middlewareId}`;
1026
1125
  const session = ReplSession.getOrCreate(sessionKey, {
@@ -1064,6 +1163,6 @@ function createREPLMiddleware(options = {}) {
1064
1163
  });
1065
1164
  }
1066
1165
  //#endregion
1067
- export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, ReplSession, SKILL_MODULE_EXTENSIONS, createREPLMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
1166
+ export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, ReplSession, SKILL_MODULE_EXTENSIONS, createCodeInterpreterMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
1068
1167
 
1069
1168
  //# sourceMappingURL=index.js.map