pi-supernova 0.8.2 → 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 +188 -51
- package/docs/CHANGELOG.md +86 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +14 -30
- 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 +6 -1
- 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 -227
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +2 -1
- 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 +21 -75
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +6 -5
- package/src/runtime/runtime.js +77 -253
- 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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
2
|
+
import {isString,isObject,looksLikePath} from '../shared/decode.js';
|
|
3
|
+
import {truncateChars} from '../output/format.js';
|
|
4
|
+
import {gatherReadArgs,normalizeRead,decodeReadValue,assertReadPaths} from '../contract/read.js';
|
|
5
|
+
import {classifyEdit} from '../contract/edit.js';
|
|
6
|
+
import {normalizeBash} from '../contract/bash.js';
|
|
7
|
+
|
|
8
|
+
const PER_PATH_HINT = "; use Promise.allSettled(paths.map(path => read(path))) for per-path outcomes";
|
|
9
|
+
|
|
10
|
+
function unwrapValue(res) {
|
|
11
|
+
if (res?.ok === false) throw new Error(String(res.value ?? res.error ?? "host tool failed"));
|
|
12
|
+
|
|
13
|
+
if ("value" in Object(res)) return res.value;
|
|
14
|
+
|
|
15
|
+
return res;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unwrapRead(res, args) {
|
|
19
|
+
const value = unwrapValue(res);
|
|
20
|
+
|
|
21
|
+
if ((args.complete === true || args.json !== undefined) && res?.truncated) throw new Error("incomplete read: complete:true or json refuses truncated host output");
|
|
22
|
+
|
|
23
|
+
return decodeReadItem(args, value, res);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decodeReadItem(args, value, res) {
|
|
27
|
+
if (!res?.typed) return decodeReadValue(args, value);
|
|
28
|
+
// JSON selectors used to cross RPC as separately encoded JSON values. Keep
|
|
29
|
+
// their mutable results independent, but put the copies in the guest heap.
|
|
30
|
+
return res.cloneItems ? value.map(item => structuredClone(item)) : value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Keep details/truncated reachable but out of the returned literal unless they carry signal. */
|
|
34
|
+
function leanEnvelope(res) {
|
|
35
|
+
if (!isObject(res)) return res;
|
|
36
|
+
|
|
37
|
+
if ("details" in res) Object.defineProperty(res, "details", { value: res.details, enumerable: false, writable: true });
|
|
38
|
+
|
|
39
|
+
for (const key of Object.keys(res)) if (res[key] === undefined) delete res[key];
|
|
40
|
+
|
|
41
|
+
if (res.truncated === false) delete res.truncated;
|
|
42
|
+
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function swallow(promise) {
|
|
47
|
+
promise.catch(() => {});
|
|
48
|
+
|
|
49
|
+
return promise;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function throwReadPathError(target, detail) {
|
|
53
|
+
throw new Error(`read failed for ${target}: ${detail}${PER_PATH_HINT}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function missingResolvedIndex(values, paths) {
|
|
57
|
+
return values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(paths[index]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function settleReadItem(wave, index, res) {
|
|
61
|
+
const job = wave[index];
|
|
62
|
+
if (!job) throw new Error("invalid streamed read index: " + index);
|
|
63
|
+
wave[index] = null; // Release resolver closures and their potentially large values.
|
|
64
|
+
try { job.resolve(unwrapRead(res,job.args)); }
|
|
65
|
+
catch (error) { job.reject(error); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function settleInlineWave(wave,res,received) {
|
|
69
|
+
unwrapValue(res);
|
|
70
|
+
if (received || !Array.isArray(res.items) || res.items.length !== wave.length) throw new Error("invalid batch read response");
|
|
71
|
+
for (let i=0;i<wave.length;i++) {
|
|
72
|
+
const error = res.itemErrors?.[i];
|
|
73
|
+
settleReadItem(wave,i,{...res,ok:!error,value:error ?? res.items[i],items:undefined});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function rpcReadWave(rpc, wave) {
|
|
78
|
+
if (wave.length === 1) {
|
|
79
|
+
const res = leanEnvelope(await rpc("call",["read",wave[0].args]));
|
|
80
|
+
settleReadItem(wave,0,res);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const args = {...wave[0].args,path:wave.map(job=>job.args.path),_independent:true};
|
|
84
|
+
let received = 0, last;
|
|
85
|
+
const onItem = (index,res) => {
|
|
86
|
+
if (!Number.isInteger(index) || !wave[index] || last?.index === index) throw new Error("invalid streamed read response");
|
|
87
|
+
received++;
|
|
88
|
+
// Keep the final read pending until the host RPC/barrier itself has settled.
|
|
89
|
+
// An awaited batch must never look complete while its host call is running.
|
|
90
|
+
if (received === wave.length) last = {index,res};
|
|
91
|
+
else settleReadItem(wave,index,res);
|
|
92
|
+
};
|
|
93
|
+
const res = leanEnvelope(await rpc("call",["read",args],onItem));
|
|
94
|
+
if (res?.streamed) {
|
|
95
|
+
if (received !== wave.length || !last) throw new Error("incomplete streamed read response");
|
|
96
|
+
settleReadItem(wave,last.index,last.res);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
settleInlineWave(wave,res,received);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rejectReadWave(wave, error) {
|
|
103
|
+
for (const job of wave) job?.reject(error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function dispatchReadWaves(pending, pendingReadWaves, rpc) {
|
|
107
|
+
for (let start=0;start<pending.length;start+=64) {
|
|
108
|
+
const wave = pending.slice(start,start+64);
|
|
109
|
+
const delivery = rpcReadWave(rpc,wave).catch(error=>rejectReadWave(wave,error));
|
|
110
|
+
pendingReadWaves.add(delivery);
|
|
111
|
+
void delivery.finally(()=>pendingReadWaves.delete(delivery));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function enqueueCompatibleRead(readState, flushReads, args) {
|
|
116
|
+
const key = JSON.stringify({ ...args, path: undefined });
|
|
117
|
+
|
|
118
|
+
if (readState.queued.length && readState.queued[0].key !== key) flushReads();
|
|
119
|
+
|
|
120
|
+
const promise = new Promise((resolve, reject) => {
|
|
121
|
+
readState.queued.push({ args, key, resolve, reject });
|
|
122
|
+
|
|
123
|
+
if (readState.queued.length === 1) queueMicrotask(flushReads);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return swallow(promise);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readManyPaths(readOne, args, paths) {
|
|
130
|
+
assertReadPaths(paths);
|
|
131
|
+
const values = await Promise.all(paths.map(async item => {
|
|
132
|
+
try { return await readOne({...args,path:item}); }
|
|
133
|
+
catch (error) { throwReadPathError(item,error.message); }
|
|
134
|
+
}));
|
|
135
|
+
const missing = args.resolve ? missingResolvedIndex(values,paths) : -1;
|
|
136
|
+
if (missing >= 0) throwReadPathError(paths[missing],"not_found");
|
|
137
|
+
return values;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runSpeculation(fn, token, checkpointScope, drainReads, rpc) {
|
|
141
|
+
let began = false;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await drainReads();
|
|
145
|
+
await rpc("speculateBegin", []);
|
|
146
|
+
began = true;
|
|
147
|
+
const value = await checkpointScope.run(token, fn);
|
|
148
|
+
await drainReads();
|
|
149
|
+
await rpc("speculateCommit", []);
|
|
150
|
+
|
|
151
|
+
return { ok: true, committed: true, value };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
await drainReads();
|
|
154
|
+
|
|
155
|
+
if (began) await rpc("speculateRollback", []);
|
|
156
|
+
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatBashFailure(command, res) {
|
|
162
|
+
let exitCode;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
exitCode = JSON.parse(res.details).exitCode;
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
const output = String(res.value).trimEnd();
|
|
169
|
+
const suffix = Number.isInteger(exitCode) ? " (exit " + exitCode + ")" : "";
|
|
170
|
+
|
|
171
|
+
return "command failed" + suffix + ": " + truncateChars(command, 240, "command").text + (output ? "\n" + output : "");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function markTruncatedOutput(res, text) {
|
|
175
|
+
if (res?.truncated && isString(text) && !text.includes("truncated")) return text + "\n…[output truncated]…";
|
|
176
|
+
|
|
177
|
+
return text;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildGuestApi(rpc, batchRead) {
|
|
181
|
+
const checkpointScope = new AsyncLocalStorage();
|
|
182
|
+
let checkpoint = null;
|
|
183
|
+
|
|
184
|
+
const assertScope = () => {
|
|
185
|
+
const token = checkpointScope.getStore();
|
|
186
|
+
|
|
187
|
+
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Post calls in submission order. The host alone owns file concurrency and
|
|
191
|
+
// read/shell/checkpoint barriers; a second guest queue would serialize edits.
|
|
192
|
+
async function checkpointEdit(fn) {
|
|
193
|
+
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
194
|
+
const token = {};
|
|
195
|
+
checkpoint = token;
|
|
196
|
+
try { return await runSpeculation(fn, token, checkpointScope, drainReads, rpc); }
|
|
197
|
+
finally { checkpoint = null; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Coalesce already-started compatible reads without rewriting JS control flow.
|
|
201
|
+
const readState = { queued: [], waves: new Set() };
|
|
202
|
+
|
|
203
|
+
function flushReads() {
|
|
204
|
+
const pending = readState.queued;
|
|
205
|
+
readState.queued = [];
|
|
206
|
+
dispatchReadWaves(pending, readState.waves, rpc);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function drainReads() {
|
|
210
|
+
for (;;) {
|
|
211
|
+
flushReads();
|
|
212
|
+
const waves = [...readState.waves];
|
|
213
|
+
|
|
214
|
+
if (!waves.length) return;
|
|
215
|
+
await Promise.allSettled(waves);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const invoke = (name, args) => {
|
|
220
|
+
assertScope(); flushReads();
|
|
221
|
+
|
|
222
|
+
return swallow(rpc("call", [name, args]).then(leanEnvelope));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const read = async (p, a, b) => {
|
|
226
|
+
assertScope();
|
|
227
|
+
const args = normalizeRead(gatherReadArgs(p, a, b));
|
|
228
|
+
p = args.path;
|
|
229
|
+
|
|
230
|
+
if (Array.isArray(p)) {
|
|
231
|
+
const values = await readManyPaths(read, args, p);
|
|
232
|
+
for (const item of p) if (isString(item)) noteReadPath({ path: item }, values);
|
|
233
|
+
|
|
234
|
+
return values;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const readValue = !batchRead
|
|
238
|
+
? unwrapRead(await invoke("read", args), args)
|
|
239
|
+
: await enqueueCompatibleRead(readState, flushReads, args);
|
|
240
|
+
noteReadPath(args, readValue);
|
|
241
|
+
|
|
242
|
+
return readValue;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const readFiles = new Set();
|
|
246
|
+
|
|
247
|
+
function noteReadPath(args, value) {
|
|
248
|
+
if (isString(args.path) && looksLikePath(args.path)) readFiles.add(args.path);
|
|
249
|
+
if (isObject(value) && isString(value.path) && looksLikePath(value.path)) readFiles.add(value.path);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const write = async (p, content) => {
|
|
253
|
+
const args = isObject(p) ? p : { path: p, content };
|
|
254
|
+
|
|
255
|
+
if (args.append !== true && args.replace !== true && isString(args.path) && readFiles.has(args.path)) {
|
|
256
|
+
throw new Error("file was already read this program; use edit(oldText, newText) or edit(view, ...). write({path,content,replace:true}) replaces anyway");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return unwrapValue(await invoke("write", args));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const edit = async (p, oldText, newText) => {
|
|
263
|
+
const classified = classifyEdit(p, oldText, newText);
|
|
264
|
+
|
|
265
|
+
if (classified.kind === "checkpoint") return checkpointEdit(classified.fn);
|
|
266
|
+
|
|
267
|
+
return unwrapValue(await invoke(classified.command, classified.args));
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const bash = async (command, opts) => {
|
|
271
|
+
const args = normalizeBash(command, opts);
|
|
272
|
+
command = args.command;
|
|
273
|
+
const res = await invoke("bash", args);
|
|
274
|
+
|
|
275
|
+
if (res?.ok === false) throw new Error(formatBashFailure(command, res));
|
|
276
|
+
|
|
277
|
+
return markTruncatedOutput(res, unwrapValue(res));
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return { read, write, edit, bash };
|
|
281
|
+
}
|