@langchain/quickjs 0.2.5 → 0.3.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
@@ -23,82 +23,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let langchain = require("langchain");
25
25
  let zod_v4 = require("zod/v4");
26
- let deepagents = require("deepagents");
27
26
  let dedent = require("dedent");
28
- dedent = __toESM(dedent);
27
+ dedent = __toESM(dedent, 1);
28
+ let _langchain_langgraph = require("@langchain/langgraph");
29
+ let deepagents = require("deepagents");
29
30
  let quickjs_emscripten = require("quickjs-emscripten");
30
31
  let quickjs_emscripten_core = require("quickjs-emscripten-core");
31
- let json_schema_to_typescript = require("json-schema-to-typescript");
32
- let _langchain_core_utils_json_schema = require("@langchain/core/utils/json_schema");
32
+ let node_path_posix = require("node:path/posix");
33
+ node_path_posix = __toESM(node_path_posix, 1);
33
34
  let acorn = require("acorn");
34
35
  let _sveltejs_acorn_typescript = require("@sveltejs/acorn-typescript");
35
36
  let estree_walker = require("estree-walker");
36
37
  let magic_string = require("magic-string");
37
- magic_string = __toESM(magic_string);
38
- let _langchain_langgraph = require("@langchain/langgraph");
39
- //#region src/utils.ts
40
- /**
41
- * Convert a snake_case or kebab-case string to camelCase.
42
- */
43
- function toCamelCase(name) {
44
- return name.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
45
- }
46
- /**
47
- * Format the result of a REPL evaluation for the agent.
48
- */
49
- function formatReplResult(result) {
50
- const parts = [];
51
- if (result.logs.length > 0) parts.push(result.logs.join("\n"));
52
- if (result.ok) {
53
- if (result.value !== void 0) {
54
- const formatted = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
55
- parts.push(`→ ${formatted}`);
56
- }
57
- } else if (result.error) {
58
- const errName = result.error.name || "Error";
59
- const errMsg = result.error.message || "Unknown error";
60
- parts.push(`${errName}: ${errMsg}`);
61
- if (result.error.stack) parts.push(result.error.stack);
62
- }
63
- return parts.join("\n") || "(no output)";
64
- }
65
- function safeToJsonSchema(schema) {
66
- try {
67
- return (0, _langchain_core_utils_json_schema.toJsonSchema)(schema);
68
- } catch {
69
- return;
70
- }
71
- }
72
- async function schemaToInterface(jsonSchema, interfaceName) {
73
- return (await (0, json_schema_to_typescript.compile)({
74
- ...jsonSchema,
75
- additionalProperties: false
76
- }, interfaceName, {
77
- bannerComment: "",
78
- additionalProperties: false
79
- })).replace(/^export /, "").trimEnd();
80
- }
81
- function capitalize(s) {
82
- return s.charAt(0).toUpperCase() + s.slice(1);
83
- }
84
- async function toolToTypeSignature(name, description, jsonSchema) {
85
- const inputType = `${capitalize(name)}Input`;
86
- if (!jsonSchema || !jsonSchema.properties) return dedent.default`
87
- /**
88
- * ${description}
89
- */
90
- async tools.${name}(input: Record<string, unknown>): Promise<string>
91
- `;
92
- return dedent.default`
93
- ${await schemaToInterface(jsonSchema, inputType)}
94
-
95
- /**
96
- * ${description}
97
- */
98
- async tools.${name}(input: ${inputType}): Promise<string>
99
- `;
100
- }
101
- //#endregion
38
+ magic_string = __toESM(magic_string, 1);
39
+ let json_schema_to_typescript = require("json-schema-to-typescript");
40
+ let _langchain_core_utils_json_schema = require("@langchain/core/utils/json_schema");
102
41
  //#region src/transform.ts
