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.
Files changed (66) hide show
  1. package/README.md +188 -51
  2. package/docs/CHANGELOG.md +86 -1
  3. package/docs/TOKEN_COSTS.md +38 -0
  4. package/index.js +10 -175
  5. package/package.json +2 -1
  6. package/src/adapters/bash.js +14 -30
  7. package/src/adapters/errors.js +1 -9
  8. package/src/adapters/read-focus.js +98 -0
  9. package/src/adapters/read-image.js +51 -0
  10. package/src/adapters/read-json.js +42 -0
  11. package/src/adapters/read-text.js +71 -0
  12. package/src/adapters/read.js +66 -635
  13. package/src/bridge/catalog.js +3 -2
  14. package/src/bridge/host-bridge.js +35 -167
  15. package/src/bridge/tool-registry.js +104 -0
  16. package/src/bridge/trace.js +41 -0
  17. package/src/context/evidence-graph.js +249 -0
  18. package/src/context/evidence-rank.js +153 -0
  19. package/src/context/evidence.js +10 -424
  20. package/src/context/query.js +71 -0
  21. package/src/context/repo-index.js +8 -162
  22. package/src/context/search-files.js +19 -0
  23. package/src/context/search.js +2 -24
  24. package/src/context/snap-search.js +202 -0
  25. package/src/context/snap.js +5 -266
  26. package/src/context/source-entry.js +112 -0
  27. package/src/contract/bash.js +6 -1
  28. package/src/contract/program.js +36 -0
  29. package/src/contract/read.js +8 -53
  30. package/src/fs/check.js +1 -1
  31. package/src/fs/commit.js +161 -0
  32. package/src/fs/diff.js +11 -15
  33. package/src/fs/directory.js +79 -0
  34. package/src/fs/file-io.js +100 -0
  35. package/src/fs/glob.js +54 -0
  36. package/src/fs/json-size.js +54 -0
  37. package/src/fs/lines.js +117 -0
  38. package/src/fs/read-window.js +74 -0
  39. package/src/fs/session-resource.js +50 -0
  40. package/src/fs/text-ops.js +7 -227
  41. package/src/fs/vfs.js +5 -239
  42. package/src/fs/workspace.js +2 -1
  43. package/src/output/bottleneck.js +13 -67
  44. package/src/output/final.js +114 -0
  45. package/src/output/format.js +94 -5
  46. package/src/output/outcome.js +91 -0
  47. package/src/runtime/batch-input.js +68 -0
  48. package/src/runtime/guest-api.js +281 -0
  49. package/src/runtime/guest-worker.js +62 -333
  50. package/src/runtime/parallel.js +41 -39
  51. package/src/runtime/program-batch.js +21 -75
  52. package/src/runtime/program-file.js +3 -11
  53. package/src/runtime/program.js +141 -0
  54. package/src/runtime/reference.js +6 -5
  55. package/src/runtime/runtime.js +77 -253
  56. package/src/runtime/worker-pool.js +91 -0
  57. package/src/shared/decode.js +22 -8
  58. package/src/shared/image-worker.js +30 -0
  59. package/src/shared/image.js +78 -0
  60. package/src/shared/png.js +57 -0
  61. package/src/shared/result.js +77 -0
  62. package/src/shared/syntax-context.js +61 -3
  63. package/src/ui/host-render.js +104 -0
  64. package/src/ui/progress.js +51 -0
  65. package/src/ui/render.js +21 -421
  66. package/src/ui/trace.js +277 -0
@@ -2,10 +2,11 @@ import { isString } from "../shared/decode.js";
2
2
 
