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