103
42
  /**
104
43
  * AST-based code transform pipeline for the REPL.
@@ -259,6 +198,260 @@ function findLastNonEmptyNode(nodes, s) {
259
198
  function isExpression(node) {
260
199
  return node.type === "ExpressionStatement";
261
200
  }
201
+ /**
202
+ * Strip TypeScript type syntax from an ES-module source so QuickJS can
203
+ * evaluate it as a standard JS module.
204
+ *
205
+ * Unlike `transformForEval`, this keeps `import`/`export` declarations,
206
+ * does not hoist to `globalThis`, and does not wrap in an IIFE.
207
+ * On parse failure the original source is returned unchanged.
208
+ */
209
+ function stripTypeSyntax(code) {
210
+ let ast;
211
+ try {
212
+ ast = TSParser.parse(code, {
213
+ ecmaVersion: "latest",
214
+ sourceType: "module",
215
+ locations: true
216
+ });
217
+ } catch {
218
+ return code;
219
+ }
220
+ const magicString = new magic_string.default(code);
221
+ const program = ast;
222
+ for (const node of program.body) {
223
+ if (isTSOnlyNode(node)) {
224
+ magicString.remove(node.start, node.end);
225
+ continue;
226
+ }
227
+ (0, estree_walker.walk)(node, { enter(n) {
228
+ stripTypeAnnotationFromNode(magicString, n);
229
+ } });
230
+ }
231
+ return magicString.toString();
232
+ }
233
+ //#endregion
234
+ //#region src/skills.ts
235
+ /**
236
+ * File extensions the loader will enumerate from a skill directory.
237
+ */
238
+ const SKILL_MODULE_EXTENSIONS = [
239
+ ".js",
240
+ ".mjs",
241
+ ".cjs",
242
+ ".ts",
243
+ ".mts",
244
+ ".cts",
245
+ ".jsx",
246
+ ".tsx"
247
+ ];
248
+ /**
249
+ * Hard cap on total bytes pulled for one skill's bundle (1 MiB).
250
+ */
251
+ const MAX_SKILL_BUNDLE_BYTES = 1 * 1024 * 1024;
252
+ /**
253
+ * Validates a skill name against the spec's kebab-case rule.
254
+ */
255
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
256
+ /**
257
+ * Matches `"@/skills/<name>"` or `'@/skills/<name>'` references in source.
258
+ * Template literals and computed specifiers are not caught.
259
+ */
260
+ const SKILL_SPECIFIER_RE = /["']@\/skills\/([a-z0-9]+(?:-[a-z0-9]+)*)["']/g;
261
+ /**
262
+ * List every code-extension file under `skillDir` (recursive).
263
+ */
264
+ async function enumerateCodeFiles(backend, skillDir, skillName) {
265
+ const seen = /* @__PURE__ */ new Set();
266
+ for (const ext of SKILL_MODULE_EXTENSIONS) {
267
+ const result = await backend.glob(`**/*${ext}`, skillDir);
268
+ if (result.error !== void 0) throw new Error(`Skill '${skillName}': failed to list '${skillDir}': ${result.error}`);
269
+ const matches = result.files ?? [];
270
+ for (const match of matches) seen.add(match.path);
271
+ }
272
+ return [...seen].sort();
273
+ }
274
+ /**
275
+ * Decode download responses into [path, source] pairs.
276
+ */
277
+ function decodeFiles(responses, skillName) {
278
+ const decoder = new TextDecoder("utf-8", { fatal: true });
279
+ const pairs = [];
280
+ for (const response of responses) {
281
+ if (response.error !== null || response.content === null) throw new Error(`Skill '${skillName}': failed to download '${response.path}': ${response.error ?? "no content"}`);
282
+ let source;
283
+ try {
284
+ source = decoder.decode(response.content);
285
+ } catch {
286
+ throw new Error(`Skill '${skillName}': file '${response.path}' is not valid UTF-8`);
287
+ }
288
+ pairs.push([response.path, source]);
289
+ }
290
+ return pairs;
291
+ }
292
+ /**
293
+ * Throws an Error when the total decoded size of all files exceeds
294
+ * `MAX_SKILL_BUNDLE_BYTES`. Counts characters rather than bytes, which
295
+ * over-counts multi-byte UTF-8. Intentionally errs toward rejection.
296
+ */
297
+ function validateBundleSize(pairs, skillName) {
298
+ let total = 0;
299
+ for (const [, source] of pairs) total += source.length;
300
+ if (total > 1048576) throw new Error(`Skill '${skillName}': bundle exceeds ${MAX_SKILL_BUNDLE_BYTES} bytes (total ${total})`);
301
+ }
302
+ /**
303
+ * Express `absolutePath` as a POSIX-relative path under `skillDir`.
304
+ * Throws an Error if the path escapes the skill directory which indicates
305
+ * a backend bug, not a user error.
306
+ */
307
+ function relativeUnder(skillDir, absolutePath, skillName) {
308
+ const rel = node_path_posix.relative(skillDir, absolutePath);
309
+ if (rel === "" || rel.startsWith("..")) throw new Error(`Skill '${skillName}': file ${absolutePath} is not under '${skillDir}'`);
310
+ return rel;
311
+ }
312
+ /**
313
+ * Build the relative-path → source map, applying `stripTypeSyntax` to each file.
314
+ */
315
+ function buildFilesMap(skillDir, entryRel, pairs, skillName) {
316
+ const files = /* @__PURE__ */ new Map();
317
+ let entryPresent = false;
318
+ for (const [absPath, source] of pairs) {
319
+ const rel = relativeUnder(skillDir, absPath, skillName);
320
+ files.set(rel, stripTypeSyntax(source));
321
+ if (rel === entryRel) entryPresent = true;
322
+ }
323
+ if (!entryPresent) throw new Error(`Skill '${skillName}': module path '${entryRel}' did not match any file in the skill directory`);
324
+ return files;
325
+ }
326
+ /**
327
+ * Build a `LoadedSkill` from a skill's metadata and a backend handle.
328
+ *
329
+ * Enumerates code files under the skill directory, downloads them,
330
+ * strips TypeScript syntax, and validates the entrypoint is present.
331
+ */
332
+ async function loadSkill(metadata, backend) {
333
+ const name = metadata.name;
334
+ if (!SKILL_NAME_RE.test(name)) throw new Error(`Skill name '${name}' is not a valid kebab-case identifier`);
335
+ const entryRel = metadata.module;
336
+ if (entryRel === void 0 || entryRel === "") throw new Error(`Skill '${name}' has no 'module' frontmatter key - only skills with a declared entrypoint are installable`);
337
+ const adapted = (0, deepagents.adaptBackendProtocol)(backend);
338
+ if (adapted.downloadFiles === void 0) throw new Error(`Skill '${name}': backend does not implement downloadFiles`);
339
+ const skillDir = node_path_posix.dirname(metadata.path);
340
+ const codeFiles = await enumerateCodeFiles(adapted, skillDir, name);
341
+ if (codeFiles.length === 0) throw new Error(`Skill '${name}': no JS/TS files under '${skillDir}'`);
342
+ const filePairs = decodeFiles(await adapted.downloadFiles(codeFiles), name);
343
+ validateBundleSize(filePairs, name);
344
+ const files = buildFilesMap(skillDir, entryRel, filePairs, name);
345
+ return {
346
+ name,
347
+ specifier: `@/skills/${name}`,
348
+ entryRel,
349
+ files
350
+ };
351
+ }
352
+ /**
353
+ * Extract skill names referenced by `"@/skills/<name>"` literals in source.
354
+ *
355
+ * Used as a pre-eval scan so the middleware can surface `SkillNotAvailable`
356
+ * before evaluation starts. Dynamic imports with computed specifiers are
357
+ * not detected.
358
+ */
359
+ function scanSkillReferences(source) {
360
+ const names = /* @__PURE__ */ new Set();
361
+ const matches = source.matchAll(SKILL_SPECIFIER_RE);
362
+ for (const match of matches) names.add(match[1]);
363
+ return names;
364
+ }
365
+ //#endregion
366
+ //#region src/errors.ts
367
+ /**
368
+ * Thrown when a single eval exhausts its configured PTC call budget.
369
+ */
370
+ var PTCCallBudgetExceededError = class extends Error {
371
+ limit;
372
+ attempted;
373
+ functionName;
374
+ constructor(options) {
375
+ super(`PTC call budget exceeded (limit=${options.limit}, attempted=${options.attempted}, function=${options.functionName})`);
376
+ this.name = "PTCCallBudgetExceededError";
377
+ this.limit = options.limit;
378
+ this.attempted = options.attempted;
379
+ this.functionName = options.functionName;
380
+ }
381
+ };
382
+ //#endregion
383
+ //#region src/utils.ts
384
+ /**
385
+ * Convert a snake_case or kebab-case string to camelCase.
386
+ */
387
+ function toCamelCase(name) {
388
+ return name.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
389
+ }
390
+ /**
391
+ * Format the result of a REPL evaluation for the agent.
392
+ */
393
+ function formatReplResult(result) {
394
+ const parts = [];
395
+ if (result.logs.length > 0) {
396
+ let logsText = result.logs.join("\n");
397
+ if (result.logsDroppedChars > 0) logsText += `\n[truncated ${result.logsDroppedChars} chars]`;
398
+ parts.push(logsText);
399
+ }
400
+ if (result.ok) {
401
+ if (result.value !== void 0) {
402
+ const formatted = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
403
+ parts.push(`→ ${formatted}`);
404
+ }
405
+ } else if (result.error) {
406
+ const errName = result.error.name || "Error";
407
+ const errMsg = result.error.message || "Unknown error";
408
+ parts.push(`${errName}: ${errMsg}`);
409
+ if (result.error.stack) parts.push(result.error.stack);
410
+ }
411
+ return parts.join("\n") || "(no output)";
412
+ }
413
+ function safeToJsonSchema(schema) {
414
+ try {
415
+ return (0, _langchain_core_utils_json_schema.toJsonSchema)(schema);
416
+ } catch {
417
+ return;
418
+ }
419
+ }
420
+ async function schemaToInterface(jsonSchema, interfaceName) {
421
+ return (await (0, json_schema_to_typescript.compile)({
422
+ ...jsonSchema,
423
+ additionalProperties: false
424
+ }, interfaceName, {
425
+ bannerComment: "",
426
+ additionalProperties: false
427
+ })).replace(/^export /, "").trimEnd();
428
+ }
429
+ function capitalize(s) {
430
+ return s.charAt(0).toUpperCase() + s.slice(1);
431
+ }
432
+ async function toolToTypeSignature(name, description, jsonSchema) {
433
+ const inputType = `${capitalize(name)}Input`;
434
+ if (!jsonSchema || !jsonSchema.properties) return dedent.default`
435
+ /**
436
+ * ${description}
437
+ */
438
+ async tools.${name}(input: Record<string, unknown>): Promise<string>
439
+ `;
440
+ return dedent.default`
441
+ ${await schemaToInterface(jsonSchema, inputType)}
442
+
443
+ /**
444
+ * ${description}
445
+ */
446
+ async tools.${name}(input: ${inputType}): Promise<string>
447
+ `;
448
+ }
449
+ /**
450
+ * Render a pre-eval error when referenced skills are not available on the agent.
451
+ */
452
+ function formatSkillNotAvailable(missing) {
453
+ return `Skills unavailable: ${[...missing].sort().join(", ")}`;
454
+ }
262
455
  //#endregion
263
456
  //#region src/session.ts
264
457
  /**
@@ -278,65 +471,237 @@ function isExpression(node) {
278
471
  * It holds an `id` that keys into a static session map. The heavy QuickJS
279
472
  * runtime is lazily started on the first `.eval()` call, making the session
280
473
  * safe across graph interrupts and checkpointing.
281
- *
282
- * File writes inside the REPL are buffered (`pendingWrites`) and only
283
- * flushed to the backend after a script finishes executing. Call
284
- * `session.flushWrites(backend)` after eval to persist them.
285
474
  */
286
- const DEFAULT_MEMORY_LIMIT = 50 * 1024 * 1024;
475
+ const DEFAULT_MEMORY_LIMIT = 64 * 1024 * 1024;
287
476
  const DEFAULT_MAX_STACK_SIZE = 320 * 1024;
288
- const DEFAULT_EXECUTION_TIMEOUT = 3e4;
289
- let asyncModulePromise;
290
- async function getAsyncModule() {
291
- if (!asyncModulePromise) asyncModulePromise = (async () => {
292
- const variant = await import("@jitl/quickjs-ng-wasmfile-release-asyncify");
293
- return (0, quickjs_emscripten_core.newQuickJSAsyncWASMModuleFromVariant)(variant.default ?? variant);
294
- })();
295
- return asyncModulePromise;
477
+ const DEFAULT_EXECUTION_TIMEOUT = 5e3;
478
+ const DEFAULT_MAX_PTC_CALLS = 256;
479
+ const DEFAULT_MAX_RESULTS_CHARS = 4e3;
480
+ 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);
484
+ }
485
+ function makeErrorSource(message) {
486
+ return `throw { name: "Error", message: ${JSON.stringify(message)} };`;
487
+ }
488
+ /**
489
+ * Parse a canonicalized skill specifier into `{ name, rel }`.
490
+ * Returns `undefined` for anything that isn't a valid `@/skills/<name>` or
491
+ * `@/skills/<name>/<rel>` shape. `rel` is absent for the bare form.
492
+ */
493
+ function parseSkillSpecifier(specifier) {
494
+ if (!specifier.startsWith("@/skills/")) return;
495
+ const tail = specifier.slice(9);
496
+ const slashIdx = tail.indexOf("/");
497
+ const name = slashIdx === -1 ? tail : tail.slice(0, slashIdx);
498
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) return;
499
+ const rel = slashIdx === -1 ? void 0 : tail.slice(slashIdx + 1);
500
+ if (rel !== void 0 && rel === "") return;
501
+ return {
502
+ name,
503
+ rel
504
+ };
296
505
  }
