pi-supernova 0.5.0 → 0.6.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 +73 -11
- package/docs/CHANGELOG.md +46 -0
- package/docs/TOKEN_COSTS.md +63 -29
- package/index.js +7 -4
- package/package.json +2 -2
- package/src/bridge/catalog.js +14 -10
- package/src/bridge/host-bridge.js +810 -145
- package/src/bridge/native-tools.js +16 -6
- package/src/context/evidence.js +13 -5
- package/src/context/fuzzy.js +34 -13
- package/src/context/outline.js +11 -2
- package/src/context/repo-index.js +150 -15
- package/src/context/search.js +70 -17
- package/src/context/snap.js +98 -46
- package/src/context/surface.js +15 -9
- package/src/fs/check.js +10 -2
- package/src/fs/patch.js +4 -2
- package/src/fs/vfs.js +133 -36
- package/src/fs/workspace.js +7 -1
- package/src/output/bottleneck.js +36 -11
- package/src/output/format.js +44 -29
- package/src/runtime/guest-worker.js +135 -39
- package/src/runtime/parallel.js +4 -1
- package/src/runtime/program-batch.js +17 -6
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +5 -5
- package/src/runtime/runtime.js +17 -6
- package/src/shared/decode.js +15 -3
- package/src/ui/omp-frame.js +11 -4
- package/src/ui/render.js +2 -2
package/src/fs/workspace.js
CHANGED
|
@@ -16,6 +16,9 @@ const realNearest = new Map();
|
|
|
16
16
|
const PATH_CACHE_MAX = 2048;
|
|
17
17
|
|
|
18
18
|
export function clearPathCache() {
|
|
19
|
+
cachedCwd = null;
|
|
20
|
+
cachedResolvedCwd = null;
|
|
21
|
+
realRoots.clear();
|
|
19
22
|
realNearest.clear();
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -126,7 +129,10 @@ export async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = f
|
|
|
126
129
|
export async function runCommand(argv, options = {}) {
|
|
127
130
|
options.signal?.throwIfAborted();
|
|
128
131
|
const cwd = options.cwd || process.cwd();
|
|
129
|
-
const
|
|
132
|
+
const requestedTimeout = Number(options.timeoutMs === undefined ? 60_000 : options.timeoutMs);
|
|
133
|
+
|
|
134
|
+
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error("command timeoutMs must be a positive finite number");
|
|
135
|
+
const timeoutMs = Math.max(1, Math.min(2_147_483_647, Math.floor(requestedTimeout)));
|
|
130
136
|
const maxOutputChars = options.maxOutputChars ?? 2 * 1024 * 1024;
|
|
131
137
|
|
|
132
138
|
return new Promise((resolve, reject) => {
|
package/src/output/bottleneck.js
CHANGED
|
@@ -19,7 +19,7 @@ function detailsOf(raw) {
|
|
|
19
19
|
export function hostResultFailed(raw) {
|
|
20
20
|
const details = detailsOf(raw);
|
|
21
21
|
|
|
22
|
-
return raw?.isError === true || details?.ok === false || (Number.isInteger(details?.exitCode) && details.exitCode !== 0);
|
|
22
|
+
return raw?.isError === true || raw?.ok === false || details?.ok === false || (Number.isInteger(details?.exitCode) && details.exitCode !== 0);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function extractRawString(raw) {
|
|
@@ -92,12 +92,16 @@ function summarizeDetails(value, budget = 2000) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function spill(fullText, config) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
try {
|
|
96
|
+
if (!isString(config.spillDir) || !config.spillDir) return undefined;
|
|
97
|
+
fs.mkdirSync(config.spillDir, { recursive: true, mode: 0o700 });
|
|
98
|
+
const file = path.join(config.spillDir, Date.now() + "-" + randomUUID().slice(0, 8) + ".txt");
|
|
99
|
+
fs.writeFileSync(file, fullText, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
100
|
+
|
|
101
|
+
return file;
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
export function packageHostResult(raw, config) {
|
|
@@ -108,7 +112,10 @@ export function packageHostResult(raw, config) {
|
|
|
108
112
|
const capped = truncateChars(text, maxChars, "host-result");
|
|
109
113
|
let truncated = capped.truncated || details?.outputTruncated === true;
|
|
110
114
|
const image = raw?.content?.find(part => part?.type === "image");
|
|
111
|
-
const
|
|
115
|
+
const directoryEntries = image === undefined && details?.directory === true && Array.isArray(details.entries) && json(details.entries).length <= maxChars
|
|
116
|
+
? details.entries
|
|
117
|
+
: undefined;
|
|
118
|
+
const result = { ok: !hostResultFailed(raw), value: image ?? directoryEntries ?? capped.text, truncated };
|
|
112
119
|
|
|
113
120
|
if (details !== undefined) result.details = summarizeDetails(batch ? { ...details, items: undefined } : details);
|
|
114
121
|
|
|
@@ -117,7 +124,25 @@ export function packageHostResult(raw, config) {
|
|
|
117
124
|
let remaining = maxChars;
|
|
118
125
|
result.items = batch.map((item, index) => {
|
|
119
126
|
if (item?.type === "image") return item;
|
|
120
|
-
const
|
|
127
|
+
const share = details.independent === true ? maxChars : Math.floor(remaining / (batch.length - index));
|
|
128
|
+
|
|
129
|
+
if (!isString(item)) {
|
|
130
|
+
const encoded = json(item);
|
|
131
|
+
|
|
132
|
+
if (encoded.length <= share) {
|
|
133
|
+
remaining -= encoded.length;
|
|
134
|
+
|
|
135
|
+
return item;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const bounded = truncateChars(encoded, share, "host-result");
|
|
139
|
+
remaining -= bounded.text.length;
|
|
140
|
+
truncated ||= bounded.truncated;
|
|
141
|
+
|
|
142
|
+
return bounded.text;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const bounded = truncateChars(item, share, "host-result");
|
|
121
146
|
remaining -= bounded.text.length;
|
|
122
147
|
truncated ||= bounded.truncated;
|
|
123
148
|
|
|
@@ -128,10 +153,10 @@ export function packageHostResult(raw, config) {
|
|
|
128
153
|
result.truncated = truncated;
|
|
129
154
|
|
|
130
155
|
if (truncated) {
|
|
131
|
-
result.originalChars = batch ? batch.reduce((sum, item) => sum +
|
|
156
|
+
result.originalChars = batch ? batch.reduce((sum, item) => sum + (isString(item) ? item.length : json(item).length), 0) : text.length;
|
|
132
157
|
|
|
133
158
|
if (config.spillDir) {
|
|
134
|
-
const pointer = spill(batch ? batch.join("\n---\n") : text, config);
|
|
159
|
+
const pointer = spill(batch ? batch.map(item => isString(item) ? item : json(item)).join("\n---\n") : text, config);
|
|
135
160
|
|
|
136
161
|
if (pointer) {
|
|
137
162
|
result.spill = pointer;
|
package/src/output/format.js
CHANGED
|
@@ -143,7 +143,8 @@ function formatPrimitive(value) {
|
|
|
143
143
|
|
|
144
144
|
if (Number.isNaN(value) || value === Infinity || value === -Infinity) return String(value);
|
|
145
145
|
|
|
146
|
-
return JSON.stringify(value) ?? String(value);
|
|
146
|
+
try { return JSON.stringify(value) ?? String(value); }
|
|
147
|
+
catch { return String(value); }
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/**
|
|
@@ -168,39 +169,46 @@ function formatFlatWithin(value, limit) {
|
|
|
168
169
|
return true;
|
|
169
170
|
};
|
|
170
171
|
|
|
171
|
-
return walkFlat(value, push) ? parts.join("") : null;
|
|
172
|
+
return walkFlat(value, push, new Set()) ? parts.join("") : null;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
function walkFlat(value, push) {
|
|
175
|
+
function walkFlat(value, push, seen) {
|
|
175
176
|
if (value?.[RAW_TEXT] !== undefined) return push(value[RAW_TEXT]);
|
|
176
177
|
|
|
177
178
|
if (!isObject(value) && !Array.isArray(value)) return push(formatPrimitive(value));
|
|
178
179
|
|
|
179
|
-
if (
|
|
180
|
-
|
|
180
|
+
if (seen.has(value)) return push("[Circular]");
|
|
181
|
+
seen.add(value);
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
try {
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
if (value.length === 0) return push("[]");
|
|
183
186
|
|
|
184
|
-
|
|
185
|
-
if (i && !push(",")) return false;
|
|
187
|
+
if (!push("[")) return false;
|
|
186
188
|
|
|
187
|
-
|
|
189
|
+
for (let i = 0; i < value.length; i++) {
|
|
190
|
+
if (i && !push(",")) return false;
|
|
191
|
+
|
|
192
|
+
if (!walkFlat(value[i] === undefined ? null : value[i], push, seen)) return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return push("]");
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
}
|
|
198
|
+
const keys = Object.keys(value).filter((key) => value[key] !== undefined);
|
|
192
199
|
|
|
193
|
-
|
|
200
|
+
if (keys.length === 0) return push("{}");
|
|
194
201
|
|
|
195
|
-
|
|
202
|
+
for (let i = 0; i < keys.length; i++) {
|
|
203
|
+
if (!push((i ? "," : "{") + formatKey(keys[i]) + ":")) return false;
|
|
196
204
|
|
|
197
|
-
|
|
198
|
-
|
|
205
|
+
if (!walkFlat(value[keys[i]], push, seen)) return false;
|
|
206
|
+
}
|
|
199
207
|
|
|
200
|
-
|
|
208
|
+
return push("}");
|
|
209
|
+
} finally {
|
|
210
|
+
seen.delete(value);
|
|
201
211
|
}
|
|
202
|
-
|
|
203
|
-
return push("}");
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
/**
|
|
@@ -209,24 +217,31 @@ function walkFlat(value, push) {
|
|
|
209
217
|
* indent is one space. Whitespace is what costs tokens: this measures ~43% fewer
|
|
210
218
|
* than JSON.stringify(value, null, 2) on typical shaped returns (gpt-tokenizer).
|
|
211
219
|
*/
|
|
212
|
-
export function formatValue(value, indent = "", width = FORMAT_WIDTH) {
|
|
220
|
+
export function formatValue(value, indent = "", width = FORMAT_WIDTH, seen = new Set()) {
|
|
213
221
|
if (value?.[RAW_TEXT] !== undefined) return value[RAW_TEXT];
|
|
214
222
|
|
|
215
223
|
if (!isObject(value) && !Array.isArray(value)) return formatPrimitive(value);
|
|
216
|
-
|
|
224
|
+
if (seen.has(value)) return "[Circular]";
|
|
225
|
+
seen.add(value);
|
|
217
226
|
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
try {
|
|
228
|
+
const flat = formatFlatWithin(value, width - indent.length);
|
|
220
229
|
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
if (flat !== null) return flat;
|
|
231
|
+
const pad = indent + " ";
|
|
223
232
|
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
if (value.length === 0) return "[]";
|
|
235
|
+
|
|
236
|
+
return "[\n" + value.map((item) => pad + formatValue(item === undefined ? null : item, pad, width, seen)).join(",\n") + "\n" + indent + "]";
|
|
237
|
+
}
|
|
226
238
|
|
|
227
|
-
|
|
239
|
+
const keys = Object.keys(value).filter((key) => value[key] !== undefined);
|
|
228
240
|
|
|
229
|
-
|
|
241
|
+
if (keys.length === 0) return "{}";
|
|
230
242
|
|
|
231
|
-
|
|
243
|
+
return "{\n" + keys.map((key) => pad + formatKey(key) + ":" + formatValue(value[key], pad, width, seen)).join(",\n") + "\n" + indent + "}";
|
|
244
|
+
} finally {
|
|
245
|
+
seen.delete(value);
|
|
246
|
+
}
|
|
232
247
|
}
|
|
@@ -110,18 +110,41 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
110
110
|
if (token ? token !== checkpoint : checkpoint !== null) throw new Error("await the active edit checkpoint before issuing other commands; completed checkpoints cannot issue commands");
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
let operationTail = Promise.resolve();
|
|
114
|
+
|
|
115
|
+
const enqueueHost = operation => {
|
|
116
|
+
const next = operationTail.then(operation);
|
|
117
|
+
operationTail = next.then(() => {}, () => {});
|
|
118
|
+
|
|
119
|
+
return next;
|
|
120
|
+
};
|
|
121
|
+
|
|
113
122
|
const nova = {
|
|
114
|
-
call
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
call(name, args) {
|
|
124
|
+
assertScope(); flushReads();
|
|
125
|
+
const promise = enqueueHost(() => rpc("call", [name, args]).then(leanEnvelope));
|
|
126
|
+
|
|
127
|
+
promise.catch(() => {});
|
|
128
|
+
|
|
129
|
+
return promise;
|
|
130
|
+
},
|
|
131
|
+
callMany(calls) {
|
|
132
|
+
assertScope(); flushReads();
|
|
133
|
+
const promise = enqueueHost(async () => {
|
|
134
|
+
const wave = await rpc("callMany", [calls]);
|
|
135
|
+
const results = Array.isArray(wave?.results) ? wave.results : Array.isArray(wave) ? wave : [];
|
|
136
|
+
Object.defineProperties(results, {
|
|
137
|
+
mode: { value: wave?.mode, enumerable: false },
|
|
138
|
+
reason: { value: wave?.reason, enumerable: false },
|
|
139
|
+
results: { value: results, enumerable: false },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return results;
|
|
122
143
|
});
|
|
123
144
|
|
|
124
|
-
|
|
145
|
+
promise.catch(() => {});
|
|
146
|
+
|
|
147
|
+
return promise;
|
|
125
148
|
},
|
|
126
149
|
async speculate(fn) {
|
|
127
150
|
if (checkpoint) throw new Error("edit checkpoints cannot overlap or nest; await the current checkpoint");
|
|
@@ -130,30 +153,31 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
130
153
|
let began = false;
|
|
131
154
|
|
|
132
155
|
try {
|
|
133
|
-
|
|
134
|
-
await rpc("speculateBegin", []);
|
|
156
|
+
await drainReads();
|
|
157
|
+
await enqueueHost(() => rpc("speculateBegin", []));
|
|
135
158
|
began = true;
|
|
136
159
|
const value = await checkpointScope.run(token, fn);
|
|
137
|
-
|
|
138
|
-
await rpc("speculateCommit", []);
|
|
160
|
+
await drainReads();
|
|
161
|
+
await enqueueHost(() => rpc("speculateCommit", []));
|
|
139
162
|
|
|
140
163
|
return { ok: true, committed: true, value };
|
|
141
164
|
} catch (err) {
|
|
142
|
-
|
|
165
|
+
await drainReads();
|
|
143
166
|
|
|
144
|
-
if (began) await rpc("speculateRollback", []);
|
|
167
|
+
if (began) await enqueueHost(() => rpc("speculateRollback", []));
|
|
145
168
|
|
|
146
169
|
return { ok: false, committed: false, error: err instanceof Error ? err.message : String(err) };
|
|
147
170
|
} finally { checkpoint = null; }
|
|
148
171
|
},
|
|
149
|
-
surface
|
|
150
|
-
evidence
|
|
151
|
-
snap
|
|
172
|
+
surface(filePath) { assertScope(); flushReads(); const promise = enqueueHost(() => rpc("call", ["surface", { path: filePath }]).then(unwrapJsonValue)); promise.catch(() => {}); return promise; },
|
|
173
|
+
evidence(query, opts) { assertScope(); flushReads(); const promise = enqueueHost(() => rpc("call", ["evidence", { query, ...opts }]).then(unwrapJsonValue)); promise.catch(() => {}); return promise; },
|
|
174
|
+
snap(query, targetPath) { assertScope(); flushReads(); const promise = enqueueHost(() => rpc("call", ["snap", { query, path: targetPath }]).then(unwrapJsonValue)); promise.catch(() => {}); return promise; },
|
|
152
175
|
has: (name) => availableSet.has(name),
|
|
153
176
|
};
|
|
154
177
|
|
|
155
178
|
// Coalesce already-started compatible reads without rewriting JS control flow.
|
|
156
179
|
let queuedReads = [];
|
|
180
|
+
const pendingReadWaves = new Set();
|
|
157
181
|
|
|
158
182
|
function flushReads() {
|
|
159
183
|
const pending = queuedReads;
|
|
@@ -163,13 +187,13 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
163
187
|
const wave = pending.slice(start, start + 64);
|
|
164
188
|
const args = { ...wave[0].args, path: wave.map(job => job.args.path), _independent: true };
|
|
165
189
|
|
|
166
|
-
const run = wave.length === 1
|
|
167
|
-
?
|
|
168
|
-
:
|
|
190
|
+
const run = enqueueHost(() => wave.length === 1
|
|
191
|
+
? rpc("call", ["read", wave[0].args]).then(leanEnvelope).then(res => ({ values: [unwrapRead(res, wave[0].args)], errors: [] }))
|
|
192
|
+
: rpc("call", ["read", args]).then(leanEnvelope).then(res => { unwrapRead(res, args);
|
|
169
193
|
|
|
170
|
-
return { values: res.items, errors: res.itemErrors ?? [] }; });
|
|
194
|
+
return { values: res.items, errors: res.itemErrors ?? [] }; }));
|
|
171
195
|
|
|
172
|
-
|
|
196
|
+
const delivery = run.then(({ values, errors }) => {
|
|
173
197
|
if (!Array.isArray(values) || values.length !== wave.length) throw new Error("invalid batch read response");
|
|
174
198
|
|
|
175
199
|
for (let i = 0; i < wave.length; i++) {
|
|
@@ -179,31 +203,80 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
179
203
|
else wave[i].resolve(value);
|
|
180
204
|
}
|
|
181
205
|
}).catch(error => { for (const job of wave) job.reject(error); });
|
|
206
|
+
|
|
207
|
+
pendingReadWaves.add(delivery);
|
|
208
|
+
void delivery.finally(() => pendingReadWaves.delete(delivery));
|
|
182
209
|
}
|
|
183
210
|
}
|
|
184
211
|
|
|
185
|
-
|
|
212
|
+
async function drainReads() {
|
|
213
|
+
for (;;) {
|
|
214
|
+
flushReads();
|
|
215
|
+
const waves = [...pendingReadWaves];
|
|
216
|
+
|
|
217
|
+
if (!waves.length) return;
|
|
218
|
+
await Promise.allSettled(waves);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const invoke = (name, args) => {
|
|
223
|
+
assertScope(); flushReads();
|
|
224
|
+
const promise = nova.call(name, args);
|
|
225
|
+
|
|
226
|
+
promise.catch(() => {});
|
|
227
|
+
|
|
228
|
+
return promise;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const readArgs = (p, a, b) => {
|
|
232
|
+
if (isObject(p) && !Array.isArray(p)) {
|
|
233
|
+
const args = { ...p, path: p.path ?? p.target ?? p.query };
|
|
186
234
|
|
|
187
|
-
|
|
235
|
+
if (p.path === undefined && p.target !== undefined) delete args.target;
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
237
|
+
return args;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b };
|
|
241
|
+
};
|
|
192
242
|
|
|
193
243
|
const read = async (p, a, b) => {
|
|
194
244
|
assertScope();
|
|
195
245
|
const args = sessionJsonArgs(readArgs(p, a, b));
|
|
196
|
-
if (isString(args.path) && args.resolve === undefined && args.json === undefined && !looksLikePath(args.path) && !/^(?:agent|artifact):\/\//i.test(args.path)) {
|
|
246
|
+
if (isString(args.path) && args.resolve === undefined && args.complete !== true && args.json === undefined && args.about === undefined && args.query === undefined && !looksLikePath(args.path) && !/^(?:agent|artifact):\/\//i.test(args.path)) {
|
|
197
247
|
args.resolve = true;
|
|
198
248
|
}
|
|
199
249
|
validateJsonRead(args);
|
|
200
|
-
const decode = value => args.resolve || args.json !== undefined ? JSON.parse(value) : value;
|
|
201
250
|
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
for (const [key, value] of [["resolve", args.resolve], ["complete", args.complete], ["outline", args.outline], ["evidence", args.evidence]]) {
|
|
252
|
+
if (value !== undefined && typeof value !== "boolean") throw new Error("read " + key + " must be a boolean");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (args.about !== undefined && !isString(args.about)) throw new Error("read about must be a string");
|
|
256
|
+
if (args.query !== undefined && !isString(args.query)) throw new Error("read query must be a string");
|
|
257
|
+
const focusModes = [args.about !== undefined, args.query !== undefined, args.outline === true].filter(Boolean).length;
|
|
258
|
+
|
|
259
|
+
if (focusModes > 1 || (args.outline === true && args.evidence === true)) throw new Error("read accepts only one of about, query, outline, or evidence");
|
|
260
|
+
if (args.resolve === true && args.complete === true) throw new Error("read accepts either resolve or complete, not both");
|
|
261
|
+
const decode = value => {
|
|
262
|
+
if ((!args.resolve && args.json === undefined) || !isString(value)) return value;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(value);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const selector = args.json === undefined ? "" : " (" + (Array.isArray(args.json) ? args.json.join(", ") : String(args.json)) + ")";
|
|
268
|
+
throw new Error("JSON read failed for " + String(args.path ?? args.target ?? "resource") + selector + ": " + (error instanceof Error ? error.message : String(error)));
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (args.complete === true && (args.outline || args.evidence || args.about || args.query)) throw new Error("complete:true requires a raw file read, not a source view");
|
|
273
|
+
const evidencePath = isObject(p) && !Array.isArray(p) ? p.path : (isString(p) && looksLikePath(p) ? p : undefined);
|
|
204
274
|
p = args.path;
|
|
205
275
|
|
|
206
|
-
if (args.evidence)
|
|
276
|
+
if (args.evidence) {
|
|
277
|
+
const query = args.about ?? args.query ?? (isString(p) ? p : undefined);
|
|
278
|
+
return unwrapJsonValue(await invoke("evidence", { ...args, path: evidencePath, query }));
|
|
279
|
+
}
|
|
207
280
|
|
|
208
281
|
if (args.outline) return unwrapJsonValue(await invoke("surface", args));
|
|
209
282
|
|
|
@@ -211,9 +284,24 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
211
284
|
if (p.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
212
285
|
|
|
213
286
|
for (const item of p) if (!isString(item) || !item.trim()) throw new Error("read paths must be non-empty strings");
|
|
214
|
-
const readEach = () =>
|
|
287
|
+
const readEach = async () => {
|
|
288
|
+
const values = await Promise.all(p.map(item => read({ ...args, path: item })));
|
|
289
|
+
const missing = args.resolve ? values.findIndex((value, index) => value?.status === "not_found" && looksLikePath(p[index])) : -1;
|
|
215
290
|
|
|
216
|
-
|
|
291
|
+
if (missing >= 0) throw new Error(`read failed for ${p[missing]}: not_found; use Promise.allSettled(paths.map(path => read(path))) for per-path outcomes`);
|
|
292
|
+
|
|
293
|
+
return values;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const guardedReadEach = () => {
|
|
297
|
+
const promise = readEach();
|
|
298
|
+
|
|
299
|
+
promise.catch(() => {});
|
|
300
|
+
|
|
301
|
+
return promise;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (!batchRead || args.resolve || p.some(item => /^(agent|artifact):\/\/.*\?/i.test(item))) return guardedReadEach();
|
|
217
305
|
const res = await invoke("read", args);
|
|
218
306
|
const failed = res?.itemErrors?.findIndex(error => error != null) ?? -1;
|
|
219
307
|
|
|
@@ -223,7 +311,7 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
223
311
|
if (Array.isArray(res?.items)) return res.items.map(decode);
|
|
224
312
|
|
|
225
313
|
// Captured host executor without batch support: fan out.
|
|
226
|
-
return
|
|
314
|
+
return guardedReadEach();
|
|
227
315
|
}
|
|
228
316
|
|
|
229
317
|
if (!batchRead) {
|
|
@@ -237,11 +325,15 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
237
325
|
|
|
238
326
|
if (queuedReads.length && queuedReads[0].key !== key) flushReads();
|
|
239
327
|
|
|
240
|
-
|
|
328
|
+
const promise = new Promise((resolve, reject) => {
|
|
241
329
|
queuedReads.push({ args, key, resolve, reject });
|
|
242
330
|
|
|
243
331
|
if (queuedReads.length === 1) queueMicrotask(flushReads);
|
|
244
332
|
}).then(decode);
|
|
333
|
+
|
|
334
|
+
promise.catch(() => {});
|
|
335
|
+
|
|
336
|
+
return promise;
|
|
245
337
|
};
|
|
246
338
|
|
|
247
339
|
const write = async (p, content) => unwrapValue(await invoke("write", isObject(p) ? p : { path: p, content }));
|
|
@@ -301,6 +393,7 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
301
393
|
if (!isString(args.command) || !Array.isArray(args.args)) throw new Error("bash argv requires a command string and an array of string args");
|
|
302
394
|
|
|
303
395
|
for (let i = 0; i < args.args.length; i++) if (!isString(args.args[i])) throw new Error("bash argv requires a command string and an array of string args");
|
|
396
|
+
args.args = args.args.map(String);
|
|
304
397
|
|
|
305
398
|
// Literal argv is a Supernova-owned contract. Host bash tools often ignore
|
|
306
399
|
// `args` and would run only `command` (bare `ssh`). Windows still needs a shell.
|
|
@@ -335,7 +428,7 @@ function buildGuestApi(available, batchRead, runId, _nativeArgv) {
|
|
|
335
428
|
return text;
|
|
336
429
|
};
|
|
337
430
|
|
|
338
|
-
return { read, write, edit, bash };
|
|
431
|
+
return { read, write, edit, bash, nova };
|
|
339
432
|
}
|
|
340
433
|
|
|
341
434
|
function makeConsole(runId, limits) {
|
|
@@ -359,7 +452,10 @@ function makeConsole(runId, limits) {
|
|
|
359
452
|
if (isString(a)) return a;
|
|
360
453
|
|
|
361
454
|
try {
|
|
362
|
-
|
|
455
|
+
const plain = toPlain(a);
|
|
456
|
+
const encoded = JSON.stringify(plain);
|
|
457
|
+
|
|
458
|
+
return encoded === undefined ? String(plain) : encoded;
|
|
363
459
|
} catch {
|
|
364
460
|
return String(a);
|
|
365
461
|
}
|
package/src/runtime/parallel.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { isFunction } from "../shared/decode.js";
|
|
1
|
+
import { isFunction, isObject } from "../shared/decode.js";
|
|
2
2
|
|
|
3
3
|
const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "evidence", "surface", "asgrep_search", "asgrep_status", "ast_grep", "web_search"]);
|
|
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
7
|
export function isMutatingTool(name, config = {}, args = {}, definition) {
|
|
8
|
+
if (!isObject(args)) args = {};
|
|
9
|
+
name = String(name);
|
|
10
|
+
|
|
8
11
|
if ((config.mutatingTools ?? []).includes(name)) return true;
|
|
9
12
|
|
|
10
13
|
if ((config.mutatingPrefixes ?? []).some(prefix => prefix && name.startsWith(prefix))) return true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isObject, isString } from "../shared/decode.js";
|
|
2
2
|
import { truncateChars } from "../output/format.js";
|
|
3
3
|
|
|
4
|
-
const textOf = result => result.content.filter(block => block
|
|
4
|
+
const textOf = result => (Array.isArray(result?.content) ? result.content : []).filter(block => block?.type === "text").map(block => block.text).join("\n");
|
|
5
5
|
|
|
6
6
|
const mutationTotals = results => results.reduce((total, result) => {
|
|
7
7
|
const m = result.details?.mutations;
|
|
@@ -24,7 +24,7 @@ export function programBatchText(results, total, stopped = "") {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function batchInputs(params, config) {
|
|
27
|
-
if (["code","file"
|
|
27
|
+
if (["code","file"].some(key => params[key] !== undefined)) throw new Error("programs cannot combine with top-level code or file; no programs ran");
|
|
28
28
|
|
|
29
29
|
if (!Array.isArray(params.programs) || !params.programs.length || params.programs.length > 32) throw new Error("programs requires 1..32 entries; no programs ran");
|
|
30
30
|
|
|
@@ -35,20 +35,31 @@ function batchInputs(params, config) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const hasDefault = params.data !== undefined;
|
|
38
39
|
let encoded;
|
|
39
40
|
|
|
40
|
-
try { encoded = JSON.stringify(params.programs); } catch { throw new Error("programs must be JSON-serializable; no programs ran"); }
|
|
41
|
+
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"); }
|
|
41
42
|
|
|
42
43
|
if (encoded.length > (config.maxCodeChars ?? 48000)) throw new Error("programs JSON exceeds the code character budget; no programs ran");
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
const parsed = JSON.parse(encoded);
|
|
46
|
+
|
|
47
|
+
if (!hasDefault) return parsed;
|
|
48
|
+
if (!Object.hasOwn(parsed,"data")) throw new Error("data must be JSON-serializable; no programs ran");
|
|
49
|
+
|
|
50
|
+
// The runtime snapshots data separately for each fresh guest. An explicit
|
|
51
|
+
// entry replaces the default wholesale; falsy values are not missing values.
|
|
52
|
+
return parsed.programs.map(program => program.data === undefined ? {...program,data:parsed.data} : program);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/** Explicit known continuations, not inferred plans, retries, or a shared heap. */
|
|
48
56
|
export async function runProgramBatch(id, params, signal, onUpdate, ctx, config, execute) {
|
|
49
57
|
const programs = batchInputs(params,config);
|
|
58
|
+
const requestedTimeout = params.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs);
|
|
59
|
+
|
|
60
|
+
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error("program batch timeoutMs must be a positive finite number");
|
|
50
61
|
const started = performance.now();
|
|
51
|
-
const timeout =
|
|
62
|
+
const timeout = requestedTimeout;
|
|
52
63
|
const deadline = started + timeout;
|
|
53
64
|
const controller = new AbortController();
|
|
54
65
|
const combined = signal ? AbortSignal.any([signal,controller.signal]) : controller.signal;
|
|
@@ -76,7 +87,7 @@ export async function runProgramBatch(id, params, signal, onUpdate, ctx, config,
|
|
|
76
87
|
trace.push(...(result.details?.trace ?? []));
|
|
77
88
|
let image = 0;
|
|
78
89
|
|
|
79
|
-
for (const block of result.content) if (block
|
|
90
|
+
for (const block of Array.isArray(result?.content) ? result.content : []) if (block?.type === "image" && isString(block.data)) {
|
|
80
91
|
imageBytes += Buffer.byteLength(block.data,"base64");
|
|
81
92
|
|
|
82
93
|
if (images.length >= 16 || imageBytes > 20*1024*1024) { stopped = "batch image budget exceeded; remaining programs did not run"; break; }
|
|
@@ -4,6 +4,9 @@ import { resolveWorkspacePath } from "../fs/workspace.js";
|
|
|
4
4
|
/** Explicit source reuse, never a cached program or a persistent guest heap. */
|
|
5
5
|
export async function readProgramFile(file, cwd, maxChars, signal) {
|
|
6
6
|
signal?.throwIfAborted();
|
|
7
|
+
const chars = Number(maxChars);
|
|
8
|
+
|
|
9
|
+
if (!Number.isInteger(chars) || chars <= 0) throw new Error("program file maxChars must be a positive integer");
|
|
7
10
|
const target = await resolveWorkspacePath(cwd, file, "program file", false, true);
|
|
8
11
|
// A FIFO must fail without waiting for a writer or occupying an I/O worker.
|
|
9
12
|
const handle = await fs.open(target, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
@@ -15,8 +18,8 @@ export async function readProgramFile(file, cwd, maxChars, signal) {
|
|
|
15
18
|
if (!stat.isFile()) throw new Error("program file requires a regular file");
|
|
16
19
|
// Every UTF-16 unit needs at most three UTF-8 bytes. Also check streamed size:
|
|
17
20
|
// another editor can grow the file after stat. No prefix-only execution.
|
|
18
|
-
const maxBytes =
|
|
19
|
-
const tooLarge = () => new Error("code exceeds " +
|
|
21
|
+
const maxBytes = chars * 3;
|
|
22
|
+
const tooLarge = () => new Error("code exceeds " + chars + " characters; split the program");
|
|
20
23
|
|
|
21
24
|
if (stat.size > maxBytes) throw tooLarge();
|
|
22
25
|
const chunks = [];
|
|
@@ -33,7 +36,7 @@ export async function readProgramFile(file, cwd, maxChars, signal) {
|
|
|
33
36
|
// Do not silently replace invalid bytes in executable source. Preserve BOMs.
|
|
34
37
|
const code = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
|
|
35
38
|
|
|
36
|
-
if (code.length >
|
|
39
|
+
if (code.length > chars) throw tooLarge();
|
|
37
40
|
|
|
38
41
|
return code;
|
|
39
42
|
} finally { await handle.close(); }
|
package/src/runtime/reference.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Complete model-facing API reference; kept in every request, not moved into history.
|
|
2
2
|
export const REFERENCE = `JavaScript async body or arrow with read, write, edit, bash. Strings stay raw. Exactly one of code or file; file rereads that program (same limits, cwd, fresh guest). Caps are UTF-16. Put Markdown/scripts/argv in data.
|
|
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,
|
|
4
|
+
read(path|paths, offset?, limit?) → raw text or text[]; read(directory) → entries[]; up to 64 paths
|
|
5
|
+
read(imagePath) → image (PNG/JPEG/GIF/WebP/BMP); 20 MiB max, 16 attachments max
|
|
6
|
+
read({path,json:".field"}) → parsed JSON; .items[0:3], quoted keys, 1-64 selectors, or true; 16 MiB cap; no jq
|
|
7
7
|
read("agent://id?q=.answer") → JSON field from session artifacts
|
|
8
|
-
read("symbol or question") → same view as resolve:true
|
|
8
|
+
read("symbol or question") → same view as resolve:true; source questions use at most 16 keywords
|
|
9
9
|
read({query,resolve:true}) → {status,path,line,lines,text,complete,nextOffset?}
|
|
10
10
|
read(path,{about}) → matching windows; read({query,evidence:true}) → ranked evidence; read({path,outline:true}) → declarations
|
|
11
11
|
write(path, text) → replace; write({path,content,append:true}) → append without a prior read
|
|
@@ -15,4 +15,4 @@ bash(command,{cwd?,timeoutMs?}) → bounded output; nonzero throws
|
|
|
15
15
|
bash({command,args}) → literal argv, no shell expansion of args
|
|
16
16
|
|
|
17
17
|
edit oldText is an exact substring of read(); a miss includes a numbered window. Found is a span, not the file; uncertain returns ambiguous, not_found, or incomplete. Check resolve:true status; edit(view,text) replaces that window; edit(view,old,new) is unique inside it. complete:true rejects partial files; use about or offset/limit for large audits. Array reads reject failures; Promise.allSettled for per-path outcomes. Edits stage until success; bash commits preceding writes.
|
|
18
|
-
programs:[{code|file,data?},...] sequential fresh guests, separate commits;
|
|
18
|
+
programs:[{code|file,data?},...] sequential fresh guests, separate commits; top-level data defaults per entry, explicit entry data replaces it. Stop on failure keeps earlier commits. Separate calls when the next step needs a model decision.`;
|