pi-supernova 0.6.0 → 0.7.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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +104 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +289 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. package/src/ui/render.js +241 -145
@@ -67,14 +67,7 @@ export function isTestPath(filePath) {
67
67
  return segments.some((s) => TEST_SEGMENTS.has(s)) || /\.(test|spec)\./.test(base);
68
68
  }
69
69
 
70
- /** Reject scheme:// and scheme:/ paths. A single-letter drive (C:/) stays a filesystem path. */
71
- export function assertFilesystemPath(inputPath, opName, allowSessionRead = false) {
72
- if (inputPath == null || !isString(inputPath) || !inputPath.trim()) {
73
- throw new Error(`${opName} requires path`);
74
- }
75
-
76
- const trimmed = inputPath.trim();
77
-
70
+ function rejectUriPath(trimmed, opName, allowSessionRead) {
78
71
  if (/^(?:agent|artifact):\/\//i.test(trimmed)) {
79
72
  if (allowSessionRead) return trimmed;
80
73
  throw new Error(`${opName} requires a filesystem path; session resource URIs are read-only`);
@@ -82,19 +75,27 @@ export function assertFilesystemPath(inputPath, opName, allowSessionRead = false
82
75
 
83
76
  const uri = /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/.exec(trimmed);
84
77
 
85
- if (uri) {
86
- const scheme = uri[1];
87
- const rest = uri[2];
88
- const windowsDrive = scheme.length === 1 && (rest.startsWith("/") || rest.startsWith("\\"));
78
+ if (!uri) return trimmed;
79
+ const scheme = uri[1];
80
+ const rest = uri[2];
81
+ const windowsDrive = scheme.length === 1 && (rest.startsWith("/") || rest.startsWith("\\"));
89
82
 
90
- if (!windowsDrive && (rest.startsWith("//") || rest.startsWith("/"))) {
91
- throw new Error(`${opName} does not accept ${scheme}: URI paths; use a workspace filesystem path`);
92
- }
83
+ if (!windowsDrive && (rest.startsWith("//") || rest.startsWith("/"))) {
84
+ throw new Error(`${opName} does not accept ${scheme}: URI paths; use a workspace filesystem path`);
93
85
  }
94
86
 
95
87
  return trimmed;
96
88
  }
97
89
 
90
+ /** Reject scheme:// and scheme:/ paths. A single-letter drive (C:/) stays a filesystem path. */
91
+ export function assertFilesystemPath(inputPath, opName, allowSessionRead = false) {
92
+ if (inputPath == null || !isString(inputPath) || !inputPath.trim()) {
93
+ throw new Error(`${opName} requires path`);
94
+ }
95
+
96
+ return rejectUriPath(inputPath.trim(), opName, allowSessionRead);
97
+ }
98
+
98
99
  export async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = false, fresh = false) {
99
100
  const trimmed = assertFilesystemPath(inputPath, opName);
100
101
  const resolvedCwd = getResolvedCwd(cwd);
@@ -126,104 +127,114 @@ export async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = f
126
127
  return target;
127
128
  }
128
129
 
129
- export async function runCommand(argv, options = {}) {
130
- options.signal?.throwIfAborted();
131
- const cwd = options.cwd || process.cwd();
130
+ function commandTimeoutMs(options) {
132
131
  const requestedTimeout = Number(options.timeoutMs === undefined ? 60_000 : options.timeoutMs);
133
132
 
134
133
  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)));
134
+
135
+ return Math.max(1, Math.min(2_147_483_647, Math.floor(requestedTimeout)));
136
+ }
137
+
138
+ function spawnCommand(argv, options, cwd) {
139
+ return spawn(argv[0], argv.slice(1), {
140
+ cwd, env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32",
141
+ });
142
+ }
143
+
144
+ function signalProcessTree(child, signal) {
145
+ if (!child.pid) return;
146
+
147
+ if (process.platform === "win32") {
148
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
149
+ killer.on("error", () => child.kill(signal));
150
+ } else {
151
+ try { process.kill(-child.pid, signal); } catch (err) { if (err.code !== "ESRCH") child.kill(signal); }
152
+ }
153
+ }
154
+
155
+ function failCommand(state, error) {
156
+ if (state.settled) return;
157
+ state.settled = true;
158
+ state.cleanup();
159
+ // Keep bounded diagnostic output when a command times out or is cancelled.
160
+ error.stdout = state.stdout;
161
+ error.stderr = state.stderr;
162
+ error.outputTruncated = state.outputTruncated;
163
+ const output = [state.stdout, state.stderr].filter(Boolean).join("\n").trimEnd();
164
+
165
+ if (output) error.message += "\n" + output;
166
+
167
+ if (state.outputTruncated) error.message += "\n[output truncated]";
168
+ state.reject(error);
169
+ }
170
+
171
+ function terminateCommand(state, error) {
172
+ if (state.settled || state.terminationError) return;
173
+ state.terminationError = error;
174
+ signalProcessTree(state.child, "SIGTERM");
175
+ // Keep ownership after the direct child exits: descendants may ignore SIGTERM.
176
+ state.escalation = setTimeout(() => { signalProcessTree(state.child, "SIGKILL"); failCommand(state, error); }, 150);
177
+ }
178
+
179
+ function appendCommandOutput(state, current, chunk) {
180
+ const remaining = Math.max(0, state.maxOutputChars - state.stdout.length - state.stderr.length);
181
+
182
+ if (chunk.length > remaining) state.outputTruncated = true;
183
+
184
+ return remaining ? current + chunk.slice(0, remaining) : current;
185
+ }
186
+
187
+ function onCommandClose(state, code, signal) {
188
+ if (state.settled) return;
189
+
190
+ if (state.terminationError) {
191
+ // A closed pipe alone says nothing about descendants. Only ESRCH proves
192
+ // the owned POSIX group is gone; otherwise retain the escalation timer.
193
+ if (process.platform !== "win32" && state.child.pid) {
194
+ try { process.kill(-state.child.pid, 0); }
195
+ catch (error) { if (error.code === "ESRCH") failCommand(state, state.terminationError); }
196
+ }
197
+
198
+ return;
199
+ }
200
+
201
+ state.settled = true;
202
+ state.cleanup();
203
+ state.resolve({ stdout: state.stdout, stderr: state.stderr, exitCode: code ?? (128 + (constants.signals[signal] ?? 1)), signal, outputTruncated: state.outputTruncated });
204
+ }
205
+
206
+ function attachCommandIO(state, options, argv, timeoutMs) {
207
+ const { child } = state;
208
+ const onAbort = () => terminateCommand(state, new Error("aborted"));
209
+ state.cleanup = () => {
210
+ clearTimeout(state.timer);
211
+ clearTimeout(state.escalation);
212
+ options.signal?.removeEventListener("abort", onAbort);
213
+ };
214
+ state.timer = setTimeout(() => terminateCommand(state, new Error("command timed out after " + timeoutMs + "ms: " + (options.commandLabel ?? argv.join(" ")))), timeoutMs);
215
+ child.stdout.setEncoding("utf8");
216
+ child.stderr.setEncoding("utf8");
217
+ child.stdout.on("data", chunk => { state.stdout = appendCommandOutput(state, state.stdout, chunk); });
218
+ child.stderr.on("data", chunk => { state.stderr = appendCommandOutput(state, state.stderr, chunk); });
219
+ child.on("error", error => failCommand(state, error));
220
+ child.on("close", (code, signal) => onCommandClose(state, code, signal));
221
+ options.signal?.addEventListener("abort", onAbort, { once: true });
222
+
223
+ if (options.signal?.aborted) onAbort();
224
+ }
225
+
226
+ export async function runCommand(argv, options = {}) {
227
+ options.signal?.throwIfAborted();
228
+ const cwd = options.cwd || process.cwd();
229
+ const timeoutMs = commandTimeoutMs(options);
136
230
  const maxOutputChars = options.maxOutputChars ?? 2 * 1024 * 1024;
137
231
 
138
232
  return new Promise((resolve, reject) => {
139
- const child = spawn(argv[0], argv.slice(1), {
140
- cwd, env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32",
141
- });
142
-
143
- let stdout = "";
144
- let stderr = "";
145
- let settled = false;
146
- let outputTruncated = false;
147
- let terminationError;
148
- let escalation;
149
-
150
- const cleanup = () => {
151
- clearTimeout(timer);
152
- clearTimeout(escalation);
153
- options.signal?.removeEventListener("abort", onAbort);
154
- };
155
-
156
- const fail = error => {
157
- if (settled) return;
158
- settled = true;
159
- cleanup();
160
- // Keep bounded diagnostic output when a command times out or is cancelled.
161
- error.stdout = stdout;
162
- error.stderr = stderr;
163
- error.outputTruncated = outputTruncated;
164
- const output = [stdout, stderr].filter(Boolean).join("\n").trimEnd();
165
-
166
- if (output) error.message += "\n" + output;
167
-
168
- if (outputTruncated) error.message += "\n[output truncated]";
169
- reject(error);
170
- };
171
-
172
- const signalTree = signal => {
173
- if (!child.pid) return;
174
-
175
- if (process.platform === "win32") {
176
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
177
- killer.on("error", () => child.kill(signal));
178
- } else {
179
- try { process.kill(-child.pid, signal); } catch (err) { if (err.code !== "ESRCH") child.kill(signal); }
180
- }
181
- };
182
-
183
- const terminate = error => {
184
- if (settled || terminationError) return;
185
- terminationError = error;
186
- signalTree("SIGTERM");
187
- // Keep ownership after the direct child exits: descendants may ignore SIGTERM.
188
- escalation = setTimeout(() => { signalTree("SIGKILL"); fail(error); }, 150);
189
- };
190
-
191
- const onAbort = () => terminate(new Error("aborted"));
192
- const timer = setTimeout(() => terminate(new Error("command timed out after " + timeoutMs + "ms: " + (options.commandLabel ?? argv.join(" ")))), timeoutMs);
193
-
194
- const append = (current, chunk) => {
195
- const remaining = Math.max(0, maxOutputChars - stdout.length - stderr.length);
196
-
197
- if (chunk.length > remaining) outputTruncated = true;
198
-
199
- return remaining ? current + chunk.slice(0, remaining) : current;
200
- };
201
-
202
- child.stdout.setEncoding("utf8");
203
- child.stderr.setEncoding("utf8");
204
- child.stdout.on("data", chunk => { stdout = append(stdout, chunk); });
205
- child.stderr.on("data", chunk => { stderr = append(stderr, chunk); });
206
- child.on("error", fail);
207
- child.on("close", (code, signal) => {
208
- if (settled) return;
209
-
210
- if (terminationError) {
211
- // A closed pipe alone says nothing about descendants. Only ESRCH proves
212
- // the owned POSIX group is gone; otherwise retain the escalation timer.
213
- if (process.platform !== "win32" && child.pid) {
214
- try { process.kill(-child.pid, 0); }
215
- catch (error) { if (error.code === "ESRCH") fail(terminationError); }
216
- }
217
-
218
- return;
219
- }
220
-
221
- settled = true;
222
- cleanup();
223
- resolve({ stdout, stderr, exitCode: code ?? (128 + (constants.signals[signal] ?? 1)), signal, outputTruncated });
224
- });
225
- options.signal?.addEventListener("abort", onAbort, { once: true });
226
-
227
- if (options.signal?.aborted) onAbort();
233
+ const child = spawnCommand(argv, options, cwd);
234
+ attachCommandIO({
235
+ child, resolve, reject, stdout: "", stderr: "", settled: false,
236
+ outputTruncated: false, terminationError: undefined, escalation: undefined,
237
+ maxOutputChars, timer: undefined, cleanup() {},
238
+ }, options, argv, timeoutMs);
228
239
  });
229
240
  }
@@ -36,59 +36,69 @@ function extractRawString(raw) {
36
36
  return json(raw);
37
37
  }
38
38
 
39
- /** Bound JSON before serialization; preserve small scalar fields such as exitCode. */
40
- function summarizeDetails(value, budget = 2000) {
41
- const encoded = json(value);
39
+ function fitString(input, limit) {
40
+ let low = 0;
41
+ let high = Math.min(input.length, limit);
42
42
 
43
- if (encoded.length <= budget) return encoded;
44
- const snapshot = JSON.parse(encoded);
43
+ while (low < high) {
44
+ const mid = Math.ceil((low + high) / 2);
45
+
46
+ if (json(truncateChars(input, mid).text).length <= limit) low = mid;
47
+ else high = mid - 1;
48
+ }
49
+
50
+ return truncateChars(input, low).text;
51
+ }
52
+
53
+ function assignBounded(out, key, bounded) {
54
+ if (Array.isArray(out)) out.push(bounded);
55
+ else Object.defineProperty(out, key, { value: bounded, enumerable: true, configurable: true });
56
+ }
45
57
 
46
- const fit = (input, limit) => {
47
- const serialized = json(input);
58
+ function rollbackBounded(out, key) {
59
+ if (Array.isArray(out)) out.pop();
60
+ else delete out[key];
61
+ }
48
62
 
49
- if (serialized.length <= limit) return input;
63
+ function fitContainer(input, limit) {
64
+ const out = Array.isArray(input) ? [] : { truncated: true };
65
+ const entries = Object.entries(input);
50
66
 
51
- if (isString(input)) {
52
- let low = 0;
53
- let high = Math.min(input.length, limit);
67
+ if (!Array.isArray(input)) entries.sort((a, b) => json(a[1]).length - json(b[1]).length);
54
68
 
55
- while (low < high) {
56
- const mid = Math.ceil((low + high) / 2);
69
+ for (const [key, child] of entries) {
70
+ const used = json(out).length;
71
+ const overhead = Array.isArray(out) ? 1 : json(key).length + 2;
72
+ const available = limit - used - overhead;
57
73
 
58
- if (json(truncateChars(input, mid).text).length <= limit) low = mid;
59
- else high = mid - 1;
60
- }
74
+ if (available < 4) break;
75
+ assignBounded(out, key, fit(child, available));
61
76
 
62
- return truncateChars(input, low).text;
63
- }
77
+ if (json(out).length > limit) rollbackBounded(out, key);
78
+ }
64
79
 
65
- if (!isObject(input) && !Array.isArray(input)) return null;
66
- const out = Array.isArray(input) ? [] : { truncated: true };
67
- const entries = Object.entries(input);
80
+ return out;
81
+ }
68
82
 
69
- if (!Array.isArray(input)) entries.sort((a, b) => json(a[1]).length - json(b[1]).length);
83
+ function fit(input, limit) {
84
+ const serialized = json(input);
70
85
 
71
- for (const [key, child] of entries) {
72
- const used = json(out).length;
73
- const overhead = Array.isArray(out) ? 1 : json(key).length + 2;
74
- const available = limit - used - overhead;
86
+ if (serialized.length <= limit) return input;
75
87
 
76
- if (available < 4) break;
77
- const bounded = fit(child, available);
88
+ if (isString(input)) return fitString(input, limit);
78
89
 
79
- if (Array.isArray(out)) out.push(bounded);
80
- else Object.defineProperty(out, key, { value: bounded, enumerable: true, configurable: true });
90
+ if (!isObject(input) && !Array.isArray(input)) return null;
91
+
92
+ return fitContainer(input, limit);
93
+ }
81
94
 
82
- if (json(out).length > limit) {
83
- if (Array.isArray(out)) out.pop();
84
- else delete out[key];
85
- }
86
- }
95
+ /** Bound JSON before serialization; preserve small scalar fields such as exitCode. */
96
+ function summarizeDetails(value, budget = 2000) {
97
+ const encoded = json(value);
87
98
 
88
- return out;
89
- };
99
+ if (encoded.length <= budget) return encoded;
90
100
 
91
- return json(fit(snapshot, budget));
101
+ return json(fit(JSON.parse(encoded), budget));
92
102
  }
93
103
 
94
104
  function spill(fullText, config) {
@@ -104,122 +114,191 @@ function spill(fullText, config) {
104
114
  }
105
115
  }
106
116
 
107
- export function packageHostResult(raw, config) {
108
- const maxChars = config.maxCallResultChars ?? 65536;
109
- const details = detailsOf(raw);
110
- const batch = details?.batch === true && Array.isArray(details.items) ? details.items : undefined;
111
- const text = batch ? "" : extractRawString(raw);
112
- const capped = truncateChars(text, maxChars, "host-result");
113
- let truncated = capped.truncated || details?.outputTruncated === true;
114
- const image = raw?.content?.find(part => part?.type === "image");
115
- const directoryEntries = image === undefined && details?.directory === true && Array.isArray(details.entries) && json(details.entries).length <= maxChars
117
+ function batchFromDetails(details) {
118
+ return details?.batch === true && Array.isArray(details.items) ? details.items : undefined;
119
+ }
120
+
121
+ function hostImage(raw) {
122
+ return raw?.content?.find(part => part?.type === "image");
123
+ }
124
+
125
+ function directoryEntriesIfFit(image, details, maxChars) {
126
+ return image === undefined && details?.directory === true && Array.isArray(details.entries) && json(details.entries).length <= maxChars
116
127
  ? details.entries
117
128
  : undefined;
118
- const result = { ok: !hostResultFailed(raw), value: image ?? directoryEntries ?? capped.text, truncated };
129
+ }
119
130
 
131
+ function hostTruncated(capped, details) {
132
+ return capped.truncated || details?.outputTruncated === true;
133
+ }
134
+
135
+ function hostResultValue(image, directoryEntries, capped) {
136
+ return image ?? directoryEntries ?? capped.text;
137
+ }
138
+
139
+ function attachDetails(result, details, batch) {
120
140
  if (details !== undefined) result.details = summarizeDetails(batch ? { ...details, items: undefined } : details);
141
+ }
121
142
 
122
- if (batch) {
123
- result.itemErrors = (details.itemErrors ?? []).map(error => error == null ? null : truncateChars(String(error), Math.max(1, Math.floor(maxChars / batch.length)), "error").text);
124
- let remaining = maxChars;
125
- result.items = batch.map((item, index) => {
126
- if (item?.type === "image") return item;
127
- const share = details.independent === true ? maxChars : Math.floor(remaining / (batch.length - index));
143
+ function boundNonStringItem(item, share) {
144
+ const encoded = json(item);
128
145
 
129
- if (!isString(item)) {
130
- const encoded = json(item);
146
+ if (encoded.length <= share) return { value: item, used: encoded.length, truncated: false };
147
+ const bounded = truncateChars(encoded, share, "host-result");
131
148
 
132
- if (encoded.length <= share) {
133
- remaining -= encoded.length;
149
+ return { value: bounded.text, used: bounded.text.length, truncated: bounded.truncated };
150
+ }
134
151
 
135
- return item;
136
- }
152
+ function boundBatchItem(item, share) {
153
+ if (item?.type === "image") return { value: item, used: 0, truncated: false };
137
154
 
138
- const bounded = truncateChars(encoded, share, "host-result");
139
- remaining -= bounded.text.length;
140
- truncated ||= bounded.truncated;
155
+ if (!isString(item)) return boundNonStringItem(item, share);
156
+ const bounded = truncateChars(item, share, "host-result");
141
157
 
142
- return bounded.text;
143
- }
158
+ return { value: bounded.text, used: bounded.text.length, truncated: bounded.truncated };
159
+ }
144
160
 
145
- const bounded = truncateChars(item, share, "host-result");
146
- remaining -= bounded.text.length;
147
- truncated ||= bounded.truncated;
161
+ function boundItemError(error, maxChars, batchLength) {
162
+ return error == null ? null : truncateChars(String(error), Math.max(1, Math.floor(maxChars / batchLength)), "error").text;
163
+ }
148
164
 
149
- return bounded.text;
150
- });
151
- }
165
+ function packageBatchItems(batch, details, maxChars) {
166
+ const itemErrors = (details.itemErrors ?? []).map(error => boundItemError(error, maxChars, batch.length));
167
+ let remaining = maxChars;
168
+ let truncated = false;
169
+ const items = batch.map((item, index) => {
170
+ const share = details.independent === true ? maxChars : Math.floor(remaining / (batch.length - index));
171
+ const bounded = boundBatchItem(item, share);
172
+ remaining -= bounded.used;
173
+ truncated ||= bounded.truncated;
174
+
175
+ return bounded.value;
176
+ });
152
177
 
178
+ return { items, itemErrors, truncated };
179
+ }
180
+
181
+ function attachBatch(result, batch, details, maxChars) {
182
+ if (!batch) return false;
183
+ const packed = packageBatchItems(batch, details, maxChars);
184
+ result.itemErrors = packed.itemErrors;
185
+ result.items = packed.items;
186
+
187
+ return packed.truncated;
188
+ }
189
+
190
+ function originalBatchChars(batch) {
191
+ return batch.reduce((sum, item) => sum + (isString(item) ? item.length : json(item).length), 0);
192
+ }
193
+
194
+ function spillPayload(batch, text) {
195
+ return batch ? batch.map(item => isString(item) ? item : json(item)).join("\n---\n") : text;
196
+ }
197
+
198
+ function applySpillFooter(result, text, pointer, maxChars, capped) {
199
+ const footer = "\n[full output spilled to " + pointer + "]";
200
+ result.value = footer.length <= maxChars
201
+ ? truncateChars(text, maxChars - footer.length, "host-result").text + footer
202
+ : capped.text;
203
+ }
204
+
205
+ function attachSpill(result, batch, text, config, maxChars, capped) {
206
+ if (!config.spillDir) return;
207
+ const pointer = spill(spillPayload(batch, text), config);
208
+
209
+ if (!pointer) return;
210
+ result.spill = pointer;
211
+
212
+ if (!batch) applySpillFooter(result, text, pointer, maxChars, capped);
213
+ }
214
+
215
+ function attachTruncation(result, truncated, batch, text, config, maxChars, capped) {
153
216
  result.truncated = truncated;
154
217
 
155
- if (truncated) {
156
- result.originalChars = batch ? batch.reduce((sum, item) => sum + (isString(item) ? item.length : json(item).length), 0) : text.length;
218
+ if (!truncated) return;
219
+ result.originalChars = batch ? originalBatchChars(batch) : text.length;
220
+ attachSpill(result, batch, text, config, maxChars, capped);
221
+ }
157
222
 
158
- if (config.spillDir) {
159
- const pointer = spill(batch ? batch.map(item => isString(item) ? item : json(item)).join("\n---\n") : text, config);
223
+ export function packageHostResult(raw, config) {
224
+ const maxChars = config.maxCallResultChars ?? 65536;
225
+ const details = detailsOf(raw);
226
+ const batch = batchFromDetails(details);
227
+ const text = batch ? "" : extractRawString(raw);
228
+ const capped = truncateChars(text, maxChars, "host-result");
229
+ let truncated = hostTruncated(capped, details);
230
+ const image = hostImage(raw);
231
+ const directoryEntries = directoryEntriesIfFit(image, details, maxChars);
232
+ const result = { ok: !hostResultFailed(raw), value: hostResultValue(image, directoryEntries, capped), truncated };
233
+ attachDetails(result, details, batch);
234
+ truncated ||= attachBatch(result, batch, details, maxChars);
235
+ attachTruncation(result, truncated, batch, text, config, maxChars, capped);
236
+
237
+ return result;
238
+ }
239
+
240
+ function collectImage(input, acc) {
241
+ if (!(input?.type === "image" && isString(input.data) && isString(input.mimeType) && input.mimeType.startsWith("image/"))) return null;
242
+ const size = Buffer.byteLength(input.data, "base64");
160
243
 
161
- if (pointer) {
162
- result.spill = pointer;
244
+ if (acc.images.length >= 16 || acc.imageBytes + size > 20 * 1024 * 1024) {
245
+ acc.imageOverflow = true;
163
246
 
164
- if (!batch) {
165
- const footer = "\n[full output spilled to " + pointer + "]";
166
- result.value = footer.length <= maxChars
167
- ? truncateChars(text, maxChars - footer.length, "host-result").text + footer
168
- : capped.text;
169
- }
170
- }
171
- }
247
+ return "[image omitted: exceeds 16 attachments or 20 MiB]";
172
248
  }
173
249
 
174
- return result;
250
+ acc.imageBytes += size;
251
+ acc.images.push({ type: "image", data: input.data, mimeType: input.mimeType });
252
+
253
+ return `[image ${acc.images.length}: ${input.mimeType}]`;
175
254
  }
176
255
 
177
- export function packageFinalReturn(value, logs, config) {
178
- const images = [];
179
- let imageBytes = 0;
180
- let imageOverflow = false;
256
+ function collectImages(input, acc) {
257
+ const replaced = collectImage(input, acc);
181
258
 
182
- const collect = input => {
183
- if (input?.type === "image" && isString(input.data) && isString(input.mimeType) && input.mimeType.startsWith("image/")) {
184
- const size = Buffer.byteLength(input.data, "base64");
259
+ if (replaced !== null) return replaced;
185
260
 
186
- if (images.length >= 16 || imageBytes + size > 20 * 1024 * 1024) {
187
- imageOverflow = true;
261
+ if (Array.isArray(input)) return input.map(child => collectImages(child, acc));
188
262
 
189
- return "[image omitted: exceeds 16 attachments or 20 MiB]";
190
- }
263
+ if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, collectImages(child, acc)]));
191
264
 
192
- imageBytes += size;
193
- images.push({ type: "image", data: input.data, mimeType: input.mimeType });
265
+ return input;
266
+ }
194
267
 
195
- return `[image ${images.length}: ${input.mimeType}]`;
196
- }
268
+ function serializeReturn(value, formatted, maxReturn, imageOverflow) {
269
+ if (formatted.length <= maxReturn) return { text: formatted, truncated: imageOverflow };
197
270
 
198
- if (Array.isArray(input)) return input.map(collect);
271
+ if (Array.isArray(value) && value.length && value.every(isString)) return { text: formatBoundedStringArray(value, maxReturn), truncated: true };
199
272
 
200
- if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, collect(child)]));
273
+ return { ...truncateChars(formatted, maxReturn, "return"), truncated: true };
274
+ }
201
275
 