297
506
  /**
507
+ * Return the `@/skills/<name>` prefix for the skill that owns `base`, or `undefined`.
508
+ */
509
+ function matchSkillPrefix(base) {
510
+ const parsed = parseSkillSpecifier(base);
511
+ if (parsed === void 0) return;
512
+ return `@/skills/${parsed.name}`;
513
+ }
514
+ /**
515
+ * Return the directory portion of a slash-separated specifier path.
516
+ */
517
+ function posixDirname(p) {
518
+ const idx = p.lastIndexOf("/");
519
+ if (idx === -1) return "";
520
+ return p.slice(0, idx);
521
+ }
522
+ /**
523
+ * POSIX join for slash-separated specifiers. Avoids `node:path/posix`
524
+ * since session.ts is consumed in browser bundles.
525
+ */
526
+ function posixJoin(base, rel) {
527
+ const out = [];
528
+ const segments = `${base}/${rel}`.split("/");
529
+ for (const segment of segments) {
530
+ if (segment === "" || segment === ".") continue;
531
+ if (segment === "..") {
532
+ out.pop();
533
+ continue;
534
+ }
535
+ out.push(segment);
536
+ }
537
+ return out.join("/");
538
+ }
539
+ /**
540
+ * Fixed-size character buffer for capturing console output from the QuickJS VM.
541
+ *
542
+ * Lines are accumulated up to `maxChars`. Once the cap is reached, excess
543
+ * characters are counted as dropped rather than silently discarded without
544
+ * attribution, so callers can surface a truncation notice to the user.
545
+ */
546
+ var ConsoleBuffer = class {
547
+ maxChars;
548
+ buffer = "";
549
+ droppedChars = 0;
550
+ constructor(maxChars) {
551
+ this.maxChars = Math.max(maxChars, 0);
552
+ }
553
+ /**
554
+ * Append `line` to the buffer.
555
+ *
556
+ * If the buffer is already full the entire line is counted as dropped.
557
+ * If `line` partially fits, the fitting prefix is stored and the remainder
558
+ * is counted as dropped.
559
+ */
560
+ append(line) {
561
+ const remaining = this.maxChars - this.buffer.length;
562
+ if (remaining <= 0) {
563
+ this.droppedChars += line.length;
564
+ return;
565
+ }
566
+ if (line.length <= remaining) this.buffer += line;
567
+ else {
568
+ this.buffer += line.slice(0, remaining);
569
+ this.droppedChars += line.length - remaining;
570
+ }
571
+ }
572
+ /**
573
+ * Return the buffered output and dropped-character count as `[buffered,
574
+ * droppedChars]`, then reset both to zero.
575
+ */
576
+ drain() {
577
+ const out = this.buffer;
578
+ const dropped = this.droppedChars;
579
+ this.buffer = "";
580
+ this.droppedChars = 0;
581
+ return [out, dropped];
582
+ }
583
+ };
584
+ /**
298
585
  * Sandboxed JavaScript REPL session backed by QuickJS WASM.
299
586
  *
300
587
  * Serializable — holds an `id` that keys into a static session map.
301
588
  * The QuickJS runtime is lazily started on the first `.eval()` call
302
589
  * and reconnected if a session with the same id already exists.
303
590
  * This makes it safe to store in LangGraph state across interrupts.
304
- *
305
- * File writes are buffered during execution and flushed via
306
- * `flushWrites(backend)` after eval completes.
307
591
  */
