pi-supernova 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +244 -54
- package/docs/CHANGELOG.md +111 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +23 -33
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +18 -2
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -242
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +7 -2
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +35 -68
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +11 -10
- package/src/runtime/runtime.js +94 -261
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {parseBatchPayload,batchTimeoutMs} from "./batch-input.js";
|
|
2
|
+
import { isString } from "../shared/decode.js";
|
|
2
3
|
import { truncateChars } from "../output/format.js";
|
|
3
4
|
|
|
4
5
|
const textOf = result => (Array.isArray(result?.content) ? result.content : []).filter(block => block?.type === "text").map(block => block.text).join("\n");
|
|
@@ -28,66 +29,6 @@ export function programBatchText(results, total, stopped = "", failed = 0) {
|
|
|
28
29
|
}).join("");
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function assertProgramEntry(p, defaults = {}) {
|
|
32
|
-
const source = p?.code === undefined && p?.file === undefined ? defaults : p;
|
|
33
|
-
|
|
34
|
-
if (!isObject(p) || Array.isArray(p) || Object.keys(p).some(key => !["code","file","data"].includes(key)) ||
|
|
35
|
-
((source.code === undefined) === (source.file === undefined)) || !isString(source.code ?? source.file) || !(source.code ?? source.file).trim()) {
|
|
36
|
-
throw new Error("each program requires code OR file (own or shared), with optional data; no nested batches or per-entry timeouts; no programs ran");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const objectData = value => isObject(value) && !Array.isArray(value);
|
|
41
|
-
|
|
42
|
-
function applyBatchDefaults(parsed, mergeData) {
|
|
43
|
-
if (mergeData && !objectData(parsed.data)) throw new Error("mergeData requires top-level object data; no programs ran");
|
|
44
|
-
const source = parsed.code !== undefined ? {code:parsed.code} : parsed.file !== undefined ? {file:parsed.file} : {};
|
|
45
|
-
|
|
46
|
-
return parsed.programs.map(program => {
|
|
47
|
-
assertProgramEntry(program, source);
|
|
48
|
-
const entry = program.code === undefined && program.file === undefined ? {...source,...program} : program;
|
|
49
|
-
|
|
50
|
-
if (mergeData) {
|
|
51
|
-
if (program.data !== undefined && !objectData(program.data)) throw new Error("mergeData requires object data in every explicit entry; no programs ran");
|
|
52
|
-
// Shallow own-property overlay, including literal __proto__ keys. The
|
|
53
|
-
// runtime snapshots data again per guest; no mutable heap is shared.
|
|
54
|
-
entry.data = {...parsed.data,...program.data};
|
|
55
|
-
} else if (program.data === undefined && Object.hasOwn(parsed,"data")) entry.data = parsed.data;
|
|
56
|
-
|
|
57
|
-
return entry;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseBatchPayload(params, config) {
|
|
62
|
-
if (!Array.isArray(params.programs) || !params.programs.length || params.programs.length > 32) throw new Error("programs requires 1..32 entries; no programs ran");
|
|
63
|
-
if (params.mergeData !== undefined && params.mergeData !== true && params.mergeData !== false) throw new Error("mergeData must be boolean; no programs ran");
|
|
64
|
-
const defaults = Object.fromEntries(["code","file","data"].filter(key => params[key] !== undefined).map(key => [key,params[key]]));
|
|
65
|
-
|
|
66
|
-
if (defaults.code !== undefined || defaults.file !== undefined) assertProgramEntry(defaults);
|
|
67
|
-
for (const p of params.programs) assertProgramEntry(p, defaults);
|
|
68
|
-
const hasDefaults = Object.keys(defaults).length > 0;
|
|
69
|
-
let encoded;
|
|
70
|
-
|
|
71
|
-
try { encoded = JSON.stringify(hasDefaults ? {programs:params.programs,...defaults} : params.programs); } catch { throw new Error("programs and defaults must be JSON-serializable; no programs ran"); }
|
|
72
|
-
|
|
73
|
-
if (encoded.length > (config.maxCodeChars ?? 48000)) throw new Error("programs JSON exceeds the code character budget (including shared code/file/data); no programs ran");
|
|
74
|
-
const parsed = hasDefaults ? JSON.parse(encoded) : {programs:JSON.parse(encoded)};
|
|
75
|
-
|
|
76
|
-
if (Object.hasOwn(defaults,"data") && !Object.hasOwn(parsed,"data")) throw new Error("data must be JSON-serializable; no programs ran");
|
|
77
|
-
|
|
78
|
-
// Validate and expand every entry before executing any. Defaults count once
|
|
79
|
-
// against admission, not once for each independent guest receiving a copy.
|
|
80
|
-
return applyBatchDefaults(parsed, params.mergeData === true);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function batchTimeoutMs(params, config) {
|
|
84
|
-
const requestedTimeout = params.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs);
|
|
85
|
-
|
|
86
|
-
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error("program batch timeoutMs must be a positive finite number");
|
|
87
|
-
|
|
88
|
-
return requestedTimeout;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
32
|
const MAX_PARALLEL_PROGRAMS = 8;
|
|
92
33
|
|
|
93
34
|
class ProgramBatch {
|
|
@@ -136,7 +77,7 @@ class ProgramBatch {
|
|
|
136
77
|
}
|
|
137
78
|
|
|
138
79
|
collectImages(result, i) {
|
|
139
|
-
for (const block of
|
|
80
|
+
for (const block of imageBlocks(result)) {
|
|
140
81
|
this.imageBytes += Buffer.byteLength(block.data,"base64");
|
|
141
82
|
|
|
142
83
|
if (this.images.length >= 16 || this.imageBytes > 20*1024*1024) { this.imageDropped = true; continue; }
|
|
@@ -156,16 +97,26 @@ class ProgramBatch {
|
|
|
156
97
|
this.collectImages(result, i);
|
|
157
98
|
}
|
|
158
99
|
|
|
100
|
+
parallelBudgetStop(settled) {
|
|
101
|
+
const results = settled.filter(Boolean);
|
|
102
|
+
const {images, bytes} = imageTotals(settled);
|
|
103
|
+
let kind;
|
|
104
|
+
if (images > 16 || bytes > 20 * 1024 * 1024) kind = "image";
|
|
105
|
+
else if (results.some(result => result.details?.logTruncated) || results.reduce((n, result) => n + (result.details?.logs?.length ?? 0), 0) > (this.config.maxLogLines ?? 100)) kind = "log";
|
|
106
|
+
return kind ? "batch " + kind + " budget exceeded; completed commits remain" : "";
|
|
107
|
+
}
|
|
108
|
+
|
|
159
109
|
async runParallel() {
|
|
160
110
|
const limit = Math.min(this.programs.length, MAX_PARALLEL_PROGRAMS);
|
|
161
111
|
const settled = Array.from({ length: this.programs.length });
|
|
162
112
|
let next = 0;
|
|
163
113
|
|
|
164
114
|
await Promise.all(Array.from({length: limit}, async () => {
|
|
165
|
-
while (next < this.programs.length && !this.combined.aborted && performance.now() < this.deadline) {
|
|
115
|
+
while (next < this.programs.length && !this.stopped && !this.combined.aborted && performance.now() < this.deadline) {
|
|
166
116
|
const i = next++;
|
|
167
117
|
settled[i] = await this.runOne(this.programs[i], i);
|
|
168
118
|
this.live[i] = [];
|
|
119
|
+
this.stopped ||= this.parallelBudgetStop(settled);
|
|
169
120
|
}
|
|
170
121
|
}));
|
|
171
122
|
|
|
@@ -174,7 +125,7 @@ class ProgramBatch {
|
|
|
174
125
|
this.takeSettled(settled[i], i);
|
|
175
126
|
}
|
|
176
127
|
|
|
177
|
-
if (settled.includes(undefined) || this.combined.aborted || performance.now() >= this.deadline) this.stopped = "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
128
|
+
if ((!this.stopped && settled.includes(undefined)) || this.combined.aborted || performance.now() >= this.deadline) this.stopped = "batch deadline or cancellation; earlier commits remain" + this.deadlineNote();
|
|
178
129
|
}
|
|
179
130
|
|
|
180
131
|
sequentialStop(result, i) {
|
|
@@ -183,9 +134,8 @@ class ProgramBatch {
|
|
|
183
134
|
if (this.imageDropped) stopped = "batch image budget exceeded; remaining programs did not run";
|
|
184
135
|
|
|
185
136
|
if (result.details?.ok === false) stopped = "program " + (i+1) + " failed; remaining programs did not run; earlier commits remain";
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (text.length + this.imageTextChars() > this.config.maxReturnChars || result.details?.returnTruncated) stopped ||= "batch output budget exceeded; remaining programs did not run; earlier commits remain";
|
|
137
|
+
// Display clipping is not an execution failure. Finish every requested entry
|
|
138
|
+
// unless a real execution/resource limit stops it; boundedText caps delivery.
|
|
189
139
|
|
|
190
140
|
if (result.details?.logTruncated) stopped ||= "batch log budget exceeded; remaining programs did not run; earlier commits remain";
|
|
191
141
|
|
|
@@ -222,6 +172,8 @@ class ProgramBatch {
|
|
|
222
172
|
const failed = this.results.filter(result => result.details?.ok === false).length;
|
|
223
173
|
const bounded = this.boundedText(failed);
|
|
224
174
|
const content = [{type:"text",text:bounded.text}];
|
|
175
|
+
const logs = this.results.flatMap(result => result.details?.logs ?? []);
|
|
176
|
+
const logLimit = this.config.maxLogLines ?? 100;
|
|
225
177
|
this.images.forEach((image,i) => content.push({type:"text",text:this.imageLabels[i]},image));
|
|
226
178
|
|
|
227
179
|
// Return a typed stop report instead of throwing away earlier results/images.
|
|
@@ -230,7 +182,7 @@ class ProgramBatch {
|
|
|
230
182
|
programs:this.results,attempted:this.results.length,total:this.programs.length,stopped:this.stopped,parallel:this.parallel,
|
|
231
183
|
result:bounded.truncated ? bounded.text : this.results.map(result=>result.details?.result),
|
|
232
184
|
returnTruncated:bounded.truncated || this.results.some(result=>result.details?.returnTruncated),
|
|
233
|
-
logTruncated:this.results.some(result=>result.details?.logTruncated),logs:
|
|
185
|
+
logTruncated:logs.length > logLimit || this.results.some(result=>result.details?.logTruncated),logs:logs.slice(0,logLimit),trace:this.trace,mutations:mutationTotals(this.results)}};
|
|
234
186
|
}
|
|
235
187
|
|
|
236
188
|
async run() {
|
|
@@ -249,3 +201,18 @@ export async function runProgramBatch(id, params, signal, onUpdate, ctx, config,
|
|
|
249
201
|
|
|
250
202
|
return new ProgramBatch(id, params, signal, onUpdate, ctx, config, execute, programs, batchTimeoutMs(params, config)).run();
|
|
251
203
|
}
|
|
204
|
+
|
|
205
|
+
function imageBlocks(result) {
|
|
206
|
+
return (Array.isArray(result?.content) ? result.content : []).filter(block => block?.type === "image" && isString(block.data));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function imageTotals(settled) {
|
|
210
|
+
let images = 0, bytes = 0;
|
|
211
|
+
for (const result of settled) {
|
|
212
|
+
for (const block of imageBlocks(result)) {
|
|
213
|
+
images++;
|
|
214
|
+
bytes += Buffer.byteLength(block.data, "base64");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {images, bytes};
|
|
218
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readLimitedBytes } from "../fs/file-io.js";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import { resolveWorkspacePath } from "../fs/workspace.js";
|
|
3
4
|
|
|
@@ -21,22 +22,13 @@ export async function readProgramFile(file, cwd, maxChars, signal) {
|
|
|
21
22
|
const maxBytes = chars * 3;
|
|
22
23
|
const tooLarge = () => new Error("code exceeds " + chars + " characters; split the program");
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
const chunks = [];
|
|
26
|
-
let bytes = 0;
|
|
27
|
-
|
|
28
|
-
for await (const chunk of handle.createReadStream({ end: maxBytes, autoClose: false, signal })) {
|
|
29
|
-
bytes += chunk.length;
|
|
30
|
-
|
|
31
|
-
if (bytes > maxBytes) throw tooLarge();
|
|
32
|
-
chunks.push(chunk);
|
|
33
|
-
}
|
|
25
|
+
const bytes = await readLimitedBytes(handle, stat, maxBytes, file, signal, tooLarge);
|
|
34
26
|
|
|
35
27
|
signal?.throwIfAborted();
|
|
36
28
|
// Do not silently replace invalid bytes in executable source. Preserve BOMs.
|
|
37
29
|
let code;
|
|
38
30
|
|
|
39
|
-
try { code = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
31
|
+
try { code = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
40
32
|
catch { throw new Error("program file " + file + " is not valid UTF-8 (encoded data could not be decoded); save it as UTF-8 text"); }
|
|
41
33
|
|
|
42
34
|
if (code.length > chars) throw tooLarge();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {parse} from 'acorn';
|
|
2
|
+
import {isObject,isString} from '../shared/decode.js';
|
|
3
|
+
import {guestImportMessage,isDeniedGuestImport} from './guest-deny-imports.js';
|
|
4
|
+
|
|
5
|
+
const PARSE_OPTIONS = { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true };
|
|
6
|
+
|
|
7
|
+
const FUNCTION_TYPES = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
8
|
+
|
|
9
|
+
function hasReturn(node) {
|
|
10
|
+
if (!isObject(node)) return false;
|
|
11
|
+
|
|
12
|
+
if (node.type === "ReturnStatement") return true;
|
|
13
|
+
|
|
14
|
+
if (FUNCTION_TYPES.has(node.type)) return false;
|
|
15
|
+
|
|
16
|
+
return Object.values(node).some(value => Array.isArray(value) ? value.some(hasReturn) : hasReturn(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseExpressionFunction(code) {
|
|
20
|
+
try {
|
|
21
|
+
const program = parse(code, PARSE_OPTIONS);
|
|
22
|
+
const statements = program.body.filter(node => node.type !== "EmptyStatement");
|
|
23
|
+
const statement = statements.length === 1 ? statements[0] : undefined;
|
|
24
|
+
const candidate = statement?.type === "ExpressionStatement" ? statement.expression : statement;
|
|
25
|
+
|
|
26
|
+
if (candidate && FUNCTION_TYPES.has(candidate.type)) {
|
|
27
|
+
return { program, expression: candidate, expressionSource: code.slice(statement.start, statement.end).replace(/;\s*$/, "") };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { program };
|
|
31
|
+
} catch (bodyError) {
|
|
32
|
+
const expressionSource = code.trimEnd().replace(/;+\s*$/, "");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const wrapped = parse("(" + expressionSource + "\n)", PARSE_OPTIONS);
|
|
36
|
+
const expression = wrapped.body[0]?.expression;
|
|
37
|
+
|
|
38
|
+
if (!expression || !FUNCTION_TYPES.has(expression.type)) throw bodyError;
|
|
39
|
+
|
|
40
|
+
return { expression, expressionSource };
|
|
41
|
+
} catch { throw bodyError; }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function deniedSpecifier(node) {
|
|
46
|
+
if (node?.type === "Literal" && isString(node.value)) return node.value;
|
|
47
|
+
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value?.cooked;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertGuestImports(node) {
|
|
51
|
+
if (Array.isArray(node)) { node.forEach(assertGuestImports); return; }
|
|
52
|
+
if (!isObject(node)) return;
|
|
53
|
+
if (["ImportDeclaration", "ImportExpression"].includes(node.type)) rejectGuestImport(node.source);
|
|
54
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "require") rejectGuestImport(node.arguments?.[0]);
|
|
55
|
+
for (const key of Object.keys(node)) {
|
|
56
|
+
if (!["start", "end", "loc", "range"].includes(key)) assertGuestImports(node[key]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function prepareProgram(code) {
|
|
61
|
+
const parsed = parseExpressionFunction(code);
|
|
62
|
+
assertGuestImports(parsed.program ?? parsed.expression);
|
|
63
|
+
const body = parsed.expression ? "return await (" + parsed.expressionSource + "\n)();" : code;
|
|
64
|
+
const returns = parsed.expression
|
|
65
|
+
? parsed.expression.type === "ArrowFunctionExpression" && parsed.expression.body.type !== "BlockStatement" || hasReturn(parsed.expression.body)
|
|
66
|
+
: hasReturn(parsed.program);
|
|
67
|
+
|
|
68
|
+
return { body, hasReturn: returns };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function admitData(data, cap) {
|
|
72
|
+
if (data === undefined) return { data };
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const encoded = JSON.stringify(data);
|
|
76
|
+
|
|
77
|
+
if (encoded === undefined) return { error: "data must be JSON-serializable" };
|
|
78
|
+
if (encoded.length > cap) return { error: "data exceeds " + cap + " characters (serialized JSON: " + encoded.length + " UTF-16 characters); no commands ran. Split literal inputs across invocations; large text can use write({path,content,append:true}) chunks without omitting content" };
|
|
79
|
+
|
|
80
|
+
return { data: JSON.parse(encoded) };
|
|
81
|
+
} catch { return { error: "data must be JSON-serializable" }; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function admitCode({ code, file, cap }) {
|
|
85
|
+
if ((code === undefined) === (file === undefined)) return { error: "supply exactly one of code or file; no commands ran" };
|
|
86
|
+
if (file === undefined && (!isString(code) || !code.trim())) return { error: "code must be a non-empty string" };
|
|
87
|
+
if (file === undefined && code.length > cap) return { error: "code exceeds " + cap + " characters; split large writes into write({path,content,append:true}) chunks" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function admitTimeout(config) {
|
|
91
|
+
const requestedTimeout = Number(config.timeoutMs === undefined ? 60000 : config.timeoutMs);
|
|
92
|
+
|
|
93
|
+
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) return { error: "timeoutMs must be a positive finite number" };
|
|
94
|
+
|
|
95
|
+
return { timeoutMs: Math.max(1, Math.min(2_147_483_647, Math.floor(requestedTimeout))) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function admitGuest({ code, file, data, config }) {
|
|
99
|
+
const cap = config.maxCodeChars ?? 48000;
|
|
100
|
+
const codeError = admitCode({ code, file, cap });
|
|
101
|
+
|
|
102
|
+
if (codeError) return codeError;
|
|
103
|
+
const admitted = admitData(data, cap);
|
|
104
|
+
|
|
105
|
+
if (admitted.error) return admitted;
|
|
106
|
+
const timeout = admitTimeout(config);
|
|
107
|
+
|
|
108
|
+
if (timeout.error) return timeout;
|
|
109
|
+
|
|
110
|
+
return { data: admitted.data, timeoutMs: timeout.timeoutMs };
|
|
111
|
+
}
|
|
112
|
+
export { admitGuest, prepareProgram };
|
|
113
|
+
|
|
114
|
+
function rejectGuestImport(source) {
|
|
115
|
+
const spec = deniedSpecifier(source);
|
|
116
|
+
const reason = spec && isDeniedGuestImport(spec) ? guestImportMessage(spec) : "guest cannot import modules; use read, edit, write, or bash";
|
|
117
|
+
throw new Error(reason + "; no commands ran");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function containingAwait(node, offset) {
|
|
121
|
+
if (Array.isArray(node)) return node.map(child => containingAwait(child, offset)).find(Boolean);
|
|
122
|
+
if (!isObject(node) || offset < node.start || offset >= node.end) return null;
|
|
123
|
+
for (const child of Object.values(node)) {
|
|
124
|
+
const found = containingAwait(child, offset);
|
|
125
|
+
if (found) return found;
|
|
126
|
+
}
|
|
127
|
+
return node.type === "AwaitExpression" ? node : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// V8 points at 'await'; JSC points at the called function's parenthesis. Report
|
|
131
|
+
// the enclosing await expression consistently, using source syntax, not offsets.
|
|
132
|
+
export function normalizeGuestLocation(source, location) {
|
|
133
|
+
if (!location?.awaited) return location;
|
|
134
|
+
const {line, col} = location;
|
|
135
|
+
const prefix = source.split("\n").slice(0, line - 1).join("\n");
|
|
136
|
+
const offset = prefix.length + Number(line > 1) + col - 1;
|
|
137
|
+
try {
|
|
138
|
+
const node = containingAwait(parse(source, {...PARSE_OPTIONS, locations:true}), offset);
|
|
139
|
+
return node ? {line:node.loc.start.line, col:node.loc.start.column + 1} : location;
|
|
140
|
+
} catch { return location; }
|
|
141
|
+
}
|
package/src/runtime/reference.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Standing tool description: sent on every request. No result or history compression.
|
|
2
|
-
export const REFERENCE = `JS body/async arrow
|
|
3
|
-
read(path|paths,offset=1,limit?) → raw text/text[]; directories → entries; images: PNG/JPEG/GIF/WebP (≤16
|
|
4
|
-
|
|
5
|
-
read({path,json:selector}) → parsed JSON
|
|
2
|
+
export const REFERENCE = `JS body/async arrow: read/write/edit/bash; no fs/import/require. file: workspace scripts; data: literals (≤48000 JSON chars).
|
|
3
|
+
read(path|paths,offset=1,limit?) → raw text/text[]; directories → entries; images: PNG/JPEG/GIF/WebP (≤16 images/20 MiB).
|
|
4
|
+
Text ≤64 MiB internally; complete:true requires the whole file. Display alone is capped; return a summary or read(path,{offset:1,limit:80}). Larger files/JSONL: bounded bash parser.
|
|
5
|
+
read({path,json:selector}) → parsed JSON: ".field", ".a[0:3]", ".a.length", quoted keys, true; input ≤16 MiB, selections ≤64 MiB storage, no jq. Values retain their types.
|
|
6
6
|
read("symbol or question") = read({query,resolve:true}) → view; check status; view.text is a span, not the file.
|
|
7
7
|
read(path,{about}) → windows; read({query,evidence:true}) → ranked evidence; read({path,outline:true}) → declarations.
|
|
8
8
|
write(path,text) replaces unread workspace files; write({path,content,append:true}) appends without reading. After read: edit or replace:true.
|
|
9
|
-
edit(path,oldText,newText) | edit({path,edits:[{oldText,newText}]}) exact
|
|
10
|
-
edit(view,text) replaces
|
|
11
|
-
bash(command,{cwd?,timeoutMs?}) | bash({command,args}) literal argv; bounded output, nonzero throws; inherits
|
|
12
|
-
Edits
|
|
13
|
-
programs:[{code?,file?,data?}] inherits
|
|
14
|
-
|
|
9
|
+
edit(path,oldText,newText) | edit({path,edits:[{oldText,newText}]}) unique exact read text; returns numbered windows/checks/references.
|
|
10
|
+
edit(view,text) replaces span; edit(view,old,new) uniquely matches within it. edit(async()=>{...}) checkpoint: merge on success, rollback/rethrow on failure.
|
|
11
|
+
bash(command,{cwd?,timeoutMs?}) | bash({command,args}) literal argv for scripts; bounded output, nonzero throws. Outer timeoutMs caps ALL waits/commands; bash inherits unless overridden.
|
|
12
|
+
Edits commit on success/before bash. Promise.allSettled keeps partial results.
|
|
13
|
+
programs:[{code?,file?,data?}] inherits source/data; entries override. mergeData:true shallow object merge (entry keys win).
|
|
14
|
+
Promise.all: ≤8 reads or disjoint-file mutations; same-file serial; bash/checkpoint barriers.
|
|
15
|
+
Fresh guests/separate commits. Sequential failure stops; prior commits stay. parallel:true for disjoint entries. ONE call for known work; split for new decisions.
|
|
15
16
|
`;
|