@langchain/quickjs 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +665 -339
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -50
- package/dist/index.d.ts +206 -50
- package/dist/index.js +654 -336
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
const DEFAULT_MEMORY_LIMIT =
|
|
449
|
+
const DEFAULT_MEMORY_LIMIT = 64 * 1024 * 1024;
|
|
262
450
|
const DEFAULT_MAX_STACK_SIZE = 320 * 1024;
|
|
263
|
-
const DEFAULT_EXECUTION_TIMEOUT =
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
451
|
+
const DEFAULT_EXECUTION_TIMEOUT = 5e3;
|
|
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)} };`;
|
|
271
461
|
}
|
|
272
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);
|
|
495
|
+
}
|
|
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, captureConsole = true } = 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;
|
|
311
|
-
this.
|
|
312
|
-
if (
|
|
313
|
-
this.
|
|
314
|
-
if (
|
|
592
|
+
this.consoleBuffer = new ConsoleBuffer(maxResultChars);
|
|
593
|
+
if (captureConsole) this.setupConsole();
|
|
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 `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));
|
|
@@ -574,74 +906,26 @@ var ReplSession = class ReplSession {
|
|
|
574
906
|
//#endregion
|
|
575
907
|
//#region src/middleware.ts
|
|
576
908
|
/**
|
|
577
|
-
*
|
|
909
|
+
* REPL middleware for deepagents.
|
|
578
910
|
*
|
|
579
|
-
* Provides
|
|
911
|
+
* Provides an `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
|
-
|
|
587
|
-
|
|
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
|
-
];
|
|
599
|
-
const REPL_SYSTEM_PROMPT = dedent`
|
|
600
|
-
## TypeScript/JavaScript REPL (\`js_eval\`)
|
|
601
|
-
|
|
602
|
-
You have access to a sandboxed TypeScript/JavaScript REPL running in an isolated interpreter.
|
|
603
|
-
TypeScript syntax (type annotations, interfaces, generics, \`as\` casts) is supported and stripped at evaluation time.
|
|
604
|
-
Variables, functions, and closures persist across calls within the same session.
|
|
605
|
-
|
|
606
|
-
### Hard rules
|
|
607
|
-
|
|
608
|
-
- **No network, no filesystem** — only the helpers below. Do not attempt \`fetch\`, \`require\`, or \`import\`.
|
|
609
|
-
- **Cite your sources** — when reporting values from files, include the path and key/index so the user can verify.
|
|
610
|
-
- **Use console.log()** for output — it is captured and returned. \`console.warn()\` and \`console.error()\` are also available.
|
|
611
|
-
- **Reuse state from previous cells** — variables, functions, and results from earlier \`js_eval\` calls persist across calls. Reference them by name in follow-up cells instead of re-embedding data as inline JSON literals.
|
|
612
|
-
|
|
613
|
-
### First-time usage
|
|
614
|
-
|
|
615
|
-
\`\`\`typescript
|
|
616
|
-
// Read a file from the agent's virtual filesystem
|
|
617
|
-
const raw: string = await readFile("/data.json");
|
|
618
|
-
const data = JSON.parse(raw) as { n: number };
|
|
619
|
-
console.log(data);
|
|
620
|
-
|
|
621
|
-
// Write results back
|
|
622
|
-
await writeFile("/output.txt", JSON.stringify({ result: data.n }));
|
|
623
|
-
\`\`\`
|
|
624
|
-
|
|
625
|
-
### API Reference — built-in globals
|
|
626
|
-
|
|
627
|
-
\`\`\`typescript
|
|
628
|
-
/**
|
|
629
|
-
* Read a file from the agent's virtual filesystem. Throws if the file does not exist.
|
|
630
|
-
*/
|
|
631
|
-
async readFile(path: string): Promise<string>
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Write a file to the agent's virtual filesystem.
|
|
635
|
-
*/
|
|
636
|
-
async writeFile(path: string, content: string): Promise<void>
|
|
637
|
-
\`\`\`
|
|
638
|
-
|
|
639
|
-
### Limitations
|
|
914
|
+
* - Programmatic tool calling (PTC) — expose agent or custom tools inside the REPL
|
|
915
|
+
*/
|
|
916
|
+
const DEFAULT_TOOL_NAME = "eval";
|
|
917
|
+
function renderReplSystemPrompt(opts) {
|
|
918
|
+
return dedent`
|
|
919
|
+
### Interpreter
|
|
640
920
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
921
|
+
An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
|
|
922
|
+
- State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
|
|
923
|
+
- Top-level \`await\` works; Promises resolve before the call returns.
|
|
924
|
+
- Sandboxed: no filesystem, no stdlib, no network, no real clock, no \`fetch\`, no \`require\`.
|
|
925
|
+
- Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
|
|
926
|
+
- \`console.log\` output is captured and returned alongside the result.
|
|
927
|
+
`;
|
|
928
|
+
}
|
|
645
929
|
/**
|
|
646
930
|
* Generate the PTC API Reference section for the system prompt.
|
|
647
931
|
*/
|
|
@@ -678,74 +962,108 @@ async function generatePtcPrompt(tools) {
|
|
|
678
962
|
`;
|
|
679
963
|
}
|
|
680
964
|
/**
|
|
681
|
-
*
|
|
965
|
+
* Resolves a mixed list of tool names and tool instances into a flat list of
|
|
966
|
+
* StructuredToolInterface objects. Strings are looked up by name in agentTools;
|
|
967
|
+
* instances are included directly without requiring agent registration. Strings
|
|
968
|
+
* that don't match any agent tool are silently omitted.
|
|
682
969
|
*/
|
|
683
|
-
function
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
970
|
+
function resolveToolList(items, agentTools) {
|
|
971
|
+
const agentByName = new Map(agentTools.map((t) => [t.name, t]));
|
|
972
|
+
return items.flatMap((item) => {
|
|
973
|
+
if (typeof item === "string") {
|
|
974
|
+
const found = agentByName.get(item);
|
|
975
|
+
return found ? [found] : [];
|
|
976
|
+
}
|
|
977
|
+
return [item];
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Pull `skillsMetadata` from the task input, resolve the backend, and push
|
|
982
|
+
* both into the session. Short-circuits with a `SkillNotAvailable` error if
|
|
983
|
+
* the source references skills the agent doesn't have.
|
|
984
|
+
*/
|
|
985
|
+
async function prepareSkillsForEval(session, skillsBackend, code) {
|
|
986
|
+
const taskInput = getCurrentTaskInput();
|
|
987
|
+
const metadata = taskInput?.skillsMetadata ?? [];
|
|
988
|
+
const referenced = scanSkillReferences(code);
|
|
989
|
+
if (referenced.size > 0) {
|
|
990
|
+
const known = new Set(metadata.map((m) => m.name));
|
|
991
|
+
const missing = [];
|
|
992
|
+
for (const name of referenced) if (!known.has(name)) missing.push(name);
|
|
993
|
+
if (missing.length > 0) {
|
|
994
|
+
session.setSkillsContext(void 0);
|
|
995
|
+
return formatSkillNotAvailable(missing);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const resolved = await resolveBackend(skillsBackend, { state: taskInput });
|
|
999
|
+
session.setSkillsContext({
|
|
1000
|
+
metadata,
|
|
1001
|
+
backend: resolved
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Create the REPL middleware.
|
|
1006
|
+
*/
|
|
1007
|
+
function createREPLMiddleware(options = {}) {
|
|
1008
|
+
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, toolName = DEFAULT_TOOL_NAME, captureConsole = true } = options;
|
|
1009
|
+
if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
|
|
1010
|
+
const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
|
|
1011
|
+
toolName,
|
|
1012
|
+
timeout: executionTimeoutMs / 1e3,
|
|
1013
|
+
memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024))
|
|
1014
|
+
});
|
|
1015
|
+
const middlewareId = crypto.randomUUID();
|
|
687
1016
|
let cachedPtcPrompt = null;
|
|
688
1017
|
let ptcTools = [];
|
|
689
1018
|
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 [];
|
|
1019
|
+
if (!ptc) return [];
|
|
1020
|
+
return resolveToolList(ptc, allTools.filter((t) => t.name !== toolName));
|
|
709
1021
|
}
|
|
710
1022
|
return createMiddleware({
|
|
711
|
-
name: "
|
|
1023
|
+
name: "REPLMiddleware",
|
|
712
1024
|
tools: [tool(async (input, config) => {
|
|
713
|
-
const
|
|
714
|
-
const
|
|
715
|
-
...config,
|
|
716
|
-
state: getCurrentTaskInput(config) || {}
|
|
717
|
-
});
|
|
718
|
-
const session = ReplSession.getOrCreate(threadId, {
|
|
1025
|
+
const sessionKey = `${config.configurable?.thread_id || "__default__"}:${middlewareId}`;
|
|
1026
|
+
const session = ReplSession.getOrCreate(sessionKey, {
|
|
719
1027
|
memoryLimitBytes,
|
|
720
1028
|
maxStackSizeBytes,
|
|
721
|
-
|
|
722
|
-
tools: ptcTools
|
|
1029
|
+
maxPtcCalls,
|
|
1030
|
+
tools: ptcTools,
|
|
1031
|
+
skillsEnabled: skillsBackend !== void 0,
|
|
1032
|
+
maxResultChars,
|
|
1033
|
+
captureConsole
|
|
723
1034
|
});
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1035
|
+
if (skillsBackend !== void 0) {
|
|
1036
|
+
const setupError = await prepareSkillsForEval(session, skillsBackend, input.code);
|
|
1037
|
+
if (setupError !== void 0) return setupError;
|
|
1038
|
+
}
|
|
1039
|
+
return formatReplResult(await session.eval(input.code, executionTimeoutMs));
|
|
727
1040
|
}, {
|
|
728
|
-
name:
|
|
1041
|
+
name: toolName,
|
|
729
1042
|
description: dedent`
|
|
730
1043
|
Evaluate TypeScript/JavaScript code in a sandboxed REPL. State persists across calls.
|
|
731
|
-
Use readFile(path) and writeFile(path, content) for file access.
|
|
732
1044
|
Use console.log() for output. Returns the result of the last expression.
|
|
1045
|
+
If file or other tools are available, call them via the tools namespace: await tools.readFile({ path }).
|
|
1046
|
+
If skills are configured, dynamically import them: await import("@/skills/<name>").
|
|
733
1047
|
`,
|
|
1048
|
+
metadata: { ls_code_input_language: "javascript" },
|
|
734
1049
|
schema: z.object({ code: z.string().describe("TypeScript/JavaScript code to evaluate in the sandboxed REPL") })
|
|
735
1050
|
})],
|
|
736
1051
|
wrapModelCall: async (request, handler) => {
|
|
737
|
-
|
|
738
|
-
ptcTools = usePtc ? filterToolsForPtc(agentTools) : [];
|
|
1052
|
+
ptcTools = filterToolsForPtc(request.tools || []);
|
|
739
1053
|
if (ptcTools.length > 0 && !cachedPtcPrompt) cachedPtcPrompt = await generatePtcPrompt(ptcTools);
|
|
740
1054
|
const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(cachedPtcPrompt || "");
|
|
741
1055
|
return handler({
|
|
742
1056
|
...request,
|
|
743
1057
|
systemMessage
|
|
744
1058
|
});
|
|
1059
|
+
},
|
|
1060
|
+
afterAgent: async (_state, runtime) => {
|
|
1061
|
+
const sessionKey = `${runtime.configurable?.thread_id ?? "__default__"}:${middlewareId}`;
|
|
1062
|
+
ReplSession.deleteSession(sessionKey);
|
|
745
1063
|
}
|
|
746
1064
|
});
|
|
747
1065
|
}
|
|
748
1066
|
//#endregion
|
|
749
|
-
export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT,
|
|
1067
|
+
export { DEFAULT_EXECUTION_TIMEOUT, DEFAULT_MAX_PTC_CALLS, DEFAULT_MAX_STACK_SIZE, DEFAULT_MEMORY_LIMIT, MAX_SKILL_BUNDLE_BYTES, PTCCallBudgetExceededError, ReplSession, SKILL_MODULE_EXTENSIONS, createREPLMiddleware, formatReplResult, formatSkillNotAvailable, loadSkill, scanSkillReferences, stripTypeSyntax, toCamelCase, transformForEval };
|
|
750
1068
|
|
|
751
1069
|
//# sourceMappingURL=index.js.map
|