308
592
  var ReplSession = class ReplSession {
309
593
  static sessions = /* @__PURE__ */ new Map();
310
594
  id;
311
- pendingWrites = [];
312
595
  runtime = null;
313
596
  context = null;
314
- logs = [];
315
- _options;
316
- _backend = null;
597
+ consoleBuffer = new ConsoleBuffer(DEFAULT_MAX_RESULTS_CHARS);
598
+ options;
599
+ skillsContext;
600
+ skillsLoaded = /* @__PURE__ */ new Map();
601
+ skillsFailed = /* @__PURE__ */ new Map();
602
+ maxPtcCalls;
603
+ ptcCallsRemaining = null;
317
604
  constructor(id, options = {}) {
318
605
  this.id = id;
319
- this._options = options;
320
- }
321
- get backend() {
322
- return this._backend;
323
- }
324
- set backend(b) {
325
- this._backend = b ? (0, deepagents.adaptBackendProtocol)(b) : null;
606
+ this.options = options;
607
+ this.maxPtcCalls = options.maxPtcCalls !== void 0 ? options.maxPtcCalls : 256;
326
608
  }
327
609
  async ensureStarted() {
328
610
  if (this.runtime) return;
329
- const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, backend, tools } = this._options;
330
- const runtime = (await getAsyncModule()).newRuntime();
611
+ const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, skillsEnabled = false, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, captureConsole = true } = this.options;
612
+ const runtime = (await newAsyncModule()).newRuntime();
331
613
  runtime.setMemoryLimit(memoryLimitBytes);
332
614
  runtime.setMaxStackSize(maxStackSizeBytes);
333
615
  const context = runtime.newContext();
334
616
  this.runtime = runtime;
335
617
  this.context = context;
