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