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