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
package/src/output/bottleneck.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import {READ_VALUE} from "../shared/result.js";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { isString, isObject
|
|
5
|
-
import { truncateChars
|
|
5
|
+
import { isString, isObject } from "../shared/decode.js";
|
|
6
|
+
import { truncateChars } from "./format.js";
|
|
6
7
|
|
|
7
8
|
function json(value) {
|
|
8
9
|
try { return JSON.stringify(value) ?? "null"; } catch { return JSON.stringify(String(value)); }
|
|
@@ -221,8 +222,16 @@ function attachTruncation(result, truncated, batch, text, config, maxChars, capp
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
export function packageHostResult(raw, config) {
|
|
224
|
-
const maxChars = config.maxCallResultChars ?? 65536;
|
|
225
225
|
const details = detailsOf(raw);
|
|
226
|
+
if (raw && Object.hasOwn(raw, READ_VALUE)) {
|
|
227
|
+
const batch = batchFromDetails(details);
|
|
228
|
+
const result = {ok: !hostResultFailed(raw), value: raw[READ_VALUE], typed: true, cloneItems: details?.jsonMany === true, truncated: details?.outputTruncated === true};
|
|
229
|
+
attachDetails(result, details, batch);
|
|
230
|
+
if (batch) { result.items = batch; result.itemErrors = details.itemErrors ?? []; }
|
|
231
|
+
if (details?.streamed) result.streamed = true;
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
const maxChars = config.maxCallResultChars ?? 65536;
|
|
226
235
|
const batch = batchFromDetails(details);
|
|
227
236
|
const text = batch ? "" : extractRawString(raw);
|
|
228
237
|
const capped = truncateChars(text, maxChars, "host-result");
|
|
@@ -237,67 +246,4 @@ export function packageHostResult(raw, config) {
|
|
|
237
246
|
return result;
|
|
238
247
|
}
|
|
239
248
|
|
|
240
|
-
|
|
241
|
-
if (!(input?.type === "image" && isString(input.data) && isString(input.mimeType) && input.mimeType.startsWith("image/"))) return null;
|
|
242
|
-
assertModelImageMime(input.mimeType);
|
|
243
|
-
const size = Buffer.byteLength(input.data, "base64");
|
|
244
|
-
|
|
245
|
-
acc.imageCount += 1;
|
|
246
|
-
acc.imageBytes += size;
|
|
247
|
-
if (acc.imageCount > 16 || acc.imageBytes > 20 * 1024 * 1024) {
|
|
248
|
-
acc.imageOverflow = true;
|
|
249
|
-
return "[image over budget]";
|
|
250
|
-
}
|
|
251
|
-
acc.images.push({ type: "image", data: input.data, mimeType: input.mimeType });
|
|
252
|
-
|
|
253
|
-
return `[image ${acc.images.length}: ${input.mimeType}]`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function collectImages(input, acc) {
|
|
257
|
-
const replaced = collectImage(input, acc);
|
|
258
|
-
|
|
259
|
-
if (replaced !== null) return replaced;
|
|
260
|
-
|
|
261
|
-
return mapChangedChildren(input, collectImages, acc);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function serializeReturn(value, formatted, maxReturn, imageOverflow) {
|
|
265
|
-
if (formatted.length <= maxReturn) return { text: formatted, truncated: imageOverflow };
|
|
266
|
-
|
|
267
|
-
if (Array.isArray(value) && value.length && value.every(isString)) return { text: formatBoundedStringArray(value, maxReturn), truncated: true };
|
|
268
|
-
|
|
269
|
-
return { ...truncateChars(formatted, maxReturn, "return"), truncated: true };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function clipLogLine(line, maxLogLineChars) {
|
|
273
|
-
const result = truncateChars(line, maxLogLineChars, "log");
|
|
274
|
-
|
|
275
|
-
return { text: result.text, truncated: result.truncated };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function clipLogs(logs, config) {
|
|
279
|
-
const maxLines = config.maxLogLines ?? 100;
|
|
280
|
-
let logTruncated = logs.length > maxLines;
|
|
281
|
-
const clipped = logs.slice(0, maxLines).map(line => {
|
|
282
|
-
const result = clipLogLine(line, config.maxLogLineChars ?? 4096);
|
|
283
|
-
logTruncated ||= result.truncated;
|
|
284
|
-
|
|
285
|
-
return result.text;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return { logs: clipped, logTruncated };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function packageFinalReturn(value, logs, config) {
|
|
292
|
-
const acc = { images: [], imageCount: 0, imageBytes: 0, imageOverflow: false };
|
|
293
|
-
value = collectImages(value, acc);
|
|
294
|
-
if (acc.imageOverflow) {
|
|
295
|
-
throw new Error(`image attachment budget exceeded: ${acc.imageCount} images / ${acc.imageBytes} bytes; limit is 16 images / 20971520 bytes (20 MiB). No images returned; return fewer or smaller images per program`);
|
|
296
|
-
}
|
|
297
|
-
const maxReturn = config.maxReturnChars ?? 32000;
|
|
298
|
-
const serialized = serializeReturn(value, formatReturn(value), maxReturn, acc.imageOverflow);
|
|
299
|
-
const clipped = clipLogs(logs, config);
|
|
300
|
-
|
|
301
|
-
return { returnValue: serialized.truncated ? serialized.text : value, returnText: serialized.text,
|
|
302
|
-
returnTruncated: serialized.truncated, logs: clipped.logs, logTruncated: clipped.logTruncated, images: acc.images };
|
|
303
|
-
}
|
|
249
|
+
export {packageFinalReturn} from "./final.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {isString,mapChangedChildren,assertModelImageMime,decodeImageData} from "../shared/decode.js";
|
|
2
|
+
import {assertPng} from "../shared/png.js";
|
|
3
|
+
import {truncateChars,formatReturn,formatBoundedStringArray,formatBoundedValue,isStringArray} from "./format.js";
|
|
4
|
+
|
|
5
|
+
function collectImage(input, acc) {
|
|
6
|
+
if (!(input?.type === "image" && isString(input.data) && isString(input.mimeType) && input.mimeType.startsWith("image/"))) return null;
|
|
7
|
+
assertModelImageMime(input.mimeType);
|
|
8
|
+
const size = Buffer.byteLength(input.data, "base64");
|
|
9
|
+
|
|
10
|
+
acc.imageCount += 1;
|
|
11
|
+
acc.imageBytes += size;
|
|
12
|
+
if (acc.imageCount > 16 || acc.imageBytes > 20 * 1024 * 1024) {
|
|
13
|
+
acc.imageOverflow = true;
|
|
14
|
+
return "[image over budget]";
|
|
15
|
+
}
|
|
16
|
+
const bytes = decodeImageData(input.data);
|
|
17
|
+
if (input.mimeType === "image/png") assertPng(bytes);
|
|
18
|
+
acc.images.push({ type: "image", data: input.data, mimeType: input.mimeType });
|
|
19
|
+
|
|
20
|
+
return `[image ${acc.images.length}: ${input.mimeType}]`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function displayWeight(value,key) {
|
|
24
|
+
// Count even omitted undefined fields: typed result metadata crosses RPC too.
|
|
25
|
+
const field = isString(key) ? key.length+1 : 0;
|
|
26
|
+
return field + (isString(value) ? value.length : 1) + (Array.isArray(value) ? value.length : 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectImages(input, acc, key) {
|
|
30
|
+
const replaced = collectImage(input, acc);
|
|
31
|
+
acc.displayUnits += displayWeight(replaced ?? input,key);
|
|
32
|
+
if (replaced !== null) return replaced;
|
|
33
|
+
|
|
34
|
+
return mapChangedChildren(input, collectImages, acc);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serializeReturn(value, formatted, maxReturn, imageOverflow) {
|
|
38
|
+
if (formatted.length <= maxReturn) return { text: formatted, truncated: imageOverflow };
|
|
39
|
+
|
|
40
|
+
if (isStringArray(value)) return { text: formatBoundedStringArray(value, maxReturn), truncated: true };
|
|
41
|
+
|
|
42
|
+
return { ...truncateChars(formatted, maxReturn, "return"), truncated: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clipLogLine(line, maxLogLineChars) {
|
|
46
|
+
const result = truncateChars(line, maxLogLineChars, "log");
|
|
47
|
+
|
|
48
|
+
return { text: result.text, truncated: result.truncated };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clipLogs(logs, config) {
|
|
52
|
+
const maxLines = config.maxLogLines ?? 100;
|
|
53
|
+
let logTruncated = logs.length > maxLines;
|
|
54
|
+
const clipped = logs.slice(0, maxLines).map(line => {
|
|
55
|
+
const result = clipLogLine(line, config.maxLogLineChars ?? 4096);
|
|
56
|
+
logTruncated ||= result.truncated;
|
|
57
|
+
|
|
58
|
+
return result.text;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { logs: clipped, logTruncated };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isSourceView(value) {
|
|
65
|
+
return value?.status === "found" && isString(value.path) && isString(value.text) && Array.isArray(value.lines);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sourcePreview(value, maxReturn) {
|
|
69
|
+
if (!isSourceView(value)) return null;
|
|
70
|
+
if (value.text.length < maxReturn / 2) return null;
|
|
71
|
+
if (value.text.length <= maxReturn && formatReturn(value).length <= maxReturn-256) return null;
|
|
72
|
+
const base = {...value,text:"",complete:false,nextOffset:value.lines[0]+1};
|
|
73
|
+
let room = maxReturn - formatReturn(base).length - 512;
|
|
74
|
+
while (room > 0) {
|
|
75
|
+
const end = value.text.lastIndexOf("\n",room-1) + 1;
|
|
76
|
+
if (!end || end >= value.text.length) return null;
|
|
77
|
+
const text = value.text.slice(0,end);
|
|
78
|
+
const lines = [value.lines[0],value.lines[0]+text.match(/\n/g).length-1];
|
|
79
|
+
const preview = {...base,text,lines,nextOffset:lines[1]+1};
|
|
80
|
+
if (formatReturn(preview).length <= maxReturn-256) return preview;
|
|
81
|
+
room = Math.floor(room / 2);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function presentValue(value, maxReturn, oversized) {
|
|
87
|
+
if (isString(value)) {
|
|
88
|
+
const bounded = truncateChars(value,maxReturn,"return");
|
|
89
|
+
const result = serializeReturn(value,formatReturn(bounded.text),maxReturn,false);
|
|
90
|
+
result.truncated ||= bounded.truncated;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
if (isStringArray(value) && oversized) {
|
|
94
|
+
return {text:formatBoundedStringArray(value,maxReturn),truncated:true};
|
|
95
|
+
}
|
|
96
|
+
if (oversized) return {text:formatBoundedValue(value,maxReturn),truncated:true};
|
|
97
|
+
return serializeReturn(value,formatReturn(value),maxReturn,false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function packageFinalReturn(value, logs, config) {
|
|
101
|
+
const acc = { images: [], imageCount: 0, imageBytes: 0, imageOverflow: false, displayUnits:0 };
|
|
102
|
+
value = collectImages(value, acc);
|
|
103
|
+
if (acc.imageOverflow) {
|
|
104
|
+
throw new Error(`image attachment budget exceeded: ${acc.imageCount} images / ${acc.imageBytes} bytes; limit is 16 images / 20971520 bytes (20 MiB). No images returned; return fewer or smaller images per program`);
|
|
105
|
+
}
|
|
106
|
+
const maxReturn = config.maxReturnChars ?? 32000;
|
|
107
|
+
const preview = sourcePreview(value,maxReturn);
|
|
108
|
+
if (preview) value = preview;
|
|
109
|
+
const serialized = presentValue(value,maxReturn,!preview && acc.displayUnits>maxReturn);
|
|
110
|
+
const clipped = clipLogs(logs, config);
|
|
111
|
+
|
|
112
|
+
return { returnValue: serialized.truncated ? serialized.text : value, returnText: serialized.text,
|
|
113
|
+
returnTruncated: serialized.truncated || Boolean(preview), logs: clipped.logs, logTruncated: clipped.logTruncated, images: acc.images };
|
|
114
|
+
}
|
package/src/output/format.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isString, isObject, mapChangedChildren } from "../shared/decode.js";
|
|
2
|
+
import {maxJsonStringPrefix} from "../fs/json-size.js";
|
|
2
3
|
|
|
3
4
|
function normalizeText(text) {
|
|
4
5
|
return isString(text) ? text : String(text ?? "");
|
|
@@ -76,24 +77,40 @@ function hasUnpairedSurrogate(text) {
|
|
|
76
77
|
return SURROGATE_CODE_UNIT.test(text) && UNPAIRED_SURROGATE.test(text);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
export function isStringArray(value) {
|
|
81
|
+
if (!Array.isArray(value) || !value.length) return false;
|
|
82
|
+
for (const item of value) if (!isString(item)) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
function hasWellFormedStrings(values) {
|
|
80
87
|
for (const value of values) if (!isString(value) || hasUnpairedSurrogate(value)) return false;
|
|
81
88
|
|
|
82
89
|
return true;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
function boundedStringText(value, limit) {
|
|
93
|
+
const text = truncateChars(value,limit,"return").text;
|
|
94
|
+
return hasUnpairedSurrogate(text) ? truncateChars(JSON.stringify(text),limit,"return").text : text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Give displayed array items a fair share; disclose any omitted items. */
|
|
86
98
|
export function formatBoundedStringArray(values, budget) {
|
|
87
99
|
const n = values.length;
|
|
88
100
|
const header = "strings[" + n + "]\n";
|
|
89
101
|
let remaining = Math.max(0, budget - header.length);
|
|
90
102
|
let out = header;
|
|
103
|
+
if (header.length >= budget) return truncateChars(header,budget,"return").text;
|
|
104
|
+
const omitted = count => "…[return truncated; " + count + " string items omitted]…\n";
|
|
91
105
|
|
|
92
106
|
for (let i = 0; i < n; i++) {
|
|
93
107
|
const itemHeader = "[" + i + "] " + values[i].length + " UTF-16 units\n";
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
const footer = i + 1 < n ? omitted(n - i - 1) : "";
|
|
109
|
+
if (remaining < itemHeader.length + Math.min(32,values[i].length) + 1 + footer.length) {
|
|
110
|
+
return out + truncateChars(omitted(n - i),remaining,"return").text;
|
|
111
|
+
}
|
|
112
|
+
const per = Math.min(remaining - itemHeader.length - 1 - footer.length, Math.max(32, Math.floor(remaining / (n - i)) - itemHeader.length - 1));
|
|
113
|
+
const chunk = itemHeader + boundedStringText(values[i],per) + "\n";
|
|
97
114
|
remaining = Math.max(0, remaining - chunk.length);
|
|
98
115
|
out += chunk;
|
|
99
116
|
}
|
|
@@ -149,7 +166,7 @@ function framedReturn(value) {
|
|
|
149
166
|
|
|
150
167
|
/** Lossless framing for source arrays, not string escaping or source compression. */
|
|
151
168
|
export function formatReturn(value) {
|
|
152
|
-
if (isString(value)) return value;
|
|
169
|
+
if (isString(value)) return hasUnpairedSurrogate(value) ? JSON.stringify(value) : value;
|
|
153
170
|
const rawArray = formatRawStringArray(value);
|
|
154
171
|
|
|
155
172
|
if (rawArray !== null) return rawArray;
|
|
@@ -287,3 +304,75 @@ export function formatValue(value, indent = "", width = FORMAT_WIDTH, seen = new
|
|
|
287
304
|
seen.delete(value);
|
|
288
305
|
}
|
|
289
306
|
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
function displayKeys(value) {
|
|
310
|
+
return Array.isArray(value) ? value.keys() : Object.keys(value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A cheap lower bound stops before expanding large/shared result subtrees. */
|
|
314
|
+
export function displayExceeds(value, budget) {
|
|
315
|
+
const seen = new Set();
|
|
316
|
+
const spend = units => (budget -= units) < 0;
|
|
317
|
+
function container(item) {
|
|
318
|
+
if (seen.has(item)) return false;
|
|
319
|
+
seen.add(item);
|
|
320
|
+
const array = Array.isArray(item);
|
|
321
|
+
if (array && spend(item.length)) return true;
|
|
322
|
+
for (const key of displayKeys(item)) {
|
|
323
|
+
if (!array && spend(key.length+1)) return true;
|
|
324
|
+
if (visit(item[key])) return true;
|
|
325
|
+
}
|
|
326
|
+
seen.delete(item);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
function visit(item) {
|
|
330
|
+
if (spend(isString(item) ? item.length : 1)) return true;
|
|
331
|
+
return isObject(item) || Array.isArray(item) ? container(item) : false;
|
|
332
|
+
}
|
|
333
|
+
return visit(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Display-only prefix; never constructs an oversized JSON/string rendering. */
|
|
337
|
+
export function formatBoundedValue(value, budget, label = "return") {
|
|
338
|
+
const footer = "\n…["+label+" truncated]…";
|
|
339
|
+
const parts = [];
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
let remaining = Math.max(0,budget-footer.length), stopped = false;
|
|
342
|
+
const push = text => {
|
|
343
|
+
if (text.length > remaining) { stopped = true; return false; }
|
|
344
|
+
remaining -= text.length; parts.push(text); return true;
|
|
345
|
+
};
|
|
346
|
+
function quoted(text) {
|
|
347
|
+
const end = maxJsonStringPrefix(text,remaining);
|
|
348
|
+
push(JSON.stringify(text.slice(0,end)));
|
|
349
|
+
if (end < text.length) stopped = true;
|
|
350
|
+
}
|
|
351
|
+
function visitChild(item,key,array) {
|
|
352
|
+
if (!array) { quoted(key); push(":"); }
|
|
353
|
+
visit(array && item[key] === undefined ? null : item[key]);
|
|
354
|
+
}
|
|
355
|
+
function container(item) {
|
|
356
|
+
if (seen.has(item)) { push('"[Circular]"'); return; }
|
|
357
|
+
seen.add(item);
|
|
358
|
+
const array = Array.isArray(item);
|
|
359
|
+
push(array ? "[" : "{");
|
|
360
|
+
let first = true;
|
|
361
|
+
for (const key of displayKeys(item)) {
|
|
362
|
+
if (stopped) break;
|
|
363
|
+
if (!first) push(",");
|
|
364
|
+
first = false;
|
|
365
|
+
visitChild(item,key,array);
|
|
366
|
+
}
|
|
367
|
+
if (!stopped) push(array ? "]" : "}");
|
|
368
|
+
seen.delete(item);
|
|
369
|
+
}
|
|
370
|
+
function visit(item) {
|
|
371
|
+
if (stopped) return;
|
|
372
|
+
if (isString(item)) quoted(item);
|
|
373
|
+
else if (isObject(item) || Array.isArray(item)) container(item);
|
|
374
|
+
else push(formatPrimitive(item));
|
|
375
|
+
}
|
|
376
|
+
visit(value);
|
|
377
|
+
return truncateChars(parts.join("")+footer,budget,"return").text;
|
|
378
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {isString} from '../shared/decode.js';
|
|
2
|
+
import {truncateChars,formatBoundedStringArray,isStringArray} from './format.js';
|
|
3
|
+
|
|
4
|
+
function result(text, details) {
|
|
5
|
+
return { content: [{ type: "text", text }], details };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function logsBlock(outcome, tail = "") {
|
|
9
|
+
if (!outcome.logs?.length && !outcome.logTruncated) return "";
|
|
10
|
+
|
|
11
|
+
return `\n--- logs${outcome.logTruncated ? " [logs truncated]" : ""}\n${outcome.logs?.join("\n") ?? ""}${tail}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mutationText(outcome) {
|
|
15
|
+
const m = outcome.mutations;
|
|
16
|
+
|
|
17
|
+
if (!m) return "";
|
|
18
|
+
const external = m.external ? "; external calls attempted=" + m.external + ", their side effects cannot be rolled back" : "";
|
|
19
|
+
const uncertain = m.pendingCommits || m.recoveryFailed ? "; filesystem outcome uncertain: inspect disk and any recovery backups before retrying" : "";
|
|
20
|
+
|
|
21
|
+
return "\nmutations: committed=" + m.committed + " rolledBack=" + m.rolledBack + " (file versions)" + external + uncertain;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mutationReceipts(trace) {
|
|
25
|
+
if (!Array.isArray(trace)) return "";
|
|
26
|
+
|
|
27
|
+
return trace
|
|
28
|
+
.filter(row => row?.ok && (row.name === "write" || row.name === "edit") && isString(row.resultText) && row.resultText)
|
|
29
|
+
.map(row => row.resultText)
|
|
30
|
+
.join("\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Corrective hint, emitted only when a turn actually split. Independent work
|
|
34
|
+
// belongs in one program: a split cannot use the single prewarmed worker and pays
|
|
35
|
+
// one extra spawn per sibling. Costs nothing until it fires, so it needs no room in
|
|
36
|
+
// the tool definition.
|
|
37
|
+
function splitTurnHint(outcome) {
|
|
38
|
+
return outcome.overlappedTurn ? ` (${outcome.overlappedTurn} supernova calls ran at once; independent work belongs in one program)` : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function errorText(outcome, call) {
|
|
42
|
+
return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome)}${splitTurnHint(outcome)}
|
|
43
|
+
error: ${outcome.error}${logsBlock(outcome)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function successText(outcome, call) {
|
|
47
|
+
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
48
|
+
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
49
|
+
const m = outcome.mutations;
|
|
50
|
+
const showMutations = m && (m.committed || m.rolledBack || m.external || m.pendingCommits || m.recoveryFailed) ? mutationText(outcome) : "";
|
|
51
|
+
|
|
52
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${showMutations}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fitOutput(outcome, call, limit, format) {
|
|
56
|
+
let text = format(outcome, call);
|
|
57
|
+
|
|
58
|
+
if (text.length <= limit) return text;
|
|
59
|
+
outcome.returnTruncated = true;
|
|
60
|
+
const wrapper = format({ ...outcome, resultText: "", logs: [] }, call);
|
|
61
|
+
const room = Math.max(256, limit - wrapper.length);
|
|
62
|
+
|
|
63
|
+
if (isStringArray(outcome.result)) {
|
|
64
|
+
outcome.resultText = formatBoundedStringArray(outcome.result, room);
|
|
65
|
+
} else if (isString(outcome.resultText) && outcome.resultText.length > room) {
|
|
66
|
+
outcome.resultText = truncateChars(outcome.resultText, room, "output").text;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
text = format(outcome, call);
|
|
70
|
+
|
|
71
|
+
return text.length <= limit ? text : truncateChars(text, limit, "output").text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function attachReceipts(outcome, trace) {
|
|
75
|
+
if (outcome.ok && outcome.result === undefined) {
|
|
76
|
+
const receipts = mutationReceipts(trace);
|
|
77
|
+
|
|
78
|
+
if (receipts) {
|
|
79
|
+
outcome.resultText = receipts;
|
|
80
|
+
outcome.undefinedReturn = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function throwIfFailed(outcome, visible, response) {
|
|
86
|
+
if (outcome.ok) return response;
|
|
87
|
+
const error = new Error(visible);
|
|
88
|
+
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
export { result, errorText, successText, fitOutput, attachReceipts, throwIfFailed };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {isObject,isString} from '../shared/decode.js';
|
|
2
|
+
|
|
3
|
+
function assertProgramEntry(p, defaults = {}) {
|
|
4
|
+
const source = p?.code === undefined && p?.file === undefined ? defaults : p;
|
|
5
|
+
if (!objectData(p) || Object.keys(p).some(key => !["code", "file", "data"].includes(key)) || !validProgramSource(source)) {
|
|
6
|
+
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");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const objectData = value => isObject(value) && !Array.isArray(value);
|
|
11
|
+
|
|
12
|
+
function applyBatchDefaults(parsed, mergeData) {
|
|
13
|
+
if (mergeData && !objectData(parsed.data)) throw new Error("mergeData requires top-level object data; no programs ran");
|
|
14
|
+
const source = parsed.code !== undefined ? {code:parsed.code} : parsed.file !== undefined ? {file:parsed.file} : {};
|
|
15
|
+
|
|
16
|
+
return parsed.programs.map(program => {
|
|
17
|
+
assertProgramEntry(program, source);
|
|
18
|
+
const entry = program.code === undefined && program.file === undefined ? {...source,...program} : program;
|
|
19
|
+
|
|
20
|
+
if (mergeData) {
|
|
21
|
+
if (program.data !== undefined && !objectData(program.data)) throw new Error("mergeData requires object data in every explicit entry; no programs ran");
|
|
22
|
+
// Shallow own-property overlay, including literal __proto__ keys. The
|
|
23
|
+
// runtime snapshots data again per guest; no mutable heap is shared.
|
|
24
|
+
entry.data = {...parsed.data,...program.data};
|
|
25
|
+
} else if (program.data === undefined && Object.hasOwn(parsed,"data")) entry.data = parsed.data;
|
|
26
|
+
|
|
27
|
+
return entry;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseBatchPayload(params, config) {
|
|
32
|
+
assertBatchOptions(params);
|
|
33
|
+
const defaults = Object.fromEntries(["code", "file", "data"].filter(key => params[key] !== undefined).map(key => [key, params[key]]));
|
|
34
|
+
if (defaults.code !== undefined || defaults.file !== undefined) assertProgramEntry(defaults);
|
|
35
|
+
for (const p of params.programs) assertProgramEntry(p, defaults);
|
|
36
|
+
// Validate before serialization and again after snapshotting: toJSON may change an entry.
|
|
37
|
+
return applyBatchDefaults(snapshotBatch(params, defaults, config), params.mergeData === true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function batchTimeoutMs(params, config) {
|
|
41
|
+
const requestedTimeout = params.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs);
|
|
42
|
+
|
|
43
|
+
if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error("program batch timeoutMs must be a positive finite number");
|
|
44
|
+
|
|
45
|
+
return requestedTimeout;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validProgramSource(source) {
|
|
49
|
+
const value = source.code ?? source.file;
|
|
50
|
+
return (source.code === undefined) !== (source.file === undefined) && isString(value) && value.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertBatchOptions(params) {
|
|
54
|
+
if (!Array.isArray(params.programs) || !params.programs.length || params.programs.length > 32) throw new Error("programs requires 1..32 entries; no programs ran");
|
|
55
|
+
if (params.mergeData !== undefined && ![true, false].includes(params.mergeData)) throw new Error("mergeData must be boolean; no programs ran");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function snapshotBatch(params, defaults, config) {
|
|
59
|
+
const hasDefaults = Object.keys(defaults).length > 0;
|
|
60
|
+
let encoded;
|
|
61
|
+
try { encoded = JSON.stringify(hasDefaults ? {programs:params.programs,...defaults} : params.programs); }
|
|
62
|
+
catch { throw new Error("programs and defaults must be JSON-serializable; no programs ran"); }
|
|
63
|
+
if (encoded.length > (config.maxCodeChars ?? 48000)) throw new Error("programs JSON exceeds the code character budget (including shared code/file/data); no programs ran");
|
|
64
|
+
const parsed = hasDefaults ? JSON.parse(encoded) : {programs:JSON.parse(encoded)};
|
|
65
|
+
if (Object.hasOwn(defaults,"data") && !Object.hasOwn(parsed,"data")) throw new Error("data must be JSON-serializable; no programs ran");
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
export { parseBatchPayload, batchTimeoutMs };
|