336
- this.setupConsole();
337
- if (backend) this._backend = (0, deepagents.adaptBackendProtocol)(backend);
338
- this.injectVfs();
339
- if (tools && tools.length > 0) this.injectTools(tools);
618
+ this.consoleBuffer = new ConsoleBuffer(maxResultChars);
619
+ if (captureConsole) this.setupConsole();
620
+ if (tools !== void 0 && tools.length > 0) this.injectTools(tools);
621
+ if (skillsEnabled) this.installModuleLoader();
622
+ }
623
+ /**
624
+ * Load the skill into cache on first access and replay cached errors.
625
+ */
626
+ async ensureSkillLoaded(name) {
627
+ const cached = this.skillsLoaded.get(name);
628
+ if (cached !== void 0) return cached;
629
+ const cachedError = this.skillsFailed.get(name);
630
+ if (cachedError !== void 0) throw cachedError;
631
+ const ctx = this.skillsContext;
632
+ if (ctx === void 0) throw new Error(`Skill '${name}' referenced but skills are not configured for this session`);
633
+ const metadata = ctx.metadata.find((m) => m.name === name);
634
+ if (metadata === void 0) throw new Error(`Skill '${name}' referenced but not available on this agent`);
635
+ try {
636
+ const loaded = await loadSkill(metadata, ctx.backend);
637
+ this.skillsLoaded.set(name, loaded);
638
+ return loaded;
639
+ } catch (err) {
640
+ this.skillsFailed.set(name, err);
641
+ throw err;
642
+ }
643
+ }
644
+ async resolveSpecifier(specifier) {
645
+ const parsed = parseSkillSpecifier(specifier);
646
+ 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
+ }
653
+ if (parsed.rel === void 0) {
654
+ const source = loaded.files.get(loaded.entryRel);
655
+ if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': entrypoint '${loaded.entryRel}' missing from bundle`);
656
+ return source;
657
+ }
658
+ const source = loaded.files.get(parsed.rel);
659
+ if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': '${parsed.rel}' not found in bundle`);
660
+ return source;
661
+ }
662
+ /**
663
+ * Canonicalize an `import` specifier. Bare specifiers pass through;
664
+ * relative specifiers are resolved against the importing module's path.
665
+ * Traversal out of a skill's `@/skills/<name>/` namespace is rejected.
666
+ */
667
+ normalizeSpecifier(base, requested) {
668
+ if (!(requested.startsWith("./") || requested.startsWith("../"))) return requested;
669
+ const parsed = parseSkillSpecifier(base);
670
+ const resolved = posixJoin(parsed !== void 0 && parsed.rel === void 0 ? base : posixDirname(base), requested);
671
+ const skillPrefix = matchSkillPrefix(base);
672
+ if (skillPrefix === void 0) return resolved;
673
+ if (!resolved.startsWith(`${skillPrefix}/`)) return `__resolve_error__:${requested} escapes ${skillPrefix}`;
674
+ return resolved;
675
+ }
676
+ /**
677
+ * Wire the QuickJS module loader and normalizer on this session's runtime.
678
+ */
679
+ installModuleLoader() {
680
+ if (this.runtime === null) return;
681
+ this.runtime.setModuleLoader(async (specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
682
+ }
683
+ /**
684
+ * Initialise the per-eval PTC counter. Called at the top of every `eval()`.
685
+ */
686
+ resetPtcBudget() {
687
+ this.ptcCallsRemaining = this.maxPtcCalls === null ? null : this.maxPtcCalls;
688
+ }
689
+ /**
690
+ * Decrement the PTC call counter and throw if the budget is exhausted.
691
+ * `null` budget means unlimited — returns immediately without decrementing.
692
+ */
693
+ consumePtcBudget(functionName) {
694
+ if (this.ptcCallsRemaining === null) return;
695
+ if (this.ptcCallsRemaining > 0) {
696
+ this.ptcCallsRemaining--;
697
+ return;
698
+ }
699
+ const limit = this.maxPtcCalls ?? 0;
700
+ throw new PTCCallBudgetExceededError({
701
+ limit,
702
+ attempted: limit + 1,
703
+ functionName
704
+ });
340
705
  }
341
706
  /**
342
707
  * Get or create a session for the given id.
@@ -347,10 +712,7 @@ var ReplSession = class ReplSession {
347
712
  */
348
713
  static getOrCreate(id, options = {}) {
349
714
  const existing = ReplSession.sessions.get(id);
350
- if (existing) {
351
- if (options.backend) existing._backend = (0, deepagents.adaptBackendProtocol)(options.backend);
352
- return existing;
353
- }
715
+ if (existing) return existing;
354
716
  const session = new ReplSession(id, options);
355
717
  ReplSession.sessions.set(id, session);
356
718
  return session;
@@ -362,6 +724,31 @@ var ReplSession = class ReplSession {
362
724
  return ReplSession.sessions.get(id) ?? null;
363
725
  }
364
726
  /**
727
+ * Returns true if any session exists whose key equals `threadId` or starts
728
+ * with `threadId:`. Useful for tests that need to confirm a session was
729
+ * created without knowing the full `threadId:middlewareId` key.
730
+ */
731
+ static hasAnyForThread(threadId) {
732
+ const prefix = `${threadId}:`;
733
+ for (const key of ReplSession.sessions.keys()) if (key === threadId || key.startsWith(prefix)) return true;
734
+ return false;
735
+ }
736
+ /**
737
+ * Dispose and remove the session with the given key, if it exists.
738
+ */
739
+ static deleteSession(key) {
740
+ const session = ReplSession.sessions.get(key);
741
+ if (session) session.dispose();
742
+ }
743
+ /**
744
+ * Push the current skills metadata + backend into the session.
745
+ * Called by the middleware once per `eval` invocation, before eval runs.
746
+ * Pass `undefined` to clear the context (no skill imports will resolve).
747
+ */
748
+ setSkillsContext(ctx) {
749
+ this.skillsContext = ctx;
750
+ }
751
+ /**
365
752
  * Evaluate code in this session.
366
753
  *
367
754
  * Lazily starts the QuickJS runtime on the first call. Code is
@@ -374,88 +761,94 @@ var ReplSession = class ReplSession {
374
761
  await this.ensureStarted();
375
762
  const runtime = this.runtime;
376
763
  const context = this.context;
377
- this.logs.length = 0;
378
- if (timeoutMs >= 0) runtime.setInterruptHandler((0, quickjs_emscripten.shouldInterruptAfterDeadline)(Date.now() + timeoutMs));
379
- else runtime.setInterruptHandler(() => false);
380
- const transformed = transformForEval(code);
381
- const result = await context.evalCodeAsync(transformed);
382
- if (result.error) {
383
- const error = context.dump(result.error);
384
- result.error.dispose();
764
+ const drainLogs = () => {
765
+ const [raw, dropped] = this.consoleBuffer.drain();
385
766
  return {
386
- ok: false,
387
- error,
388
- logs: [...this.logs]
767
+ logs: raw.length > 0 ? raw.split("\n").filter((l) => l.length > 0) : [],
768
+ logsDroppedChars: dropped
389
769
  };
390
- }
391
- const promiseState = context.getPromiseState(result.value);
392
- if (promiseState.type === "fulfilled") {
393
- if (promiseState.notAPromise) {
394
- const value = context.dump(result.value);
395
- result.value.dispose();
770
+ };
771
+ this.resetPtcBudget();
772
+ try {
773
+ if (timeoutMs >= 0) runtime.setInterruptHandler((0, quickjs_emscripten.shouldInterruptAfterDeadline)(Date.now() + timeoutMs));
774
+ else runtime.setInterruptHandler(() => false);
775
+ const transformed = transformForEval(code);
776
+ const result = await context.evalCodeAsync(transformed);
777
+ if (result.error) {
778
+ const error = context.dump(result.error);
779
+ result.error.dispose();
396
780
  return {
397
- ok: true,
398
- value,
399
- logs: [...this.logs]
781
+ ok: false,
782
+ error,
783
+ ...drainLogs()
400
784
  };
401
785
  }
402
- const value = context.dump(promiseState.value);
403
- promiseState.value.dispose();
404
- result.value.dispose();
405
- return {
406
- ok: true,
407
- value,
408
- logs: [...this.logs]
409
- };
410
- }
411
- if (promiseState.type === "rejected") {
412
- const error = context.dump(promiseState.error);
413
- promiseState.error.dispose();
414
- result.value.dispose();
415
- return {
416
- ok: false,
417
- error,
418
- logs: [...this.logs]
419
- };
420
- }
421
- const noTimeout = timeoutMs < 0;
422
- const deadline = noTimeout ? Infinity : Date.now() + timeoutMs;
423
- while (noTimeout || Date.now() < deadline) {
424
- context.runtime.executePendingJobs();
425
- const state = context.getPromiseState(result.value);
426
- if (state.type === "fulfilled") {
427
- const value = context.dump(state.value);
428
- state.value.dispose();
786
+ const promiseState = context.getPromiseState(result.value);
787
+ if (promiseState.type === "fulfilled") {
788
+ if (promiseState.notAPromise) {
789
+ const value = context.dump(result.value);
790
+ result.value.dispose();
791
+ return {
792
+ ok: true,
793
+ value,
794
+ ...drainLogs()
795
+ };
796
+ }
797
+ const value = context.dump(promiseState.value);
798
+ promiseState.value.dispose();
429
799
  result.value.dispose();
430
800
  return {
431
801
  ok: true,
432
802
  value,
433
- logs: [...this.logs]
803
+ ...drainLogs()
434
804
  };
435
805
  }
436
- if (state.type === "rejected") {
437
- const error = context.dump(state.error);
438
- state.error.dispose();
806
+ if (promiseState.type === "rejected") {
807
+ const error = context.dump(promiseState.error);
808
+ promiseState.error.dispose();
439
809
  result.value.dispose();
440
810
  return {
441
811
  ok: false,
442
812
  error,
443
- logs: [...this.logs]
813
+ ...drainLogs()
444
814
  };
445
815
  }
446
- await new Promise((r) => setTimeout(r, 1));
816
+ const noTimeout = timeoutMs < 0;
817
+ const deadline = noTimeout ? Infinity : Date.now() + timeoutMs;
818
+ while (noTimeout || Date.now() < deadline) {
819
+ context.runtime.executePendingJobs();
820
+ const state = context.getPromiseState(result.value);
821
+ if (state.type === "fulfilled") {
822
+ const value = context.dump(state.value);
823
+ state.value.dispose();
824
+ result.value.dispose();
825
+ return {
826
+ ok: true,
827
+ value,
828
+ ...drainLogs()
829
+ };
830
+ }
831
+ if (state.type === "rejected") {
832
+ const error = context.dump(state.error);
833
+ state.error.dispose();
834
+ result.value.dispose();
835
+ return {
836
+ ok: false,
837
+ error,
838
+ ...drainLogs()
839
+ };
840
+ }
841
+ await new Promise((r) => setTimeout(r, 1));
842
+ }
843
+ result.value.dispose();
844
+ return {
845
+ ok: false,
846
+ error: { message: "Promise timed out — execution interrupted" },
847
+ ...drainLogs()
848
+ };
849
+ } finally {
850
+ this.ptcCallsRemaining = null;
447
851
  }
448
- result.value.dispose();
449
- return {
450
- ok: false,
451
- error: { message: "Promise timed out — execution interrupted" },
452
- logs: [...this.logs]
453
- };
454
- }
455
- async flushWrites(backend) {
456
- const adapted = (0, deepagents.adaptBackendProtocol)(backend);
457
- const writes = this.pendingWrites.splice(0);
458
- for (const { path, content } of writes) await adapted.write(path, content);
459
852
  }
460
853
  dispose() {
461
854
  try {
@@ -484,7 +877,6 @@ var ReplSession = class ReplSession {
484
877
  }
485
878
  setupConsole() {
486
879
  const context = this.context;
487
- const logs = this.logs;
488
880
  const consoleHandle = context.newObject();
489
881
  for (const method of [
490
882
  "log",
@@ -495,7 +887,8 @@ var ReplSession = class ReplSession {
495
887
  ]) {
496
888
  const fnHandle = context.newFunction(method, (...args) => {
497
889
  const formatted = args.map((a) => context.dump(a)).map((a) => typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)).join(" ");
498
- logs.push(method === "log" || method === "info" || method === "debug" ? formatted : `[${method}] ${formatted}`);
890
+ const line = method === "log" || method === "info" || method === "debug" ? formatted : `[${method}] ${formatted}`;
891
+ this.consoleBuffer.append(line + "\n");
499
892
  });
500
893
  context.setProp(consoleHandle, method, fnHandle);
501
894
  fnHandle.dispose();
@@ -503,67 +896,6 @@ var ReplSession = class ReplSession {
503
896
  context.setProp(context.global, "console", consoleHandle);
504
897
  consoleHandle.dispose();
505
898
  }
506
- injectVfs() {
507
- const context = this.context;
508
- const getBackend = () => this._backend;
509
- const { pendingWrites } = this;
510
- const readFileHandle = context.newFunction("readFile", (pathHandle) => {
511
- const backend = getBackend();
512
- if (!backend) {
513
- const promise = context.newPromise();
514
- const err = context.newError("Backend not available");
515
- promise.reject(err);
516
- err.dispose();
517
- promise.settled.then(context.runtime.executePendingJobs);
518
- return promise.handle;
519
- }
520
- const path = context.getString(pathHandle);
521
- const promise = context.newPromise();
522
- (async () => {
523
- try {
524
- const result = await backend.readRaw(path);
525
- if (result.error || !result.data) {
526
- const err = context.newError(`ENOENT: no such file or directory '${path}'.`);
527
- promise.reject(err);
528
- err.dispose();
529
- } else {
530
- const content = Array.isArray(result.data.content) ? result.data.content.join("\n") : typeof result.data.content === "string" ? result.data.content : null;
531
- if (content === null) {
532
- const err = context.newError(`Cannot read binary file '${path}' as text.`);
533
- promise.reject(err);
534
- err.dispose();
535
- return;
536
- }
537
- const val = context.newString(content);
538
- promise.resolve(val);
539
- val.dispose();
540
- }
541
- } catch {
542
- const err = context.newError(`ENOENT: no such file or directory '${path}'.`);
543
- promise.reject(err);
544
- err.dispose();
545
- }
546
- promise.settled.then(context.runtime.executePendingJobs);
547
- })();
548
- return promise.handle;
549
- });
550
- context.setProp(context.global, "readFile", readFileHandle);
551
- readFileHandle.dispose();
552
- const writeFileHandle = context.newFunction("writeFile", (pathHandle, contentHandle) => {
553
- const path = context.getString(pathHandle);
554
- const content = context.getString(contentHandle);
555
- const promise = context.newPromise();
556
- pendingWrites.push({
557
- path,
558
- content
559
- });
560
- promise.resolve(context.undefined);
561
- promise.settled.then(context.runtime.executePendingJobs);
562
- return promise.handle;
563
- });
564
- context.setProp(context.global, "writeFile", writeFileHandle);
565
- writeFileHandle.dispose();
566
- }
567
899
  injectTools(tools) {
568
900
  const context = this.context;
569
901
  const toolsNs = context.newObject();
@@ -574,6 +906,7 @@ var ReplSession = class ReplSession {
574
906
  const promise = context.newPromise();
575
907
  (async () => {
576
908
  try {
909
+ this.consumePtcBudget(camelName);
577
910
  const rawInput = typeof input === "object" && input !== null ? input : {};
578
911
  const result = await t.invoke(rawInput);
579
912
  const val = context.newString(typeof result === "string" ? result : JSON.stringify(result));
@@ -599,74 +932,26 @@ var ReplSession = class ReplSession {
599
932
  //#endregion
600
933
  //#region src/middleware.ts
601
934
  /**
602
- * QuickJS REPL middleware for deepagents.
935
+ * REPL middleware for deepagents.
603
936
  *
604
- * Provides a `js_eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
937
+ * Provides an `eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
605
938
  * interpreter. Supports:
606
939
  * - Persistent state across evaluations (true REPL)
607
- * - VFS integration via readFile/writeFile
608
- * - Programmatic tool calling (PTC)
609
- */
610
- /**
611
- * Backend-provided tools excluded from PTC by default.
612
- * These are redundant inside the REPL since VFS helpers (readFile/writeFile)
613
- * already cover file I/O against the agent's in-memory working set.
614
- */
615
- const DEFAULT_PTC_EXCLUDED_TOOLS = [
616
- "ls",
617
- "read_file",
618
- "write_file",
619
- "edit_file",
620
- "glob",
621
- "grep",
622
- "execute"
623
- ];
624
- const REPL_SYSTEM_PROMPT = dedent.default`
625
- ## TypeScript/JavaScript REPL (\`js_eval\`)
626
-
627
- You have access to a sandboxed TypeScript/JavaScript REPL running in an isolated interpreter.
628
- TypeScript syntax (type annotations, interfaces, generics, \`as\` casts) is supported and stripped at evaluation time.
629
- Variables, functions, and closures persist across calls within the same session.
630
-
631
- ### Hard rules
632
-
633
- - **No network, no filesystem** — only the helpers below. Do not attempt \`fetch\`, \`require\`, or \`import\`.
634
- - **Cite your sources** — when reporting values from files, include the path and key/index so the user can verify.
635
- - **Use console.log()** for output — it is captured and returned. \`console.warn()\` and \`console.error()\` are also available.
636
- - **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.
637
-
638
- ### First-time usage
639
-
640
- \`\`\`typescript
641
- // Read a file from the agent's virtual filesystem
642
- const raw: string = await readFile("/data.json");
643
- const data = JSON.parse(raw) as { n: number };
644
- console.log(data);
645
-
646
- // Write results back
647
- await writeFile("/output.txt", JSON.stringify({ result: data.n }));
648
- \`\`\`
649
-
650
- ### API Reference — built-in globals
651
-
652
- \`\`\`typescript
653
- /**
654
- * Read a file from the agent's virtual filesystem. Throws if the file does not exist.
655
- */
656
- async readFile(path: string): Promise<string>
657
-
658
- /**
659
- * Write a file to the agent's virtual filesystem.
660
- */
661
- async writeFile(path: string, content: string): Promise<void>
662
- \`\`\`
663
-
664
- ### Limitations
940
+ * - Programmatic tool calling (PTC) — expose agent or custom tools inside the REPL
941
+ */
942
+ const DEFAULT_TOOL_NAME = "eval";
943
+ function renderReplSystemPrompt(opts) {
944
+ return dedent.default`
945
+ ### Interpreter
665
946
 
666
- - ES2023+ syntax with TypeScript support. No Node.js APIs, no \`require\`, no \`import\`.
667
- - Output is truncated beyond a fixed character limit be selective about what you log.
668
- - Execution timeout per call (default 30 s).
669
- `;
947
+ An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
948
+ - State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
949
+ - Top-level \`await\` works; Promises resolve before the call returns.
950
+ - Sandboxed: no filesystem, no stdlib, no network, no real clock, no \`fetch\`, no \`require\`.
951
+ - Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
952
+ - \`console.log\` output is captured and returned alongside the result.
953
+ `;
954
+ }
670
955
  /**
671
956
  * Generate the PTC API Reference section for the system prompt.
672
957
  */
@@ -703,81 +988,122 @@ async function generatePtcPrompt(tools) {
703
988
  `;
704
989
  }
705
990
  /**
706
- * Create the QuickJS REPL middleware.
991
+ * Resolves a mixed list of tool names and tool instances into a flat list of
992
+ * StructuredToolInterface objects. Strings are looked up by name in agentTools;
993
+ * instances are included directly without requiring agent registration. Strings
994
+ * that don't match any agent tool are silently omitted.
707
995
  */
708
- function createQuickJSMiddleware(options = {}) {
709
- const { backend = (runtime) => new deepagents.StateBackend(runtime), ptc = false, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null } = options;
710
- const usePtc = ptc !== false;
711
- const baseSystemPrompt = customSystemPrompt || REPL_SYSTEM_PROMPT;
996
+ function resolveToolList(items, agentTools) {
997
+ const agentByName = new Map(agentTools.map((t) => [t.name, t]));
998
+ return items.flatMap((item) => {
999
+ if (typeof item === "string") {
1000
+ const found = agentByName.get(item);
1001
+ return found ? [found] : [];
1002
+ }
1003
+ return [item];
1004
+ });
1005
+ }
1006
+ /**
1007
+ * Pull `skillsMetadata` from the task input, resolve the backend, and push
1008
+ * both into the session. Short-circuits with a `SkillNotAvailable` error if
1009
+ * the source references skills the agent doesn't have.
1010
+ */
1011
+ async function prepareSkillsForEval(session, skillsBackend, code) {
1012
+ const taskInput = (0, _langchain_langgraph.getCurrentTaskInput)();
1013
+ const metadata = taskInput?.skillsMetadata ?? [];
1014
+ const referenced = scanSkillReferences(code);
1015
+ if (referenced.size > 0) {
1016
+ const known = new Set(metadata.map((m) => m.name));
1017
+ const missing = [];
1018
+ for (const name of referenced) if (!known.has(name)) missing.push(name);
1019
+ if (missing.length > 0) {
1020
+ session.setSkillsContext(void 0);
1021
+ return formatSkillNotAvailable(missing);
1022
+ }
1023
+ }
1024
+ const resolved = await (0, deepagents.resolveBackend)(skillsBackend, { state: taskInput });
1025
+ session.setSkillsContext({
1026
+ metadata,
1027
+ backend: resolved
1028
+ });
1029
+ }
1030
+ /**
1031
+ * Create the REPL middleware.
1032
+ */
1033
+ function createREPLMiddleware(options = {}) {
1034
+ 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;
1035
+ if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
1036
+ const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
1037
+ toolName,
1038
+ timeout: executionTimeoutMs / 1e3,
1039
+ memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024))
1040
+ });
1041
+ const middlewareId = crypto.randomUUID();
712
1042
  let cachedPtcPrompt = null;
713
1043
  let ptcTools = [];
714
1044
  function filterToolsForPtc(allTools) {
715
- if (ptc === false) return [];
716
- const candidates = allTools.filter((t) => t.name !== "js_eval");
717
- if (ptc === true) {
718
- const excluded = new Set(DEFAULT_PTC_EXCLUDED_TOOLS);
719
- return candidates.filter((t) => !excluded.has(t.name));
720
- }
721
- if (Array.isArray(ptc)) {
722
- const included = new Set(ptc);
723
- return candidates.filter((t) => included.has(t.name));
724
- }
725
- if ("include" in ptc) {
726
- const included = new Set(ptc.include);
727
- return candidates.filter((t) => included.has(t.name));
728
- }
729
- if ("exclude" in ptc) {
730
- const excluded = new Set([...DEFAULT_PTC_EXCLUDED_TOOLS, ...ptc.exclude]);
731
- return candidates.filter((t) => !excluded.has(t.name));
732
- }
733
- return [];
1045
+ if (!ptc) return [];
1046
+ return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
734
1047
  }
735
1048
  return (0, langchain.createMiddleware)({
736
- name: "QuickJSMiddleware",
1049
+ name: "REPLMiddleware",
737
1050
  tools: [(0, langchain.tool)(async (input, config) => {
738
- const threadId = config.configurable?.thread_id || "__default__";
739
- const resolvedBackend = await (0, deepagents.resolveBackend)(backend, {
740
- ...config,
741
- state: (0, _langchain_langgraph.getCurrentTaskInput)(config) || {}
742
- });
743
- const session = ReplSession.getOrCreate(threadId, {
1051
+ const sessionKey = `${config.configurable?.thread_id || "__default__"}:${middlewareId}`;
1052
+ const session = ReplSession.getOrCreate(sessionKey, {
744
1053
  memoryLimitBytes,
745
1054
  maxStackSizeBytes,
746
- backend: resolvedBackend,
747
- tools: ptcTools
1055
+ maxPtcCalls,
1056
+ tools: ptcTools,
1057
+ skillsEnabled: skillsBackend !== void 0,
1058
+ maxResultChars,
1059
+ captureConsole
748
1060
  });
749
- const result = await session.eval(input.code, executionTimeoutMs);
750
- await session.flushWrites(resolvedBackend);
751
- return formatReplResult(result);
1061
+ if (skillsBackend !== void 0) {
1062
+ const setupError = await prepareSkillsForEval(session, skillsBackend, input.code);
1063
+ if (setupError !== void 0) return setupError;
1064
+ }
1065
+ return formatReplResult(await session.eval(input.code, executionTimeoutMs));
752
1066
  }, {
753
- name: "js_eval",
1067
+ name: toolName,
754
1068
  description: dedent.default`
755
1069
  Evaluate TypeScript/JavaScript code in a sandboxed REPL. State persists across calls.
756
- Use readFile(path) and writeFile(path, content) for file access.
757
1070
  Use console.log() for output. Returns the result of the last expression.
1071
+ If file or other tools are available, call them via the tools namespace: await tools.readFile({ path }).
1072
+ If skills are configured, dynamically import them: await import("@/skills/<name>").
758
1073
  `,
1074
+ metadata: { ls_code_input_language: "javascript" },
759
1075
  schema: zod_v4.z.object({ code: zod_v4.z.string().describe("TypeScript/JavaScript code to evaluate in the sandboxed REPL") })
760
1076
  })],
761
1077
  wrapModelCall: async (request, handler) => {
762
- const agentTools = request.tools || [];
763
- ptcTools = usePtc ? filterToolsForPtc(agentTools) : [];
1078
+ ptcTools = filterToolsForPtc(request.tools || []);
764
1079
  if (ptcTools.length > 0 && !cachedPtcPrompt) cachedPtcPrompt = await generatePtcPrompt(ptcTools);
765
1080
  const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(cachedPtcPrompt || "");
766
1081
  return handler({
767
1082
  ...request,
768
1083
  systemMessage
769
1084
  });
1085
+ },
1086
+ afterAgent: async (_state, runtime) => {
1087
+ const sessionKey = `${runtime.configurable?.thread_id ?? "__default__"}:${middlewareId}`;
1088
+ ReplSession.deleteSession(sessionKey);
770
1089
  }
771
1090
  });
772
1091
  }
773
1092
  //#endregion
774
1093
  exports.DEFAULT_EXECUTION_TIMEOUT = DEFAULT_EXECUTION_TIMEOUT;
1094
+ exports.DEFAULT_MAX_PTC_CALLS = DEFAULT_MAX_PTC_CALLS;
775
1095
  exports.DEFAULT_MAX_STACK_SIZE = DEFAULT_MAX_STACK_SIZE;
776
1096
  exports.DEFAULT_MEMORY_LIMIT = DEFAULT_MEMORY_LIMIT;
777
- exports.DEFAULT_PTC_EXCLUDED_TOOLS = DEFAULT_PTC_EXCLUDED_TOOLS;
1097
+ exports.MAX_SKILL_BUNDLE_BYTES = MAX_SKILL_BUNDLE_BYTES;
1098
+ exports.PTCCallBudgetExceededError = PTCCallBudgetExceededError;
778
1099
  exports.ReplSession = ReplSession;
779
- exports.createQuickJSMiddleware = createQuickJSMiddleware;
1100
+ exports.SKILL_MODULE_EXTENSIONS = SKILL_MODULE_EXTENSIONS;
1101
+ exports.createREPLMiddleware = createREPLMiddleware;
780
1102
  exports.formatReplResult = formatReplResult;
1103
+ exports.formatSkillNotAvailable = formatSkillNotAvailable;
1104
+ exports.loadSkill = loadSkill;
1105
+ exports.scanSkillReferences = scanSkillReferences;
1106
+ exports.stripTypeSyntax = stripTypeSyntax;
781
1107
  exports.toCamelCase = toCamelCase;
782
1108
  exports.transformForEval = transformForEval;
783
1109