3
3
  /** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
4
4
  function osaCell(a, b, rows, i, j) {
5
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5
+ const cost = Number(a[i - 1] !== b[j - 1]);
6
6
  let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
7
7
 
8
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
8
+ // At a boundary, the missing character cannot equal an in-range character.
9
+ if (a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
9
10
 
10
11
  return best;
11
12
  }
@@ -1,15 +1,17 @@
1
+ import {createToolRegistry} from './tool-registry.js';
2
+ import {traceArgs,finishRecord} from './trace.js';
1
3
  import * as fs from "node:fs/promises";
2
4
  import * as path from "node:path";
3
- import { packageHostResult, hostResultFailed } from "../output/bottleneck.js";
4
- import { truncateChars } from "../output/format.js";
5
- import { isString, isFunction, isObject } from "../shared/decode.js";
5
+ import { packageHostResult } from "../output/bottleneck.js";
6
+
7
+ import { errorMessage, isString, isFunction } from "../shared/decode.js";
6
8
  import { isMutatingTool, runParallelWave, createNativeScheduler } from "../runtime/parallel.js";
7
9
  import { unknownToolMessage } from "./catalog.js";
8
- import { toolIsCallable, resolveInvokeTarget } from "./invoke.js";
10
+ import { resolveInvokeTarget } from "./invoke.js";
9
11
  import { buildWriteDiff } from "../fs/diff.js";
10
12
  import { WorkspaceIndex } from "../context/repo-index.js";
11
13
  import { SeenLedger } from "../context/ledger.js";
12
- import { CausalVfs } from "../fs/vfs.js";
14
+ import { CausalVfs, resolveCommitTarget } from "../fs/vfs.js";
13
15
  import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "../fs/workspace.js";
14
16
  import { createNativeAdapters } from "../adapters/index.js";
15
17
  import { resultDiff, boundedWriteDiff, writeSnapshot } from "../fs/text-ops.js";
@@ -31,8 +33,6 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
31
33
  const natives = createNativeAdapters(getCwd, vfs, config, index, ledger, hooks);
32
34
  let callCount = 0;
33
35
  let activeCtx = null;
34
- let hostSession = null;
35
- let boundSessionId;
36
36
  let activeSignal = undefined;
37
37
  let trace = [];
38
38
  let callListener = null;
@@ -72,109 +72,16 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
72
72
  return env;
73
73
  };
74
74
 
75
- function captureHostTool(tool, excluded) {
76
- return tool && isString(tool.name) && isFunction(tool.execute) && tool.name !== "supernova" && !excluded.has(tool.name);
77
- }
78
-
79
- function wrapHostRegister() {
80
- if (registry || !pi || !isFunction(pi.registerTool)) return;
81
- const original = pi.registerTool.bind(pi);
82
- const excluded = new Set(config.excludeTools || []);
83
- pi.registerTool = (tool) => {
84
- if (captureHostTool(tool, excluded)) {
85
- executors.set(tool.name, tool.execute.bind(tool));
86
- definitions.set(tool.name, tool);
87
- }
88
-
89
- return original(tool);
90
- };
91
- }
92
-
93
- wrapHostRegister();
75
+ const tools = createToolRegistry({pi,config,registry,natives,executors,definitions});
76
+ const {refreshTools,isCallable,externalNames,hostTool,evalToolNames} = tools;
94
77
 
95
78
  function bindCallContext(ctx, signal) {
96
79
  activeCtx = ctx || null;
97
- const sessionId = ctx?.sessionManager?.getSessionId?.();
98
- boundSessionId = sessionId;
99
- const registry = pi?.pi?.AgentRegistry?.global?.();
100
- let sessions = [];
101
-
102
- try { sessions = registry?.list?.() ?? []; } catch {}
103
- if (!Array.isArray(sessions)) sessions = [];
104
- hostSession = sessionId
105
- ? sessions.map(ref => ref.session).find(session => !session?.isDisposed && session?.sessionManager?.getSessionId?.() === sessionId) ?? null
106
- : null;
80
+ tools.bindSession(ctx);
107
81
  activeSignal = signal;
108
82
  vfs.signal = signal;
109
83
  }
110
84
 
111
- function evalToolNames() {
112
- try { return hostSession?.getEvalBridgeToolNames?.() ?? []; }
113
- catch { return []; }
114
- }
115
-
116
- function hostTool(name) {
117
- if (!hostSession) return undefined;
118
- const metadata = definitions.get(name);
119
-
120
- // Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
121
- if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
122
-
123
- try { return hostSession.getToolForEvalBridge?.(name); }
124
- catch { return undefined; }
125
- }
126
-
127
- function callableEnv() {
128
- return {
129
- excluded: new Set(config.excludeTools || []),
130
- hostSession,
131
- natives,
132
- executors,
133
- sessionInvalid: () => hostSession && (hostSession.isDisposed || hostSession.sessionManager?.getSessionId?.() !== boundSessionId),
134
- nativeOwned: name => Object.hasOwn(natives, name) && !executors.has(name)
135
- && (!hostSession || !definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"),
136
- evalAllows: name => {
137
- if (!evalToolNames().includes(name) && definitions.has(name)) return false;
138
-
139
- return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
140
- },
141
- listed: name => {
142
- let activeTools;
143
-
144
- try { activeTools = isFunction(pi?.getActiveTools) ? pi.getActiveTools() : undefined; } catch {}
145
-
146
- if (definitions.has(name) && Array.isArray(activeTools) && !activeTools.includes(name)) return false;
147
-
148
- return executors.has(name) || Object.hasOwn(natives, name);
149
- },
150
- hostTool,
151
- };
152
- }
153
-
154
- function isCallable(name) {
155
- return toolIsCallable(name, callableEnv());
156
- }
157
-
158
- function refreshTools() {
159
- let listed = [];
160
-
161
- try { listed = pi?.getAllTools?.() ?? []; } catch {}
162
- const tools = Array.isArray(listed) ? listed : [];
163
-
164
- for (const tool of tools) {
165
- if (!isString(tool?.name)) continue;
166
- definitions.set(tool.name, { ...definitions.get(tool.name), ...tool });
167
-
168
- if (!hostSession && isFunction(tool.execute)) executors.set(tool.name, tool.execute.bind(tool));
169
- }
170
-
171
- return [...definitions.values()].filter(tool => isCallable(tool.name));
172
- }
173
-
174
- function externalNames() {
175
- return [...definitions.keys()].filter(name => !!hostTool(name) || executors.has(name));
176
- }
177
-
178
85
  function resetCallBudget() {
179
86
  closed = false;
180
87
  vfs.closed = false;
@@ -185,26 +92,6 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
185
92
  clearPathCache();
186
93
  }
187
94
 
188
- function getTrace() {
189
- return [...trace];
190
- }
191
-
192
- function setCallListener(fn) {
193
- callListener = isFunction(fn) ? fn : null;
194
- }
195
-
196
- function beginSpeculation() {
197
- return vfs.begin();
198
- }
199
-
200
- async function commitSpeculation() {
201
- return await vfs.commit();
202
- }
203
-
204
- function rollbackSpeculation() {
205
- return vfs.rollback();
206
- }
207
-
208
95
  function notifyCall(record) {
209
96
  if (!callListener) return;
210
97
 
@@ -261,25 +148,6 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
261
148
  notifyCall(record);
262
149
  }
263
150
 
264
- function traceArgs(args) {
265
- if (!isObject(args)) return {};
266
- const out = {};
267
-
268
- for (const key of ["path", "target", "query", "pattern", "command", "cwd", "glob", "action", "op"]) {
269
- const value = args[key];
270
-
271
- if (isString(value)) out[key] = truncateChars(value, 240, "trace").text;
272
- // eslint-disable-next-line anti-slop/no-runtime-typeof -- display-only label: the value is already handled, this names its kind for the trace.
273
- else if (Array.isArray(value)) out[key] = value.slice(0, 128).map(item => isString(item) ? truncateChars(item, 240, "trace").text : typeof item);
274
- }
275
-
276
- if (isString(args.content)) out.content = args.content.length + " chars";
277
- if (Array.isArray(args.edits)) out.edits = args.edits.length + " edits";
278
- if (Array.isArray(args.args)) out.args = args.args.length + " argv";
279
-
280
- return out;
281
- }
282
-
283
151
  function assertOwnedOverride(name, args) {
284
152
  if (name === "read" && (args?.json !== undefined || /^(agent|artifact):\/\/.*\?/i.test(String(args?.path)))) throw new Error("JSON projection requires the Supernova-owned read adapter, not an external override");
285
153
  if (name === "write" && args?.append === true) throw new Error("append requires the Supernova-owned write adapter, not an external override");
@@ -296,7 +164,7 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
296
164
 
297
165
  try {
298
166
  const res = await target.exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, target.delegated
299
- ? { ...activeCtx, settings: hostSession.settings, toolNames: evalToolNames(), autoApprove: false }
167
+ ? { ...activeCtx, settings: tools.session.settings, toolNames: evalToolNames(), autoApprove: false }
300
168
  : activeCtx);
301
169
 
302
170
  completeRecord(record, res, fallbackDiff);
@@ -307,8 +175,8 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
307
175
  }
308
176
  }
309
177
 
310
- async function invokeNative(target, args, record) {
311
- const res = await target.native(target.argvOwned ? { ...args, args: args.args.map(String) } : args || {}, activeSignal);
178
+ async function invokeNative(target, args, record, onItem) {
179
+ const res = await target.native(target.argvOwned ? { ...args, args: args.args.map(String) } : args || {}, activeSignal, onItem);
312
180
  completeRecord(record, res);
313
181
 
314
182
  return res;
@@ -317,11 +185,11 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
317
185
  function failRecord(record, error) {
318
186
  record.ok = false;
319
187
  record.ms = Date.now() - record.time;
320
- record.error = error instanceof Error ? error.message : String(error);
188
+ record.error = errorMessage(error);
321
189
  notifyCall(record);
322
190
  }
323
191
 
324
- async function invokeRaw(name, args) {
192
+ async function invokeRaw(name, args, onItem) {
325
193
  assertRunOpen(name);
326
194
  const callId = ++sharedRegistry.callSeq;
327
195
  assertCallableTarget(name);
@@ -336,10 +204,10 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
336
204
  notifyCall(record);
337
205
 
338
206
  try {
339
- const target = resolveInvokeTarget(name, args, { hostTool, hostSession, executors, natives });
207
+ const target = resolveInvokeTarget(name, args, { hostTool, hostSession: tools.session, executors, natives });
340
208
 
341
209
  if (target.kind === "override") return await invokeOverride(target, name, args, record, callId);
342
- if (target.kind === "native") return await invokeNative(target, args, record);
210
+ if (target.kind === "native") return await invokeNative(target, args, record, onItem);
343
211
 
344
212
  throw new Error(unknownToolMessage(name, [...executors.keys(), ...Object.keys(natives)]));
345
213
  } catch (error) {
@@ -348,25 +216,25 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
348
216
  }
349
217
  }
350
218
 
351
- function finishRecord(record, res) {
352
- record.ms = Date.now() - record.time;
353
- record.ok = !hostResultFailed(res);
354
- const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
355
-
356
- if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
357
- const text = isObject(res) && Array.isArray(res.content)
358
- ? res.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n")
359
- : undefined;
219
+ function fileMutationKey(name, args) {
220
+ if (!["edit", "write", "apply_patch"].includes(name) || !isString(args?.path)) return;
221
+ // Overrides can mutate more than their declared path: keep them global.
222
+ if (hostTool(name) || executors.has(name)) return;
360
223
 
361
- if (text) record.resultText = truncateChars(text, 4096, "trace").text;
224
+ return async () => {
225
+ const target = await resolveWorkspacePath(getCwd(), args.path, name, false, true);
226
+ // Share the commit identity, including symlinks and not-yet-created files.
227
+ return (await resolveCommitTarget(target)).target;
228
+ };
362
229
  }
363
230
 
364
- async function call(name, args) {
231
+ async function call(name, args, onItem) {
365
232
  if (!isString(name) || !name) throw new Error("nova.call requires a tool name");
366
- const invoke = async () => packageHostResult(await invokeRaw(name, args), config);
233
+ const deliver = onItem ? (index, raw) => onItem(index, packageHostResult(raw, config)) : undefined;
234
+ const invoke = async () => packageHostResult(await invokeRaw(name, args, deliver), config);
367
235
  const kind = isMutatingTool(name, config, args, definitions.get(name)) ? "write" : "read";
368
236
 
369
- return scheduler.schedule(kind, invoke, activeSignal);
237
+ return scheduler.schedule(kind, invoke, activeSignal, fileMutationKey(name, args));
370
238
  }
371
239
 
372
240
  async function callMany(calls) {
@@ -433,13 +301,13 @@ export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedg
433
301
  close() { closed = true; vfs.closed = true; },
434
302
  bindCallContext,
435
303
  resetCallBudget,
436
- getTrace,
304
+ getTrace: () => [...trace],
437
305
  getMutations: () => ({ ...vfs.mutations }),
438
- setCallListener,
306
+ setCallListener: fn => { callListener = isFunction(fn) ? fn : null; },
439
307
  barrier: run => scheduler.schedule("write", run, activeSignal),
440
- beginSpeculation,
441
- commitSpeculation,
442
- rollbackSpeculation,
308
+ beginSpeculation: () => vfs.begin(),
309
+ commitSpeculation: async () => await vfs.commit(),
310
+ rollbackSpeculation: () => vfs.rollback(),
443
311
  getOverlayDepth: () => vfs.getOverlayDepth(),
444
312
  call,
445
313
  callMany,
@@ -0,0 +1,104 @@
1
+ import {isString,isFunction} from '../shared/decode.js';
2
+ import {toolIsCallable} from './invoke.js';
3
+
4
+ export function createToolRegistry({pi,config,registry,natives,executors,definitions}) {
5
+ let hostSession = null, boundSessionId;
6
+ function captureHostTool(tool, excluded) {
7
+ return tool && isString(tool.name) && isFunction(tool.execute) && tool.name !== "supernova" && !excluded.has(tool.name);
8
+ }
9
+
10
+ function wrapHostRegister() {
11
+ if (registry || !pi || !isFunction(pi.registerTool)) return;
12
+ const original = pi.registerTool.bind(pi);
13
+ const excluded = new Set(config.excludeTools || []);
14
+ pi.registerTool = (tool) => {
15
+ if (captureHostTool(tool, excluded)) {
16
+ executors.set(tool.name, tool.execute.bind(tool));
17
+ definitions.set(tool.name, tool);
18
+ }
19
+
20
+ return original(tool);
21
+ };
22
+ }
23
+
24
+ function evalToolNames() {
25
+ try { return hostSession?.getEvalBridgeToolNames?.() ?? []; }
26
+ catch { return []; }
27
+ }
28
+
29
+ function hostTool(name) {
30
+ if (!hostSession) return undefined;
31
+ const metadata = definitions.get(name);
32
+
33
+ // Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
34
+ if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
35
+
36
+ try { return hostSession.getToolForEvalBridge?.(name); }
37
+ catch { return undefined; }
38
+ }
39
+
40
+ function callableEnv() {
41
+ return {
42
+ excluded: new Set(config.excludeTools || []),
43
+ hostSession,
44
+ natives,
45
+ executors,
46
+ sessionInvalid: () => hostSession && (hostSession.isDisposed || hostSession.sessionManager?.getSessionId?.() !== boundSessionId),
47
+ nativeOwned: name => Object.hasOwn(natives, name) && !executors.has(name)
48
+ && (!hostSession || !definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"),
49
+ evalAllows: name => {
50
+ if (!evalToolNames().includes(name) && definitions.has(name)) return false;
51
+
52
+ return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
53
+ },
54
+ listed: name => {
55
+ let activeTools;
56
+
57
+ try { activeTools = isFunction(pi?.getActiveTools) ? pi.getActiveTools() : undefined; } catch {}
58
+
59
+ if (definitions.has(name) && Array.isArray(activeTools) && !activeTools.includes(name)) return false;
60
+
61
+ return executors.has(name) || Object.hasOwn(natives, name);
62
+ },
63
+ hostTool,
64
+ };
65
+ }
66
+
67
+ function isCallable(name) {
68
+ return toolIsCallable(name, callableEnv());
69
+ }
70
+
71
+ function refreshTools() {
72
+ let listed = [];
73
+
74
+ try { listed = pi?.getAllTools?.() ?? []; } catch {}
75
+ const tools = Array.isArray(listed) ? listed : [];
76
+
77
+ for (const tool of tools) {
78
+ if (!isString(tool?.name)) continue;
79
+ definitions.set(tool.name, { ...definitions.get(tool.name), ...tool });
80
+
81
+ if (!hostSession && isFunction(tool.execute)) executors.set(tool.name, tool.execute.bind(tool));
82
+ }
83
+
84
+ return [...definitions.values()].filter(tool => isCallable(tool.name));
85
+ }
86
+
87
+ function externalNames() {
88
+ return [...definitions.keys()].filter(name => !!hostTool(name) || executors.has(name));
89
+ }
90
+ function bindSession(ctx) {
91
+ const sessionId = ctx?.sessionManager?.getSessionId?.();
92
+ boundSessionId = sessionId;
93
+ const registry = pi?.pi?.AgentRegistry?.global?.();
94
+ let sessions = [];
95
+
96
+ try { sessions = registry?.list?.() ?? []; } catch {}
97
+ if (!Array.isArray(sessions)) sessions = [];
98
+ hostSession = sessionId
99
+ ? sessions.map(ref => ref.session).find(session => !session?.isDisposed && session?.sessionManager?.getSessionId?.() === sessionId) ?? null
100
+ : null;
101
+ }
102
+ wrapHostRegister();
103
+ return {refreshTools,isCallable,externalNames,hostTool,evalToolNames,bindSession,get session(){return hostSession;}};
104
+ }
@@ -0,0 +1,41 @@
1
+ import {READ_PREVIEW} from "../shared/result.js";
2
+ import {isObject,isString} from '../shared/decode.js';
3
+ import {truncateChars} from '../output/format.js';
4
+ import {hostResultFailed} from '../output/bottleneck.js';
5
+
6
+ function traceArgs(args) {
7
+ if (!isObject(args)) return {};
8
+ const out = {};
9
+
10
+ for (const key of ["path", "target", "query", "pattern", "command", "cwd", "glob", "action", "op"]) {
11
+ const value = args[key];
12
+
13
+ if (isString(value)) out[key] = truncateChars(value, 240, "trace").text;
14
+ // eslint-disable-next-line anti-slop/no-runtime-typeof -- display-only label: the value is already handled, this names its kind for the trace.
15
+ else if (Array.isArray(value)) out[key] = value.slice(0, 128).map(item => isString(item) ? truncateChars(item, 240, "trace").text : typeof item);
16
+ }
17
+
18
+ if (isString(args.content)) out.content = args.content.length + " chars";
19
+ if (Array.isArray(args.edits)) out.edits = args.edits.length + " edits";
20
+ if (Array.isArray(args.args)) out.args = args.args.length + " argv";
21
+
22
+ return out;
23
+ }
24
+
25
+ function resultText(res) {
26
+ return isObject(res) && Array.isArray(res.content)
27
+ ? res.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n")
28
+ : undefined;
29
+ }
30
+
31
+ function finishRecord(record, res) {
32
+ record.ms = Date.now() - record.time;
33
+ record.ok = !hostResultFailed(res);
34
+ const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
35
+
36
+ if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
37
+ const text = isObject(res) && Object.hasOwn(res,READ_PREVIEW) ? res[READ_PREVIEW] : resultText(res);
38
+
39
+ if (text) record.resultText = truncateChars(text, 4096, "trace").text;
40
+ }
41
+ export { traceArgs, finishRecord };