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