202
- return input;
203
- };
276
+ function clipLogLine(line, maxLogLineChars) {
277
+ const result = truncateChars(line, maxLogLineChars, "log");
204
278
 
205
- value = collect(value);
206
- const maxReturn = config.maxReturnChars ?? 32000;
207
- const formatted = formatReturn(value);
208
- const serialized = formatted.length <= maxReturn
209
- ? { text: formatted, truncated: imageOverflow }
210
- : Array.isArray(value) && value.length && value.every(isString)
211
- ? { text: formatBoundedStringArray(value, maxReturn), truncated: true }
212
- : { ...truncateChars(formatted, maxReturn, "return"), truncated: true };
279
+ return { text: result.text, truncated: result.truncated };
280
+ }
281
+
282
+ function clipLogs(logs, config) {
213
283
  const maxLines = config.maxLogLines ?? 100;
214
284
  let logTruncated = logs.length > maxLines;
215
-
216
285
  const clipped = logs.slice(0, maxLines).map(line => {
217
- const result = truncateChars(line, config.maxLogLineChars ?? 4096, "log");
286
+ const result = clipLogLine(line, config.maxLogLineChars ?? 4096);
218
287
  logTruncated ||= result.truncated;
219
288
 
220
289
  return result.text;
221
290
  });
222
291
 
292
+ return { logs: clipped, logTruncated };
293
+ }
294
+
295
+ export function packageFinalReturn(value, logs, config) {
296
+ const acc = { images: [], imageBytes: 0, imageOverflow: false };
297
+ value = collectImages(value, acc);
298
+ const maxReturn = config.maxReturnChars ?? 32000;
299
+ const serialized = serializeReturn(value, formatReturn(value), maxReturn, acc.imageOverflow);
300
+ const clipped = clipLogs(logs, config);
301
+
223
302
  return { returnValue: serialized.truncated ? serialized.text : value, returnText: serialized.text,
224
- returnTruncated: serialized.truncated, logs: clipped, logTruncated, images };
303
+ returnTruncated: serialized.truncated, logs: clipped.logs, logTruncated: clipped.logTruncated, images: acc.images };
225
304
  }