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