@gajae-code/coding-agent 0.12.4 → 0.12.6
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/CHANGELOG.md +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
package/src/tools/output-meta.ts
CHANGED
|
@@ -15,19 +15,80 @@ import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
|
15
15
|
import { getDefault, type Settings } from "../config/settings";
|
|
16
16
|
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
17
17
|
import type { Theme } from "../modes/theme/theme";
|
|
18
|
-
import { sessionArtifactCapability } from "../session/session-manager";
|
|
18
|
+
import { type SessionArtifactCapability, sessionArtifactCapability } from "../session/session-manager";
|
|
19
19
|
import {
|
|
20
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
20
21
|
formatMiddleElisionMarker,
|
|
22
|
+
hasCurrentExecutionArtifactProof,
|
|
21
23
|
type OutputSummary,
|
|
22
24
|
type ReadWindow,
|
|
23
25
|
type TruncationResult,
|
|
26
|
+
truncateHeadBytes,
|
|
24
27
|
truncateMiddle,
|
|
25
28
|
truncateTail,
|
|
26
29
|
} from "../session/streaming-output";
|
|
30
|
+
import { isValidArtifactId } from "../utils/artifact-id";
|
|
31
|
+
|
|
32
|
+
export { isValidArtifactId } from "../utils/artifact-id";
|
|
27
33
|
|
|
28
34
|
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
29
35
|
import { renderError } from "./tool-errors";
|
|
30
36
|
|
|
37
|
+
const kCurrentExecutionArtifactProof = Symbol("OutputMeta.CurrentExecutionArtifactProof");
|
|
38
|
+
const pendingArtifactLookupsByOwner = new WeakMap<SessionArtifactCapability, Set<Promise<boolean>>>();
|
|
39
|
+
|
|
40
|
+
function bindCurrentExecutionArtifactProof(truncation: TruncationMeta, summary: OutputSummary): void {
|
|
41
|
+
const artifactId = truncation.artifactId;
|
|
42
|
+
if (!artifactId || !hasCurrentExecutionArtifactProof(summary, artifactId, summary.output)) return;
|
|
43
|
+
Object.defineProperty(truncation, kCurrentExecutionArtifactProof, {
|
|
44
|
+
value: { artifactId, payload: summary.output, consumed: false },
|
|
45
|
+
enumerable: false,
|
|
46
|
+
configurable: false,
|
|
47
|
+
writable: false,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasBoundCurrentExecutionArtifactProof(
|
|
52
|
+
truncation: TruncationMeta,
|
|
53
|
+
artifactId: string,
|
|
54
|
+
payload: string,
|
|
55
|
+
): boolean {
|
|
56
|
+
const proof = (
|
|
57
|
+
truncation as TruncationMeta & {
|
|
58
|
+
[kCurrentExecutionArtifactProof]?: { artifactId: string; payload: string; consumed: boolean };
|
|
59
|
+
}
|
|
60
|
+
)[kCurrentExecutionArtifactProof];
|
|
61
|
+
if (!proof || proof.artifactId !== artifactId || proof.payload !== payload || proof.consumed) return false;
|
|
62
|
+
proof.consumed = true;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pendingArtifactLookupsForOwner(capability: SessionArtifactCapability): Set<Promise<boolean>> {
|
|
67
|
+
let pending = pendingArtifactLookupsByOwner.get(capability);
|
|
68
|
+
if (!pending) {
|
|
69
|
+
pending = new Set();
|
|
70
|
+
pendingArtifactLookupsByOwner.set(capability, pending);
|
|
71
|
+
}
|
|
72
|
+
return pending;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveArtifactForCurrentSessionBounded(
|
|
76
|
+
capability: SessionArtifactCapability | undefined,
|
|
77
|
+
artifactId: string,
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
if (!capability) return false;
|
|
80
|
+
const pending = pendingArtifactLookupsForOwner(capability);
|
|
81
|
+
if (pending.size >= INLINE_ARTIFACT_PENDING_LIMIT) return false;
|
|
82
|
+
const lookup = Promise.resolve()
|
|
83
|
+
.then(async () => (await capability.getArtifactPath(artifactId)) !== null)
|
|
84
|
+
.catch(() => false);
|
|
85
|
+
pending.add(lookup);
|
|
86
|
+
void lookup.then(
|
|
87
|
+
() => pending.delete(lookup),
|
|
88
|
+
() => pending.delete(lookup),
|
|
89
|
+
);
|
|
90
|
+
return Promise.race([lookup, Bun.sleep(INLINE_ARTIFACT_SAVE_WAIT_MS).then(() => false)]);
|
|
91
|
+
}
|
|
31
92
|
/**
|
|
32
93
|
* Truncation metadata for the output notice.
|
|
33
94
|
*/
|
|
@@ -55,8 +116,16 @@ export interface TruncationMeta {
|
|
|
55
116
|
elidedLines?: number;
|
|
56
117
|
/** Artifact ID when output was persisted; completeness is tracked separately below. */
|
|
57
118
|
artifactId?: string;
|
|
119
|
+
/** Artifact existence was proven by a successful save or publisher receipt. */
|
|
120
|
+
artifactVerified?: boolean;
|
|
58
121
|
/** Bytes omitted from an artifact after its hard storage cap was reached. */
|
|
59
122
|
artifactTruncatedBytes?: number;
|
|
123
|
+
/** Bytes dropped before Bash received the native output stream. */
|
|
124
|
+
sourceTruncatedBytes?: number;
|
|
125
|
+
/** Exact source capture completeness could not be proven. */
|
|
126
|
+
sourceCaptureIncomplete?: boolean;
|
|
127
|
+
/** Bytes omitted from the visible output by per-line column capping. */
|
|
128
|
+
columnDroppedBytes?: number;
|
|
60
129
|
/** Bounded diagnostic when artifact writer creation, write, or finalization failed. */
|
|
61
130
|
artifactFailureDiagnostic?: string;
|
|
62
131
|
/** Next offset for pagination (head truncation only) */
|
|
@@ -97,6 +166,8 @@ export interface OutputMeta {
|
|
|
97
166
|
source?: SourceMeta;
|
|
98
167
|
diagnostics?: DiagnosticMeta;
|
|
99
168
|
limits?: LimitsMeta;
|
|
169
|
+
/** Diagnostics were omitted to preserve the configured inline byte cap. */
|
|
170
|
+
diagnosticsOmitted?: boolean;
|
|
100
171
|
}
|
|
101
172
|
|
|
102
173
|
// =============================================================================
|
|
@@ -108,6 +179,9 @@ export interface TruncationOptions {
|
|
|
108
179
|
startLine?: number;
|
|
109
180
|
totalFileLines?: number;
|
|
110
181
|
artifactId?: string;
|
|
182
|
+
artifactVerified?: boolean;
|
|
183
|
+
artifactFailureDiagnostic?: string;
|
|
184
|
+
sourceCaptureIncomplete?: boolean;
|
|
111
185
|
maxBytes?: number;
|
|
112
186
|
noticeOwner?: "body";
|
|
113
187
|
}
|
|
@@ -125,6 +199,8 @@ export interface TruncationTextOptions {
|
|
|
125
199
|
totalBytes?: number;
|
|
126
200
|
maxBytes?: number;
|
|
127
201
|
artifactId?: string;
|
|
202
|
+
artifactVerified?: boolean;
|
|
203
|
+
sourceCaptureIncomplete?: boolean;
|
|
128
204
|
noticeOwner?: "body";
|
|
129
205
|
}
|
|
130
206
|
|
|
@@ -147,7 +223,17 @@ export class OutputMetaBuilder {
|
|
|
147
223
|
truncation(result: TruncationResult, options: TruncationOptions): this {
|
|
148
224
|
if (!result.truncated) return this;
|
|
149
225
|
|
|
150
|
-
const {
|
|
226
|
+
const {
|
|
227
|
+
direction,
|
|
228
|
+
startLine = 1,
|
|
229
|
+
totalFileLines,
|
|
230
|
+
artifactId,
|
|
231
|
+
artifactVerified,
|
|
232
|
+
artifactFailureDiagnostic,
|
|
233
|
+
sourceCaptureIncomplete,
|
|
234
|
+
noticeOwner,
|
|
235
|
+
maxBytes,
|
|
236
|
+
} = options;
|
|
151
237
|
const outputLines = result.outputLines ?? result.totalLines;
|
|
152
238
|
const outputBytes = result.outputBytes ?? result.totalBytes;
|
|
153
239
|
const isMiddle = direction === "middle" || result.truncatedBy === "middle";
|
|
@@ -163,12 +249,6 @@ export class OutputMetaBuilder {
|
|
|
163
249
|
if (isMiddle) {
|
|
164
250
|
const elidedLines = result.elidedLines ?? Math.max(0, effectiveTotalLines - outputLines);
|
|
165
251
|
const elidedBytes = result.elidedBytes ?? Math.max(0, result.totalBytes - outputBytes);
|
|
166
|
-
// Reconstruct head/tail line ranges. The kept output spans the first
|
|
167
|
-
// `headLines` lines and the last `tailLines` lines of the source; lines
|
|
168
|
-
// in the middle (count == elidedLines) are dropped.
|
|
169
|
-
const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
|
|
170
|
-
const headLines = Math.ceil(keptLines / 2);
|
|
171
|
-
const tailLines = keptLines - headLines;
|
|
172
252
|
this.#meta.truncation = {
|
|
173
253
|
direction: "middle",
|
|
174
254
|
truncatedBy: "middle",
|
|
@@ -178,12 +258,14 @@ export class OutputMetaBuilder {
|
|
|
178
258
|
outputLines,
|
|
179
259
|
outputBytes,
|
|
180
260
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
181
|
-
headRange:
|
|
182
|
-
tailRange:
|
|
183
|
-
tailLines > 0 ? { start: effectiveTotalLines - tailLines + 1, end: effectiveTotalLines } : undefined,
|
|
261
|
+
headRange: result.firstLinePartial ? undefined : result.headRange,
|
|
262
|
+
tailRange: result.lastLinePartial ? undefined : result.tailRange,
|
|
184
263
|
elidedLines,
|
|
185
264
|
elidedBytes,
|
|
186
265
|
artifactId,
|
|
266
|
+
artifactVerified,
|
|
267
|
+
artifactFailureDiagnostic,
|
|
268
|
+
sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
|
|
187
269
|
};
|
|
188
270
|
return this;
|
|
189
271
|
}
|
|
@@ -208,8 +290,11 @@ export class OutputMetaBuilder {
|
|
|
208
290
|
outputLines,
|
|
209
291
|
outputBytes,
|
|
210
292
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
211
|
-
shownRange: { start: shownStart, end: shownEnd },
|
|
293
|
+
shownRange: direction === "tail" && result.lastLinePartial ? undefined : { start: shownStart, end: shownEnd },
|
|
212
294
|
artifactId,
|
|
295
|
+
artifactVerified,
|
|
296
|
+
artifactFailureDiagnostic,
|
|
297
|
+
sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
|
|
213
298
|
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
214
299
|
};
|
|
215
300
|
|
|
@@ -221,6 +306,7 @@ export class OutputMetaBuilder {
|
|
|
221
306
|
windows: ReadWindow,
|
|
222
307
|
options: {
|
|
223
308
|
artifactId?: string;
|
|
309
|
+
artifactVerified?: boolean;
|
|
224
310
|
noticeOwner?: "body";
|
|
225
311
|
maxBytes?: number;
|
|
226
312
|
rangeBase?: "file" | "window";
|
|
@@ -228,7 +314,7 @@ export class OutputMetaBuilder {
|
|
|
228
314
|
): this {
|
|
229
315
|
if (windows.kind === "full") return this;
|
|
230
316
|
|
|
231
|
-
const { artifactId, noticeOwner, rangeBase } = options;
|
|
317
|
+
const { artifactId, artifactVerified, noticeOwner, rangeBase } = options;
|
|
232
318
|
const maxBytes = options.maxBytes ?? (windows as ReadWindow & { maxBytes?: number }).maxBytes;
|
|
233
319
|
const outputLinesOverride = (windows as ReadWindow & { outputLinesOverride?: number }).outputLinesOverride;
|
|
234
320
|
const outputBytesOverride = (windows as ReadWindow & { outputBytesOverride?: number }).outputBytesOverride;
|
|
@@ -264,6 +350,7 @@ export class OutputMetaBuilder {
|
|
|
264
350
|
sourceBytes: partialTail.sourceLineBytes,
|
|
265
351
|
},
|
|
266
352
|
artifactId,
|
|
353
|
+
artifactVerified,
|
|
267
354
|
};
|
|
268
355
|
return this;
|
|
269
356
|
}
|
|
@@ -282,6 +369,7 @@ export class OutputMetaBuilder {
|
|
|
282
369
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
283
370
|
shownRange: { start: head.origin.startLine, end: head.origin.endLine },
|
|
284
371
|
artifactId,
|
|
372
|
+
artifactVerified,
|
|
285
373
|
nextOffset: head.origin.endLine + 1,
|
|
286
374
|
};
|
|
287
375
|
return this;
|
|
@@ -301,6 +389,7 @@ export class OutputMetaBuilder {
|
|
|
301
389
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
302
390
|
shownRange: { start: tail.origin.startLine, end: tail.origin.endLine },
|
|
303
391
|
artifactId,
|
|
392
|
+
artifactVerified,
|
|
304
393
|
};
|
|
305
394
|
return this;
|
|
306
395
|
}
|
|
@@ -319,10 +408,12 @@ export class OutputMetaBuilder {
|
|
|
319
408
|
outputBytes: head.bytes + tail.bytes + Buffer.byteLength(marker, "utf-8") + 2,
|
|
320
409
|
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
321
410
|
headRange: { start: head.origin.startLine, end: head.origin.endLine },
|
|
322
|
-
tailRange:
|
|
411
|
+
tailRange:
|
|
412
|
+
tail.kind === "partial-line" ? undefined : { start: tail.origin.startLine, end: tail.origin.endLine },
|
|
323
413
|
elidedLines: windows.elidedLines,
|
|
324
414
|
elidedBytes: windows.elidedBytes,
|
|
325
415
|
artifactId,
|
|
416
|
+
artifactVerified,
|
|
326
417
|
};
|
|
327
418
|
return this;
|
|
328
419
|
}
|
|
@@ -330,14 +421,45 @@ export class OutputMetaBuilder {
|
|
|
330
421
|
/** Add truncation info from OutputSummary. No-op if not truncated or artifact evidence is absent. */
|
|
331
422
|
truncationFromSummary(summary: OutputSummary, options: TruncationSummaryOptions): this {
|
|
332
423
|
const artifactFailureDiagnostic = summary.artifactFailureDiagnostic;
|
|
424
|
+
const artifactLossValid = isValidLossCounter(summary.artifactTruncatedBytes);
|
|
425
|
+
const sourceLossValid = isValidLossCounter(summary.sourceTruncatedBytes);
|
|
426
|
+
const columnLossValid =
|
|
427
|
+
isValidLossCounter(summary.columnDroppedBytes) &&
|
|
428
|
+
isValidLossCounter(summary.columnTruncatedLines) &&
|
|
429
|
+
isValidLossCounter(summary.columnMax);
|
|
430
|
+
const sourceCaptureFlagValid =
|
|
431
|
+
summary.sourceCaptureIncomplete === undefined || typeof summary.sourceCaptureIncomplete === "boolean";
|
|
333
432
|
const artifactTruncatedBytes =
|
|
334
|
-
summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
|
|
433
|
+
artifactLossValid && summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
|
|
335
434
|
? summary.artifactTruncatedBytes
|
|
336
435
|
: undefined;
|
|
436
|
+
const sourceTruncatedBytes =
|
|
437
|
+
sourceLossValid && summary.sourceTruncatedBytes != null && summary.sourceTruncatedBytes > 0
|
|
438
|
+
? summary.sourceTruncatedBytes
|
|
439
|
+
: undefined;
|
|
440
|
+
const columnDroppedBytes =
|
|
441
|
+
columnLossValid && summary.columnDroppedBytes != null && summary.columnDroppedBytes > 0
|
|
442
|
+
? summary.columnDroppedBytes
|
|
443
|
+
: undefined;
|
|
444
|
+
const sourceCaptureIncomplete =
|
|
445
|
+
summary.sourceCaptureIncomplete === true ||
|
|
446
|
+
!sourceCaptureFlagValid ||
|
|
447
|
+
!artifactLossValid ||
|
|
448
|
+
!sourceLossValid ||
|
|
449
|
+
!columnLossValid
|
|
450
|
+
? true
|
|
451
|
+
: undefined;
|
|
452
|
+
if (columnLossValid && (summary.columnTruncatedLines ?? 0) > 0 && (summary.columnMax ?? 0) > 0) {
|
|
453
|
+
this.columnTruncated(summary.columnMax ?? 0);
|
|
454
|
+
}
|
|
455
|
+
const sourceCoordinatesIncomplete =
|
|
456
|
+
sourceTruncatedBytes !== undefined || sourceCaptureIncomplete === true || columnDroppedBytes !== undefined;
|
|
337
457
|
const hasArtifactEvidence =
|
|
338
458
|
summary.artifactId !== undefined ||
|
|
339
459
|
artifactFailureDiagnostic !== undefined ||
|
|
340
|
-
artifactTruncatedBytes !== undefined
|
|
460
|
+
artifactTruncatedBytes !== undefined ||
|
|
461
|
+
sourceTruncatedBytes !== undefined ||
|
|
462
|
+
sourceCaptureIncomplete !== undefined;
|
|
341
463
|
if (!summary.truncated && !hasArtifactEvidence) return this;
|
|
342
464
|
|
|
343
465
|
const { direction, startLine = 1, totalFileLines, noticeOwner } = options;
|
|
@@ -346,18 +468,26 @@ export class OutputMetaBuilder {
|
|
|
346
468
|
summary.artifactId !== undefined && summary.output.includes(`artifact://${summary.artifactId}`);
|
|
347
469
|
const bodyOwnsArtifact =
|
|
348
470
|
bodyHasArtifact &&
|
|
349
|
-
(artifactTruncatedBytes === undefined
|
|
350
|
-
|
|
351
|
-
|
|
471
|
+
(artifactTruncatedBytes === undefined &&
|
|
472
|
+
sourceTruncatedBytes === undefined &&
|
|
473
|
+
sourceCaptureIncomplete === undefined &&
|
|
474
|
+
summary.columnDroppedBytes === undefined
|
|
475
|
+
? true
|
|
476
|
+
: summary.artifactId !== undefined &&
|
|
477
|
+
summary.output.includes(
|
|
478
|
+
formatArtifactReference(
|
|
479
|
+
summary.artifactId,
|
|
480
|
+
artifactTruncatedBytes,
|
|
481
|
+
sourceTruncatedBytes,
|
|
482
|
+
sourceCaptureIncomplete,
|
|
483
|
+
),
|
|
484
|
+
));
|
|
352
485
|
const owner =
|
|
353
486
|
noticeOwner !== undefined ? { noticeOwner } : bodyOwnsArtifact ? { noticeOwner: "body" as const } : {};
|
|
354
487
|
|
|
355
488
|
// Middle elision: the sink retained head + tail with an elision marker.
|
|
356
489
|
if (summary.elidedBytes != null && summary.elidedBytes > 0) {
|
|
357
490
|
const elidedLines = summary.elidedLines ?? Math.max(0, totalLines - summary.outputLines);
|
|
358
|
-
const keptLines = Math.max(0, summary.outputLines - 1); // -1 for marker line
|
|
359
|
-
const headLines = Math.ceil(keptLines / 2);
|
|
360
|
-
const tailLines = keptLines - headLines;
|
|
361
491
|
this.#meta.truncation = {
|
|
362
492
|
direction: "middle",
|
|
363
493
|
truncatedBy: "middle",
|
|
@@ -366,14 +496,19 @@ export class OutputMetaBuilder {
|
|
|
366
496
|
totalBytes: summary.totalBytes,
|
|
367
497
|
outputLines: summary.outputLines,
|
|
368
498
|
outputBytes: summary.outputBytes,
|
|
369
|
-
headRange:
|
|
370
|
-
tailRange:
|
|
499
|
+
headRange: sourceCoordinatesIncomplete || summary.firstLinePartial ? undefined : summary.headRange,
|
|
500
|
+
tailRange: sourceCoordinatesIncomplete || summary.lastLinePartial ? undefined : summary.tailRange,
|
|
371
501
|
elidedBytes: summary.elidedBytes,
|
|
372
502
|
elidedLines,
|
|
373
503
|
artifactId: summary.artifactId,
|
|
504
|
+
artifactVerified: summary.artifactVerified,
|
|
374
505
|
artifactTruncatedBytes,
|
|
506
|
+
sourceTruncatedBytes,
|
|
507
|
+
sourceCaptureIncomplete,
|
|
508
|
+
columnDroppedBytes,
|
|
375
509
|
artifactFailureDiagnostic,
|
|
376
510
|
};
|
|
511
|
+
bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
|
|
377
512
|
return this;
|
|
378
513
|
}
|
|
379
514
|
|
|
@@ -403,12 +538,20 @@ export class OutputMetaBuilder {
|
|
|
403
538
|
totalBytes: summary.totalBytes,
|
|
404
539
|
outputLines: summary.outputLines,
|
|
405
540
|
outputBytes: summary.outputBytes,
|
|
406
|
-
shownRange:
|
|
541
|
+
shownRange:
|
|
542
|
+
sourceCoordinatesIncomplete || (direction === "tail" && summary.lastLinePartial)
|
|
543
|
+
? undefined
|
|
544
|
+
: { start: shownStart, end: shownEnd },
|
|
407
545
|
artifactId: summary.artifactId,
|
|
546
|
+
artifactVerified: summary.artifactVerified,
|
|
408
547
|
artifactTruncatedBytes,
|
|
548
|
+
sourceTruncatedBytes,
|
|
549
|
+
sourceCaptureIncomplete,
|
|
550
|
+
columnDroppedBytes,
|
|
409
551
|
artifactFailureDiagnostic,
|
|
410
|
-
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
552
|
+
nextOffset: direction === "head" && !sourceCoordinatesIncomplete ? shownEnd + 1 : undefined,
|
|
411
553
|
};
|
|
554
|
+
bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
|
|
412
555
|
|
|
413
556
|
return this;
|
|
414
557
|
}
|
|
@@ -453,8 +596,13 @@ export class OutputMetaBuilder {
|
|
|
453
596
|
outputBytes,
|
|
454
597
|
maxBytes: options.maxBytes,
|
|
455
598
|
artifactId: options.artifactId,
|
|
456
|
-
|
|
457
|
-
|
|
599
|
+
artifactVerified: options.artifactVerified,
|
|
600
|
+
shownRange:
|
|
601
|
+
options.sourceCaptureIncomplete || options.direction === "middle"
|
|
602
|
+
? undefined
|
|
603
|
+
: { start: shownStart, end: shownEnd },
|
|
604
|
+
sourceCaptureIncomplete: options.sourceCaptureIncomplete || undefined,
|
|
605
|
+
nextOffset: options.direction === "head" && !options.sourceCaptureIncomplete ? shownEnd + 1 : undefined,
|
|
458
606
|
};
|
|
459
607
|
|
|
460
608
|
return this;
|
|
@@ -549,33 +697,196 @@ export function formatFullOutputReference(artifactId: string): string {
|
|
|
549
697
|
return `Read artifact://${artifactId} for full output`;
|
|
550
698
|
}
|
|
551
699
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
700
|
+
function boundArtifactFailureDiagnostic(error: unknown): string {
|
|
701
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
|
|
702
|
+
return truncateHeadBytes(message || "unknown storage error", 512).text;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const INLINE_ARTIFACT_SAVE_WAIT_MS = 500;
|
|
706
|
+
const INLINE_ARTIFACT_PENDING_LIMIT = 64;
|
|
707
|
+
const pendingInlineArtifactSavesByOwner = new WeakMap<SessionArtifactCapability, Set<Promise<unknown>>>();
|
|
708
|
+
|
|
709
|
+
function pendingInlineArtifactSavesForOwner(capability: SessionArtifactCapability): Set<Promise<unknown>> {
|
|
710
|
+
let pending = pendingInlineArtifactSavesByOwner.get(capability);
|
|
711
|
+
if (!pending) {
|
|
712
|
+
pending = new Set<Promise<unknown>>();
|
|
713
|
+
pendingInlineArtifactSavesByOwner.set(capability, pending);
|
|
714
|
+
}
|
|
715
|
+
return pending;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function saveInlineArtifactBounded(
|
|
719
|
+
capability: SessionArtifactCapability,
|
|
720
|
+
save: () => Promise<string | null | undefined>,
|
|
721
|
+
resolvePath: (artifactId: string) => Promise<string | null>,
|
|
722
|
+
): Promise<
|
|
723
|
+
{ status: "ok"; artifactId: string; artifactTruncatedBytes?: number } | { status: "failed"; diagnostic: string }
|
|
724
|
+
> {
|
|
725
|
+
const pendingInlineArtifactSaves = pendingInlineArtifactSavesForOwner(capability);
|
|
726
|
+
if (pendingInlineArtifactSaves.size >= INLINE_ARTIFACT_PENDING_LIMIT) {
|
|
727
|
+
return { status: "failed", diagnostic: "pending artifact save limit reached for this session" };
|
|
728
|
+
}
|
|
729
|
+
const promise = Promise.resolve()
|
|
730
|
+
.then(save)
|
|
731
|
+
.then(async artifactId => {
|
|
732
|
+
if (!isValidArtifactId(artifactId)) {
|
|
733
|
+
return { status: "failed", diagnostic: "storage returned an invalid artifact id" } as const;
|
|
734
|
+
}
|
|
735
|
+
const artifactPath = await resolvePath(artifactId);
|
|
736
|
+
if (!artifactPath) {
|
|
737
|
+
return { status: "failed", diagnostic: "saved artifact is not protocol-resolvable" } as const;
|
|
738
|
+
}
|
|
739
|
+
return { status: "ok", artifactId } as const;
|
|
740
|
+
})
|
|
741
|
+
.catch(error => ({ status: "failed", diagnostic: boundArtifactFailureDiagnostic(error) }) as const);
|
|
742
|
+
pendingInlineArtifactSaves.add(promise);
|
|
743
|
+
void promise.then(
|
|
744
|
+
() => pendingInlineArtifactSaves.delete(promise),
|
|
745
|
+
() => pendingInlineArtifactSaves.delete(promise),
|
|
746
|
+
);
|
|
747
|
+
return Promise.race([
|
|
748
|
+
promise,
|
|
749
|
+
Bun.sleep(INLINE_ARTIFACT_SAVE_WAIT_MS).then(
|
|
750
|
+
() => ({ status: "failed", diagnostic: `did not settle within ${INLINE_ARTIFACT_SAVE_WAIT_MS}ms` }) as const,
|
|
751
|
+
),
|
|
752
|
+
]);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function genericArtifactOmittedBytes(content: string): number | undefined {
|
|
756
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
757
|
+
if (contentBytes <= DEFAULT_ARTIFACT_MAX_BYTES) return undefined;
|
|
758
|
+
const retained = truncateHeadBytes(content, DEFAULT_ARTIFACT_MAX_BYTES);
|
|
759
|
+
return contentBytes - retained.bytes;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const ARTIFACT_FAILURE_PREVIEW_DEFAULT_BYTES = 8 * 1024;
|
|
763
|
+
|
|
764
|
+
export interface ArtifactFailurePreview {
|
|
765
|
+
text: string;
|
|
766
|
+
omittedBytes: number;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Build a bounded, UTF-8-safe preview when artifact storage cannot retain the full payload. */
|
|
770
|
+
export function createArtifactFailurePreview(
|
|
771
|
+
fullText: string,
|
|
772
|
+
maxInlineBytes: number | undefined,
|
|
773
|
+
diagnostic: string,
|
|
774
|
+
): ArtifactFailurePreview {
|
|
775
|
+
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
776
|
+
const previewLimit =
|
|
777
|
+
maxInlineBytes !== undefined && maxInlineBytes > 0 ? maxInlineBytes : ARTIFACT_FAILURE_PREVIEW_DEFAULT_BYTES;
|
|
778
|
+
const boundedDiagnostic = boundArtifactFailureDiagnostic(diagnostic);
|
|
779
|
+
let bodyBudget = Math.min(Math.max(0, totalBytes - 1), previewLimit);
|
|
780
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
781
|
+
const body = truncateTail(fullText, { maxBytes: bodyBudget }).content;
|
|
782
|
+
const bodyBytes = Buffer.byteLength(body, "utf-8");
|
|
783
|
+
const omittedBytes = Math.max(1, totalBytes - bodyBytes);
|
|
784
|
+
const marker = `\n\n[${omittedBytes} bytes omitted; no artifact available: ${boundedDiagnostic}]\n\n`;
|
|
785
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
786
|
+
if (bodyBytes + markerBytes <= previewLimit) {
|
|
787
|
+
return { text: `${body}${marker}`, omittedBytes };
|
|
788
|
+
}
|
|
789
|
+
bodyBudget = Math.max(0, Math.min(bodyBudget - 1, previewLimit - markerBytes));
|
|
790
|
+
}
|
|
791
|
+
const marker = `\n\n[${totalBytes} bytes omitted; no artifact available: ${boundedDiagnostic}]\n\n`;
|
|
792
|
+
return { text: truncateHeadBytes(marker, previewLimit).text, omittedBytes: totalBytes };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Format an artifact reference without claiming completeness when capture or storage omitted bytes. */
|
|
796
|
+
export function formatArtifactReference(
|
|
797
|
+
artifactId: string,
|
|
798
|
+
artifactTruncatedBytes?: number,
|
|
799
|
+
sourceTruncatedBytes?: number,
|
|
800
|
+
sourceCaptureIncomplete?: boolean,
|
|
801
|
+
): string {
|
|
802
|
+
const omissions: string[] = [];
|
|
803
|
+
if (!isValidLossCounter(sourceTruncatedBytes) || !isValidLossCounter(artifactTruncatedBytes)) {
|
|
804
|
+
omissions.push("loss accounting was invalid");
|
|
805
|
+
}
|
|
806
|
+
if (sourceTruncatedBytes != null && sourceTruncatedBytes > 0) {
|
|
807
|
+
omissions.push(`at least ${formatBytes(sourceTruncatedBytes)} dropped before Bash capture`);
|
|
808
|
+
}
|
|
809
|
+
if (sourceCaptureIncomplete) {
|
|
810
|
+
omissions.push("source capture completeness could not be proven");
|
|
811
|
+
}
|
|
556
812
|
if (artifactTruncatedBytes != null && artifactTruncatedBytes > 0) {
|
|
557
|
-
|
|
813
|
+
omissions.push(`at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap`);
|
|
558
814
|
}
|
|
559
|
-
return
|
|
815
|
+
return omissions.length > 0
|
|
816
|
+
? `Read artifact://${artifactId} for retained output (${omissions.join("; ")})`
|
|
817
|
+
: formatFullOutputReference(artifactId);
|
|
560
818
|
}
|
|
561
819
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
820
|
+
export interface ArtifactEvidence {
|
|
821
|
+
artifactId?: string;
|
|
822
|
+
artifactVerified?: boolean;
|
|
823
|
+
artifactTruncatedBytes?: number;
|
|
824
|
+
sourceTruncatedBytes?: number;
|
|
825
|
+
sourceCaptureIncomplete?: boolean;
|
|
826
|
+
artifactFailureDiagnostic?: string;
|
|
827
|
+
columnDroppedBytes?: number;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function formatArtifactEvidenceNotice(evidence: ArtifactEvidence): string | undefined {
|
|
831
|
+
const parts: string[] = [];
|
|
832
|
+
if (
|
|
833
|
+
isValidArtifactId(evidence.artifactId) &&
|
|
834
|
+
evidence.artifactVerified === true &&
|
|
835
|
+
!evidence.artifactFailureDiagnostic
|
|
836
|
+
) {
|
|
837
|
+
parts.push(
|
|
838
|
+
formatArtifactReference(
|
|
839
|
+
evidence.artifactId,
|
|
840
|
+
evidence.artifactTruncatedBytes,
|
|
841
|
+
evidence.sourceTruncatedBytes,
|
|
842
|
+
evidence.sourceCaptureIncomplete || (evidence.columnDroppedBytes ?? 0) > 0,
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
if ((evidence.columnDroppedBytes ?? 0) > 0) {
|
|
846
|
+
parts.push(`Visible line caps omitted ${formatBytes(evidence.columnDroppedBytes ?? 0)}`);
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
if (evidence.artifactId && evidence.artifactVerified !== true) {
|
|
850
|
+
parts.push("Artifact availability could not be proven");
|
|
851
|
+
}
|
|
852
|
+
if ((evidence.sourceTruncatedBytes ?? 0) > 0) {
|
|
853
|
+
parts.push(
|
|
854
|
+
`Bash capture omitted at least ${formatBytes(evidence.sourceTruncatedBytes ?? 0)} before artifact storage`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (evidence.sourceCaptureIncomplete) {
|
|
858
|
+
parts.push("Source capture completeness could not be proven before retained output was finalized");
|
|
859
|
+
}
|
|
860
|
+
if ((evidence.artifactTruncatedBytes ?? 0) > 0) {
|
|
861
|
+
parts.push(`Artifact storage omitted at least ${formatBytes(evidence.artifactTruncatedBytes ?? 0)}`);
|
|
862
|
+
}
|
|
863
|
+
if ((evidence.columnDroppedBytes ?? 0) > 0) {
|
|
864
|
+
parts.push(`Visible line caps omitted ${formatBytes(evidence.columnDroppedBytes ?? 0)}`);
|
|
865
|
+
}
|
|
866
|
+
if (parts.length > 0) {
|
|
867
|
+
parts.push("no artifact reference is available");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (evidence.artifactFailureDiagnostic) {
|
|
871
|
+
parts.push(`Artifact storage failed: ${evidence.artifactFailureDiagnostic}`);
|
|
872
|
+
}
|
|
873
|
+
if (!evidence.artifactId && parts.length > 0 && !parts.includes("no artifact reference is available")) {
|
|
874
|
+
parts.push("no artifact reference is available");
|
|
570
875
|
}
|
|
571
|
-
|
|
572
|
-
|
|
876
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function formatTruncationArtifactNotice(truncation: TruncationMeta): string {
|
|
880
|
+
return formatArtifactEvidenceNotice(truncation) ?? "";
|
|
573
881
|
}
|
|
574
882
|
|
|
575
883
|
function hasArtifactNotice(truncation: TruncationMeta): boolean {
|
|
576
884
|
return (
|
|
577
885
|
truncation.artifactId != null ||
|
|
578
886
|
(truncation.artifactTruncatedBytes ?? 0) > 0 ||
|
|
887
|
+
(truncation.sourceTruncatedBytes ?? 0) > 0 ||
|
|
888
|
+
truncation.sourceCaptureIncomplete ||
|
|
889
|
+
(truncation.columnDroppedBytes ?? 0) > 0 ||
|
|
579
890
|
truncation.artifactFailureDiagnostic != null
|
|
580
891
|
);
|
|
581
892
|
}
|
|
@@ -588,6 +899,16 @@ function formatTruncationRangeTotal(truncation: TruncationMeta): string {
|
|
|
588
899
|
|
|
589
900
|
export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
590
901
|
const rangeTotal = formatTruncationRangeTotal(truncation);
|
|
902
|
+
if ((truncation.sourceTruncatedBytes ?? 0) > 0 || truncation.sourceCaptureIncomplete) {
|
|
903
|
+
let notice = `Showing ${truncation.outputLines} retained line${truncation.outputLines === 1 ? "" : "s"} from an incomplete Bash capture`;
|
|
904
|
+
if (truncation.truncatedBy === "bytes" && truncation.maxBytes != null) {
|
|
905
|
+
notice += ` (${formatBytes(truncation.maxBytes)} limit)`;
|
|
906
|
+
}
|
|
907
|
+
if (hasArtifactNotice(truncation)) {
|
|
908
|
+
notice += `. ${formatTruncationArtifactNotice(truncation)}`;
|
|
909
|
+
}
|
|
910
|
+
return notice;
|
|
911
|
+
}
|
|
591
912
|
if (truncation.partialLine) {
|
|
592
913
|
let notice = `Showing last ${formatBytes(truncation.partialLine.bytes)} of line ${truncation.partialLine.line} of ${rangeTotal}`;
|
|
593
914
|
if (truncation.partialLine.sourceBytes > truncation.partialLine.bytes) {
|
|
@@ -614,7 +935,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
|
614
935
|
} else if (headPart && tailPart) {
|
|
615
936
|
notice = `Showing ${headPart} and ${tailPart} of ${rangeTotal}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`;
|
|
616
937
|
} else {
|
|
617
|
-
notice = `Showing
|
|
938
|
+
notice = `Showing retained head and tail fragments of ${rangeTotal}; ${formatBytes(elidedBytes)} middle bytes elided`;
|
|
618
939
|
}
|
|
619
940
|
if (hasArtifactNotice(truncation)) {
|
|
620
941
|
notice += `. ${formatTruncationArtifactNotice(truncation)}`;
|
|
@@ -688,8 +1009,12 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
688
1009
|
let diagnosticsNotice = "";
|
|
689
1010
|
if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
|
|
690
1011
|
const d = meta.diagnostics;
|
|
691
|
-
|
|
1012
|
+
const grouped = formatGroupedDiagnosticMessages(d.messages);
|
|
1013
|
+
const bounded = truncateHeadBytes(grouped, 1024);
|
|
1014
|
+
const suffix = bounded.bytes < Buffer.byteLength(grouped, "utf-8") ? "\n[diagnostics truncated]" : "";
|
|
1015
|
+
diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${bounded.text}${suffix}`;
|
|
692
1016
|
}
|
|
1017
|
+
if (meta.diagnosticsOmitted) parts.push("Diagnostics omitted by inline result cap");
|
|
693
1018
|
|
|
694
1019
|
const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";
|
|
695
1020
|
return notice + diagnosticsNotice;
|
|
@@ -791,13 +1116,15 @@ function getSpillConfig(s: Settings | undefined) {
|
|
|
791
1116
|
| "tools.maxInlineResultBytes"
|
|
792
1117
|
| "tools.readArtifactSpillThreshold";
|
|
793
1118
|
const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
|
|
1119
|
+
const configuredInlineKiB = get("tools.maxInlineResultBytes");
|
|
1120
|
+
const maxInlineBytes = configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : 0;
|
|
794
1121
|
return {
|
|
795
1122
|
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
796
1123
|
readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
|
|
797
1124
|
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
798
1125
|
tailLines: get("tools.artifactTailLines"),
|
|
799
1126
|
headBytes: get("tools.artifactHeadBytes") * 1024,
|
|
800
|
-
maxInlineBytes
|
|
1127
|
+
maxInlineBytes,
|
|
801
1128
|
};
|
|
802
1129
|
}
|
|
803
1130
|
|
|
@@ -818,7 +1145,12 @@ export const BASH_DEFAULT_OUTPUT_TAIL_BYTES = 1024;
|
|
|
818
1145
|
* budget still wins.
|
|
819
1146
|
*/
|
|
820
1147
|
export function resolveBashOutputSinkTailBytes(s: Settings): number {
|
|
821
|
-
|
|
1148
|
+
const configuredTailBytes = s.get("tools.artifactTailBytes");
|
|
1149
|
+
const hasExplicitTailBytes =
|
|
1150
|
+
typeof s.has === "function" ? s.has("tools.artifactTailBytes") : configuredTailBytes !== undefined;
|
|
1151
|
+
return hasExplicitTailBytes && configuredTailBytes !== undefined
|
|
1152
|
+
? configuredTailBytes * 1024
|
|
1153
|
+
: BASH_DEFAULT_OUTPUT_TAIL_BYTES;
|
|
822
1154
|
}
|
|
823
1155
|
/**
|
|
824
1156
|
* Bash keeps only the tail unless the user explicitly opts into the shared
|
|
@@ -826,7 +1158,111 @@ export function resolveBashOutputSinkTailBytes(s: Settings): number {
|
|
|
826
1158
|
* tools without silently turning Bash back into middle-elision mode.
|
|
827
1159
|
*/
|
|
828
1160
|
export function resolveBashOutputSinkHeadBytes(s: Settings): number {
|
|
829
|
-
|
|
1161
|
+
const configuredHeadBytes = s.get("tools.artifactHeadBytes");
|
|
1162
|
+
const hasExplicitHeadBytes =
|
|
1163
|
+
typeof s.has === "function" ? s.has("tools.artifactHeadBytes") : configuredHeadBytes !== undefined;
|
|
1164
|
+
return hasExplicitHeadBytes && configuredHeadBytes !== undefined ? configuredHeadBytes * 1024 : 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function isValidLossCounter(value: number | undefined): boolean {
|
|
1168
|
+
return value === undefined || (Number.isSafeInteger(value) && value >= 0);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function stripBodyArtifactReferences(result: AgentToolResult, artifactId: string): AgentToolResult["content"] {
|
|
1172
|
+
return result.content.map(block => {
|
|
1173
|
+
if (block.type !== "text") return block;
|
|
1174
|
+
const lines = block.text.split("\n");
|
|
1175
|
+
const kept = lines.filter(line => {
|
|
1176
|
+
const trimmed = line.trim();
|
|
1177
|
+
return !(
|
|
1178
|
+
(trimmed.startsWith("[raw output:") && trimmed.includes(`artifact://${artifactId}`)) ||
|
|
1179
|
+
trimmed.startsWith(`Read artifact://${artifactId} `)
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
1182
|
+
return kept.length === lines.length ? block : { ...block, text: kept.join("\n") };
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async function sanitizeArtifactIdEvidence(
|
|
1187
|
+
result: AgentToolResult,
|
|
1188
|
+
context: AgentToolContext | undefined,
|
|
1189
|
+
): Promise<AgentToolResult> {
|
|
1190
|
+
const meta: OutputMeta | undefined = result.details?.meta;
|
|
1191
|
+
const truncation = meta?.truncation;
|
|
1192
|
+
if (!truncation) return result;
|
|
1193
|
+
const artifactId = truncation.artifactId;
|
|
1194
|
+
const artifactIdValid = artifactId === undefined || isValidArtifactId(artifactId);
|
|
1195
|
+
const countersValid =
|
|
1196
|
+
isValidLossCounter(truncation.artifactTruncatedBytes) &&
|
|
1197
|
+
isValidLossCounter(truncation.sourceTruncatedBytes) &&
|
|
1198
|
+
isValidLossCounter(truncation.columnDroppedBytes);
|
|
1199
|
+
const sourceCaptureFlagValid =
|
|
1200
|
+
truncation.sourceCaptureIncomplete === undefined || typeof truncation.sourceCaptureIncomplete === "boolean";
|
|
1201
|
+
const sourceCoordinatesIncomplete =
|
|
1202
|
+
!countersValid ||
|
|
1203
|
+
!sourceCaptureFlagValid ||
|
|
1204
|
+
truncation.sourceCaptureIncomplete === true ||
|
|
1205
|
+
(truncation.sourceTruncatedBytes ?? 0) > 0;
|
|
1206
|
+
const artifactCapability = artifactCapabilityForContext(context);
|
|
1207
|
+
const artifactResolvable =
|
|
1208
|
+
artifactId === undefined ? true : await resolveArtifactForCurrentSessionBounded(artifactCapability, artifactId);
|
|
1209
|
+
const resultPayload = result.content.map(block => (block.type === "text" ? block.text : "")).join("");
|
|
1210
|
+
const artifactProven =
|
|
1211
|
+
artifactId === undefined ||
|
|
1212
|
+
(truncation.artifactVerified === true &&
|
|
1213
|
+
hasBoundCurrentExecutionArtifactProof(truncation, artifactId, resultPayload) &&
|
|
1214
|
+
artifactResolvable);
|
|
1215
|
+
const artifactFailed = truncation.artifactFailureDiagnostic !== undefined;
|
|
1216
|
+
const invalidateArtifact = artifactId !== undefined && (!artifactIdValid || !artifactProven || artifactFailed);
|
|
1217
|
+
const staleFullClaim =
|
|
1218
|
+
artifactId !== undefined &&
|
|
1219
|
+
(sourceCoordinatesIncomplete ||
|
|
1220
|
+
(truncation.artifactTruncatedBytes ?? 0) > 0 ||
|
|
1221
|
+
(truncation.columnDroppedBytes ?? 0) > 0);
|
|
1222
|
+
if (
|
|
1223
|
+
artifactIdValid &&
|
|
1224
|
+
countersValid &&
|
|
1225
|
+
sourceCaptureFlagValid &&
|
|
1226
|
+
!sourceCoordinatesIncomplete &&
|
|
1227
|
+
!invalidateArtifact &&
|
|
1228
|
+
!staleFullClaim
|
|
1229
|
+
)
|
|
1230
|
+
return result;
|
|
1231
|
+
const diagnostic = !artifactIdValid
|
|
1232
|
+
? "existing metadata contained an invalid artifact id"
|
|
1233
|
+
: artifactFailed
|
|
1234
|
+
? truncation.artifactFailureDiagnostic
|
|
1235
|
+
: !artifactProven
|
|
1236
|
+
? artifactResolvable
|
|
1237
|
+
? "existing metadata did not prove artifact availability"
|
|
1238
|
+
: "existing artifact is not protocol-resolvable in the current session"
|
|
1239
|
+
: undefined;
|
|
1240
|
+
return {
|
|
1241
|
+
...result,
|
|
1242
|
+
content:
|
|
1243
|
+
artifactId && (invalidateArtifact || staleFullClaim)
|
|
1244
|
+
? stripBodyArtifactReferences(result, artifactId)
|
|
1245
|
+
: result.content,
|
|
1246
|
+
details: {
|
|
1247
|
+
...(result.details ?? {}),
|
|
1248
|
+
meta: {
|
|
1249
|
+
...meta,
|
|
1250
|
+
truncation: {
|
|
1251
|
+
...truncation,
|
|
1252
|
+
noticeOwner: invalidateArtifact || staleFullClaim ? undefined : truncation.noticeOwner,
|
|
1253
|
+
artifactId: invalidateArtifact ? undefined : artifactId,
|
|
1254
|
+
artifactVerified: invalidateArtifact ? false : truncation.artifactVerified,
|
|
1255
|
+
sourceCaptureIncomplete:
|
|
1256
|
+
!countersValid || !sourceCaptureFlagValid || truncation.sourceCaptureIncomplete || undefined,
|
|
1257
|
+
shownRange: sourceCoordinatesIncomplete ? undefined : truncation.shownRange,
|
|
1258
|
+
headRange: sourceCoordinatesIncomplete ? undefined : truncation.headRange,
|
|
1259
|
+
tailRange: sourceCoordinatesIncomplete ? undefined : truncation.tailRange,
|
|
1260
|
+
nextOffset: sourceCoordinatesIncomplete ? undefined : truncation.nextOffset,
|
|
1261
|
+
artifactFailureDiagnostic: diagnostic,
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
};
|
|
830
1266
|
}
|
|
831
1267
|
|
|
832
1268
|
/**
|
|
@@ -858,35 +1294,95 @@ async function spillLargeResultToArtifact(
|
|
|
858
1294
|
return result;
|
|
859
1295
|
}
|
|
860
1296
|
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
1297
|
+
const { threshold, readThreshold, tailBytes, tailLines, headBytes, maxInlineBytes } = getSpillConfig(
|
|
1298
|
+
context?.settings,
|
|
1299
|
+
);
|
|
864
1300
|
// `read` manages its own per-range truncation, but the combined multi-range
|
|
865
1301
|
// output has no cap — enforce a read-specific (higher) combined threshold
|
|
866
1302
|
// instead of exempting read entirely. 0 disables read spill (backstop only).
|
|
867
1303
|
const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
|
|
868
|
-
if (effectiveThreshold <= 0) return result;
|
|
869
|
-
|
|
870
|
-
// Skip if tool already saved an artifact
|
|
871
1304
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
872
|
-
if (existingMeta?.truncation?.artifactId) return result;
|
|
873
1305
|
|
|
874
|
-
// Measure total text content
|
|
1306
|
+
// Measure total text content before any threshold early return so the absolute
|
|
1307
|
+
// inline cap is enforced even when token-based spilling is disabled.
|
|
875
1308
|
const textParts: string[] = [];
|
|
876
1309
|
for (const block of result.content) {
|
|
877
|
-
if (block.type === "text" && block.text)
|
|
878
|
-
textParts.push(block.text);
|
|
879
|
-
}
|
|
1310
|
+
if (block.type === "text" && block.text) textParts.push(block.text);
|
|
880
1311
|
}
|
|
881
1312
|
if (textParts.length === 0) return result;
|
|
882
1313
|
|
|
883
1314
|
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
1315
|
+
const existingTruncation = existingMeta?.truncation;
|
|
1316
|
+
const existingArtifactProven =
|
|
1317
|
+
existingTruncation?.artifactId !== undefined &&
|
|
1318
|
+
isValidArtifactId(existingTruncation.artifactId) &&
|
|
1319
|
+
existingTruncation.artifactFailureDiagnostic === undefined &&
|
|
1320
|
+
existingTruncation.artifactVerified === true;
|
|
1321
|
+
if (existingArtifactProven) return result;
|
|
884
1322
|
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
885
|
-
|
|
1323
|
+
const exceedsInlineCap = maxInlineBytes > 0 && totalBytes > maxInlineBytes;
|
|
1324
|
+
if (!exceedsInlineCap && (effectiveThreshold <= 0 || totalBytes <= effectiveThreshold)) return result;
|
|
886
1325
|
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
1326
|
+
const artifactCapability = artifactCapabilityForContext(context);
|
|
1327
|
+
const saveFailure = (diagnostic: string): AgentToolResult => {
|
|
1328
|
+
const failurePreview = createArtifactFailurePreview(
|
|
1329
|
+
fullText,
|
|
1330
|
+
maxInlineBytes > 0 ? maxInlineBytes : undefined,
|
|
1331
|
+
diagnostic,
|
|
1332
|
+
);
|
|
1333
|
+
const outputLines = failurePreview.text.length > 0 ? failurePreview.text.split("\n").length : 0;
|
|
1334
|
+
const outputBytes = Buffer.byteLength(failurePreview.text, "utf-8");
|
|
1335
|
+
const failureTruncation: TruncationMeta = {
|
|
1336
|
+
...(existingTruncation ?? {}),
|
|
1337
|
+
direction: "tail",
|
|
1338
|
+
truncatedBy: "bytes",
|
|
1339
|
+
noticeOwner: undefined,
|
|
1340
|
+
totalLines: fullText.length > 0 ? fullText.split("\n").length : 0,
|
|
1341
|
+
totalBytes,
|
|
1342
|
+
outputLines,
|
|
1343
|
+
outputBytes,
|
|
1344
|
+
maxBytes: maxInlineBytes > 0 ? maxInlineBytes : undefined,
|
|
1345
|
+
shownRange: undefined,
|
|
1346
|
+
headRange: undefined,
|
|
1347
|
+
tailRange: undefined,
|
|
1348
|
+
nextOffset: undefined,
|
|
1349
|
+
artifactId: undefined,
|
|
1350
|
+
artifactVerified: false,
|
|
1351
|
+
sourceCaptureIncomplete: true,
|
|
1352
|
+
artifactFailureDiagnostic: diagnostic,
|
|
1353
|
+
};
|
|
1354
|
+
const content: (TextContent | ImageContent)[] = result.content.filter(block => block.type !== "text");
|
|
1355
|
+
content.push({ type: "text", text: failurePreview.text });
|
|
1356
|
+
return {
|
|
1357
|
+
...result,
|
|
1358
|
+
content,
|
|
1359
|
+
details: {
|
|
1360
|
+
...(result.details ?? {}),
|
|
1361
|
+
meta: { ...(existingMeta ?? {}), truncation: failureTruncation },
|
|
1362
|
+
},
|
|
1363
|
+
};
|
|
1364
|
+
};
|
|
1365
|
+
if (!artifactCapability) return saveFailure("artifact storage is unavailable");
|
|
1366
|
+
|
|
1367
|
+
// Save full output through the bounded, validating path. Failed threshold
|
|
1368
|
+
// spills preserve their diagnostic even when the absolute backstop is disabled.
|
|
1369
|
+
const saveOutcome = await saveInlineArtifactBounded(
|
|
1370
|
+
artifactCapability,
|
|
1371
|
+
() => artifactCapability.saveArtifact(fullText, toolName),
|
|
1372
|
+
artifactId => artifactCapability.getArtifactPath(artifactId),
|
|
1373
|
+
);
|
|
1374
|
+
if (saveOutcome.status !== "ok") return saveFailure(saveOutcome.diagnostic);
|
|
1375
|
+
|
|
1376
|
+
const artifactId = saveOutcome.artifactId;
|
|
1377
|
+
const newArtifactOmissions = genericArtifactOmittedBytes(fullText) ?? 0;
|
|
1378
|
+
const priorArtifactOmissions = isValidLossCounter(existingTruncation?.artifactTruncatedBytes)
|
|
1379
|
+
? (existingTruncation?.artifactTruncatedBytes ?? 0)
|
|
1380
|
+
: 0;
|
|
1381
|
+
const artifactTruncatedBytes = Math.max(newArtifactOmissions, priorArtifactOmissions) || undefined;
|
|
1382
|
+
const sourceTruncatedBytes = existingTruncation?.sourceTruncatedBytes;
|
|
1383
|
+
const columnDroppedBytes = existingTruncation?.columnDroppedBytes;
|
|
1384
|
+
const sourceCaptureIncomplete =
|
|
1385
|
+
existingTruncation?.sourceCaptureIncomplete || (columnDroppedBytes ?? 0) > 0 || undefined;
|
|
890
1386
|
|
|
891
1387
|
// Truncate: middle elision when a head budget is configured, otherwise tail-only.
|
|
892
1388
|
const useMiddle = headBytes > 0;
|
|
@@ -902,25 +1398,20 @@ async function spillLargeResultToArtifact(
|
|
|
902
1398
|
maxLines: tailLines,
|
|
903
1399
|
});
|
|
904
1400
|
|
|
905
|
-
// Replace text blocks with single truncated block, keep images
|
|
1401
|
+
// Replace text blocks with single truncated block, keep images.
|
|
906
1402
|
const newContent: (TextContent | ImageContent)[] = [];
|
|
907
1403
|
for (const block of result.content) {
|
|
908
|
-
if (block.type !== "text")
|
|
909
|
-
newContent.push(block);
|
|
910
|
-
}
|
|
1404
|
+
if (block.type !== "text") newContent.push(block);
|
|
911
1405
|
}
|
|
912
1406
|
newContent.push({ type: "text", text: truncated.content });
|
|
913
1407
|
|
|
914
|
-
// Build truncation meta
|
|
1408
|
+
// Build truncation meta.
|
|
915
1409
|
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
916
1410
|
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
917
1411
|
let truncationMeta: TruncationMeta;
|
|
918
1412
|
if (truncated.truncatedBy === "middle") {
|
|
919
1413
|
const elidedLines = truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines);
|
|
920
1414
|
const elidedBytes = truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes);
|
|
921
|
-
const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
|
|
922
|
-
const headLines = Math.ceil(keptLines / 2);
|
|
923
|
-
const tailLineCount = keptLines - headLines;
|
|
924
1415
|
truncationMeta = {
|
|
925
1416
|
direction: "middle",
|
|
926
1417
|
truncatedBy: "middle",
|
|
@@ -929,14 +1420,16 @@ async function spillLargeResultToArtifact(
|
|
|
929
1420
|
outputLines,
|
|
930
1421
|
outputBytes,
|
|
931
1422
|
maxBytes: headBytes + tailBytes,
|
|
932
|
-
headRange:
|
|
933
|
-
tailRange:
|
|
934
|
-
tailLineCount > 0
|
|
935
|
-
? { start: truncated.totalLines - tailLineCount + 1, end: truncated.totalLines }
|
|
936
|
-
: undefined,
|
|
1423
|
+
headRange: sourceCaptureIncomplete ? undefined : truncated.headRange,
|
|
1424
|
+
tailRange: sourceCaptureIncomplete ? undefined : truncated.tailRange,
|
|
937
1425
|
elidedLines,
|
|
938
1426
|
elidedBytes,
|
|
939
1427
|
artifactId,
|
|
1428
|
+
artifactVerified: true,
|
|
1429
|
+
artifactTruncatedBytes,
|
|
1430
|
+
sourceTruncatedBytes,
|
|
1431
|
+
sourceCaptureIncomplete,
|
|
1432
|
+
columnDroppedBytes,
|
|
940
1433
|
};
|
|
941
1434
|
} else {
|
|
942
1435
|
const shownStart = truncated.totalLines - outputLines + 1;
|
|
@@ -948,8 +1441,13 @@ async function spillLargeResultToArtifact(
|
|
|
948
1441
|
outputLines,
|
|
949
1442
|
outputBytes,
|
|
950
1443
|
maxBytes: tailBytes,
|
|
951
|
-
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
1444
|
+
shownRange: sourceCaptureIncomplete ? undefined : { start: shownStart, end: truncated.totalLines },
|
|
952
1445
|
artifactId,
|
|
1446
|
+
artifactVerified: true,
|
|
1447
|
+
artifactTruncatedBytes,
|
|
1448
|
+
sourceTruncatedBytes,
|
|
1449
|
+
sourceCaptureIncomplete,
|
|
1450
|
+
columnDroppedBytes,
|
|
953
1451
|
};
|
|
954
1452
|
}
|
|
955
1453
|
|
|
@@ -996,26 +1494,68 @@ async function enforceInlineResultBackstop(
|
|
|
996
1494
|
textParts.push(block.text);
|
|
997
1495
|
}
|
|
998
1496
|
}
|
|
999
|
-
if (textParts.length === 0) return result;
|
|
1000
1497
|
|
|
1001
1498
|
const renderedText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
1002
1499
|
const fullText = stripBodyOwnedTruncationFooter(renderedText, result.details);
|
|
1003
|
-
const totalBytes = Buffer.byteLength(
|
|
1004
|
-
if (totalBytes <= maxInlineBytes) return result;
|
|
1005
|
-
|
|
1006
|
-
// Reuse an existing artifact (avoid double-artifacting); otherwise save the
|
|
1007
|
-
// full output so the truncated view keeps a reference to the complete text.
|
|
1500
|
+
const totalBytes = Buffer.byteLength(renderedText, "utf-8");
|
|
1008
1501
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
1009
|
-
|
|
1502
|
+
const existingNoticeBytes = Buffer.byteLength(formatOutputNotice(existingMeta), "utf-8");
|
|
1503
|
+
if (totalBytes + existingNoticeBytes <= maxInlineBytes) return result;
|
|
1504
|
+
|
|
1505
|
+
// Reuse only storage-complete artifacts. Source-loss evidence remains bound
|
|
1506
|
+
// to the rendered body even when a new complete artifact is saved.
|
|
1507
|
+
const existingTruncation = existingMeta?.truncation;
|
|
1508
|
+
const previousArtifactId = existingTruncation?.artifactId;
|
|
1509
|
+
const previousArtifactIdValid = previousArtifactId === undefined || isValidArtifactId(previousArtifactId);
|
|
1510
|
+
const previousArtifactProven =
|
|
1511
|
+
previousArtifactId !== undefined && previousArtifactIdValid && existingTruncation?.artifactVerified === true;
|
|
1512
|
+
const existingArtifactStorageComplete =
|
|
1513
|
+
previousArtifactProven && existingTruncation?.artifactFailureDiagnostic === undefined;
|
|
1514
|
+
let artifactId = existingArtifactStorageComplete ? previousArtifactId : undefined;
|
|
1515
|
+
let artifactFailureDiagnostic = previousArtifactIdValid
|
|
1516
|
+
? existingTruncation?.artifactFailureDiagnostic
|
|
1517
|
+
: "existing metadata contained an invalid artifact id";
|
|
1518
|
+
let artifactTruncatedBytes = existingTruncation?.artifactTruncatedBytes;
|
|
1519
|
+
let artifactVerified = existingArtifactStorageComplete;
|
|
1520
|
+
let savedReplacementArtifact = false;
|
|
1010
1521
|
const artifactCapability = artifactCapabilityForContext(context);
|
|
1011
|
-
if (!artifactId && artifactCapability) {
|
|
1012
|
-
|
|
1522
|
+
if (!artifactId && artifactCapability && fullText.length > 0) {
|
|
1523
|
+
const saveOutcome = await saveInlineArtifactBounded(
|
|
1524
|
+
artifactCapability,
|
|
1525
|
+
() => artifactCapability.saveArtifact(fullText, toolName),
|
|
1526
|
+
artifactId => artifactCapability.getArtifactPath(artifactId),
|
|
1527
|
+
);
|
|
1528
|
+
if (saveOutcome.status === "ok") {
|
|
1529
|
+
artifactId = saveOutcome.artifactId;
|
|
1530
|
+
const newArtifactOmissions = genericArtifactOmittedBytes(fullText) ?? 0;
|
|
1531
|
+
const priorArtifactOmissions = artifactTruncatedBytes ?? 0;
|
|
1532
|
+
artifactTruncatedBytes = Math.max(newArtifactOmissions, priorArtifactOmissions) || undefined;
|
|
1533
|
+
artifactFailureDiagnostic = undefined;
|
|
1534
|
+
artifactVerified = true;
|
|
1535
|
+
savedReplacementArtifact = true;
|
|
1536
|
+
} else {
|
|
1537
|
+
artifactFailureDiagnostic = saveOutcome.diagnostic;
|
|
1538
|
+
}
|
|
1539
|
+
} else if (!artifactId && !artifactCapability) {
|
|
1540
|
+
artifactFailureDiagnostic ??= "artifact storage is unavailable";
|
|
1013
1541
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1542
|
+
const sourceTruncatedBytes = existingTruncation?.sourceTruncatedBytes;
|
|
1543
|
+
const columnDroppedBytes = existingTruncation?.columnDroppedBytes;
|
|
1544
|
+
const sourceCaptureIncomplete =
|
|
1545
|
+
existingTruncation?.sourceCaptureIncomplete ||
|
|
1546
|
+
(savedReplacementArtifact && (columnDroppedBytes ?? 0) > 0) ||
|
|
1547
|
+
undefined;
|
|
1548
|
+
const sourceCoordinatesIncomplete = (sourceTruncatedBytes ?? 0) > 0 || sourceCaptureIncomplete === true;
|
|
1549
|
+
|
|
1550
|
+
// Reserve the actual pre-existing notice plus room for the bounded truncation
|
|
1551
|
+
// evidence appended below. This prevents large diagnostics from bypassing the
|
|
1552
|
+
// final inline cap while retaining both head/tail content when space permits.
|
|
1017
1553
|
const MARKER_RESERVE = 256;
|
|
1018
|
-
const
|
|
1554
|
+
const NOTICE_RESERVE = Math.min(
|
|
1555
|
+
Math.max(0, maxInlineBytes - MARKER_RESERVE),
|
|
1556
|
+
Math.max(4096, existingNoticeBytes + 1024),
|
|
1557
|
+
);
|
|
1558
|
+
const budget = Math.max(0, maxInlineBytes - MARKER_RESERVE - NOTICE_RESERVE);
|
|
1019
1559
|
const useMiddle = headBytes > 0 && budget > 0;
|
|
1020
1560
|
let truncated = useMiddle
|
|
1021
1561
|
? truncateMiddle(fullText, {
|
|
@@ -1024,11 +1564,10 @@ async function enforceInlineResultBackstop(
|
|
|
1024
1564
|
maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
|
|
1025
1565
|
maxHeadLines: tailLines,
|
|
1026
1566
|
})
|
|
1027
|
-
: truncateTail(fullText, { maxBytes:
|
|
1567
|
+
: truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
|
|
1028
1568
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
|
|
1569
|
+
if (Buffer.byteLength(truncated.content, "utf-8") > budget) {
|
|
1570
|
+
truncated = truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
|
|
1032
1571
|
}
|
|
1033
1572
|
|
|
1034
1573
|
const newContent: (TextContent | ImageContent)[] = [];
|
|
@@ -1054,6 +1593,12 @@ async function enforceInlineResultBackstop(
|
|
|
1054
1593
|
elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
|
|
1055
1594
|
elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
|
|
1056
1595
|
artifactId,
|
|
1596
|
+
artifactVerified,
|
|
1597
|
+
artifactTruncatedBytes,
|
|
1598
|
+
sourceTruncatedBytes,
|
|
1599
|
+
sourceCaptureIncomplete,
|
|
1600
|
+
columnDroppedBytes,
|
|
1601
|
+
artifactFailureDiagnostic,
|
|
1057
1602
|
}
|
|
1058
1603
|
: {
|
|
1059
1604
|
direction: "tail",
|
|
@@ -1063,13 +1608,71 @@ async function enforceInlineResultBackstop(
|
|
|
1063
1608
|
outputLines,
|
|
1064
1609
|
outputBytes,
|
|
1065
1610
|
maxBytes: maxInlineBytes,
|
|
1066
|
-
shownRange:
|
|
1611
|
+
shownRange: sourceCoordinatesIncomplete
|
|
1612
|
+
? undefined
|
|
1613
|
+
: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
|
|
1067
1614
|
artifactId,
|
|
1615
|
+
artifactVerified,
|
|
1616
|
+
artifactTruncatedBytes,
|
|
1617
|
+
sourceTruncatedBytes,
|
|
1618
|
+
sourceCaptureIncomplete,
|
|
1619
|
+
columnDroppedBytes,
|
|
1620
|
+
artifactFailureDiagnostic,
|
|
1068
1621
|
};
|
|
1069
1622
|
|
|
1070
|
-
|
|
1623
|
+
let newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
1624
|
+
let finalText = truncated.content;
|
|
1625
|
+
let renderedBytes = Buffer.byteLength(finalText, "utf-8") + Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
|
|
1626
|
+
if (renderedBytes > maxInlineBytes && newMeta.diagnostics) {
|
|
1627
|
+
newMeta = { ...newMeta, diagnostics: undefined, diagnosticsOmitted: true };
|
|
1628
|
+
renderedBytes = Buffer.byteLength(finalText, "utf-8") + Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
|
|
1629
|
+
}
|
|
1630
|
+
if (renderedBytes > maxInlineBytes) {
|
|
1631
|
+
const compactTruncation = newMeta.truncation
|
|
1632
|
+
? {
|
|
1633
|
+
...newMeta.truncation,
|
|
1634
|
+
artifactFailureDiagnostic: newMeta.truncation.artifactFailureDiagnostic
|
|
1635
|
+
? truncateHeadBytes(newMeta.truncation.artifactFailureDiagnostic, 64).text
|
|
1636
|
+
: undefined,
|
|
1637
|
+
shownRange: undefined,
|
|
1638
|
+
headRange: undefined,
|
|
1639
|
+
tailRange: undefined,
|
|
1640
|
+
nextOffset: undefined,
|
|
1641
|
+
}
|
|
1642
|
+
: undefined;
|
|
1643
|
+
newMeta = { ...newMeta, truncation: compactTruncation, limits: undefined };
|
|
1644
|
+
const noticeBytes = Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
|
|
1645
|
+
const bodyBudget = Math.max(0, maxInlineBytes - noticeBytes);
|
|
1646
|
+
const finalTruncated = truncateTail(finalText, { maxBytes: bodyBudget, maxLines: tailLines });
|
|
1647
|
+
finalText = finalTruncated.content;
|
|
1648
|
+
if (newMeta.truncation) {
|
|
1649
|
+
newMeta = {
|
|
1650
|
+
...newMeta,
|
|
1651
|
+
truncation: {
|
|
1652
|
+
...newMeta.truncation,
|
|
1653
|
+
direction: "tail",
|
|
1654
|
+
truncatedBy: "bytes",
|
|
1655
|
+
outputBytes: Buffer.byteLength(finalText, "utf-8"),
|
|
1656
|
+
outputLines: finalText.length > 0 ? finalText.split("\n").length : 0,
|
|
1657
|
+
},
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const boundedContent = newContent.map(block => (block.type === "text" ? { ...block, text: finalText } : block));
|
|
1071
1662
|
const newDetails = { ...(result.details ?? {}), meta: newMeta };
|
|
1072
|
-
return { ...result, content:
|
|
1663
|
+
return { ...result, content: boundedContent, details: newDetails };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
export async function finalizeToolResultForDelivery(
|
|
1667
|
+
result: AgentToolResult,
|
|
1668
|
+
toolName: string,
|
|
1669
|
+
context: AgentToolContext | undefined,
|
|
1670
|
+
): Promise<AgentToolResult> {
|
|
1671
|
+
let finalized = await sanitizeArtifactIdEvidence(result, context);
|
|
1672
|
+
finalized = await spillLargeResultToArtifact(finalized, toolName, context);
|
|
1673
|
+
finalized = await enforceInlineResultBackstop(finalized, toolName, context);
|
|
1674
|
+
const meta: OutputMeta | undefined = finalized.details?.meta;
|
|
1675
|
+
return meta ? { ...finalized, content: appendOutputNotice(finalized.content, meta) } : finalized;
|
|
1073
1676
|
}
|
|
1074
1677
|
|
|
1075
1678
|
// =============================================================================
|
|
@@ -1087,24 +1690,11 @@ async function wrappedExecute(
|
|
|
1087
1690
|
const originalExecute = this[kUnwrappedExecute];
|
|
1088
1691
|
|
|
1089
1692
|
try {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
// Absolute inline-size backstop: catches oversized text the threshold spill
|
|
1096
|
-
// skipped (read exemption, tools with pre-existing partial artifact meta).
|
|
1097
|
-
result = await enforceInlineResultBackstop(result, this.name, context);
|
|
1098
|
-
|
|
1099
|
-
// Append notices from meta
|
|
1100
|
-
const meta: OutputMeta | undefined = result.details?.meta;
|
|
1101
|
-
if (meta) {
|
|
1102
|
-
return {
|
|
1103
|
-
...result,
|
|
1104
|
-
content: appendOutputNotice(result.content, meta),
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
return result;
|
|
1693
|
+
return await finalizeToolResultForDelivery(
|
|
1694
|
+
await originalExecute.call(this, toolCallId, params, signal, onUpdate, context),
|
|
1695
|
+
this.name,
|
|
1696
|
+
context,
|
|
1697
|
+
);
|
|
1108
1698
|
} catch (e) {
|
|
1109
1699
|
// Re-throw with formatted message so agent-loop sets isError flag
|
|
1110
1700
|
throw new Error(renderError(e));
|