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.
- package/README.md +27 -3
- package/docs/CHANGELOG.md +104 -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 +289 -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/fs/workspace.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|
package/src/output/bottleneck.js
CHANGED
|
@@ -36,59 +36,69 @@ function extractRawString(raw) {
|
|
|
36
36
|
return json(raw);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
function fitString(input, limit) {
|
|
40
|
+
let low = 0;
|
|
41
|
+
let high = Math.min(input.length, limit);
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
58
|
+
function rollbackBounded(out, key) {
|
|
59
|
+
if (Array.isArray(out)) out.pop();
|
|
60
|
+
else delete out[key];
|
|
61
|
+
}
|
|
48
62
|
|
|
49
|
-
|
|
63
|
+
function fitContainer(input, limit) {
|
|
64
|
+
const out = Array.isArray(input) ? [] : { truncated: true };
|
|
65
|
+
const entries = Object.entries(input);
|
|
50
66
|
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
74
|
+
if (available < 4) break;
|
|
75
|
+
assignBounded(out, key, fit(child, available));
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
if (json(out).length > limit) rollbackBounded(out, key);
|
|
78
|
+
}
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const entries = Object.entries(input);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
function fit(input, limit) {
|
|
84
|
+
const serialized = json(input);
|
|
70
85
|
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
const bounded = fit(child, available);
|
|
88
|
+
if (isString(input)) return fitString(input, limit);
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
if (!isObject(input) && !Array.isArray(input)) return null;
|
|
91
|
+
|
|
92
|
+
return fitContainer(input, limit);
|
|
93
|
+
}
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
};
|
|
99
|
+
if (encoded.length <= budget) return encoded;
|
|
90
100
|
|
|
91
|
-
return json(fit(
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
130
|
-
|
|
146
|
+
if (encoded.length <= share) return { value: item, used: encoded.length, truncated: false };
|
|
147
|
+
const bounded = truncateChars(encoded, share, "host-result");
|
|
131
148
|
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
return { value: bounded.text, used: bounded.text.length, truncated: bounded.truncated };
|
|
150
|
+
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
function boundBatchItem(item, share) {
|
|
153
|
+
if (item?.type === "image") return { value: item, used: 0, truncated: false };
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
truncated ||= bounded.truncated;
|
|
155
|
+
if (!isString(item)) return boundNonStringItem(item, share);
|
|
156
|
+
const bounded = truncateChars(item, share, "host-result");
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
return { value: bounded.text, used: bounded.text.length, truncated: bounded.truncated };
|
|
159
|
+
}
|
|
144
160
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
if (!truncated) return;
|
|
219
|
+
result.originalChars = batch ? originalBatchChars(batch) : text.length;
|
|
220
|
+
attachSpill(result, batch, text, config, maxChars, capped);
|
|
221
|
+
}
|
|
157
222
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
244
|
+
if (acc.images.length >= 16 || acc.imageBytes + size > 20 * 1024 * 1024) {
|
|
245
|
+
acc.imageOverflow = true;
|
|
163
246
|
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
const
|
|
179
|
-
let imageBytes = 0;
|
|
180
|
-
let imageOverflow = false;
|
|
256
|
+
function collectImages(input, acc) {
|
|
257
|
+
const replaced = collectImage(input, acc);
|
|
181
258
|
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
imageOverflow = true;
|
|
261
|
+
if (Array.isArray(input)) return input.map(child => collectImages(child, acc));
|
|
188
262
|
|
|
189
|
-
|
|
190
|
-
}
|
|
263
|
+
if (isObject(input)) return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, collectImages(child, acc)]));
|
|
191
264
|
|
|
192
|
-
|
|
193
|
-
|
|
265
|
+
return input;
|
|
266
|
+
}
|
|
194
267
|
|
|
195
|
-
|
|
196
|
-
|
|
268
|
+
function serializeReturn(value, formatted, maxReturn, imageOverflow) {
|
|
269
|
+
if (formatted.length <= maxReturn) return { text: formatted, truncated: imageOverflow };
|
|
197
270
|
|
|
198
|
-
|
|
271
|
+
if (Array.isArray(value) && value.length && value.every(isString)) return { text: formatBoundedStringArray(value, maxReturn), truncated: true };
|
|
199
272
|
|
|
200
|
-
|
|
273
|
+
return { ...truncateChars(formatted, maxReturn, "return"), truncated: true };
|
|
274
|
+
}
|
|
201
275
|
|
|
202
|
-
|
|
203
|
-
|
|
276
|
+
function clipLogLine(line, maxLogLineChars) {
|
|
277
|
+
const result = truncateChars(line, maxLogLineChars, "log");
|
|
204
278
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 =
|
|
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
|
}
|