@langchain/quickjs 1.0.0-alpha.0 → 1.0.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,16 +1,35 @@
1
1
  import { createMiddleware, tool } from "langchain";
2
2
  import { z } from "zod/v4";
3
- import { StateBackend, adaptBackendProtocol } from "deepagents";
3
+ import { SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY } from "deepagents";
4
4
  import dedent from "dedent";
5
5
  import { shouldInterruptAfterDeadline } from "quickjs-emscripten";
6
6
  import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core";
7
7
  import { compile } from "json-schema-to-typescript";
8
8
  import { toJsonSchema } from "@langchain/core/utils/json_schema";
9
+ import { isCommand } from "@langchain/langgraph";
10
+ import { BaseMessage } from "@langchain/core/messages";
9
11
  import { Parser } from "acorn";
10
12
  import { tsPlugin } from "@sveltejs/acorn-typescript";
11
13
  import { walk } from "estree-walker";
12
14
  import MagicString from "magic-string";
13
- import { getCurrentTaskInput } from "@langchain/langgraph";
15
+ import PQueue from "p-queue";
16
+ //#region src/errors.ts
17
+ /**
18
+ * Thrown when a single eval exhausts its configured PTC call budget.
19
+ */
20
+ var PTCCallBudgetExceededError = class extends Error {
21
+ limit;
22
+ attempted;
23
+ functionName;
24
+ constructor(options) {
25
+ super(`PTC call budget exceeded (limit=${options.limit}, attempted=${options.attempted}, function=${options.functionName})`);
26
+ this.name = "PTCCallBudgetExceededError";
27
+ this.limit = options.limit;
28
+ this.attempted = options.attempted;
29
+ this.functionName = options.functionName;
30
+ }
31
+ };
32
+ //#endregion
14
33
  //#region src/utils.ts
15
34
  /**
16
35
  * Convert a snake_case or kebab-case string to camelCase.
@@ -23,7 +42,11 @@ function toCamelCase(name) {
23
42
  */
