@langchain/quickjs 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +583 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -97
- package/dist/index.d.ts +79 -97
- package/dist/index.js +581 -387
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -23,21 +23,104 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let langchain = require("langchain");
|
|
25
25
|
let zod_v4 = require("zod/v4");
|
|
26
|
+
let deepagents = require("deepagents");
|
|
26
27
|
let dedent = require("dedent");
|
|
27
28
|
dedent = __toESM(dedent, 1);
|
|
28
|
-
let _langchain_langgraph = require("@langchain/langgraph");
|
|
29
|
-
let deepagents = require("deepagents");
|
|
30
29
|
let quickjs_emscripten = require("quickjs-emscripten");
|
|
31
30
|
let quickjs_emscripten_core = require("quickjs-emscripten-core");
|
|
32
|
-
let
|
|
33
|
-
|
|
31
|
+
let json_schema_to_typescript = require("json-schema-to-typescript");
|
|
32
|
+
let _langchain_core_utils_json_schema = require("@langchain/core/utils/json_schema");
|
|
34
33
|
let acorn = require("acorn");
|
|
35
34
|
let _sveltejs_acorn_typescript = require("@sveltejs/acorn-typescript");
|
|
36
35
|
let estree_walker = require("estree-walker");
|
|
37
36
|
let magic_string = require("magic-string");
|
|
38
37
|
magic_string = __toESM(magic_string, 1);
|
|
39
|
-
let
|
|
40
|
-
|
|
38
|
+
let p_queue = require("p-queue");
|
|
39
|
+
p_queue = __toESM(p_queue, 1);
|
|
40
|
+
//#region src/errors.ts
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a single eval exhausts its configured PTC call budget.
|
|
43
|
+
*/
|
|
44
|
+
var PTCCallBudgetExceededError = class extends Error {
|
|
45
|
+
limit;
|
|
46
|
+
attempted;
|
|
47
|
+
functionName;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
super(`PTC call budget exceeded (limit=${options.limit}, attempted=${options.attempted}, function=${options.functionName})`);
|
|
50
|
+
this.name = "PTCCallBudgetExceededError";
|
|
51
|
+
this.limit = options.limit;
|
|
52
|
+
this.attempted = options.attempted;
|
|
53
|
+
this.functionName = options.functionName;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/utils.ts
|
|
58
|
+
/**
|
|
59
|
+
* Convert a snake_case or kebab-case string to camelCase.
|
|
60
|
+
*/
|
|
61
|
+
function toCamelCase(name) {
|
|
62
|
+
return name.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format the result of a REPL evaluation for the agent.
|
|
66
|
+
*/
|
|
67
|
+
function formatReplResult(result) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (result.logs.length > 0) {
|
|
70
|
+
let logsText = result.logs.join("\n");
|
|
71
|
+
if (result.logsDroppedChars > 0) logsText += `\n[truncated ${result.logsDroppedChars} chars]`;
|
|
72
|
+
parts.push(logsText);
|
|
73
|
+
}
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
if (result.value !== void 0) {
|
|
76
|
+
const formatted = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
|
|
77
|
+
parts.push(`→ ${formatted}`);
|
|
78
|
+
}
|
|
79
|
+
} else if (result.error) {
|
|
80
|
+
const errName = result.error.name || "Error";
|
|
81
|
+
const errMsg = result.error.message || "Unknown error";
|
|
82
|
+
parts.push(`${errName}: ${errMsg}`);
|
|
83
|
+
if (result.error.stack) parts.push(result.error.stack);
|
|
84
|
+
}
|
|
85
|
+
return parts.join("\n") || "(no output)";
|
|
86
|
+
}
|
|
87
|
+
function safeToJsonSchema(schema) {
|
|
88
|
+
try {
|
|
89
|
+
return (0, _langchain_core_utils_json_schema.toJsonSchema)(schema);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function schemaToInterface(jsonSchema, interfaceName) {
|
|
95
|
+
return (await (0, json_schema_to_typescript.compile)({
|
|
96
|
+
...jsonSchema,
|
|
97
|
+
additionalProperties: false
|
|
98
|
+
}, interfaceName, {
|
|
99
|
+
bannerComment: "",
|
|
100
|
+
additionalProperties: false
|
|
101
|
+
})).replace(/^export /, "").trimEnd();
|
|
102
|
+
}
|
|
103
|
+
function capitalize(s) {
|
|
104
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
105
|
+
}
|
|
106
|
+
async function toolToTypeSignature(name, description, jsonSchema) {
|
|
107
|
+
const inputType = `${capitalize(name)}Input`;
|
|
108
|
+
if (!jsonSchema || !jsonSchema.properties) return dedent.default`
|
|
109
|
+
/**
|
|
110
|
+
* ${description}
|
|
111
|
+
*/
|
|
112
|
+
async tools.${name}(input: Record<string, unknown>): Promise<string>
|
|
113
|
+
`;
|
|
114
|
+
return dedent.default`
|
|
115
|
+
${await schemaToInterface(jsonSchema, inputType)}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ${description}
|
|
119
|
+
*/
|
|
120
|
+
async tools.${name}(input: ${inputType}): Promise<string>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
41
124
|
//#region src/transform.ts
|
|
42
125
|
/**
|
|
43
126
|
* AST-based code transform pipeline for the REPL.
|
|
@@ -113,7 +196,11 @@ function transformForEval(code) {
|
|
|
113
196
|
}
|
|
114
197
|
function isTSOnlyNode(node) {
|
|
115
198
|
const t = node.type;
|
|
116
|
-
|
|
199
|
+
if (t === "TSTypeAliasDeclaration" || t === "TSInterfaceDeclaration" || t === "TSEnumDeclaration" || t === "TSModuleDeclaration" || t === "TSDeclareFunction" || t.startsWith("TS")) return true;
|
|
200
|
+
if (t === "VariableDeclaration" && node.declare === true) return true;
|
|
201
|
+
if (t === "ImportDeclaration" && node.importKind === "type") return true;
|
|
202
|
+
if (t === "ExportNamedDeclaration" && node.exportKind === "type") return true;
|
|
203
|
+
return false;
|
|
117
204
|
}
|
|
118
205
|
/**
|
|
119
206
|
* Rewrite a top-level VariableDeclaration to globalThis assignments.
|
|
@@ -165,7 +252,11 @@ function stripTypeAnnotations(s, node) {
|
|
|
165
252
|
} });
|
|
166
253
|
}
|
|
167
254
|
function stripTypeAnnotationFromNode(s, n, offset = 0) {
|
|
168
|
-
if (n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - offset, n.typeAnnotation.end - offset);
|
|
255
|
+
if (n.optional === true && n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - 1 - offset, n.typeAnnotation.end - offset);
|
|
256
|
+
else if (n.optional === true && !n.typeAnnotation) {
|
|
257
|
+
const nameEnd = n.type === "Identifier" && typeof n.name === "string" ? n.start + n.name.length : null;
|
|
258
|
+
if (nameEnd != null) s.remove(nameEnd - offset, nameEnd + 1 - offset);
|
|
259
|
+
} else if (n.typeAnnotation && n.typeAnnotation.start != null) s.remove(n.typeAnnotation.start - offset, n.typeAnnotation.end - offset);
|
|
169
260
|
if (n.returnType && n.returnType.start != null) s.remove(n.returnType.start - offset, n.returnType.end - offset);
|
|
170
261
|
if (n.typeParameters && n.typeParameters.start != null) s.remove(n.typeParameters.start - offset, n.typeParameters.end - offset);
|
|
171
262
|
if (n.typeArguments && n.typeArguments.start != null) s.remove(n.typeArguments.start - offset, n.typeArguments.end - offset);
|
|
@@ -231,228 +322,38 @@ function stripTypeSyntax(code) {
|
|
|
231
322
|
return magicString.toString();
|
|
232
323
|
}
|
|
233
324
|
//#endregion
|
|
234
|
-
//#region src/
|
|
235
|
-
/**
|
|
236
|
-
* File extensions the loader will enumerate from a skill directory.
|
|
237
|
-
*/
|
|
238
|
-
const SKILL_MODULE_EXTENSIONS = [
|
|
239
|
-
".js",
|
|
240
|
-
".mjs",
|
|
241
|
-
".cjs",
|
|
242
|
-
".ts",
|
|
243
|
-
".mts",
|
|
244
|
-
".cts",
|
|
245
|
-
".jsx",
|
|
246
|
-
".tsx"
|
|
247
|
-
];
|
|
248
|
-
/**
|
|
249
|
-
* Hard cap on total bytes pulled for one skill's bundle (1 MiB).
|
|
250
|
-
*/
|
|
251
|
-
const MAX_SKILL_BUNDLE_BYTES = 1 * 1024 * 1024;
|
|
252
|
-
/**
|
|
253
|
-
* Validates a skill name against the spec's kebab-case rule.
|
|
254
|
-
*/
|
|
255
|
-
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
256
|
-
/**
|
|
257
|
-
* Matches `"@/skills/<name>"` or `'@/skills/<name>'` references in source.
|
|
258
|
-
* Template literals and computed specifiers are not caught.
|
|
259
|
-
*/
|
|
260
|
-
const SKILL_SPECIFIER_RE = /["']@\/skills\/([a-z0-9]+(?:-[a-z0-9]+)*)["']/g;
|
|
261
|
-
/**
|
|
262
|
-
* List every code-extension file under `skillDir` (recursive).
|
|
263
|
-
*/
|
|
264
|
-
async function enumerateCodeFiles(backend, skillDir, skillName) {
|
|
265
|
-
const seen = /* @__PURE__ */ new Set();
|
|
266
|
-
for (const ext of SKILL_MODULE_EXTENSIONS) {
|
|
267
|
-
const result = await backend.glob(`**/*${ext}`, skillDir);
|
|
268
|
-
if (result.error !== void 0) throw new Error(`Skill '${skillName}': failed to list '${skillDir}': ${result.error}`);
|
|
269
|
-
const matches = result.files ?? [];
|
|
270
|
-
for (const match of matches) seen.add(match.path);
|
|
271
|
-
}
|
|
272
|
-
return [...seen].sort();
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Decode download responses into [path, source] pairs.
|
|
276
|
-
*/
|
|
277
|
-
function decodeFiles(responses, skillName) {
|
|
278
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
279
|
-
const pairs = [];
|
|
280
|
-
for (const response of responses) {
|
|
281
|
-
if (response.error !== null || response.content === null) throw new Error(`Skill '${skillName}': failed to download '${response.path}': ${response.error ?? "no content"}`);
|
|
282
|
-
let source;
|
|
283
|
-
try {
|
|
284
|
-
source = decoder.decode(response.content);
|
|
285
|
-
} catch {
|
|
286
|
-
throw new Error(`Skill '${skillName}': file '${response.path}' is not valid UTF-8`);
|
|
287
|
-
}
|
|
288
|
-
pairs.push([response.path, source]);
|
|
289
|
-
}
|
|
290
|
-
return pairs;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Throws an Error when the total decoded size of all files exceeds
|
|
294
|
-
* `MAX_SKILL_BUNDLE_BYTES`. Counts characters rather than bytes, which
|
|
295
|
-
* over-counts multi-byte UTF-8. Intentionally errs toward rejection.
|
|
296
|
-
*/
|
|
297
|
-
function validateBundleSize(pairs, skillName) {
|
|
298
|
-
let total = 0;
|
|
299
|
-
for (const [, source] of pairs) total += source.length;
|
|
300
|
-
if (total > 1048576) throw new Error(`Skill '${skillName}': bundle exceeds ${MAX_SKILL_BUNDLE_BYTES} bytes (total ${total})`);
|
|
301
|
-
}
|
|
325
|
+
//#region src/eval-queue.ts
|
|
302
326
|
/**
|
|
303
|
-
*
|
|
304
|
-
* Throws an Error if the path escapes the skill directory which indicates
|
|
305
|
-
* a backend bug, not a user error.
|
|
306
|
-
*/
|
|
307
|
-
function relativeUnder(skillDir, absolutePath, skillName) {
|
|
308
|
-
const rel = node_path_posix.relative(skillDir, absolutePath);
|
|
309
|
-
if (rel === "" || rel.startsWith("..")) throw new Error(`Skill '${skillName}': file ${absolutePath} is not under '${skillDir}'`);
|
|
310
|
-
return rel;
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Build the relative-path → source map, applying `stripTypeSyntax` to each file.
|
|
314
|
-
*/
|
|
315
|
-
function buildFilesMap(skillDir, entryRel, pairs, skillName) {
|
|
316
|
-
const files = /* @__PURE__ */ new Map();
|
|
317
|
-
let entryPresent = false;
|
|
318
|
-
for (const [absPath, source] of pairs) {
|
|
319
|
-
const rel = relativeUnder(skillDir, absPath, skillName);
|
|
320
|
-
files.set(rel, stripTypeSyntax(source));
|
|
321
|
-
if (rel === entryRel) entryPresent = true;
|
|
322
|
-
}
|
|
323
|
-
if (!entryPresent) throw new Error(`Skill '${skillName}': module path '${entryRel}' did not match any file in the skill directory`);
|
|
324
|
-
return files;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Build a `LoadedSkill` from a skill's metadata and a backend handle.
|
|
328
|
-
*
|
|
329
|
-
* Enumerates code files under the skill directory, downloads them,
|
|
330
|
-
* strips TypeScript syntax, and validates the entrypoint is present.
|
|
331
|
-
*/
|
|
332
|
-
async function loadSkill(metadata, backend) {
|
|
333
|
-
const name = metadata.name;
|
|
334
|
-
if (!SKILL_NAME_RE.test(name)) throw new Error(`Skill name '${name}' is not a valid kebab-case identifier`);
|
|
335
|
-
const entryRel = metadata.module;
|
|
336
|
-
if (entryRel === void 0 || entryRel === "") throw new Error(`Skill '${name}' has no 'module' frontmatter key - only skills with a declared entrypoint are installable`);
|
|
337
|
-
const adapted = (0, deepagents.adaptBackendProtocol)(backend);
|
|
338
|
-
if (adapted.downloadFiles === void 0) throw new Error(`Skill '${name}': backend does not implement downloadFiles`);
|
|
339
|
-
const skillDir = node_path_posix.dirname(metadata.path);
|
|
340
|
-
const codeFiles = await enumerateCodeFiles(adapted, skillDir, name);
|
|
341
|
-
if (codeFiles.length === 0) throw new Error(`Skill '${name}': no JS/TS files under '${skillDir}'`);
|
|
342
|
-
const filePairs = decodeFiles(await adapted.downloadFiles(codeFiles), name);
|
|
343
|
-
validateBundleSize(filePairs, name);
|
|
344
|
-
const files = buildFilesMap(skillDir, entryRel, filePairs, name);
|
|
345
|
-
return {
|
|
346
|
-
name,
|
|
347
|
-
specifier: `@/skills/${name}`,
|
|
348
|
-
entryRel,
|
|
349
|
-
files
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Extract skill names referenced by `"@/skills/<name>"` literals in source.
|
|
327
|
+
* Serializes async operations on a shared WASM module.
|
|
354
328
|
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
329
|
+
* The quickjs-emscripten asyncify variant allows only one concurrent
|
|
330
|
+
* async call per module instance. This queue enforces that constraint
|
|
331
|
+
* by chaining operations into a promise queue — each caller waits for
|
|
332
|
+
* the previous one to finish before executing.
|
|
358
333
|
*/
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
this.functionName = options.functionName;
|
|
334
|
+
var AsyncEvalQueue = class {
|
|
335
|
+
tail = Promise.resolve();
|
|
336
|
+
/**
|
|
337
|
+
* Enqueue an async operation. The operation will not start until all
|
|
338
|
+
* previously enqueued operations have completed.
|
|
339
|
+
*/
|
|
340
|
+
async enqueue(fn) {
|
|
341
|
+
let release;
|
|
342
|
+
const gate = new Promise((r) => {
|
|
343
|
+
release = r;
|
|
344
|
+
});
|
|
345
|
+
const prev = this.tail;
|
|
346
|
+
this.tail = gate;
|
|
347
|
+
return prev.then(async () => {
|
|
348
|
+
try {
|
|
349
|
+
return await fn();
|
|
350
|
+
} finally {
|
|
351
|
+
release();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
380
354
|
}
|
|
381
355
|
};
|
|
382
356
|
//#endregion
|
|
383
|
-
//#region src/utils.ts
|
|
384
|
-
/**
|
|
385
|
-
* Convert a snake_case or kebab-case string to camelCase.
|
|
386
|
-
*/
|
|
387
|
-
function toCamelCase(name) {
|
|
388
|
-
return name.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Format the result of a REPL evaluation for the agent.
|
|
392
|
-
*/
|
|
393
|
-
function formatReplResult(result) {
|
|
394
|
-
const parts = [];
|
|
395
|
-
if (result.logs.length > 0) {
|
|
396
|
-
let logsText = result.logs.join("\n");
|
|
397
|
-
if (result.logsDroppedChars > 0) logsText += `\n[truncated ${result.logsDroppedChars} chars]`;
|
|
398
|
-
parts.push(logsText);
|
|
399
|
-
}
|
|
400
|
-
if (result.ok) {
|
|
401
|
-
if (result.value !== void 0) {
|
|
402
|
-
const formatted = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
|
|
403
|
-
parts.push(`→ ${formatted}`);
|
|
404
|
-
}
|
|
405
|
-
} else if (result.error) {
|
|
406
|
-
const errName = result.error.name || "Error";
|
|
407
|
-
const errMsg = result.error.message || "Unknown error";
|
|
408
|
-
parts.push(`${errName}: ${errMsg}`);
|
|
409
|
-
if (result.error.stack) parts.push(result.error.stack);
|
|
410
|
-
}
|
|
411
|
-
return parts.join("\n") || "(no output)";
|
|
412
|
-
}
|
|
413
|
-
function safeToJsonSchema(schema) {
|
|
414
|
-
try {
|
|
415
|
-
return (0, _langchain_core_utils_json_schema.toJsonSchema)(schema);
|
|
416
|
-
} catch {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
async function schemaToInterface(jsonSchema, interfaceName) {
|
|
421
|
-
return (await (0, json_schema_to_typescript.compile)({
|
|
422
|
-
...jsonSchema,
|
|
423
|
-
additionalProperties: false
|
|
424
|
-
}, interfaceName, {
|
|
425
|
-
bannerComment: "",
|
|
426
|
-
additionalProperties: false
|
|
427
|
-
})).replace(/^export /, "").trimEnd();
|
|
428
|
-
}
|
|
429
|
-
function capitalize(s) {
|
|
430
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
431
|
-
}
|
|
432
|
-
async function toolToTypeSignature(name, description, jsonSchema) {
|
|
433
|
-
const inputType = `${capitalize(name)}Input`;
|
|
434
|
-
if (!jsonSchema || !jsonSchema.properties) return dedent.default`
|
|
435
|
-
/**
|
|
436
|
-
* ${description}
|
|
437
|
-
*/
|
|
438
|
-
async tools.${name}(input: Record<string, unknown>): Promise<string>
|
|
439
|
-
`;
|
|
440
|
-
return dedent.default`
|
|
441
|
-
${await schemaToInterface(jsonSchema, inputType)}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* ${description}
|
|
445
|
-
*/
|
|
446
|
-
async tools.${name}(input: ${inputType}): Promise<string>
|
|
447
|
-
`;
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Render a pre-eval error when referenced skills are not available on the agent.
|
|
451
|
-
*/
|
|
452
|
-
function formatSkillNotAvailable(missing) {
|
|
453
|
-
return `Skills unavailable: ${[...missing].sort().join(", ")}`;
|
|
454
|
-
}
|
|
455
|
-
//#endregion
|
|
456
357
|
//#region src/session.ts
|
|
457
358
|
/**
|
|
458
359
|
* Core REPL engine built on quickjs-emscripten (asyncify variant).
|
|
@@ -477,64 +378,78 @@ const DEFAULT_MAX_STACK_SIZE = 320 * 1024;
|
|
|
477
378
|
const DEFAULT_EXECUTION_TIMEOUT = 5e3;
|
|
478
379
|
const DEFAULT_MAX_PTC_CALLS = 256;
|
|
479
380
|
const DEFAULT_MAX_RESULTS_CHARS = 4e3;
|
|
381
|
+
const LINE_NUMBER_RE = /^\s*\d+(?:\.\d+)?\t/;
|
|
480
382
|
const variantImport = import("@jitl/quickjs-ng-wasmfile-release-asyncify");
|
|
481
|
-
async function newAsyncModule() {
|
|
482
|
-
const variant = await variantImport;
|
|
483
|
-
return (0, quickjs_emscripten_core.newQuickJSAsyncWASMModuleFromVariant)(variant.default ?? variant);
|
|
484
|
-
}
|
|
485
|
-
function makeErrorSource(message) {
|
|
486
|
-
return `throw { name: "Error", message: ${JSON.stringify(message)} };`;
|
|
487
|
-
}
|
|
488
383
|
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* `@/skills/<name>/<rel>` shape. `rel` is absent for the bare form.
|
|
384
|
+
* Process-global eval queue. Serializes all evalCodeAsync calls across
|
|
385
|
+
* sessions to enforce the asyncify one-at-a-time constraint.
|
|
492
386
|
*/
|
|
493
|
-
|
|
494
|
-
if (!specifier.startsWith("@/skills/")) return;
|
|
495
|
-
const tail = specifier.slice(9);
|
|
496
|
-
const slashIdx = tail.indexOf("/");
|
|
497
|
-
const name = slashIdx === -1 ? tail : tail.slice(0, slashIdx);
|
|
498
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) return;
|
|
499
|
-
const rel = slashIdx === -1 ? void 0 : tail.slice(slashIdx + 1);
|
|
500
|
-
if (rel !== void 0 && rel === "") return;
|
|
501
|
-
return {
|
|
502
|
-
name,
|
|
503
|
-
rel
|
|
504
|
-
};
|
|
505
|
-
}
|
|
387
|
+
const sharedEvalQueue = new AsyncEvalQueue();
|
|
506
388
|
/**
|
|
507
|
-
*
|
|
389
|
+
* Process-global WASM module shared by all sessions.
|
|
390
|
+
*
|
|
391
|
+
* Each session creates its own runtime and context on this module,
|
|
392
|
+
* providing full isolation for globals, heap, and stack. The module
|
|
393
|
+
* itself is stateless between runtimes — only the compiled WASM code
|
|
394
|
+
* and Emscripten infrastructure are shared.
|
|
395
|
+
*
|
|
396
|
+
* This is safe because:
|
|
397
|
+
* - The module loader is synchronous (preloaded skill cache), so
|
|
398
|
+
* imports don't cause asyncify suspensions.
|
|
399
|
+
* - Tool injection uses the promise-based pattern (newFunction +
|
|
400
|
+
* newPromise), not newAsyncifiedFunction, so tool calls don't
|
|
401
|
+
* cause asyncify suspensions.
|
|
402
|
+
* - The eval queue serializes evalCodeAsync calls to satisfy the
|
|
403
|
+
* one-concurrent-async-call-per-module constraint.
|
|
508
404
|
*/
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
|
|
405
|
+
let sharedModulePromise;
|
|
406
|
+
function getSharedModule() {
|
|
407
|
+
if (!sharedModulePromise) sharedModulePromise = (async () => {
|
|
408
|
+
const variant = await variantImport;
|
|
409
|
+
return (0, quickjs_emscripten_core.newQuickJSAsyncWASMModuleFromVariant)(variant.default ?? variant);
|
|
410
|
+
})();
|
|
411
|
+
return sharedModulePromise;
|
|
513
412
|
}
|
|
514
413
|
/**
|
|
515
|
-
*
|
|
414
|
+
* Unwrap a PTC tool result to a plain string for use inside QuickJS.
|
|
415
|
+
*
|
|
416
|
+
* Tool results may arrive as a raw string, or as an array of LangChain
|
|
417
|
+
* content blocks (`{ type: "text", text: "..." }`). Blocks are joined
|
|
418
|
+
* with newlines; non-text block types are silently skipped. Anything
|
|
419
|
+
* else (objects, nulls) is JSON-serialised as a fallback.
|
|
420
|
+
*
|
|
421
|
+
* @param result - Raw return value from `tool.invoke()`.
|
|
422
|
+
* @returns Plain string representation of the tool output.
|
|
516
423
|
*/
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
424
|
+
function extractToolText(result) {
|
|
425
|
+
if (typeof result === "string") return result;
|
|
426
|
+
if (Array.isArray(result)) {
|
|
427
|
+
const texts = [];
|
|
428
|
+
for (const block of result) if (typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string") texts.push(block.text);
|
|
429
|
+
if (texts.length > 0) return texts.join("\n");
|
|
430
|
+
}
|
|
431
|
+
return JSON.stringify(result);
|
|
521
432
|
}
|
|
522
433
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
434
|
+
* Remove the `cat -n` line-number prefix from every line of a string.
|
|
435
|
+
*
|
|
436
|
+
* The filesystem backend formats file content with line numbers in the
|
|
437
|
+
* form `" N\t"` so human readers can navigate by line. That prefix
|
|
438
|
+
* is useful for the agent but noise for QuickJS code that parses the
|
|
439
|
+
* text programmatically (e.g. swarm reading `/context.txt`).
|
|
440
|
+
*
|
|
441
|
+
* The function is conservative: if any non-empty line lacks the prefix,
|
|
442
|
+
* the text is returned unchanged so nothing is silently corrupted.
|
|
443
|
+
*
|
|
444
|
+
* @param text - Raw file content, possibly line-number prefixed.
|
|
445
|
+
* @returns Content with line-number prefixes stripped, or the original
|
|
446
|
+
* text if it doesn't match the expected format throughout.
|
|
525
447
|
*/
|
|
526
|
-
function
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (segment === "..") {
|
|
532
|
-
out.pop();
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
out.push(segment);
|
|
536
|
-
}
|
|
537
|
-
return out.join("/");
|
|
448
|
+
function stripLineNumbers(text) {
|
|
449
|
+
const lines = text.split("\n");
|
|
450
|
+
if (lines.length === 0) return text;
|
|
451
|
+
if (!lines.every((l) => l === "" || LINE_NUMBER_RE.test(l))) return text;
|
|
452
|
+
return lines.map((l) => l.replace(LINE_NUMBER_RE, "")).join("\n");
|
|
538
453
|
}
|
|
539
454
|
/**
|
|
540
455
|
* Fixed-size character buffer for capturing console output from the QuickJS VM.
|
|
@@ -596,11 +511,26 @@ var ReplSession = class ReplSession {
|
|
|
596
511
|
context = null;
|
|
597
512
|
consoleBuffer = new ConsoleBuffer(DEFAULT_MAX_RESULTS_CHARS);
|
|
598
513
|
options;
|
|
599
|
-
skillsContext;
|
|
600
|
-
skillsLoaded = /* @__PURE__ */ new Map();
|
|
601
|
-
skillsFailed = /* @__PURE__ */ new Map();
|
|
602
514
|
maxPtcCalls;
|
|
603
515
|
ptcCallsRemaining = null;
|
|
516
|
+
subagentQueue = null;
|
|
517
|
+
bridgeDispatchRef = null;
|
|
518
|
+
/** Allowed keys in the subagent input object. */
|
|
519
|
+
static SUBAGENT_ALLOWED_KEYS = new Set([
|
|
520
|
+
"description",
|
|
521
|
+
"subagentType",
|
|
522
|
+
"responseSchema"
|
|
523
|
+
]);
|
|
524
|
+
/**
|
|
525
|
+
* Reset the shared WASM module. Forces the next session to instantiate
|
|
526
|
+
* a fresh module. Only needed in tests where module state must be
|
|
527
|
+
* isolated between test files.
|
|
528
|
+
*
|
|
529
|
+
* @internal
|
|
530
|
+
*/
|
|
531
|
+
static resetSharedModule() {
|
|
532
|
+
sharedModulePromise = void 0;
|
|
533
|
+
}
|
|
604
534
|
constructor(id, options = {}) {
|
|
605
535
|
this.id = id;
|
|
606
536
|
this.options = options;
|
|
@@ -608,8 +538,8 @@ var ReplSession = class ReplSession {
|
|
|
608
538
|
}
|
|
609
539
|
async ensureStarted() {
|
|
610
540
|
if (this.runtime) return;
|
|
611
|
-
const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools,
|
|
612
|
-
const runtime = (await
|
|
541
|
+
const { memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, tools, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, captureConsole = true } = this.options;
|
|
542
|
+
const runtime = (await getSharedModule()).newRuntime();
|
|
613
543
|
runtime.setMemoryLimit(memoryLimitBytes);
|
|
614
544
|
runtime.setMaxStackSize(maxStackSizeBytes);
|
|
615
545
|
const context = runtime.newContext();
|
|
@@ -618,67 +548,15 @@ var ReplSession = class ReplSession {
|
|
|
618
548
|
this.consoleBuffer = new ConsoleBuffer(maxResultChars);
|
|
619
549
|
if (captureConsole) this.setupConsole();
|
|
620
550
|
if (tools !== void 0 && tools.length > 0) this.injectTools(tools);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
*/
|
|
626
|
-
async ensureSkillLoaded(name) {
|
|
627
|
-
const cached = this.skillsLoaded.get(name);
|
|
628
|
-
if (cached !== void 0) return cached;
|
|
629
|
-
const cachedError = this.skillsFailed.get(name);
|
|
630
|
-
if (cachedError !== void 0) throw cachedError;
|
|
631
|
-
const ctx = this.skillsContext;
|
|
632
|
-
if (ctx === void 0) throw new Error(`Skill '${name}' referenced but skills are not configured for this session`);
|
|
633
|
-
const metadata = ctx.metadata.find((m) => m.name === name);
|
|
634
|
-
if (metadata === void 0) throw new Error(`Skill '${name}' referenced but not available on this agent`);
|
|
635
|
-
try {
|
|
636
|
-
const loaded = await loadSkill(metadata, ctx.backend);
|
|
637
|
-
this.skillsLoaded.set(name, loaded);
|
|
638
|
-
return loaded;
|
|
639
|
-
} catch (err) {
|
|
640
|
-
this.skillsFailed.set(name, err);
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
async resolveSpecifier(specifier) {
|
|
645
|
-
const parsed = parseSkillSpecifier(specifier);
|
|
646
|
-
if (parsed === void 0) return makeErrorSource(`Module not found: ${specifier}`);
|
|
647
|
-
let loaded;
|
|
648
|
-
try {
|
|
649
|
-
loaded = await this.ensureSkillLoaded(parsed.name);
|
|
650
|
-
} catch (err) {
|
|
651
|
-
return makeErrorSource(err.message ?? String(err));
|
|
652
|
-
}
|
|
653
|
-
if (parsed.rel === void 0) {
|
|
654
|
-
const source = loaded.files.get(loaded.entryRel);
|
|
655
|
-
if (source === void 0) return makeErrorSource(`Skill '${parsed.name}': entrypoint '${loaded.entryRel}' missing from bundle`);
|
|
656
|
-
return source;
|
|
551
|
+
const { subagentBridge } = this.options;
|
|
552
|
+
if (subagentBridge) {
|
|
553
|
+
this.subagentQueue = new p_queue.default({ concurrency: subagentBridge.maxConcurrency });
|
|
554
|
+
this.injectSubagentBridge(subagentBridge.dispatch);
|
|
657
555
|
}
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Canonicalize an `import` specifier. Bare specifiers pass through;
|
|
664
|
-
* relative specifiers are resolved against the importing module's path.
|
|
665
|
-
* Traversal out of a skill's `@/skills/<name>/` namespace is rejected.
|
|
666
|
-
*/
|
|
667
|
-
normalizeSpecifier(base, requested) {
|
|
668
|
-
if (!(requested.startsWith("./") || requested.startsWith("../"))) return requested;
|
|
669
|
-
const parsed = parseSkillSpecifier(base);
|
|
670
|
-
const resolved = posixJoin(parsed !== void 0 && parsed.rel === void 0 ? base : posixDirname(base), requested);
|
|
671
|
-
const skillPrefix = matchSkillPrefix(base);
|
|
672
|
-
if (skillPrefix === void 0) return resolved;
|
|
673
|
-
if (!resolved.startsWith(`${skillPrefix}/`)) return `__resolve_error__:${requested} escapes ${skillPrefix}`;
|
|
674
|
-
return resolved;
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Wire the QuickJS module loader and normalizer on this session's runtime.
|
|
678
|
-
*/
|
|
679
|
-
installModuleLoader() {
|
|
680
|
-
if (this.runtime === null) return;
|
|
681
|
-
this.runtime.setModuleLoader(async (specifier) => this.resolveSpecifier(specifier), (base, requested) => this.normalizeSpecifier(base, requested));
|
|
556
|
+
const sessionId = this.options.sessionId ?? "default";
|
|
557
|
+
const sessionIdHandle = context.newString(sessionId);
|
|
558
|
+
context.setProp(context.global, "__sessionId__", sessionIdHandle);
|
|
559
|
+
sessionIdHandle.dispose();
|
|
682
560
|
}
|
|
683
561
|
/**
|
|
684
562
|
* Initialise the per-eval PTC counter. Called at the top of every `eval()`.
|
|
@@ -741,14 +619,6 @@ var ReplSession = class ReplSession {
|
|
|
741
619
|
if (session) session.dispose();
|
|
742
620
|
}
|
|
743
621
|
/**
|
|
744
|
-
* Push the current skills metadata + backend into the session.
|
|
745
|
-
* Called by the middleware once per `eval` invocation, before eval runs.
|
|
746
|
-
* Pass `undefined` to clear the context (no skill imports will resolve).
|
|
747
|
-
*/
|
|
748
|
-
setSkillsContext(ctx) {
|
|
749
|
-
this.skillsContext = ctx;
|
|
750
|
-
}
|
|
751
|
-
/**
|
|
752
622
|
* Evaluate code in this session.
|
|
753
623
|
*
|
|
754
624
|
* Lazily starts the QuickJS runtime on the first call. Code is
|
|
@@ -773,7 +643,7 @@ var ReplSession = class ReplSession {
|
|
|
773
643
|
if (timeoutMs >= 0) runtime.setInterruptHandler((0, quickjs_emscripten.shouldInterruptAfterDeadline)(Date.now() + timeoutMs));
|
|
774
644
|
else runtime.setInterruptHandler(() => false);
|
|
775
645
|
const transformed = transformForEval(code);
|
|
776
|
-
const result = await context.evalCodeAsync(transformed);
|
|
646
|
+
const result = await sharedEvalQueue.enqueue(() => context.evalCodeAsync(transformed));
|
|
777
647
|
if (result.error) {
|
|
778
648
|
const error = context.dump(result.error);
|
|
779
649
|
result.error.dispose();
|
|
@@ -908,8 +778,9 @@ var ReplSession = class ReplSession {
|
|
|
908
778
|
try {
|
|
909
779
|
this.consumePtcBudget(camelName);
|
|
910
780
|
const rawInput = typeof input === "object" && input !== null ? input : {};
|
|
911
|
-
|
|
912
|
-
|
|
781
|
+
let text = extractToolText(await t.invoke(rawInput));
|
|
782
|
+
if (t.name === "read_file") text = stripLineNumbers(text);
|
|
783
|
+
const val = context.newString(text);
|
|
913
784
|
promise.resolve(val);
|
|
914
785
|
val.dispose();
|
|
915
786
|
} catch (e) {
|
|
@@ -928,11 +799,116 @@ var ReplSession = class ReplSession {
|
|
|
928
799
|
context.setProp(context.global, "tools", toolsNs);
|
|
929
800
|
toolsNs.dispose();
|
|
930
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Install the `task` global on the QuickJS context.
|
|
804
|
+
*
|
|
805
|
+
* Registers the host function directly as `globalThis.task`,
|
|
806
|
+
* then freezes it via `evalCode`. Structured results (when
|
|
807
|
+
* responseSchema is provided) are marshaled into native QuickJS
|
|
808
|
+
* objects on the host side — no JS wrapper needed.
|
|
809
|
+
*/
|
|
810
|
+
/**
|
|
811
|
+
* Replace the active bridge dispatch with a fresh one.
|
|
812
|
+
*
|
|
813
|
+
* Call this before each eval so the dispatch closure carries
|
|
814
|
+
* the current invocation's config (tracing callbacks, run ID, etc.)
|
|
815
|
+
* rather than the stale config from session creation.
|
|
816
|
+
*/
|
|
817
|
+
updateBridgeDispatch(dispatch) {
|
|
818
|
+
if (this.bridgeDispatchRef) this.bridgeDispatchRef.current = dispatch;
|
|
819
|
+
}
|
|
820
|
+
injectSubagentBridge(dispatch) {
|
|
821
|
+
const context = this.context;
|
|
822
|
+
const queue = this.subagentQueue;
|
|
823
|
+
this.bridgeDispatchRef = { current: dispatch };
|
|
824
|
+
const ref = this.bridgeDispatchRef;
|
|
825
|
+
const hostFn = context.newFunction("task", (inputHandle) => {
|
|
826
|
+
const input = context.dump(inputHandle);
|
|
827
|
+
const promise = context.newPromise();
|
|
828
|
+
(async () => {
|
|
829
|
+
try {
|
|
830
|
+
if (input == null || typeof input !== "object" || Array.isArray(input)) throw new Error("task: expected an object argument");
|
|
831
|
+
const obj = { ...input };
|
|
832
|
+
if ("subagent_type" in obj) {
|
|
833
|
+
obj.subagentType ??= obj.subagent_type;
|
|
834
|
+
delete obj.subagent_type;
|
|
835
|
+
}
|
|
836
|
+
if ("response_schema" in obj) {
|
|
837
|
+
obj.responseSchema ??= obj.response_schema;
|
|
838
|
+
delete obj.response_schema;
|
|
839
|
+
}
|
|
840
|
+
const unknownKeys = Object.keys(obj).filter((k) => !ReplSession.SUBAGENT_ALLOWED_KEYS.has(k));
|
|
841
|
+
if (unknownKeys.length > 0) throw new Error(`task: unknown keys: ${unknownKeys.join(", ")}. Allowed: ${[...ReplSession.SUBAGENT_ALLOWED_KEYS].join(", ")}`);
|
|
842
|
+
const { description, subagentType, responseSchema } = obj;
|
|
843
|
+
if (typeof description !== "string" || description.length === 0) throw new Error("task: 'description' is required and must be a non-empty string");
|
|
844
|
+
if (typeof subagentType !== "string" || subagentType.length === 0) throw new Error("task: 'subagentType' is required and must be a non-empty string");
|
|
845
|
+
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");
|
|
846
|
+
const result = await queue.add(() => ref.current({
|
|
847
|
+
description,
|
|
848
|
+
subagentType,
|
|
849
|
+
...responseSchema !== void 0 && { responseSchema }
|
|
850
|
+
}));
|
|
851
|
+
if (typeof result === "string") {
|
|
852
|
+
const val = context.newString(result);
|
|
853
|
+
promise.resolve(val);
|
|
854
|
+
val.dispose();
|
|
855
|
+
} else {
|
|
856
|
+
const jsonResult = context.evalCode(`(${JSON.stringify(result)})`);
|
|
857
|
+
if (jsonResult.error) {
|
|
858
|
+
const errDump = context.dump(jsonResult.error);
|
|
859
|
+
jsonResult.error.dispose();
|
|
860
|
+
throw new Error(`task: failed to marshal structured response: ${JSON.stringify(errDump)}`);
|
|
861
|
+
}
|
|
862
|
+
promise.resolve(jsonResult.value);
|
|
863
|
+
jsonResult.value.dispose();
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {
|
|
866
|
+
const msg = e != null && typeof e.message === "string" ? e.message : String(e);
|
|
867
|
+
const err = context.newError(msg);
|
|
868
|
+
promise.reject(err);
|
|
869
|
+
err.dispose();
|
|
870
|
+
}
|
|
871
|
+
promise.settled.then(context.runtime.executePendingJobs);
|
|
872
|
+
})();
|
|
873
|
+
return promise.handle;
|
|
874
|
+
});
|
|
875
|
+
context.setProp(context.global, "task", hostFn);
|
|
876
|
+
hostFn.dispose();
|
|
877
|
+
context.evalCode("Object.freeze(globalThis.task);Object.defineProperty(globalThis, 'task', { value: globalThis.task, writable: false, configurable: false,}); undefined");
|
|
878
|
+
}
|
|
931
879
|
};
|
|
932
880
|
//#endregion
|
|
881
|
+
//#region src/subagent-dispatch.ts
|
|
882
|
+
const SCHEMA_MAX_BYTES = 4096;
|
|
883
|
+
const SCHEMA_MAX_DEPTH = 5;
|
|
884
|
+
const SCHEMA_MAX_PROPERTIES = 32;
|
|
885
|
+
/**
|
|
886
|
+
* Validate that a response schema does not exceed size, depth, or
|
|
887
|
+
* property-count limits.
|
|
888
|
+
*
|
|
889
|
+
* @throws Error if any limit is exceeded.
|
|
890
|
+
*/
|
|
891
|
+
function validateResponseSchema(schema) {
|
|
892
|
+
const serialized = JSON.stringify(schema);
|
|
893
|
+
if (serialized.length > SCHEMA_MAX_BYTES) throw new Error(`responseSchema exceeds ${SCHEMA_MAX_BYTES} byte limit (${serialized.length} bytes)`);
|
|
894
|
+
function check(node, depth, propCount) {
|
|
895
|
+
if (depth > SCHEMA_MAX_DEPTH) throw new Error(`responseSchema exceeds maximum nesting depth of ${SCHEMA_MAX_DEPTH}`);
|
|
896
|
+
const props = node.properties;
|
|
897
|
+
if (props != null && typeof props === "object" && !Array.isArray(props)) {
|
|
898
|
+
const propObj = props;
|
|
899
|
+
propCount.value += Object.keys(propObj).length;
|
|
900
|
+
if (propCount.value > SCHEMA_MAX_PROPERTIES) throw new Error(`responseSchema exceeds maximum of ${SCHEMA_MAX_PROPERTIES} properties`);
|
|
901
|
+
for (const value of Object.values(propObj)) if (value != null && typeof value === "object" && !Array.isArray(value)) check(value, depth + 1, propCount);
|
|
902
|
+
}
|
|
903
|
+
const items = node.items;
|
|
904
|
+
if (items != null && typeof items === "object" && !Array.isArray(items)) check(items, depth + 1, propCount);
|
|
905
|
+
}
|
|
906
|
+
check(schema, 0, { value: 0 });
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
933
909
|
//#region src/middleware.ts
|
|
934
910
|
/**
|
|
935
|
-
*
|
|
911
|
+
* Code Interpreter middleware for deepagents.
|
|
936
912
|
*
|
|
937
913
|
* Provides an `eval` tool that runs JavaScript in a WASM-sandboxed QuickJS
|
|
938
914
|
* interpreter. Supports:
|
|
@@ -940,6 +916,216 @@ var ReplSession = class ReplSession {
|
|
|
940
916
|
* - Programmatic tool calling (PTC) — expose agent or custom tools inside the REPL
|
|
941
917
|
*/
|
|
942
918
|
const DEFAULT_TOOL_NAME = "eval";
|
|
919
|
+
/**
|
|
920
|
+
* Render the subagent dispatch prompt section for the system message.
|
|
921
|
+
* Ported from the Python `_SUBAGENT_SYSTEM_PROMPT_TEMPLATE`.
|
|
922
|
+
*/
|
|
923
|
+
function renderSubagentPrompt(toolName) {
|
|
924
|
+
return dedent.default`
|
|
925
|
+
|
|
926
|
+
### Dispatching Subagents with \`task\`
|
|
927
|
+
|
|
928
|
+
\`task\` is your primitive for running configured subagents from inside the
|
|
929
|
+
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
|
|
930
|
+
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
|
|
931
|
+
|
|
932
|
+
#### The primitive
|
|
933
|
+
|
|
934
|
+
\`\`\`javascript
|
|
935
|
+
await task({
|
|
936
|
+
description, // full autonomous task prompt
|
|
937
|
+
subagentType, // configured subagent name
|
|
938
|
+
responseSchema, // optional JSON Schema for structured output
|
|
939
|
+
}); // -> Promise<unknown>
|
|
940
|
+
\`\`\`
|
|
941
|
+
|
|
942
|
+
\`task\` runs a full agentic loop for the selected configured subagent. The
|
|
943
|
+
subagent can use whatever tools it was configured with, iterate, inspect
|
|
944
|
+
context, and return one final result. \`subagentType\` is required; use one of
|
|
945
|
+
the configured subagent names.
|
|
946
|
+
|
|
947
|
+
\`description\` is the only prompt the subagent receives for this dispatch. Make
|
|
948
|
+
it complete: include the goal, constraints, relevant context, what to inspect,
|
|
949
|
+
and the exact shape or level of detail you expect back. Each dispatch is
|
|
950
|
+
stateless from the caller's perspective; you cannot send follow-up messages to
|
|
951
|
+
the same subagent run.
|
|
952
|
+
|
|
953
|
+
\`responseSchema\` is optional. When provided, the resolved value is already a
|
|
954
|
+
typed JavaScript value matching the schema. Do not call \`JSON.parse\` unless the
|
|
955
|
+
subagent intentionally returned a JSON string. Dynamic schemas work for
|
|
956
|
+
declarative subagents; runnable-backed subagents reject dynamic schemas because
|
|
957
|
+
their runnable is already compiled.
|
|
958
|
+
|
|
959
|
+
#### Approval model
|
|
960
|
+
|
|
961
|
+
\`task\` dispatches from inside the already-running \`${toolName}\` call. It
|
|
962
|
+
does not route through the parent agent's \`ToolNode\`-managed \`task\` tool and
|
|
963
|
+
does not trigger parent-level \`interrupt_on\` / HITL approval for each dispatch.
|
|
964
|
+
Declarative subagents still honor approval middleware configured inside their
|
|
965
|
+
own spec. If you need approval before launching a subagent from the parent, use
|
|
966
|
+
the normal \`task\` tool outside JavaScript or ensure the \`${toolName}\` call
|
|
967
|
+
itself is approval-gated.
|
|
968
|
+
|
|
969
|
+
#### Mental model
|
|
970
|
+
|
|
971
|
+
Hold your work in JS: an array of items in, an array of results out. Merge each
|
|
972
|
+
dispatch result back onto its item. Multi-stage analysis means: run a pass,
|
|
973
|
+
filter or regroup the array in JS, then run another pass over the survivors.
|
|
974
|
+
|
|
975
|
+
Prefer one \`${toolName}\` call that performs the whole workflow. Splitting the
|
|
976
|
+
workflow across multiple \`${toolName}\` calls costs model turns and forces you to
|
|
977
|
+
re-establish state.
|
|
978
|
+
|
|
979
|
+
#### Fan out with bounded concurrency
|
|
980
|
+
|
|
981
|
+
Dispatch independent work in parallel with \`Promise.all\`, but in explicit
|
|
982
|
+
batches around 10 so you do not launch hundreds of subagents at once. The bridge
|
|
983
|
+
enforces a hard per-REPL cap of 32 concurrent subagent calls.
|
|
984
|
+
|
|
985
|
+
\`\`\`javascript
|
|
986
|
+
const batchSize = 10;
|
|
987
|
+
const reviewed = [];
|
|
988
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
989
|
+
const batch = items.slice(i, i + batchSize);
|
|
990
|
+
reviewed.push(...(await Promise.all(batch.map(async (it) => {
|
|
991
|
+
const result = await task({
|
|
992
|
+
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
|
|
993
|
+
subagentType: "reviewer",
|
|
994
|
+
responseSchema: {
|
|
995
|
+
type: "object",
|
|
996
|
+
properties: {
|
|
997
|
+
vulnerabilities: {
|
|
998
|
+
type: "array",
|
|
999
|
+
items: {
|
|
1000
|
+
type: "object",
|
|
1001
|
+
properties: {
|
|
1002
|
+
type: { type: "string" },
|
|
1003
|
+
line: { type: "number" },
|
|
1004
|
+
evidence: { type: "string" },
|
|
1005
|
+
},
|
|
1006
|
+
required: ["type", "line", "evidence"],
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
required: ["vulnerabilities"],
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
return { ...it, ...result };
|
|
1014
|
+
}))));
|
|
1015
|
+
}
|
|
1016
|
+
\`\`\`
|
|
1017
|
+
|
|
1018
|
+
#### Use parent JS for cheap work; use subagents for agentic work
|
|
1019
|
+
|
|
1020
|
+
Use JavaScript in the parent REPL for deterministic orchestration: joining
|
|
1021
|
+
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
|
|
1022
|
+
If the \`tools.*\` namespace is exposed, also use it to pre-read files or collect
|
|
1023
|
+
shared data once, then pass only the relevant content to each subagent in
|
|
1024
|
+
\`description\`.
|
|
1025
|
+
|
|
1026
|
+
Use \`task\` for work that benefits from an autonomous agentic loop: reading
|
|
1027
|
+
or searching with the subagent's own tools, inspecting multiple files, following
|
|
1028
|
+
leads, making judgment calls, or producing a final synthesized report.
|
|
1029
|
+
|
|
1030
|
+
#### Pre-read shared context in the parent when useful
|
|
1031
|
+
|
|
1032
|
+
If many subagents need the same source list or file content and \`tools.*\` is
|
|
1033
|
+
available, gather that context once in the parent REPL before dispatching:
|
|
1034
|
+
|
|
1035
|
+
\`\`\`javascript
|
|
1036
|
+
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
|
|
1037
|
+
.split("\\n")
|
|
1038
|
+
.filter(Boolean);
|
|
1039
|
+
|
|
1040
|
+
const items = await Promise.all(files.map(async (file) => {
|
|
1041
|
+
const content = await tools.readFile({ file_path: file });
|
|
1042
|
+
return { file, content };
|
|
1043
|
+
}));
|
|
1044
|
+
|
|
1045
|
+
const batchSize = 10;
|
|
1046
|
+
const results = [];
|
|
1047
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
1048
|
+
const batch = items.slice(i, i + batchSize);
|
|
1049
|
+
results.push(...(await Promise.all(batch.map(async (it) => {
|
|
1050
|
+
const finding = await task({
|
|
1051
|
+
description:
|
|
1052
|
+
"Review this file for auth bypasses. Return concrete findings only.\\n\\n" +
|
|
1053
|
+
"File: " + it.file + "\\n\\n" +
|
|
1054
|
+
it.content,
|
|
1055
|
+
subagentType: "reviewer",
|
|
1056
|
+
responseSchema: {
|
|
1057
|
+
type: "object",
|
|
1058
|
+
properties: {
|
|
1059
|
+
findings: { type: "array", items: { type: "object" } },
|
|
1060
|
+
},
|
|
1061
|
+
required: ["findings"],
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
return { ...it, ...finding };
|
|
1065
|
+
}))));
|
|
1066
|
+
}
|
|
1067
|
+
\`\`\`
|
|
1068
|
+
|
|
1069
|
+
#### Compose multiple stages
|
|
1070
|
+
|
|
1071
|
+
Filter the array in JS between passes. For example: first ask subagents for a
|
|
1072
|
+
cheap classification, filter to the risky items, then dispatch deeper reviews
|
|
1073
|
+
only for those items.
|
|
1074
|
+
|
|
1075
|
+
\`\`\`javascript
|
|
1076
|
+
const tagged = [];
|
|
1077
|
+
for (let i = 0; i < items.length; i += 10) {
|
|
1078
|
+
const batch = items.slice(i, i + 10);
|
|
1079
|
+
tagged.push(...(await Promise.all(batch.map(async (it) => {
|
|
1080
|
+
const tag = await task({
|
|
1081
|
+
description: "Classify " + it.file + " as handler, util, test, or config.",
|
|
1082
|
+
subagentType: "reviewer",
|
|
1083
|
+
responseSchema: {
|
|
1084
|
+
type: "object",
|
|
1085
|
+
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
|
|
1086
|
+
required: ["kind", "risky"],
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
return { ...it, ...tag };
|
|
1090
|
+
}))));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
|
|
1094
|
+
const deepReviews = [];
|
|
1095
|
+
for (let i = 0; i < riskyHandlers.length; i += 10) {
|
|
1096
|
+
const batch = riskyHandlers.slice(i, i + 10);
|
|
1097
|
+
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
|
|
1098
|
+
const review = await task({
|
|
1099
|
+
description: "Deep security review of " + it.file + ". Cite line numbers.",
|
|
1100
|
+
subagentType: "reviewer",
|
|
1101
|
+
});
|
|
1102
|
+
return { ...it, review };
|
|
1103
|
+
}))));
|
|
1104
|
+
}
|
|
1105
|
+
\`\`\`
|
|
1106
|
+
|
|
1107
|
+
#### Get results out without flooding your context
|
|
1108
|
+
|
|
1109
|
+
Keep large result sets in JS variables. Do not \`console.log\` the full result set.
|
|
1110
|
+
If \`tools.writeFile\` is exposed, persist structured output from inside the eval:
|
|
1111
|
+
|
|
1112
|
+
\`\`\`javascript
|
|
1113
|
+
await tools.writeFile({
|
|
1114
|
+
file_path: "/results/subagent-output.json",
|
|
1115
|
+
content: JSON.stringify(deepReviews),
|
|
1116
|
+
});
|
|
1117
|
+
\`\`\`
|
|
1118
|
+
|
|
1119
|
+
Otherwise return a compact summary or a small slice of the results, not the
|
|
1120
|
+
entire intermediate dataset.
|
|
1121
|
+
|
|
1122
|
+
#### Across evals
|
|
1123
|
+
|
|
1124
|
+
Variables persist according to the interpreter persistence mode above, but
|
|
1125
|
+
re-establish what you need in each eval. Doing the whole workflow in one
|
|
1126
|
+
\`${toolName}\` call is usually simplest.
|
|
1127
|
+
`;
|
|
1128
|
+
}
|
|
943
1129
|
function renderReplSystemPrompt(opts) {
|
|
944
1130
|
return dedent.default`
|
|
945
1131
|
### Interpreter
|
|
@@ -947,7 +1133,7 @@ function renderReplSystemPrompt(opts) {
|
|
|
947
1133
|
An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
|
|
948
1134
|
- State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
|
|
949
1135
|
- Top-level \`await\` works; Promises resolve before the call returns.
|
|
950
|
-
-
|
|
1136
|
+
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (\`fetch\`, \`require\`, \`fs\`, \`process\`, real \`Date.now()\` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the \`tools.*\` namespace when it is exposed (see below); without it, the REPL is pure computation.
|
|
951
1137
|
- Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
|
|
952
1138
|
- \`console.log\` output is captured and returned alongside the result.
|
|
953
1139
|
`;
|
|
@@ -1004,34 +1190,11 @@ function resolveToolList(items, agentTools) {
|
|
|
1004
1190
|
});
|
|
1005
1191
|
}
|
|
1006
1192
|
/**
|
|
1007
|
-
*
|
|
1008
|
-
* both into the session. Short-circuits with a `SkillNotAvailable` error if
|
|
1009
|
-
* the source references skills the agent doesn't have.
|
|
1010
|
-
*/
|
|
1011
|
-
async function prepareSkillsForEval(session, skillsBackend, code) {
|
|
1012
|
-
const taskInput = (0, _langchain_langgraph.getCurrentTaskInput)();
|
|
1013
|
-
const metadata = taskInput?.skillsMetadata ?? [];
|
|
1014
|
-
const referenced = scanSkillReferences(code);
|
|
1015
|
-
if (referenced.size > 0) {
|
|
1016
|
-
const known = new Set(metadata.map((m) => m.name));
|
|
1017
|
-
const missing = [];
|
|
1018
|
-
for (const name of referenced) if (!known.has(name)) missing.push(name);
|
|
1019
|
-
if (missing.length > 0) {
|
|
1020
|
-
session.setSkillsContext(void 0);
|
|
1021
|
-
return formatSkillNotAvailable(missing);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
const resolved = await (0, deepagents.resolveBackend)(skillsBackend, { state: taskInput });
|
|
1025
|
-
session.setSkillsContext({
|
|
1026
|
-
metadata,
|
|
1027
|
-
backend: resolved
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
/**
|
|
1031
|
-
* Create the REPL middleware.
|
|
1193
|
+
* Create the Code Interpreter middleware.
|
|
1032
1194
|
*/
|
|
1033
|
-
function
|
|
1034
|
-
const { ptc, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null,
|
|
1195
|
+
function createCodeInterpreterMiddleware(options = {}) {
|
|
1196
|
+
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;
|
|
1197
|
+
const maxSubagentConcurrency = subagents ? 32 : 0;
|
|
1035
1198
|
if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
|
|
1036
1199
|
const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
|
|
1037
1200
|
toolName,
|
|
@@ -1041,27 +1204,56 @@ function createREPLMiddleware(options = {}) {
|
|
|
1041
1204
|
const middlewareId = crypto.randomUUID();
|
|
1042
1205
|
let cachedPtcPrompt = null;
|
|
1043
1206
|
let ptcTools = [];
|
|
1207
|
+
let taskTool = null;
|
|
1044
1208
|
function filterToolsForPtc(allTools) {
|
|
1045
1209
|
if (!ptc) return [];
|
|
1046
1210
|
return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
|
|
1047
1211
|
}
|
|
1212
|
+
function findTaskTool(tools) {
|
|
1213
|
+
return tools.find((t) => t.name === "task") ?? null;
|
|
1214
|
+
}
|
|
1215
|
+
function createBridgeDispatch(subagentTaskTool, config) {
|
|
1216
|
+
return async (input) => {
|
|
1217
|
+
const hasSchema = input.responseSchema != null;
|
|
1218
|
+
if (hasSchema) validateResponseSchema(input.responseSchema);
|
|
1219
|
+
const toolConfig = {
|
|
1220
|
+
...config,
|
|
1221
|
+
configurable: {
|
|
1222
|
+
...config.configurable,
|
|
1223
|
+
...hasSchema && { [deepagents.SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY]: input.responseSchema }
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
const result = await subagentTaskTool.invoke({
|
|
1227
|
+
description: input.description,
|
|
1228
|
+
subagent_type: input.subagentType
|
|
1229
|
+
}, toolConfig);
|
|
1230
|
+
if (hasSchema && typeof result === "string") try {
|
|
1231
|
+
return JSON.parse(result);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return result;
|
|
1234
|
+
}
|
|
1235
|
+
return result;
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1048
1238
|
return (0, langchain.createMiddleware)({
|
|
1049
|
-
name: "
|
|
1239
|
+
name: "CodeInterpreterMiddleware",
|
|
1050
1240
|
tools: [(0, langchain.tool)(async (input, config) => {
|
|
1051
|
-
const
|
|
1241
|
+
const threadId = config.configurable?.thread_id || "__default__";
|
|
1242
|
+
const sessionKey = `${threadId}:${middlewareId}`;
|
|
1052
1243
|
const session = ReplSession.getOrCreate(sessionKey, {
|
|
1053
1244
|
memoryLimitBytes,
|
|
1054
1245
|
maxStackSizeBytes,
|
|
1055
1246
|
maxPtcCalls,
|
|
1056
1247
|
tools: ptcTools,
|
|
1057
|
-
skillsEnabled: skillsBackend !== void 0,
|
|
1058
1248
|
maxResultChars,
|
|
1059
|
-
captureConsole
|
|
1249
|
+
captureConsole,
|
|
1250
|
+
sessionId: threadId,
|
|
1251
|
+
subagentBridge: taskTool && maxSubagentConcurrency > 0 ? {
|
|
1252
|
+
dispatch: createBridgeDispatch(taskTool, config),
|
|
1253
|
+
maxConcurrency: maxSubagentConcurrency
|
|
1254
|
+
} : void 0
|
|
1060
1255
|
});
|
|
1061
|
-
if (
|
|
1062
|
-
const setupError = await prepareSkillsForEval(session, skillsBackend, input.code);
|
|
1063
|
-
if (setupError !== void 0) return setupError;
|
|
1064
|
-
}
|
|
1256
|
+
if (taskTool && maxSubagentConcurrency > 0) session.updateBridgeDispatch(createBridgeDispatch(taskTool, config));
|
|
1065
1257
|
return formatReplResult(await session.eval(input.code, executionTimeoutMs));
|
|
1066
1258
|
}, {
|
|
1067
1259
|
name: toolName,
|
|
@@ -1069,15 +1261,17 @@ function createREPLMiddleware(options = {}) {
|
|
|
1069
1261
|
Evaluate TypeScript/JavaScript code in a sandboxed REPL. State persists across calls.
|
|
1070
1262
|
Use console.log() for output. Returns the result of the last expression.
|
|
1071
1263
|
If file or other tools are available, call them via the tools namespace: await tools.readFile({ path }).
|
|
1072
|
-
If skills are configured, dynamically import them: await import("@/skills/<name>").
|
|
1073
1264
|
`,
|
|
1074
1265
|
metadata: { ls_code_input_language: "javascript" },
|
|
1075
1266
|
schema: zod_v4.z.object({ code: zod_v4.z.string().describe("TypeScript/JavaScript code to evaluate in the sandboxed REPL") })
|
|
1076
1267
|
})],
|
|
1077
1268
|
wrapModelCall: async (request, handler) => {
|
|
1078
|
-
|
|
1269
|
+
const agentTools = request.tools || [];
|
|
1270
|
+
ptcTools = filterToolsForPtc(agentTools);
|
|
1271
|
+
if (!taskTool && maxSubagentConcurrency > 0) taskTool = findTaskTool(agentTools);
|
|
1079
1272
|
if (ptcTools.length > 0 && !cachedPtcPrompt) cachedPtcPrompt = await generatePtcPrompt(ptcTools);
|
|
1080
|
-
const
|
|
1273
|
+
const subagentPrompt = taskTool && maxSubagentConcurrency > 0 ? renderSubagentPrompt(toolName) : "";
|
|
1274
|
+
const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(subagentPrompt).concat(cachedPtcPrompt || "");
|
|
1081
1275
|
return handler({
|
|
1082
1276
|
...request,
|
|
1083
1277
|
systemMessage
|
|
@@ -1094,17 +1288,13 @@ exports.DEFAULT_EXECUTION_TIMEOUT = DEFAULT_EXECUTION_TIMEOUT;
|
|
|
1094
1288
|
exports.DEFAULT_MAX_PTC_CALLS = DEFAULT_MAX_PTC_CALLS;
|
|
1095
1289
|
exports.DEFAULT_MAX_STACK_SIZE = DEFAULT_MAX_STACK_SIZE;
|
|
1096
1290
|
exports.DEFAULT_MEMORY_LIMIT = DEFAULT_MEMORY_LIMIT;
|
|
1097
|
-
exports.MAX_SKILL_BUNDLE_BYTES = MAX_SKILL_BUNDLE_BYTES;
|
|
1098
1291
|
exports.PTCCallBudgetExceededError = PTCCallBudgetExceededError;
|
|
1099
1292
|
exports.ReplSession = ReplSession;
|
|
1100
|
-
exports.
|
|
1101
|
-
exports.createREPLMiddleware = createREPLMiddleware;
|
|
1293
|
+
exports.createCodeInterpreterMiddleware = createCodeInterpreterMiddleware;
|
|
1102
1294
|
exports.formatReplResult = formatReplResult;
|
|
1103
|
-
exports.formatSkillNotAvailable = formatSkillNotAvailable;
|
|
1104
|
-
exports.loadSkill = loadSkill;
|
|
1105
|
-
exports.scanSkillReferences = scanSkillReferences;
|
|
1106
1295
|
exports.stripTypeSyntax = stripTypeSyntax;
|
|
1107
1296
|
exports.toCamelCase = toCamelCase;
|
|
1108
1297
|
exports.transformForEval = transformForEval;
|
|
1298
|
+
exports.validateResponseSchema = validateResponseSchema;
|
|
1109
1299
|
|
|
1110
1300
|
//# sourceMappingURL=index.cjs.map
|