pi-supernova 0.6.0 → 0.7.1
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 +27 -3
- package/docs/CHANGELOG.md +114 -0
- package/docs/TOKEN_COSTS.md +13 -5
- package/index.js +120 -79
- package/package.json +1 -1
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +28 -222
- package/src/bridge/host-bridge.js +113 -1668
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -198
- package/src/context/evidence.js +140 -76
- package/src/context/fuzzy.js +42 -24
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +23 -18
- package/src/context/repo-index.js +206 -170
- package/src/context/search.js +157 -77
- package/src/context/snap.js +240 -136
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +14 -5
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +12 -8
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +94 -50
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +205 -175
- package/src/fs/workspace.js +119 -108
- package/src/output/bottleneck.js +195 -116
- package/src/output/format.js +101 -67
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +296 -292
- package/src/runtime/parallel.js +97 -64
- package/src/runtime/program-batch.js +178 -69
- package/src/runtime/reference.js +15 -14
- package/src/runtime/runtime.js +327 -187
- package/src/shared/decode.js +58 -36
- package/src/ui/omp-frame.js +59 -42
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +241 -145
package/src/runtime/parallel.js
CHANGED
|
@@ -4,33 +4,91 @@ const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "
|
|
|
4
4
|
|
|
5
5
|
const READ_ONLY_LSP = new Set(["definition", "references", "hover", "symbols", "diagnostics", "implementation", "type_definition", "incoming_calls", "outgoing_calls"]);
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
if (!isObject(args)) args = {};
|
|
9
|
-
name = String(name);
|
|
7
|
+
const NATIVE_TOOLS = ["read", "edit", "write", "bash"];
|
|
10
8
|
|
|
9
|
+
const SPECIAL_MUTATION = {
|
|
10
|
+
todo: args => args.op !== "view",
|
|
11
|
+
hub: args => !["list", "ps", "logs", "describe"].includes(args.op),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function configuredMutating(name, config) {
|
|
11
15
|
if ((config.mutatingTools ?? []).includes(name)) return true;
|
|
12
16
|
|
|
13
17
|
if ((config.mutatingPrefixes ?? []).some(prefix => prefix && name.startsWith(prefix))) return true;
|
|
14
18
|
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function lspMutates(args) {
|
|
23
|
+
const action = args.action ?? args.operation;
|
|
24
|
+
|
|
25
|
+
if (READ_ONLY_LSP.has(action)) return false;
|
|
26
|
+
|
|
27
|
+
if (["rename", "rename_file"].includes(action)) return args.apply !== false;
|
|
28
|
+
|
|
29
|
+
if (action === "code_actions") return args.apply === true;
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isMutatingTool(name, config = {}, args = {}, definition) {
|
|
35
|
+
if (!isObject(args)) args = {};
|
|
36
|
+
name = String(name);
|
|
37
|
+
|
|
38
|
+
if (configuredMutating(name, config)) return true;
|
|
39
|
+
|
|
15
40
|
if (definition?.annotations?.readOnlyHint === true) return false;
|
|
16
41
|
|
|
17
|
-
if (name === "lsp")
|
|
18
|
-
|
|
42
|
+
if (name === "lsp") return lspMutates(args);
|
|
43
|
+
const special = SPECIAL_MUTATION[name];
|
|
44
|
+
|
|
45
|
+
if (special) return special(args);
|
|
19
46
|
|
|
20
|
-
|
|
47
|
+
return !READ_ONLY_TOOLS.has(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function takeScheduledWave(queue, first, maxParallelReads) {
|
|
51
|
+
const wave = [first];
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
if (first.name !== "read") return wave;
|
|
23
54
|
|
|
24
|
-
|
|
55
|
+
while (wave.length < maxParallelReads && queue[0]?.name === "read") {
|
|
56
|
+
const next = queue.shift();
|
|
25
57
|
|
|
26
|
-
|
|
58
|
+
if (!next.cancelled) wave.push(next);
|
|
27
59
|
}
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
return wave;
|
|
62
|
+
}
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
async function runScheduledJob(job) {
|
|
65
|
+
if (job.cancelled) return;
|
|
66
|
+
job.started = true;
|
|
67
|
+
job.signal?.removeEventListener("abort", job.abort);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
job.signal?.throwIfAborted();
|
|
71
|
+
const result = await job.run();
|
|
72
|
+
job.signal?.throwIfAborted();
|
|
73
|
+
job.resolve(result);
|
|
74
|
+
} catch (error) { job.reject(error); }
|
|
75
|
+
}
|
|
32
76
|
|
|
33
|
-
|
|
77
|
+
function runScheduledWave(wave) {
|
|
78
|
+
// Settle each promise separately: one failed read must not discard its
|
|
79
|
+
// siblings or release a write barrier while other reads are still active.
|
|
80
|
+
return Promise.all(wave.map(runScheduledJob));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function attachJobAbort(job, signal, reject) {
|
|
84
|
+
job.abort = () => {
|
|
85
|
+
if (job.started) return;
|
|
86
|
+
job.cancelled = true;
|
|
87
|
+
signal.removeEventListener("abort", job.abort);
|
|
88
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
signal?.addEventListener("abort", job.abort, { once: true });
|
|
34
92
|
}
|
|
35
93
|
|
|
36
94
|
/** FIFO read waves with mutation barriers; callers keep independent promises. */
|
|
@@ -46,33 +104,14 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
46
104
|
const first = queue.shift();
|
|
47
105
|
|
|
48
106
|
if (first.cancelled) continue;
|
|
49
|
-
const wave =
|
|
107
|
+
const wave = takeScheduledWave(queue, first, maxParallelReads);
|
|
50
108
|
|
|
51
109
|
if (first.name === "read") {
|
|
52
|
-
while (wave.length < maxParallelReads && queue[0]?.name === "read") {
|
|
53
|
-
const next = queue.shift();
|
|
54
|
-
|
|
55
|
-
if (!next.cancelled) wave.push(next);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
110
|
stats.readWaves++;
|
|
59
111
|
stats.peakParallelReads = Math.max(stats.peakParallelReads, wave.length);
|
|
60
112
|
}
|
|
61
113
|
|
|
62
|
-
|
|
63
|
-
// siblings or release a write barrier while other reads are still active.
|
|
64
|
-
await Promise.all(wave.map(async job => {
|
|
65
|
-
if (job.cancelled) return;
|
|
66
|
-
job.started = true;
|
|
67
|
-
job.signal?.removeEventListener("abort", job.abort);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
job.signal?.throwIfAborted();
|
|
71
|
-
const result = await job.run();
|
|
72
|
-
job.signal?.throwIfAborted();
|
|
73
|
-
job.resolve(result);
|
|
74
|
-
} catch (error) { job.reject(error); }
|
|
75
|
-
}));
|
|
114
|
+
await runScheduledWave(wave);
|
|
76
115
|
}
|
|
77
116
|
} finally { draining = false; }
|
|
78
117
|
}
|
|
@@ -80,7 +119,7 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
80
119
|
return {
|
|
81
120
|
stats,
|
|
82
121
|
schedule(name, run, signal) {
|
|
83
|
-
if (!
|
|
122
|
+
if (!NATIVE_TOOLS.includes(name) || !isFunction(run)) {
|
|
84
123
|
return Promise.reject(new Error("scheduler requires a native tool and an executor"));
|
|
85
124
|
}
|
|
86
125
|
|
|
@@ -89,14 +128,7 @@ export function createNativeScheduler({ maxParallelReads = 8 } = {}) {
|
|
|
89
128
|
|
|
90
129
|
return new Promise((resolve, reject) => {
|
|
91
130
|
const job = { name, run, signal, resolve, reject, started: false, cancelled: false };
|
|
92
|
-
job
|
|
93
|
-
if (job.started) return;
|
|
94
|
-
job.cancelled = true;
|
|
95
|
-
signal.removeEventListener("abort", job.abort);
|
|
96
|
-
reject(signal.reason ?? new Error("aborted"));
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
signal?.addEventListener("abort", job.abort, { once: true });
|
|
131
|
+
attachJobAbort(job, signal, reject);
|
|
100
132
|
queue.push(job);
|
|
101
133
|
|
|
102
134
|
if (!draining) {
|
|
@@ -116,26 +148,27 @@ function requireArray(value, name) {
|
|
|
116
148
|
return value;
|
|
117
149
|
}
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
const
|
|
151
|
+
function waveMutates(list, meta, config) {
|
|
152
|
+
const names = meta?.names ?? [];
|
|
121
153
|
|
|
122
|
-
|
|
154
|
+
return names.length !== list.length || names.some((name, i) => isMutatingTool(name, config, meta?.calls?.[i]?.args, meta?.definitions?.[i]));
|
|
155
|
+
}
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const mutating = names.length !== list.length || names.some((name, i) => isMutatingTool(name, config, meta?.calls?.[i]?.args, meta?.definitions?.[i]));
|
|
157
|
+
function shouldRunParallel(mutating, mode, length) {
|
|
158
|
+
return !mutating && (mode === "parallel" || (mode === "auto" && length > 1));
|
|
159
|
+
}
|
|
128
160
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
161
|
+
async function settleParallel(list) {
|
|
162
|
+
// Do not finish a wave while its already-started host calls are still running.
|
|
163
|
+
const settled = await Promise.allSettled(list.map(thunk => Promise.resolve().then(thunk)));
|
|
164
|
+
const failure = settled.find(item => item.status === "rejected");
|
|
133
165
|
|
|
134
|
-
|
|
166
|
+
if (failure) throw failure.reason;
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
return { results: settled.map(item => item.value), mode: "parallel", reason: "independent-reads" };
|
|
169
|
+
}
|
|
138
170
|
|
|
171
|
+
async function runSerial(list, mutating) {
|
|
139
172
|
const results = [];
|
|
140
173
|
|
|
141
174
|
for (const thunk of list) results.push(await thunk());
|
|
@@ -143,16 +176,16 @@ export async function runParallelWave(thunks, meta, options = {}) {
|
|
|
143
176
|
return { results, mode: "serial", reason: mutating ? "mutating" : "single-or-forced" };
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
export async function
|
|
147
|
-
|
|
148
|
-
}
|
|
179
|
+
export async function runParallelWave(thunks, meta, options = {}) {
|
|
180
|
+
const list = requireArray(thunks, "parallel wave");
|
|
149
181
|
|
|
150
|
-
|
|
151
|
-
let current = requireArray(items, "pipeline");
|
|
182
|
+
if (list.some(item => !isFunction(item))) throw new TypeError("parallel wave requires functions");
|
|
152
183
|
|
|
153
|
-
if (
|
|
184
|
+
if (!list.length) return { results: [], mode: "serial", reason: "empty" };
|
|
185
|
+
const { mode = "auto", config = {} } = options;
|
|
186
|
+
const mutating = waveMutates(list, meta, config);
|
|
154
187
|
|
|
155
|
-
|
|
188
|
+
if (shouldRunParallel(mutating, mode, list.length)) return settleParallel(list);
|
|
156
189
|
|
|
157
|
-
return
|
|
190
|
+
return runSerial(list, mutating);
|
|
158
191
|
}
|
|
@@ -12,10 +12,15 @@ const mutationTotals = results => results.reduce((total, result) => {
|
|
|
12
12
|
return total;
|
|
13
13
|
}, {committed:0,rolledBack:0,external:0,pendingCommits:0,recoveryFailed:false});
|
|
14
14
|
|
|
15
|
-
export function programBatchText(results, total, stopped = "") {
|
|
15
|
+
export function programBatchText(results, total, stopped = "", failed = 0) {
|
|
16
16
|
const m = mutationTotals(results);
|
|
17
|
+
const summary = stopped
|
|
18
|
+
? "error: programs stopped: " + stopped + " " + results.length + "/" + total
|
|
19
|
+
: failed > 0
|
|
20
|
+
? "error: programs " + results.length + "/" + total + " - " + failed + " failed"
|
|
21
|
+
: "ok: programs " + results.length + "/" + total;
|
|
17
22
|
|
|
18
|
-
return
|
|
23
|
+
return summary +
|
|
19
24
|
(m.recoveryFailed || m.pendingCommits ? "; filesystem outcome uncertain: inspect disk" : "") + "\nresults (UTF-16 lengths):\n" + results.map((result,i) => {
|
|
20
25
|
const text = textOf(result);
|
|
21
26
|
|
|
@@ -23,27 +28,23 @@ export function programBatchText(results, total, stopped = "") {
|
|
|
23
28
|
}).join("");
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
function
|
|
27
|
-
if (["code","file"].
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const p of params.programs) {
|
|
32
|
-
if (!isObject(p) || Array.isArray(p) || Object.keys(p).some(key => !["code","file","data"].includes(key)) ||
|
|
33
|
-
((p.code === undefined) === (p.file === undefined)) || !isString(p.code ?? p.file) || !(p.code ?? p.file).trim()) {
|
|
34
|
-
throw new Error("each program requires code OR file, with optional data; no nested batches or per-entry timeouts; no programs ran");
|
|
35
|
-
}
|
|
31
|
+
function assertProgramEntry(p) {
|
|
32
|
+
if (!isObject(p) || Array.isArray(p) || Object.keys(p).some(key => !["code","file","data"].includes(key)) ||
|
|
33
|
+
((p.code === undefined) === (p.file === undefined)) || !isString(p.code ?? p.file) || !(p.code ?? p.file).trim()) {
|
|
34
|
+
throw new Error("each program requires code OR file, with optional data; no nested batches or per-entry timeouts; no programs ran");
|
|
36
35
|
}
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
+
function encodeBatchPayload(params) {
|
|
38
39
|
const hasDefault = params.data !== undefined;
|
|
39
40
|
let encoded;
|
|
40
41
|
|
|
41
42
|
try { encoded = JSON.stringify(hasDefault ? {programs:params.programs,data:params.data} : params.programs); } catch { throw new Error("programs and data must be JSON-serializable; no programs ran"); }
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const parsed = JSON.parse(encoded);
|
|
44
|
+
return { hasDefault, encoded };
|
|
45
|
+
}
|
|
46
46
|
|
|
47
|
+
function applyDefaultData(parsed, hasDefault) {
|
|
47
48
|
if (!hasDefault) return parsed;
|
|
48
49
|
if (!Object.hasOwn(parsed,"data")) throw new Error("data must be JSON-serializable; no programs ran");
|
|
49
50
|
|
|
@@ -52,72 +53,180 @@ function batchInputs(params, config) {
|
|
|
52
53
|
return parsed.programs.map(program => program.data === undefined ? {...program,data:parsed.data} : program);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
function parseBatchPayload(params, config) {
|
|
57
|
+
if (["code","file"].some(key => params[key] !== undefined)) throw new Error("programs cannot combine with top-level code or file; no programs ran");
|
|
58
|
+
|
|
59
|
+
if (!Array.isArray(params.programs) || !params.programs.length || params.programs.length > 32) throw new Error("programs requires 1..32 entries; no programs ran");
|
|
60
|
+
|
|
61
|
+
for (const p of params.programs) assertProgramEntry(p);
|
|
62
|
+
const { hasDefault, encoded } = encodeBatchPayload(params);
|
|
63
|
+
|
|
64
|
+
if (encoded.length > (config.maxCodeChars ?? 48000)) throw new Error("programs JSON exceeds the code character budget; no programs ran");
|
|
65
|
+
|
|
66
|
+
return applyDefaultData(JSON.parse(encoded), hasDefault);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function batchTimeoutMs(params, config) {
|
|
58
70
|
const requestedTimeout = params.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs);
|
|
59
71
|
|
|
60
72
|
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error("program batch timeoutMs must be a positive finite number");
|
|
61
|
-
const started = performance.now();
|
|
62
|
-
const timeout = requestedTimeout;
|
|
63
|
-
const deadline = started + timeout;
|
|
64
|
-
const controller = new AbortController();
|
|
65
|
-
const combined = signal ? AbortSignal.any([signal,controller.signal]) : controller.signal;
|
|
66
|
-
const timer = setTimeout(() => controller.abort(),Math.min(timeout,2147483647));
|
|
67
|
-
const budget = {calls:0,logLines:0};
|
|
68
|
-
const results = [], images = [], imageLabels = [], trace = [];
|
|
69
|
-
const imageTextChars = () => imageLabels.reduce((chars,label)=>chars+label.length+1,0);
|
|
70
|
-
let imageBytes = 0, stopped = "";
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
for (const [i, program] of programs.entries()) {
|
|
74
|
-
if (combined.aborted || performance.now() >= deadline) { stopped = "deadline or cancellation; remaining programs did not run"; break; }
|
|
75
|
-
|
|
76
|
-
let result;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
result = await execute(id + ":" + i,{...program,timeoutMs:Math.max(1,Math.ceil(deadline-performance.now()))},combined,update => {
|
|
80
|
-
try { onUpdate?.({...update,details:{...update.details,trace:[...trace,...(update.details?.trace ?? [])]}}); } catch {}
|
|
81
|
-
},ctx,budget);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
result = error.supernovaResult ?? {content:[{type:"text",text:String(error.message ?? error)}],details:{ok:false,error:String(error.message ?? error)}};
|
|
84
|
-
}
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
return requestedTimeout;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const MAX_PARALLEL_PROGRAMS = 8;
|
|
78
|
+
|
|
79
|
+
class ProgramBatch {
|
|
80
|
+
constructor(id, params, signal, onUpdate, ctx, config, execute, programs, timeout) {
|
|
81
|
+
this.id = id;
|
|
82
|
+
this.onUpdate = onUpdate;
|
|
83
|
+
this.ctx = ctx;
|
|
84
|
+
this.config = config;
|
|
85
|
+
this.execute = execute;
|
|
86
|
+
this.programs = programs;
|
|
87
|
+
this.timeout = timeout;
|
|
88
|
+
this.started = performance.now();
|
|
89
|
+
this.deadline = this.started + timeout;
|
|
90
|
+
this.controller = new AbortController();
|
|
91
|
+
this.combined = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
|
|
92
|
+
this.timer = setTimeout(() => this.controller.abort(), Math.min(timeout, 2147483647));
|
|
93
|
+
this.budget = {calls:0,logLines:0};
|
|
94
|
+
this.parallel = params.parallel === true && programs.length > 1;
|
|
95
|
+
this.results = [];
|
|
96
|
+
this.images = [];
|
|
97
|
+
this.imageLabels = [];
|
|
98
|
+
this.trace = [];
|
|
99
|
+
this.live = programs.map(() => []);
|
|
100
|
+
this.imageSeq = programs.map(() => 0);
|
|
101
|
+
this.imageBytes = 0;
|
|
102
|
+
this.stopped = "";
|
|
103
|
+
this.imageDropped = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
imageTextChars() {
|
|
107
|
+
return this.imageLabels.reduce((chars, label) => chars + label.length + 1, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
updateFor(i) {
|
|
111
|
+
return update => {
|
|
112
|
+
this.live[i] = update?.details?.trace ?? [];
|
|
113
|
+
|
|
114
|
+
try { this.onUpdate?.({...update,details:{...update?.details,trace:[...this.trace,...this.live.flat()]}}); } catch {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
runOne(program, i) {
|
|
119
|
+
return Promise.resolve()
|
|
120
|
+
.then(() => this.execute(this.id + ":" + i,{...program,timeoutMs:Math.max(1,Math.ceil(this.deadline-performance.now()))},this.combined,this.updateFor(i),this.ctx,this.budget,{parallel:this.parallel}))
|
|
121
|
+
.catch(error => error.supernovaResult ?? {content:[{type:"text",text:String(error?.message ?? error)}],details:{ok:false,error:String(error?.message ?? error)}});
|
|
122
|
+
}
|
|
89
123
|
|
|
90
|
-
|
|
91
|
-
|
|
124
|
+
collectImages(result, i) {
|
|
125
|
+
for (const block of Array.isArray(result?.content) ? result.content : []) if (block?.type === "image" && isString(block.data)) {
|
|
126
|
+
this.imageBytes += Buffer.byteLength(block.data,"base64");
|
|
92
127
|
|
|
93
|
-
|
|
128
|
+
if (this.images.length >= 16 || this.imageBytes > 20*1024*1024) { this.imageDropped = true; continue; }
|
|
94
129
|
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
this.images.push(block);
|
|
131
|
+
this.imageLabels.push("program " + (i+1) + " image " + (++this.imageSeq[i]));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
takeSettled(result, i) {
|
|
136
|
+
this.results.push(result);
|
|
137
|
+
this.trace.push(...(result.details?.trace ?? []));
|
|
138
|
+
this.collectImages(result, i);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async runParallel() {
|
|
142
|
+
const limit = Math.min(this.programs.length, MAX_PARALLEL_PROGRAMS);
|
|
143
|
+
const settled = Array.from({ length: this.programs.length });
|
|
144
|
+
let next = 0;
|
|
145
|
+
|
|
146
|
+
await Promise.all(Array.from({length: limit}, async () => {
|
|
147
|
+
while (next < this.programs.length && !this.combined.aborted && performance.now() < this.deadline) {
|
|
148
|
+
const i = next++;
|
|
149
|
+
settled[i] = await this.runOne(this.programs[i], i);
|
|
150
|
+
this.live[i] = [];
|
|
97
151
|
}
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < this.programs.length; i++) {
|
|
155
|
+
if (settled[i] === undefined) continue;
|
|
156
|
+
this.takeSettled(settled[i], i);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (settled.includes(undefined) || this.combined.aborted || performance.now() >= this.deadline) this.stopped = "batch deadline or cancellation; earlier commits remain";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
sequentialStop(result, i) {
|
|
163
|
+
let stopped = this.stopped;
|
|
164
|
+
|
|
165
|
+
if (this.imageDropped) stopped = "batch image budget exceeded; remaining programs did not run";
|
|
166
|
+
|
|
167
|
+
if (result.details?.ok === false) stopped = "program " + (i+1) + " failed; remaining programs did not run; earlier commits remain";
|
|
168
|
+
const text = programBatchText(this.results, this.programs.length, stopped);
|
|
98
169
|
|
|
99
|
-
|
|
100
|
-
const text = programBatchText(results,programs.length,stopped);
|
|
170
|
+
if (text.length + this.imageTextChars() > this.config.maxReturnChars || result.details?.returnTruncated) stopped ||= "batch output budget exceeded; remaining programs did not run; earlier commits remain";
|
|
101
171
|
|
|
102
|
-
|
|
172
|
+
if (result.details?.logTruncated) stopped ||= "batch log budget exceeded; remaining programs did not run; earlier commits remain";
|
|
103
173
|
|
|
104
|
-
|
|
174
|
+
if (this.combined.aborted || performance.now() >= this.deadline) stopped ||= "batch deadline or cancellation; earlier commits remain";
|
|
105
175
|
|
|
106
|
-
|
|
176
|
+
return stopped;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async runSequential() {
|
|
180
|
+
for (const [i, program] of this.programs.entries()) {
|
|
181
|
+
if (this.combined.aborted || performance.now() >= this.deadline) { this.stopped = "deadline or cancellation; remaining programs did not run"; break; }
|
|
107
182
|
|
|
108
|
-
|
|
183
|
+
const result = await this.runOne(program, i);
|
|
184
|
+
this.live[i] = [];
|
|
185
|
+
this.takeSettled(result, i);
|
|
186
|
+
this.stopped = this.sequentialStop(result, i);
|
|
187
|
+
|
|
188
|
+
if (this.stopped) break;
|
|
109
189
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
failNote(failed) {
|
|
193
|
+
return this.stopped || (this.parallel && failed ? failed + " program" + (failed>1?"s":"") + " failed" : undefined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
boundedText(failed) {
|
|
197
|
+
const note = this.imageDropped && !this.stopped ? "some images dropped: batch image budget" : "";
|
|
198
|
+
|
|
199
|
+
return truncateChars(programBatchText(this.results,this.programs.length,this.stopped,this.parallel ? failed : 0) + (note ? "; " + note : ""),Math.max(0,this.config.maxReturnChars-this.imageTextChars()),"batch output");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
finish() {
|
|
203
|
+
const failed = this.results.filter(result => result.details?.ok === false).length;
|
|
204
|
+
const bounded = this.boundedText(failed);
|
|
205
|
+
const content = [{type:"text",text:bounded.text}];
|
|
206
|
+
this.images.forEach((image,i) => content.push({type:"text",text:this.imageLabels[i]},image));
|
|
207
|
+
|
|
208
|
+
// Return a typed stop report instead of throwing away earlier results/images.
|
|
209
|
+
// Single-program errors retain their existing throwing behavior.
|
|
210
|
+
return {content,isError:!!this.stopped || (this.parallel && failed>0),details:{ok:!this.stopped && !(this.parallel && failed),error:this.failNote(failed),wallMs:Math.round(performance.now()-this.started),
|
|
211
|
+
programs:this.results,attempted:this.results.length,total:this.programs.length,stopped:this.stopped,parallel:this.parallel,
|
|
212
|
+
result:bounded.truncated ? bounded.text : this.results.map(result=>result.details?.result),
|
|
213
|
+
returnTruncated:bounded.truncated || this.results.some(result=>result.details?.returnTruncated),
|
|
214
|
+
logTruncated:this.results.some(result=>result.details?.logTruncated),logs:this.results.flatMap(result=>result.details?.logs ?? []),trace:this.trace,mutations:mutationTotals(this.results)}};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async run() {
|
|
218
|
+
try {
|
|
219
|
+
if (this.parallel) await this.runParallel();
|
|
220
|
+
else await this.runSequential();
|
|
221
|
+
} finally { clearTimeout(this.timer); this.controller.abort(); }
|
|
222
|
+
|
|
223
|
+
return this.finish();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Explicit known continuations, not inferred plans, retries, or a shared heap. */
|
|
228
|
+
export async function runProgramBatch(id, params, signal, onUpdate, ctx, config, execute) {
|
|
229
|
+
const programs = parseBatchPayload(params, config);
|
|
230
|
+
|
|
231
|
+
return new ProgramBatch(id, params, signal, onUpdate, ctx, config, execute, programs, batchTimeoutMs(params, config)).run();
|
|
123
232
|
}
|
package/src/runtime/reference.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
//
|
|
2
|
-
export const REFERENCE = `JavaScript async body or arrow with read, write, edit, bash.
|
|
1
|
+
// Standing tool description: sent on every request. One nova call, four commands inside.
|
|
2
|
+
export const REFERENCE = `JavaScript async body or arrow with read, write, edit, bash. file:path runs that program file instead. Put Markdown/scripts/argv in data. Guest has no fs/import/require.
|
|
3
3
|
|
|
4
|
-
read(path|paths, offset?, limit?) → raw text or text[]; read(directory) → entries[]
|
|
5
|
-
read(imagePath) → image (PNG/JPEG/GIF/WebP/BMP)
|
|
6
|
-
read({path,json:".field"}) → parsed JSON; .items[0:3], quoted keys,
|
|
7
|
-
|
|
8
|
-
read("symbol or question") → same view as resolve:true
|
|
9
|
-
read({query,resolve:true}) → {status,path,line,
|
|
4
|
+
read(path|paths, offset?, limit?) → raw text or text[]; read(directory) → entries[]
|
|
5
|
+
read(imagePath) → image (PNG/JPEG/GIF/WebP/BMP)
|
|
6
|
+
read({path,json:".field"}) → parsed JSON; .items[0:3], .items.length, quoted keys, or true; 16 MiB cap; no jq
|
|
7
|
+
raw JSON or selections over budget return {status:"too_large",path,keys} (length for arrays); narrow with json:".field" or slices
|
|
8
|
+
read("symbol or question") → same view as resolve:true
|
|
9
|
+
read({query,resolve:true}) → {status,path,line,text,...}
|
|
10
10
|
read(path,{about}) → matching windows; read({query,evidence:true}) → ranked evidence; read({path,outline:true}) → declarations
|
|
11
|
-
write(path, text) → replace; write({path,content,append:true}) → append without a prior read
|
|
12
|
-
edit(path,oldText,newText) | edit({path,edits})
|
|
13
|
-
edit(async () => {...}) → checkpoint: commit on success, rollback on throw
|
|
11
|
+
write(path, text) → replace an unread file; write({path,content,append:true}) → append without a prior read. After read use edit; replace:true overrides.
|
|
12
|
+
edit(path,oldText,newText) | edit({path,edits}) → numbered post-edit lines, checks, references
|
|
13
|
+
edit(async () => {...}) → checkpoint: commit on success, rollback on throw
|
|
14
14
|
bash(command,{cwd?,timeoutMs?}) → bounded output; nonzero throws
|
|
15
|
-
bash({command,args}) → literal argv
|
|
15
|
+
bash({command,args}) → literal argv
|
|
16
16
|
|
|
17
|
-
edit oldText is an exact substring of read(); a miss includes a numbered window. Found is a span, not the file
|
|
18
|
-
programs:[{code|file,data?},...] sequential fresh guests, separate commits; top-level data defaults per entry
|
|
17
|
+
edit oldText is an exact substring of read(); a miss includes a numbered window. Found is a span, not the file. Check resolve:true status; edit(view,text) replaces that window; edit(view,old,new) is unique inside it. complete:true rejects partial files. Array reads reject failures. Edits stage until success; bash commits preceding writes. Batch known reads, edits, and tests in this program (Promise.all, or programs with parallel:true).
|
|
18
|
+
programs:[{code|file,data?},...] sequential fresh guests, separate commits; top-level data defaults per entry. Stop on failure keeps earlier commits. parallel:true runs disjoint entries concurrently. Separate supernova calls only when the next step needs a model decision.
|
|
19
|
+
`;
|