@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 +149 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -10
- package/dist/index.d.ts +52 -10
- package/dist/index.js +146 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 =
|
|
507
|
+
const DEFAULT_MEMORY_LIMIT = 64 * 1024 * 1024;
|
|
476
508
|
const DEFAULT_MAX_STACK_SIZE = 320 * 1024;
|
|
477
|
-
const DEFAULT_EXECUTION_TIMEOUT =
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
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
|
-
|
|
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(
|
|
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 `
|
|
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
|
-
*
|
|
1034
|
+
* Code Interpreter middleware for deepagents.
|
|
936
1035
|
*
|
|
937
|
-
* Provides
|
|
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
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
1130
|
+
* Create the Code Interpreter middleware.
|
|
1039
1131
|
*/
|
|
1040
|
-
function
|
|
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 ||
|
|
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 !==
|
|
1145
|
+
return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
|
|
1050
1146
|
}
|
|
1051
1147
|
return (0, langchain.createMiddleware)({
|
|
1052
|
-
name: "
|
|
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:
|
|
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.
|
|
1200
|
+
exports.createCodeInterpreterMiddleware = createCodeInterpreterMiddleware;
|
|
1104
1201
|
exports.formatReplResult = formatReplResult;
|
|
1105
1202
|
exports.formatSkillNotAvailable = formatSkillNotAvailable;
|
|
1106
1203
|
exports.loadSkill = loadSkill;
|