24
43
  function formatReplResult(result) {
25
44
  const parts = [];
26
- if (result.logs.length > 0) parts.push(result.logs.join("\n"));
45
+ if (result.logs.length > 0) {
46
+ let logsText = result.logs.join("\n");
47
+ if (result.logsDroppedChars > 0) logsText += `\n[truncated ${result.logsDroppedChars} chars]`;
48
+ parts.push(logsText);
49
+ }
27
50
  if (result.ok) {
28
51
  if (result.value !== void 0) {
29
52
  const formatted = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
@@ -74,6 +97,59 @@ async function toolToTypeSignature(name, description, jsonSchema) {
74
97
  `;
75
98
  }
76
99
  //#endregion
100
+ //#region src/coerce.ts
101
+ /**
102
+ * Coercion of tool / subagent return values for the QuickJS bridge.
103
+ *
104
+ * The deepagents `task` tool resolves to a LangGraph `Command` whose payload
105
+ * carries the subagent's final message(s) under `update.messages`; some tools
106
+ * return a `ToolMessage` or a list of messages. The interpreter bridges need
107
+ * the underlying output, not the envelope, so this unwraps those shapes to the
108
+ * content the model actually cares about.
109
+ */
110
+ /**
111
+ * Return the trailing message content from a `Command`'s `update.messages`,
112
+ * scanning from the end for the last message that actually has content. Returns
113
+ * the command unchanged when it has no message-shaped payload.
114
+ */
115
+ function extractCommandContent(command) {
116
+ const update = command.update;
117
+ const messages = update !== null && typeof update === "object" ? update.messages : void 0;
118
+ if (Array.isArray(messages)) for (let i = messages.length - 1; i >= 0; i--) {
119
+ const message = messages[i];
120
+ if (BaseMessage.isInstance(message) && message.content != null) return message.content;
121
+ }
122
+ return command;
123
+ }
124
+ /**
125
+ * Unwrap a LangChain `Command` / `ToolMessage` / message-list envelope to the
126
+ * underlying content. Non-envelope values (strings, content-block arrays, plain
127
+ * objects) are returned unchanged.
128
+ *
129
+ * @param value The raw value returned by a tool or subagent dispatch.
130
+ * @returns The unwrapped content, or `value` itself when it isn't an envelope.
131
+ */
132
+ function unwrapToolEnvelope(value) {
133
+ if (typeof value === "string") return value;
134
+ if (isCommand(value)) {
135
+ const inner = extractCommandContent(value);
136
+ return inner === value ? value : unwrapToolEnvelope(inner);
137
+ }
138
+ if (BaseMessage.isInstance(value)) return unwrapToolEnvelope(value.content);
139
+ if (Array.isArray(value)) {
140
+ for (let i = value.length - 1; i >= 0; i--) {
141
+ const entry = value[i];
142
+ if (BaseMessage.isInstance(entry)) return unwrapToolEnvelope(entry.content);
143
+ if (isCommand(entry)) {
144
+ const inner = extractCommandContent(entry);
145
+ if (inner !== entry) return unwrapToolEnvelope(inner);
146
+ }
147
+ }
148
+ return value;
149
+ }
150
+ return value;
151
+ }
152
+ //#endregion
77
153
  //#region src/transform.ts
78
154
  /**
79
155
  * AST-based code transform pipeline for the REPL.
@@ -149,7 +225,11 @@ function transformForEval(code) {
149
225
  }
150
226
  function isTSOnlyNode(node) {
151
227
  const t = node.type;
152
- return t === "TSTypeAliasDeclaration" || t === "TSInterfaceDeclaration" || t === "TSEnumDeclaration" || t === "TSModuleDeclaration" || t === "TSDeclareFunction" || t.startsWith("TS");
228
+ if (t === "TSTypeAliasDeclaration" || t === "TSInterfaceDeclaration" || t === "TSEnumDeclaration" || t === "TSModuleDeclaration" || t === "TSDeclareFunction" || t.startsWith("TS")) return true;
229
+ if (t === "VariableDeclaration" && node.declare === true) return true;
230
+ if (t === "ImportDeclaration" && node.importKind === "type") return true;
231
+ if (t === "ExportNamedDeclaration" && node.exportKind === "type") return true;
232
+ return false;
153
233
  }
154
234
  /**
155
235
  * Rewrite a top-level VariableDeclaration to globalThis assignments.
@@ -201,7 +281,11 @@ function stripTypeAnnotations(s, node) {
201
281
  } });
202
282
  }
203
283
  function stripTypeAnnotationFromNode(s, n, offset = 0) {
204
- if (n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - offset, n.typeAnnotation.end - offset);
284
+ if (n.optional === true && n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - 1 - offset, n.typeAnnotation.end - offset);
285
+ else if (n.optional === true && !n.typeAnnotation) {
286
+ const nameEnd = n.type === "Identifier" && typeof n.name === "string" ? n.start + n.name.length : null;
287
+ if (nameEnd != null) s.remove(nameEnd - offset, nameEnd + 1 - offset);
288
+ } else if (n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - offset, n.typeAnnotation.end - offset);
205
289
  if (n.returnType && n.returnType.start != null) s.remove(n.returnType.start - offset, n.returnType.end - offset);
206
290
  if (n.typeParameters && n.typeParameters.start != null) s.remove(n.typeParameters.start - offset, n.typeParameters.end - offset);
207
291
  if (n.typeArguments && n.typeArguments.start != null) s.remove(n.typeArguments.start - offset, n.typeArguments.end - offset);
@@ -234,6 +318,70 @@ function findLastNonEmptyNode(nodes, s) {
234
318
  function isExpression(node) {
235
319
  return node.type === "ExpressionStatement";
236
320
  }
321
+ /**
322
+ * Strip TypeScript type syntax from an ES-module source so QuickJS can
323
+ * evaluate it as a standard JS module.
324
+ *
325
+ * Unlike `transformForEval`, this keeps `import`/`export` declarations,
326
+ * does not hoist to `globalThis`, and does not wrap in an IIFE.
327
+ * On parse failure the original source is returned unchanged.
328
+ */
329
+ function stripTypeSyntax(code) {
330
+ let ast;
331
+ try {
332
+ ast = TSParser.parse(code, {
333
+ ecmaVersion: "latest",
334
+ sourceType: "module",
335
+ locations: true
336
+ });
337
+ } catch {
338
+ return code;
339
+ }
340
+ const magicString = new MagicString(code);
341
+ const program = ast;
342
+ for (const node of program.body) {
343
+ if (isTSOnlyNode(node)) {
344
+ magicString.remove(node.start, node.end);
345
+ continue;
346
+ }
347
+ walk(node, { enter(n) {
348
+ stripTypeAnnotationFromNode(magicString, n);
349
+ } });
350
+ }
351
+ return magicString.toString();
352
+ }
353
+ //#endregion
354
+ //#region src/eval-queue.ts
355
+ /**
356
+ * Serializes async operations on a shared WASM module.
357
+ *
358
+ * The quickjs-emscripten asyncify variant allows only one concurrent
359
+ * async call per module instance. This queue enforces that constraint
360
+ * by chaining operations into a promise queue — each caller waits for
361
+ * the previous one to finish before executing.
362
+ */
363
+ var AsyncEvalQueue = class {
364
+ tail = Promise.resolve();
365
+ /**
366
+ * Enqueue an async operation. The operation will not start until all
367
+ * previously enqueued operations have completed.
368
+ */
369
+ async enqueue(fn) {
370
+ let release;
371
+ const gate = new Promise((r) => {
372
+ release = r;
373
+ });
374
+ const prev = this.tail;
375
+ this.tail = gate;
376
+ return prev.then(async () => {
377
+ try {
378
+ return await fn();
379
+ } finally {
380
+ release();
381
+ }
382
+ });
383
+ }
384
+ };
237
385
  //#endregion
238
386
  //#region src/session.ts
239
387
  /**
@@ -253,65 +401,215 @@ function isExpression(node) {
253
401
  * It holds an `id` that keys into a static session map. The heavy QuickJS
254
402
  * runtime is lazily started on the first `.eval()` call, making the session
255
403
  * 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
404
  */
261
- const DEFAULT_MEMORY_LIMIT = 50 * 1024 * 1024;
405
+ const DEFAULT_MEMORY_LIMIT = 64 * 1024 * 1024;
262
406
  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");
407
+ const DEFAULT_EXECUTION_TIMEOUT = 5e3;
408
+ const DEFAULT_MAX_PTC_CALLS = 256;
409
+ const DEFAULT_MAX_RESULTS_CHARS = 4e3;
410
+ const LINE_NUMBER_RE = /^\s*\d+(?:\.\d+)?\t/;
411
+ const variantImport = import("@jitl/quickjs-ng-wasmfile-release-asyncify");
412
+ /**
413
+ * Process-global eval queue. Serializes all evalCodeAsync calls across
414
+ * sessions to enforce the asyncify one-at-a-time constraint.
415
+ */
416
+ const sharedEvalQueue = new AsyncEvalQueue();
417
+ /**
418
+ * Process-global WASM module shared by all sessions.
419
+ *
420
+ * Each session creates its own runtime and context on this module,
421
+ * providing full isolation for globals, heap, and stack. The module
422
+ * itself is stateless between runtimes — only the compiled WASM code
423
+ * and Emscripten infrastructure are shared.
424
+ *
425
+ * This is safe because:
426
+ * - The module loader is synchronous (preloaded skill cache), so
427
+ * imports don't cause asyncify suspensions.
428
+ * - Tool injection uses the promise-based pattern (newFunction +
429
+ * newPromise), not newAsyncifiedFunction, so tool calls don't
430
+ * cause asyncify suspensions.
431
+ * - The eval queue serializes evalCodeAsync calls to satisfy the
432
+ * one-concurrent-async-call-per-module constraint.
433
+ */
434
+ let sharedModulePromise;
435
+ function getSharedModule() {
436
+ if (!sharedModulePromise) sharedModulePromise = (async () => {
437
+ const variant = await variantImport;
268
438
  return newQuickJSAsyncWASMModuleFromVariant(variant.default ?? variant);
269
439
  })();
270
- return asyncModulePromise;
440
+ return sharedModulePromise;
271
441
  }
272
442
  /**
443
+ * Unwrap a PTC tool result to a plain string for use inside QuickJS.
444
+ *
445
+ * Tool results may arrive as a raw string, or as an array of LangChain
446
+ * content blocks (`{ type: "text", text: "..." }`). Blocks are joined
447
+ * with newlines; non-text block types are silently skipped. Anything
448
+ * else (objects, nulls) is JSON-serialised as a fallback.
449
+ *
450
+ * @param result - Raw return value from `tool.invoke()`.
451
+ * @returns Plain string representation of the tool output.
452
+ */
453
+ function extractToolText(result) {
454
+ result = unwrapToolEnvelope(result);
455
+ if (typeof result === "string") return result;
456
+ if (Array.isArray(result)) {
457
+ const texts = [];
458
+ for (const block of result) if (typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string") texts.push(block.text);
459
+ if (texts.length > 0) return texts.join("\n");
460
+ }
461
+ return JSON.stringify(result);
462
+ }
463
+ /**
464
+ * Remove the `cat -n` line-number prefix from every line of a string.
465
+ *
466
+ * The filesystem backend formats file content with line numbers in the
467
+ * form `" N\t"` so human readers can navigate by line. That prefix
468
+ * is useful for the agent but noise for QuickJS code that parses the
469
+ * text programmatically (e.g. swarm reading `/context.txt`).
470
+ *
471
+ * The function is conservative: if any non-empty line lacks the prefix,
472
+ * the text is returned unchanged so nothing is silently corrupted.
473
+ *
474
+ * @param text - Raw file content, possibly line-number prefixed.
475
+ * @returns Content with line-number prefixes stripped, or the original
476
+ * text if it doesn't match the expected format throughout.
477
+ */
478
+ function stripLineNumbers(text) {
479
+ const lines = text.split("\n");
480
+ if (lines.length === 0) return text;
481
+ if (!lines.every((l) => l === "" || LINE_NUMBER_RE.test(l))) return text;
482
+ return lines.map((l) => l.replace(LINE_NUMBER_RE, "")).join("\n");
483
+ }
484
+ /**
485
+ * Fixed-size character buffer for capturing console output from the QuickJS VM.
486
+ *
487
+ * Lines are accumulated up to `maxChars`. Once the cap is reached, excess
488
+ * characters are counted as dropped rather than silently discarded without
489
+ * attribution, so callers can surface a truncation notice to the user.
490
+ */
491
+ var ConsoleBuffer = class {
492
+ maxChars;
493
+ buffer = "";
494
+ droppedChars = 0;
495
+ constructor(maxChars) {
496
+ this.maxChars = Math.max(maxChars, 0);
497
+ }
498
+ /**
499
+ * Append `line` to the buffer.
500
+ *
501
+ * If the buffer is already full the entire line is counted as dropped.
502
+ * If `line` partially fits, the fitting prefix is stored and the remainder
503
+ * is counted as dropped.
504
+ */
505
+ append(line) {
506
+ const remaining = this.maxChars - this.buffer.length;
507
+ if (remaining <= 0) {
508
+ this.droppedChars += line.length;
509
+ return;
510
+ }
511
+ if (line.length <= remaining) this.buffer += line;
512
+ else {
513
+ this.buffer += line.slice(0, remaining);
514
+ this.droppedChars += line.length - remaining;
515
+ }
516
+ }
517
+ /**
518
+ * Return the buffered output and dropped-character count as `[buffered,
519
+ * droppedChars]`, then reset both to zero.
520
+ */
521
+ drain() {
522
+ const out = this.buffer;
523
+ const dropped = this.droppedChars;
524
+ this.buffer = "";
525
+ this.droppedChars = 0;
526
+ return [out, dropped];
527
+ }
528
+ };
529
+ /**
273
530
  * Sandboxed JavaScript REPL session backed by QuickJS WASM.
274
531
  *
275
532
  * Serializable — holds an `id` that keys into a static session map.
276
533
  * The QuickJS runtime is lazily started on the first `.eval()` call
277
534
  * and reconnected if a session with the same id already exists.
278
535
  * 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
536
  */
283
537
  var ReplSession = class ReplSession {
284
538
  static sessions = /* @__PURE__ */ new Map();
285
539
  id;
286
- pendingWrites = [];
287
540
  runtime = null;
288
541
  context = null;
289
- logs = [];
290
- _options;
291
- _backend = null;
542
+ consoleBuffer = new ConsoleBuffer(DEFAULT_MAX_RESULTS_CHARS);
543
+ options;
544
+ maxPtcCalls;
545
+ ptcCallsRemaining = null;
546
+ subagentQueue = null;
547
+ bridgeDispatchRef = null;
548
+ /** Allowed keys in the subagent input object. */
549
+ static SUBAGENT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
550
+ "description",
551
+ "subagentType",
552
+ "responseSchema"
553
+ ]);
554
+ /**
555
+ * Reset the shared WASM module. Forces the next session to instantiate
556
+ * a fresh module. Only needed in tests where module state must be
557
+ * isolated between test files.
558
+ *
559
+ * @internal
560
+ */
561
+ static resetSharedModule() {
562
+ sharedModulePromise = void 0;
563
+ }
292
564
  constructor(id, options = {}) {
293
565
  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;
566
+ this.options = options;
567
+ this.maxPtcCalls = options.maxPtcCalls !== void 0 ? options.maxPtcCalls : 256;
301
568
  }
302
569
  async ensureStarted() {
303
570
  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();
571
+ const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, captureConsole = true } = this.options;
572
+ const runtime = (await getSharedModule()).newRuntime();
306
573
  runtime.setMemoryLimit(memoryLimitBytes);
307
574
  runtime.setMaxStackSize(maxStackSizeBytes);
308
575
  const context = runtime.newContext();
309
576
  this.runtime = runtime;
310
577
  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);
578
+ this.consoleBuffer = new ConsoleBuffer(maxResultChars);
579
+ if (captureConsole) this.setupConsole();
580
+ if (tools !== void 0 && tools.length > 0) this.injectTools(tools);
581
+ const { subagentBridge } = this.options;
582
+ if (subagentBridge) {
583
+ this.subagentQueue = new PQueue({ concurrency: subagentBridge.maxConcurrency });
584
+ this.injectSubagentBridge(subagentBridge.dispatch);
585
+ }
586
+ const sessionId = this.options.sessionId ?? "default";
587
+ const sessionIdHandle = context.newString(sessionId);
588
+ context.setProp(context.global, "__sessionId__", sessionIdHandle);
589
+ sessionIdHandle.dispose();
590
+ }
591
+ /**
592
+ * Initialise the per-eval PTC counter. Called at the top of every `eval()`.
593
+ */
594
+ resetPtcBudget() {
595
+ this.ptcCallsRemaining = this.maxPtcCalls === null ? null : this.maxPtcCalls;
596
+ }
597
+ /**
598
+ * Decrement the PTC call counter and throw if the budget is exhausted.
599
+ * `null` budget means unlimited — returns immediately without decrementing.
600
+ */
601
+ consumePtcBudget(functionName) {
602
+ if (this.ptcCallsRemaining === null) return;
603
+ if (this.ptcCallsRemaining > 0) {
604
+ this.ptcCallsRemaining--;
605
+ return;
606
+ }
607
+ const limit = this.maxPtcCalls ?? 0;
608
+ throw new PTCCallBudgetExceededError({
609
+ limit,
610
+ attempted: limit + 1,
611
+ functionName
612
+ });
315
613
  }
316
614
  /**
317
615
  * Get or create a session for the given id.
@@ -322,10 +620,7 @@ var ReplSession = class ReplSession {
322
620
  */
323
621
  static getOrCreate(id, options = {}) {
324
622
  const existing = ReplSession.sessions.get(id);
325
- if (existing) {
326
- if (options.backend) existing._backend = adaptBackendProtocol(options.backend);
327
- return existing;
328
- }
623
+ if (existing) return existing;
329
624
  const session = new ReplSession(id, options);
330
625
  ReplSession.sessions.set(id, session);
331
626
  return session;
@@ -337,6 +632,23 @@ var ReplSession = class ReplSession {
337
632
  return ReplSession.sessions.get(id) ?? null;
338
633
  }
339
634
  /**
635
+ * Returns true if any session exists whose key equals `threadId` or starts
636
+ * with `threadId:`. Useful for tests that need to confirm a session was
637
+ * created without knowing the full `threadId:middlewareId` key.
638
+ */
639
+ static hasAnyForThread(threadId) {
640
+ const prefix = `${threadId}:`;
641
+ for (const key of ReplSession.sessions.keys()) if (key === threadId || key.startsWith(prefix)) return true;
642
+ return false;
643
+ }
644
+ /**
645
+ * Dispose and remove the session with the given key, if it exists.
646
+ */
647
+ static deleteSession(key) {
648
+ const session = ReplSession.sessions.get(key);
649
+ if (session) session.dispose();
650
+ }
651
+ /**
340
652
  * Evaluate code in this session.
341
653
  *
342
654
  * Lazily starts the QuickJS runtime on the first call. Code is
@@ -349,88 +661,94 @@ var ReplSession = class ReplSession {
349
661
  await this.ensureStarted();
350
662
  const runtime = this.runtime;
351
663
  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();
664
+ const drainLogs = () => {
665
+ const [raw, dropped] = this.consoleBuffer.drain();
360
666
  return {
361
- ok: false,
362
- error,
363
- logs: [...this.logs]
667
+ logs: raw.length > 0 ? raw.split("\n").filter((l) => l.length > 0) : [],
668
+ logsDroppedChars: dropped
364
669
  };
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();
670
+ };
671
+ this.resetPtcBudget();
672
+ try {
673
+ if (timeoutMs >= 0) runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + timeoutMs));
674
+ else runtime.setInterruptHandler(() => false);
675
+ const transformed = transformForEval(code);
676
+ const result = await sharedEvalQueue.enqueue(() => context.evalCodeAsync(transformed));
677
+ if (result.error) {
678
+ const error = context.dump(result.error);
679
+ result.error.dispose();
371
680
  return {
372
- ok: true,
373
- value,
374
- logs: [...this.logs]
681
+ ok: false,
682
+ error,
683
+ ...drainLogs()
375
684
  };
376
685
  }
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();
686
+ const promiseState = context.getPromiseState(result.value);
687
+ if (promiseState.type === "fulfilled") {
688
+ if (promiseState.notAPromise) {
689
+ const value = context.dump(result.value);
690
+ result.value.dispose();
691
+ return {
692
+ ok: true,
693
+ value,
694
+ ...drainLogs()
695
+ };
696
+ }
697
+ const value = context.dump(promiseState.value);
698
+ promiseState.value.dispose();
404
699
  result.value.dispose();
405
700
  return {
406
701
  ok: true,
407
702
  value,
408
- logs: [...this.logs]
703
+ ...drainLogs()
409
704
  };
410
705
  }
411
- if (state.type === "rejected") {
412
- const error = context.dump(state.error);
413
- state.error.dispose();
706
+ if (promiseState.type === "rejected") {
707
+ const error = context.dump(promiseState.error);
708
+ promiseState.error.dispose();
414
709
  result.value.dispose();
415
710
  return {
416
711
  ok: false,
417
712
  error,
418
- logs: [...this.logs]
713
+ ...drainLogs()
419
714
  };
420
715
  }
421
- await new Promise((r) => setTimeout(r, 1));
716
+ const noTimeout = timeoutMs < 0;
717
+ const deadline = noTimeout ? Infinity : Date.now() + timeoutMs;
718
+ while (noTimeout || Date.now() < deadline) {
719
+ context.runtime.executePendingJobs();
720
+ const state = context.getPromiseState(result.value);
721
+ if (state.type === "fulfilled") {
722
+ const value = context.dump(state.value);
723
+ state.value.dispose();
724
+ result.value.dispose();
725
+ return {
726
+ ok: true,
727
+ value,
728
+ ...drainLogs()
729
+ };
730
+ }
731
+ if (state.type === "rejected") {
732
+ const error = context.dump(state.error);
733
+ state.error.dispose();
734
+ result.value.dispose();
735
+ return {
736
+ ok: false,
737
+ error,
738
+ ...drainLogs()
739
+ };
740
+ }
741
+ await new Promise((r) => setTimeout(r, 1));
742
+ }
743
+ result.value.dispose();
744
+ return {
745
+ ok: false,
746
+ error: { message: "Promise timed out — execution interrupted" },
747
+ ...drainLogs()
748
+ };
749
+ } finally {
750
+ this.ptcCallsRemaining = null;
422
751
  }
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
752
  }
435
753
  dispose() {
436
754
  try {
@@ -459,7 +777,6 @@ var ReplSession = class ReplSession {
459
777
  }
460
778
  setupConsole() {
461
779
  const context = this.context;
462
- const logs = this.logs;
463
780
  const consoleHandle = context.newObject();
464
781
  for (const method of [
465
782
  "log",
@@ -470,7 +787,8 @@ var ReplSession = class ReplSession {
470
787
  ]) {
471
788
  const fnHandle = context.newFunction(method, (...args) => {
472
789
  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}`);
790
+ const line = method === "log" || method === "info" || method === "debug" ? formatted : `[${method}] ${formatted}`;
791
+ this.consoleBuffer.append(line + "\n");
474
792
  });
475
793
  context.setProp(consoleHandle, method, fnHandle);
476
794
  fnHandle.dispose();
@@ -478,67 +796,6 @@ var ReplSession = class ReplSession {
478
796
  context.setProp(context.global, "console", consoleHandle);
479
797
  consoleHandle.dispose();
480
798
  }
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
799
  injectTools(tools) {
543
800
  const context = this.context;
544
801
  const toolsNs = context.newObject();
@@ -549,9 +806,11 @@ var ReplSession = class ReplSession {
549
806
  const promise = context.newPromise();
550
807
  (async () => {
551
808
  try {
809
+ this.consumePtcBudget(camelName);
552
810
  const rawInput = typeof input === "object" && input !== null ? input : {};
553
- const result = await t.invoke(rawInput);
554
- const val = context.newString(typeof result === "string" ? result : JSON.stringify(result));
811
+ let text = extractToolText(await t.invoke(rawInput));
812
+ if (t.name === "read_file") text = stripLineNumbers(text);
813
+ const val = context.newString(text);
555
814
  promise.resolve(val);
556
815
  val.dispose();
557
816
  } catch (e) {
@@ -570,78 +829,363 @@ var ReplSession = class ReplSession {
570
829
  context.setProp(context.global, "tools", toolsNs);
571
830
  toolsNs.dispose();
572
831
  }
832
+ /**
833
+ * Install the `task` global on the QuickJS context.
834
+ *
835
+ * Registers the host function directly as `globalThis.task`,
836
+ * then freezes it via `evalCode`. Structured results (when
837
+ * responseSchema is provided) are marshaled into native QuickJS
838
+ * objects on the host side — no JS wrapper needed.
839
+ */
840
+ /**
841
+ * Replace the active bridge dispatch with a fresh one.
842
+ *
843
+ * Call this before each eval so the dispatch closure carries
844
+ * the current invocation's config (tracing callbacks, run ID, etc.)
845
+ * rather than the stale config from session creation.
846
+ */
847
+ updateBridgeDispatch(dispatch) {
848
+ if (this.bridgeDispatchRef) this.bridgeDispatchRef.current = dispatch;
849
+ }
850
+ injectSubagentBridge(dispatch) {
851
+ const context = this.context;
852
+ const queue = this.subagentQueue;
853
+ this.bridgeDispatchRef = { current: dispatch };
854
+ const ref = this.bridgeDispatchRef;
855
+ const hostFn = context.newFunction("task", (inputHandle) => {
856
+ const input = context.dump(inputHandle);
857
+ const promise = context.newPromise();
858
+ (async () => {
859
+ try {
860
+ if (input == null || typeof input !== "object" || Array.isArray(input)) throw new Error("task: expected an object argument");
861
+ const obj = { ...input };
862
+ if ("subagent_type" in obj) {
863
+ obj.subagentType ??= obj.subagent_type;
864
+ delete obj.subagent_type;
865
+ }
866
+ if ("response_schema" in obj) {
867
+ obj.responseSchema ??= obj.response_schema;
868
+ delete obj.response_schema;
869
+ }
870
+ const unknownKeys = Object.keys(obj).filter((k) => !ReplSession.SUBAGENT_ALLOWED_KEYS.has(k));
871
+ if (unknownKeys.length > 0) throw new Error(`task: unknown keys: ${unknownKeys.join(", ")}. Allowed: ${[...ReplSession.SUBAGENT_ALLOWED_KEYS].join(", ")}`);
872
+ const { description, subagentType, responseSchema } = obj;
873
+ if (typeof description !== "string" || description.length === 0) throw new Error("task: 'description' is required and must be a non-empty string");
874
+ if (typeof subagentType !== "string" || subagentType.length === 0) throw new Error("task: 'subagentType' is required and must be a non-empty string");
875
+ if (responseSchema !== void 0 && (responseSchema == null || typeof responseSchema !== "object" || Array.isArray(responseSchema))) throw new Error("task: 'responseSchema' must be a plain object (JSON Schema) when provided");
876
+ const result = await queue.add(() => ref.current({
877
+ description,
878
+ subagentType,
879
+ ...responseSchema !== void 0 && { responseSchema }
880
+ }));
881
+ if (typeof result === "string") {
882
+ const val = context.newString(result);
883
+ promise.resolve(val);
884
+ val.dispose();
885
+ } else {
886
+ const jsonResult = context.evalCode(`(${JSON.stringify(result)})`);
887
+ if (jsonResult.error) {
888
+ const errDump = context.dump(jsonResult.error);
889
+ jsonResult.error.dispose();
890
+ throw new Error(`task: failed to marshal structured response: ${JSON.stringify(errDump)}`);
891
+ }
892
+ promise.resolve(jsonResult.value);
893
+ jsonResult.value.dispose();
894
+ }
895
+ } catch (e) {
896
+ const msg = e != null && typeof e.message === "string" ? e.message : String(e);
897
+ const err = context.newError(msg);
898
+ promise.reject(err);
899
+ err.dispose();
900
+ }
901
+ promise.settled.then(context.runtime.executePendingJobs);
902
+ })();
903
+ return promise.handle;
904
+ });
905
+ context.setProp(context.global, "task", hostFn);
906
+ hostFn.dispose();
907
+ context.evalCode("Object.freeze(globalThis.task);Object.defineProperty(globalThis, 'task', { value: globalThis.task, writable: false, configurable: false,}); undefined");
908
+ }
573
909
  };
574
910
  //#endregion
911
+ //#region src/subagent-dispatch.ts
912
+ const SCHEMA_MAX_BYTES = 4096;
913
+ const SCHEMA_MAX_DEPTH = 5;
914
+ const SCHEMA_MAX_PROPERTIES = 32;
915
+ /**
916
+ * Validate that a response schema does not exceed size, depth, or
917
+ * property-count limits.
918
+ *
919
+ * @throws Error if any limit is exceeded.
920
+ */
921
+ function validateResponseSchema(schema) {
922
+ const serialized = JSON.stringify(schema);
923
+ if (serialized.length > SCHEMA_MAX_BYTES) throw new Error(`responseSchema exceeds ${SCHEMA_MAX_BYTES} byte limit (${serialized.length} bytes)`);
924
+ function check(node, depth, propCount) {
925
+ if (depth > SCHEMA_MAX_DEPTH) throw new Error(`responseSchema exceeds maximum nesting depth of ${SCHEMA_MAX_DEPTH}`);
926
+ const props = node.properties;
927
+ if (props != null && typeof props === "object" && !Array.isArray(props)) {
928
+ const propObj = props;
929
+ propCount.value += Object.keys(propObj).length;
930
+ if (propCount.value > SCHEMA_MAX_PROPERTIES) throw new Error(`responseSchema exceeds maximum of ${SCHEMA_MAX_PROPERTIES} properties`);
931
+ for (const value of Object.values(propObj)) if (value != null && typeof value === "object" && !Array.isArray(value)) check(value, depth + 1, propCount);
932
+ }
933
+ const items = node.items;
934
+ if (items != null && typeof items === "object" && !Array.isArray(items)) check(items, depth + 1, propCount);
935
+ }
936
+ check(schema, 0, { value: 0 });
937
+ }
938
+ //#endregion
575
939
  //#region src/middleware.ts
576
940
  /**
577
- * QuickJS REPL middleware for deepagents.
941
+ * Code Interpreter middleware for deepagents.
578
942
  *
579
- * Provides a `js_eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
943
+ * Provides an `eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
580
944
  * interpreter. Supports:
581
945
  * - Persistent state across evaluations (true REPL)
582
- * - VFS integration via readFile/writeFile
583
- * - Programmatic tool calling (PTC)
946
+ * - Programmatic tool calling (PTC) — expose agent or custom tools inside the REPL
584
947
  */
948
+ const DEFAULT_TOOL_NAME = "eval";
585
949
  /**
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.
950
+ * Render the subagent dispatch prompt section for the system message.
951
+ * Ported from the Python `_SUBAGENT_SYSTEM_PROMPT_TEMPLATE`.
589
952
  */
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
640
-
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
- `;
953
+ function renderSubagentPrompt(toolName) {
954
+ return dedent`
955
+
956
+ ### Dispatching Subagents with \`task\`
957
+
958
+ \`task\` is your primitive for running configured subagents from inside the
959
+ JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
960
+ write JavaScript that fans work out to subagents and assembles their results.
961
+ You handle the orchestration - fan-out, filtering, deduplication, multi-stage
962
+ flow, and synthesis - in plain JavaScript.
963
+
964
+ #### The primitive
965
+
966
+ \`\`\`javascript
967
+ await task({
968
+ description, // full autonomous task prompt
969
+ subagentType, // configured subagent name
970
+ responseSchema, // optional JSON Schema for structured output
971
+ }); // -> Promise<unknown>
972
+ \`\`\`
973
+
974
+ \`task\` runs a full agentic loop for the selected configured subagent. The
975
+ subagent can use whatever tools it was configured with, iterate, inspect
976
+ context, and return one final result. \`subagentType\` is required; use one of
977
+ the configured subagent names.
978
+
979
+ \`description\` is the only prompt the subagent receives for this dispatch. Make
980
+ it complete: the goal, the constraints, what to inspect, and the exact shape
981
+ or level of detail you expect back. Give context as locators file paths and
982
+ symbol names — not as pasted file contents. If you already read a file while
983
+ exploring, still pass its path and let the subagent read it; do not paste back
984
+ what you read. Each dispatch is stateless from the caller's perspective; you
985
+ cannot send follow-up messages to the same subagent run.
986
+
987
+ \`responseSchema\` is optional, but set it on any dispatch whose result feeds
988
+ later code. A deterministic, typed shape is what lets you compose the next
989
+ stage reliably — index it, sort it, compare fields, branch on it, merge it —
990
+ instead of parsing free-form text. This is what makes a whole workflow
991
+ composable as one script. When provided, the resolved value is already a typed
992
+ JavaScript value matching the schema; do not call \`JSON.parse\` unless the
993
+ subagent intentionally returned a JSON string. Dynamic schemas work for
994
+ declarative subagents; runnable-backed subagents reject dynamic schemas because
995
+ their runnable is already compiled.
996
+
997
+ #### Approval model
998
+
999
+ \`task\` dispatches from inside the already-running \`${toolName}\` call. It
1000
+ does not route through the parent agent's \`ToolNode\`-managed \`task\` tool and
1001
+ does not trigger parent-level \`interrupt_on\` / HITL approval for each dispatch.
1002
+ Declarative subagents still honor approval middleware configured inside their
1003
+ own spec. If you need approval before launching a subagent from the parent, use
1004
+ the normal \`task\` tool outside JavaScript or ensure the \`${toolName}\` call
1005
+ itself is approval-gated.
1006
+
1007
+ #### Mental model
1008
+
1009
+ Hold your work in JS: an array of items in, an array of results out. Merge each
1010
+ dispatch result back onto its item. Multi-stage analysis means: run a pass,
1011
+ filter or regroup the array in JS, then run another pass over the survivors.
1012
+
1013
+ You can run the whole workflow in one \`${toolName}\` call or split it across
1014
+ several — both are fine. A single end-to-end script (generate, compare, pick a
1015
+ winner; or review every item, then synthesize) is clean when you can write it
1016
+ in one go; splitting is also fine when you want to inspect results between
1017
+ stages. Either way, don't redo work across calls — reuse what is already in
1018
+ scope (see "Reuse what earlier evals left in scope" below).
1019
+
1020
+ #### Fan out with bounded concurrency
1021
+
1022
+ Dispatch independent work in parallel with \`Promise.all\`, but in explicit
1023
+ batches around 10 so you do not launch hundreds of subagents at once. The bridge
1024
+ enforces a hard per-REPL cap of 32 concurrent subagent calls.
1025
+
1026
+ \`\`\`javascript
1027
+ const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
1028
+ const batchSize = 10;
1029
+ const reviewed = [];
1030
+ for (let i = 0; i < files.length; i += batchSize) {
1031
+ const batch = files.slice(i, i + batchSize);
1032
+ reviewed.push(...(await Promise.all(batch.map(async (file) => {
1033
+ const result = await task({
1034
+ description: "Read " + file + " and review it for SQL injection. " +
1035
+ "Cite line numbers.",
1036
+ subagentType: "reviewer",
1037
+ responseSchema: {
1038
+ type: "object",
1039
+ properties: {
1040
+ vulnerabilities: {
1041
+ type: "array",
1042
+ items: {
1043
+ type: "object",
1044
+ properties: {
1045
+ type: { type: "string" },
1046
+ line: { type: "number" },
1047
+ evidence: { type: "string" },
1048
+ },
1049
+ required: ["type", "line", "evidence"],
1050
+ },
1051
+ },
1052
+ },
1053
+ required: ["vulnerabilities"],
1054
+ },
1055
+ });
1056
+ return { file, ...result };
1057
+ }))));
1058
+ }
1059
+ \`\`\`
1060
+
1061
+ #### Explore with your own tools first, then distribute
1062
+
1063
+ You already have your normal tools for reading, listing, globbing, and
1064
+ grepping files. Use them to explore and understand the task BEFORE you write
1065
+ the orchestration script. These are ordinary tool calls, separate from the
1066
+ \`${toolName}\` tool: read the data file, list or glob the directory, grep for
1067
+ what matters, then decide how to split the work.
1068
+
1069
+ Never write \`${toolName}\` code that spawns a subagent just to read or parse a
1070
+ file or list a directory. That is a deterministic step you do yourself with a
1071
+ direct tool call; spending a whole agent loop on it is wasteful.
1072
+
1073
+ Once you understand the shape of the work, you have creative freedom in how
1074
+ you split it:
1075
+
1076
+ - One dispatch per file or per record, when the items are already separate.
1077
+ - Chunk a large input yourself — read it, split it, optionally write a small
1078
+ input file per chunk — and dispatch one subagent per chunk.
1079
+ - A cheap classification pass first, then deeper dispatches only for the items
1080
+ that warrant them.
1081
+
1082
+ Then write JavaScript in the \`${toolName}\` tool that distributes the heavy,
1083
+ agentic work to subagents with \`task()\`: analyzing file contents, exploring a
1084
+ codebase, making judgment calls, rewriting code, or synthesizing a report.
1085
+
1086
+ Hand each subagent a locator, not a payload. Subagents have their own file
1087
+ tools, so for anything that lives in a file — a file to review, rewrite, or
1088
+ audit — pass the path and let the subagent read it. Do NOT read a whole file
1089
+ just to paste its contents into the description; that bloats every dispatch
1090
+ and duplicates the file across them. Reserve inline content for small or
1091
+ derived data that has no path of its own: a single parsed record, or a chunk
1092
+ you split out of a larger input (write the chunk to its own file and pass that
1093
+ path if it is large). Assemble the results in JS.
1094
+
1095
+ #### Compose multiple stages
1096
+
1097
+ Filter the array in JS between passes. For example: first ask subagents for a
1098
+ cheap classification, filter to the risky items, then dispatch deeper reviews
1099
+ only for those items.
1100
+
1101
+ \`\`\`javascript
1102
+ const tagged = await Promise.all(files.map((file) =>
1103
+ task({
1104
+ description: "Read " + file + " and classify it as handler, util, " +
1105
+ "test, or config.",
1106
+ subagentType: "reviewer",
1107
+ responseSchema: {
1108
+ type: "object",
1109
+ properties: { kind: { type: "string" }, risky: { type: "boolean" } },
1110
+ required: ["kind", "risky"],
1111
+ },
1112
+ }).then((tag) => ({ file, ...tag }))
1113
+ ));
1114
+
1115
+ const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
1116
+ const deepReviews = await Promise.all(riskyHandlers.map((it) =>
1117
+ task({
1118
+ description: "Deep security review of " + it.file + ". Cite line numbers.",
1119
+ subagentType: "reviewer",
1120
+ }).then((review) => ({ ...it, review }))
1121
+ ));
1122
+ \`\`\`
1123
+
1124
+ #### Return results via the last expression, not \`console.log\`
1125
+
1126
+ The value of the last expression in an \`${toolName}\` call (or a resolved
1127
+ top-level \`await\`) is returned to you as the result. Make that final
1128
+ expression the variable holding your result and read it from there.
1129
+ \`console.log\` is only for incidental debugging: its output is capped and
1130
+ truncated, while the returned value is not, so never \`console.log\` your
1131
+ actual results.
1132
+
1133
+ Keep large intermediate sets in JS variables and return only a compact
1134
+ summary or a small slice, not the entire dataset. To persist full output,
1135
+ have a subagent write it, or write it with your own file tool outside the
1136
+ \`${toolName}\` call.
1137
+
1138
+ #### Reuse what earlier evals left in scope
1139
+
1140
+ The REPL is persistent within a turn: every top-level variable, function, and
1141
+ class you declare is kept and is available in your next \`${toolName}\` call
1142
+ (each is hoisted to global scope). So if a later step needs something an
1143
+ earlier eval produced or bound, **reference that variable by name** — do not
1144
+ write a new literal that re-types data a previous eval already returned or
1145
+ computed.
1146
+
1147
+ If you catch yourself pasting a big array or object of values you produced in
1148
+ an earlier call, that is the tell: the variable is still in scope, so use it.
1149
+ Re-typing prior results as a fresh literal wastes tokens and drifts from what
1150
+ actually ran.
1151
+
1152
+ \`\`\`javascript
1153
+ // An earlier eval bound this:
1154
+ // const auditResults = await Promise.all(files.map(/* ...audit... */));
1155
+
1156
+ // A later eval — reference it; do NOT paste the findings back in as a literal:
1157
+ const findings = auditResults.flatMap((r) =>
1158
+ r.findings.map((f) => ({ ...f, file: r.file }))
1159
+ );
1160
+ const verified = await Promise.all(findings.map((f) =>
1161
+ task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
1162
+ .then((v) => ({ ...f, ...v }))
1163
+ ));
1164
+ \`\`\`
1165
+
1166
+ #### When the user asks for a "workflow"
1167
+
1168
+ If the user's request mentions running a "workflow" (or otherwise uses the
1169
+ word "workflow"), fan the work out to subagents rather than doing it all
1170
+ yourself. Explore with your own tools first as needed, then write JavaScript
1171
+ in the \`${toolName}\` tool that dispatches subagents with \`task()\` and
1172
+ assembles their results. The point is to distribute the heavy work in
1173
+ parallel, not to grind through it one tool call at a time.
1174
+ `;
1175
+ }
1176
+ function renderReplSystemPrompt(opts) {
1177
+ const sideEffects = opts.hasPtc ? " External side effects from inside the REPL are reachable only via the `tools.*` namespace documented below." : " The REPL is pure computation; do any filesystem or other I/O with your normal tools, outside this tool.";
1178
+ return dedent`
1179
+ ### Interpreter
1180
+
1181
+ An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
1182
+ - State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
1183
+ - Top-level \`await\` works; Promises resolve before the call returns.
1184
+ - Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (\`fetch\`, \`require\`, \`fs\`, \`process\`, real \`Date.now()\` are unavailable or stubbed).${sideEffects}
1185
+ - Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
1186
+ - \`console.log\` output is captured and returned alongside the result.
1187
+ `;
1188
+ }
645
1189
  /**
646
1190
  * Generate the PTC API Reference section for the system prompt.
647
1191
  */
@@ -678,81 +1222,122 @@ async function generatePtcPrompt(tools) {
678
1222
  `;
679
1223
  }
680
1224
  /**
681
- * Resolve backend from factory or instance.
1225
+ * Resolves a mixed list of tool names and tool instances into a flat list of
1226
+ * StructuredToolInterface objects. Strings are looked up by name in agentTools;
1227
+ * instances are included directly without requiring agent registration. Strings
1228
+ * that don't match any agent tool are silently omitted.
1229
+ *
1230
+ * Throws if the subagent `task` tool is requested (by name or instance): it is
1231
+ * reserved for the `task()` global and cannot be a `tools.*` PTC member.
682
1232
  */
683
- function getBackend(backend, stateAndStore) {
684
- if (typeof backend === "function") return backend(stateAndStore);
685
- return backend;
1233
+ function resolveToolList(items, agentTools) {
1234
+ const agentByName = new Map(agentTools.map((t) => [t.name, t]));
1235
+ return items.flatMap((item) => {
1236
+ if ((typeof item === "string" ? item : item.name) === "task") throw new Error("The subagent `task` tool cannot be exposed via `ptc`. It is always available as the top-level `task()` global inside the REPL (with `subagentType` and `responseSchema` support); exposing it through the `tools.*` namespace would create a second, conflicting dispatch path that drops `responseSchema`. Remove \"task\" from `ptc`.");
1237
+ if (typeof item === "string") {
1238
+ const found = agentByName.get(item);
1239
+ return found ? [found] : [];
1240
+ }
1241
+ return [item];
1242
+ });
686
1243
  }
687
1244
  /**
688
- * Create the QuickJS REPL middleware.
1245
+ * Create the Code Interpreter middleware.
689
1246
  */
690
- function createQuickJSMiddleware(options = {}) {
691
- const { backend = (stateAndStore) => new StateBackend(stateAndStore), ptc = false, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null } = options;
692
- const usePtc = ptc !== false;
693
- const baseSystemPrompt = customSystemPrompt || REPL_SYSTEM_PROMPT;
1247
+ function createCodeInterpreterMiddleware(options = {}) {
1248
+ const { ptc, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null, maxPtcCalls = 256, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, toolName = DEFAULT_TOOL_NAME, captureConsole = true, subagents = true } = options;
1249
+ const maxSubagentConcurrency = subagents ? 32 : 0;
1250
+ if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
1251
+ const middlewareId = crypto.randomUUID();
694
1252
  let cachedPtcPrompt = null;
695
1253
  let ptcTools = [];
1254
+ let taskTool = null;
696
1255
  function filterToolsForPtc(allTools) {
697
- if (ptc === false) return [];
698
- const candidates = allTools.filter((t) => t.name !== "js_eval");
699
- if (ptc === true) {
700
- const excluded = new Set(DEFAULT_PTC_EXCLUDED_TOOLS);
701
- return candidates.filter((t) => !excluded.has(t.name));
702
- }
703
- if (Array.isArray(ptc)) {
704
- const included = new Set(ptc);
705
- return candidates.filter((t) => included.has(t.name));
706
- }
707
- if ("include" in ptc) {
708
- const included = new Set(ptc.include);
709
- return candidates.filter((t) => included.has(t.name));
710
- }
711
- if ("exclude" in ptc) {
712
- const excluded = new Set([...DEFAULT_PTC_EXCLUDED_TOOLS, ...ptc.exclude]);
713
- return candidates.filter((t) => !excluded.has(t.name));
714
- }
715
- return [];
1256
+ if (!ptc) return [];
1257
+ const candidates = allTools.filter((t) => t.name !== toolName);
1258
+ return resolveToolList(ptc, candidates);
1259
+ }
1260
+ function findTaskTool(tools) {
1261
+ return tools.find((t) => t.name === "task") ?? null;
1262
+ }
1263
+ function createBridgeDispatch(subagentTaskTool, config) {
1264
+ return async (input) => {
1265
+ const hasSchema = input.responseSchema != null;
1266
+ if (hasSchema) validateResponseSchema(input.responseSchema);
1267
+ const toolConfig = {
1268
+ ...config,
1269
+ configurable: {
1270
+ ...config.configurable,
1271
+ ...hasSchema && { [SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY]: input.responseSchema }
1272
+ }
1273
+ };
1274
+ const content = unwrapToolEnvelope(await subagentTaskTool.invoke({
1275
+ description: input.description,
1276
+ subagent_type: input.subagentType
1277
+ }, toolConfig));
1278
+ if (hasSchema && typeof content === "string") try {
1279
+ return JSON.parse(content);
1280
+ } catch {
1281
+ return content;
1282
+ }
1283
+ return content;
1284
+ };
716
1285
  }
717
1286
  return createMiddleware({
718
- name: "QuickJSMiddleware",
1287
+ name: "CodeInterpreterMiddleware",
719
1288
  tools: [tool(async (input, config) => {
720
1289
  const threadId = config.configurable?.thread_id || "__default__";
721
- const resolvedBackend = getBackend(backend, {
722
- state: getCurrentTaskInput(config) || {},
723
- store: config.store
724
- });
725
- const session = ReplSession.getOrCreate(threadId, {
1290
+ const sessionKey = `${threadId}:${middlewareId}`;
1291
+ const session = ReplSession.getOrCreate(sessionKey, {
726
1292
  memoryLimitBytes,
727
1293
  maxStackSizeBytes,
728
- backend: resolvedBackend,
729
- tools: ptcTools
1294
+ maxPtcCalls,
1295
+ tools: ptcTools,
1296
+ maxResultChars,
1297
+ captureConsole,
1298
+ sessionId: threadId,
1299
+ subagentBridge: taskTool && maxSubagentConcurrency > 0 ? {
1300
+ dispatch: createBridgeDispatch(taskTool, config),
1301
+ maxConcurrency: maxSubagentConcurrency
1302
+ } : void 0
730
1303
  });
731
- const result = await session.eval(input.code, executionTimeoutMs);
732
- await session.flushWrites(resolvedBackend);
733
- return formatReplResult(result);
1304
+ if (taskTool && maxSubagentConcurrency > 0) session.updateBridgeDispatch(createBridgeDispatch(taskTool, config));
1305
+ return formatReplResult(await session.eval(input.code, executionTimeoutMs));
734
1306
  }, {
735
- name: "js_eval",
1307
+ name: toolName,
736
1308
  description: dedent`
737
1309
  Evaluate TypeScript/JavaScript code in a sandboxed REPL. State persists across calls.
738
- Use readFile(path) and writeFile(path, content) for file access.
739
1310
  Use console.log() for output. Returns the result of the last expression.
1311
+ If file or other tools are available, call them via the tools namespace: await tools.readFile({ path }).
740
1312
  `,
1313
+ metadata: { ls_code_input_language: "javascript" },
741
1314
  schema: z.object({ code: z.string().describe("TypeScript/JavaScript code to evaluate in the sandboxed REPL") })
742
1315
  })],
743
1316
  wrapModelCall: async (request, handler) => {
744
1317
  const agentTools = request.tools || [];
745
- ptcTools = usePtc ? filterToolsForPtc(agentTools) : [];
1318
+ ptcTools = filterToolsForPtc(agentTools);
1319
+ if (!taskTool && maxSubagentConcurrency > 0) taskTool = findTaskTool(agentTools);
746
1320
  if (ptcTools.length > 0 && !cachedPtcPrompt) cachedPtcPrompt = await generatePtcPrompt(ptcTools);
747
- const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(cachedPtcPrompt || "");
1321
+ const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
1322
+ toolName,
1323
+ timeout: executionTimeoutMs / 1e3,
1324
+ memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024)),
1325
+ hasPtc: ptcTools.length > 0
1326
+ });
1327
+ const subagentPrompt = taskTool && maxSubagentConcurrency > 0 ? renderSubagentPrompt(toolName) : "";
1328
+ const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(subagentPrompt).concat(cachedPtcPrompt || "");
748
1329
  return handler({
749
1330
  ...request,
750
1331
  systemMessage
751
1332
  });
1333
+ },
1334
+ afterAgent: async (_state, runtime) => {
1335
+ const sessionKey = `${runtime.configurable?.thread_id ?? "__default__"}:${middlewareId}`;
1336
+ ReplSession.deleteSession(sessionKey);
752
1337
  }
753
1338
  });
754
1339
  }
755
1340
  //#endregion
756
- export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, DEFAULT_PTC_EXCLUDED_TOOLS, ReplSession, createQuickJSMiddleware, formatReplResult, toCamelCase, transformForEval };
1341
+ export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, PTCCallBudgetExceededError, ReplSession, createCodeInterpreterMiddleware, formatReplResult, stripTypeSyntax, toCamelCase, transformForEval, validateResponseSchema };
757
1342
 
758
1343
  //# sourceMappingURL=index.js.map