pi-one-ui 0.1.1
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/LICENSE +21 -0
- package/README.md +106 -0
- package/extensions/app/config/renderer.ts +452 -0
- package/extensions/app/config/shell.ts +2484 -0
- package/extensions/app/ownership.ts +18 -0
- package/extensions/app/panel.ts +533 -0
- package/extensions/app/presets.ts +36 -0
- package/extensions/features/agent-summary/core.ts +194 -0
- package/extensions/features/agent-summary/index.ts +37 -0
- package/extensions/features/compact-thinking.ts +1262 -0
- package/extensions/features/context.ts +857 -0
- package/extensions/features/reference/index.ts +643 -0
- package/extensions/features/reference/session.ts +278 -0
- package/extensions/features/reference/subagent.ts +218 -0
- package/extensions/features/shell/aliases.ts +28 -0
- package/extensions/features/shell/flush-docked-bash.ts +39 -0
- package/extensions/features/shell/startup-header.ts +357 -0
- package/extensions/features/shell/working-message.ts +226 -0
- package/extensions/index.ts +52 -0
- package/extensions/shell/accent-rail-editor.ts +112 -0
- package/extensions/shell/accent-rail-layout-patch.ts +530 -0
- package/extensions/shell/completion-menu.ts +186 -0
- package/extensions/shell/editor-metadata-format.ts +287 -0
- package/extensions/shell/editor-transfer.ts +73 -0
- package/extensions/shell/extension-status.ts +102 -0
- package/extensions/shell/footer-format.ts +388 -0
- package/extensions/shell/footer-layout.ts +144 -0
- package/extensions/shell/footer.ts +1035 -0
- package/extensions/shell/format.ts +577 -0
- package/extensions/shell/git.ts +434 -0
- package/extensions/shell/icons.ts +261 -0
- package/extensions/shell/index.ts +1659 -0
- package/extensions/shell/interaction-summary.ts +1194 -0
- package/extensions/shell/live-context.ts +82 -0
- package/extensions/shell/minimalist-editor.ts +548 -0
- package/extensions/shell/package-version.ts +675 -0
- package/extensions/shell/project-refresh.ts +130 -0
- package/extensions/shell/project-state.ts +60 -0
- package/extensions/shell/prototype-patch-registry.ts +183 -0
- package/extensions/shell/runtime.ts +915 -0
- package/extensions/shell/selector-border.ts +109 -0
- package/extensions/shell/session-lifecycle.ts +60 -0
- package/extensions/shell/settings-command.ts +2144 -0
- package/extensions/shell/settings-previews.ts +137 -0
- package/extensions/shell/state.ts +83 -0
- package/extensions/shell/style.ts +480 -0
- package/extensions/shell/telemetry.ts +90 -0
- package/extensions/shell/ui.ts +1326 -0
- package/extensions/shell/user-message-osc.ts +279 -0
- package/extensions/shell/user-message-styles.ts +266 -0
- package/extensions/shell/user-message.ts +226 -0
- package/extensions/shell/working-line-messages.ts +24 -0
- package/extensions/shell/working-line-spinners.ts +54 -0
- package/extensions/shell/working-line.ts +1603 -0
- package/extensions/tools/ansi-color.ts +44 -0
- package/extensions/tools/ansi-text.ts +52 -0
- package/extensions/tools/component-tree.ts +37 -0
- package/extensions/tools/format.ts +28 -0
- package/extensions/tools/fullscreen-detect.ts +11 -0
- package/extensions/tools/patch-keys.ts +158 -0
- package/extensions/tools/sgr-mouse.ts +59 -0
- package/extensions/tools/tool-loading-icon.ts +20 -0
- package/extensions/tools/tool-result-sanitize.ts +27 -0
- package/extensions/transcript/index.ts +65 -0
- package/extensions/transcript/renderer/compact-mode.ts +1443 -0
- package/extensions/transcript/renderer/default-mode.ts +735 -0
- package/extensions/transcript/renderer/index.ts +286 -0
- package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
- package/extensions/transcript/renderer/mouse/hover.ts +175 -0
- package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
- package/extensions/transcript/renderer/mouse/layout.ts +207 -0
- package/extensions/transcript/renderer/mouse/packets.ts +96 -0
- package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
- package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
- package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
- package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
- package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
- package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
- package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
- package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
- package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
- package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
- package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
- package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
- package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
- package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
- package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
- package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
- package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
- package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
- package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
- package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
- package/extensions/transcript/renderer/tool/grouping.ts +619 -0
- package/extensions/transcript/renderer/tool/message-display.ts +183 -0
- package/extensions/transcript/renderer/tool/names.ts +195 -0
- package/extensions/transcript/renderer/tool/result.ts +842 -0
- package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
- package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
- package/package.json +58 -0
- package/themes/cc-dark.json +91 -0
- package/themes/cc-light.json +91 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
AssistantMessageEvent,
|
|
4
|
+
} from "@earendil-works/pi-ai";
|
|
5
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { ColorSource, ColorSpec } from "../app/config/shell.ts";
|
|
8
|
+
import { formatCount, formatElapsedDuration } from "./format.ts";
|
|
9
|
+
import { isSafeSgrStylePrefix } from "./style.ts";
|
|
10
|
+
import { renderWorkingLineHigh } from "./working-line.ts";
|
|
11
|
+
|
|
12
|
+
export const TURN_SUMMARY_ENTRY_TYPE = "zentui-turn-summary";
|
|
13
|
+
const TURN_SUMMARY_VERSION = 3;
|
|
14
|
+
const SGR_RESET = "\x1b[0m";
|
|
15
|
+
|
|
16
|
+
export type InteractionTokens = Readonly<{ input: number; output: number }>;
|
|
17
|
+
export type LiveTokenDisplay = Readonly<{
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
outputApproximate: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
export type ThoughtSnapshot = Readonly<{ durationMs: number; active: boolean }>;
|
|
23
|
+
|
|
24
|
+
type SummaryBase = {
|
|
25
|
+
durationMs: number;
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TurnSummaryDataV1 = SummaryBase & { version: 1 };
|
|
31
|
+
export type TurnSummaryDataV2 = SummaryBase & {
|
|
32
|
+
version: 2;
|
|
33
|
+
stylePrefix: string;
|
|
34
|
+
};
|
|
35
|
+
export type TurnSummaryDataV3 = SummaryBase & {
|
|
36
|
+
version: 3;
|
|
37
|
+
thoughtDurationMs: number;
|
|
38
|
+
stylePrefix: string;
|
|
39
|
+
};
|
|
40
|
+
export type TurnSummaryData =
|
|
41
|
+
| TurnSummaryDataV1
|
|
42
|
+
| TurnSummaryDataV2
|
|
43
|
+
| TurnSummaryDataV3;
|
|
44
|
+
export type TurnSummaryMetrics = SummaryBase & { thoughtDurationMs: number };
|
|
45
|
+
|
|
46
|
+
type Interval = { start: number; end: number };
|
|
47
|
+
type TaggedInterval = Interval & { sources: number[] };
|
|
48
|
+
type ThoughtCoverage = { intervals: TaggedInterval[]; incomplete: boolean };
|
|
49
|
+
type ThoughtBlock = { startedAt?: number; closed: boolean };
|
|
50
|
+
|
|
51
|
+
type ContentBlock = {
|
|
52
|
+
acceptedLength: number;
|
|
53
|
+
codePoints: number;
|
|
54
|
+
pendingHighSurrogate: boolean;
|
|
55
|
+
headHash: number;
|
|
56
|
+
headUnits: number;
|
|
57
|
+
tail: string;
|
|
58
|
+
frozen: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ResponseState = {
|
|
62
|
+
ordinal: number;
|
|
63
|
+
authorized: boolean;
|
|
64
|
+
responseIdKey?: string;
|
|
65
|
+
snapshot: InteractionTokens;
|
|
66
|
+
hasSnapshot: boolean;
|
|
67
|
+
thoughtBlocks: Map<number, ThoughtBlock>;
|
|
68
|
+
contentBlocks: Map<string, ContentBlock>;
|
|
69
|
+
generatedCodePoints: number;
|
|
70
|
+
outputAnchor: number;
|
|
71
|
+
outputAnchorCodePoints: number;
|
|
72
|
+
liveOutputFloor: number;
|
|
73
|
+
lastDisplay?: LiveTokenDisplay;
|
|
74
|
+
estimateIncomplete: boolean;
|
|
75
|
+
estimateEnabled: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type RunState = {
|
|
79
|
+
ordinal: number;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
ended: boolean;
|
|
82
|
+
committed: InteractionTokens;
|
|
83
|
+
responseOrdinal: number;
|
|
84
|
+
response?: ResponseState;
|
|
85
|
+
committedResponseIds: Set<string>;
|
|
86
|
+
lastClosedResponseId?: string;
|
|
87
|
+
lastClosedDisplay?: LiveTokenDisplay;
|
|
88
|
+
responseLessClosed: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type InteractionState = {
|
|
92
|
+
startedAt: number;
|
|
93
|
+
runOrdinal: number;
|
|
94
|
+
runs: RunState[];
|
|
95
|
+
activeRun?: RunState;
|
|
96
|
+
foldedCommitted: InteractionTokens;
|
|
97
|
+
thoughtCoverage: ThoughtCoverage;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type MetricsUpdateResult = {
|
|
101
|
+
displayTokens?: LiveTokenDisplay;
|
|
102
|
+
usageChanged: boolean;
|
|
103
|
+
thoughtChanged: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type MessageEndResult =
|
|
107
|
+
| {
|
|
108
|
+
status: "accepted";
|
|
109
|
+
tokens: InteractionTokens;
|
|
110
|
+
displayTokens: LiveTokenDisplay;
|
|
111
|
+
source: "final" | "last-snapshot" | "no-snapshot";
|
|
112
|
+
}
|
|
113
|
+
| { status: "duplicate" }
|
|
114
|
+
| { status: "rejected" };
|
|
115
|
+
|
|
116
|
+
export type SettlementResult = {
|
|
117
|
+
summary: TurnSummaryMetrics;
|
|
118
|
+
nextStartedAt?: number;
|
|
119
|
+
nextTokens?: LiveTokenDisplay;
|
|
120
|
+
nextThought?: ThoughtSnapshot;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type InteractionMetricsDiagnostics = Readonly<{
|
|
124
|
+
runs: number;
|
|
125
|
+
responseIds: number;
|
|
126
|
+
activeResponses: number;
|
|
127
|
+
contentBlocks: number;
|
|
128
|
+
thoughtBlocks: number;
|
|
129
|
+
thoughtIntervals: number;
|
|
130
|
+
maxContentTailUnits: number;
|
|
131
|
+
retainedContentUnits: number;
|
|
132
|
+
estimateIncomplete: boolean;
|
|
133
|
+
}>;
|
|
134
|
+
|
|
135
|
+
const MAX_CONTENT_BLOCKS = 128;
|
|
136
|
+
const MAX_CONTENT_INDEX = 65_535;
|
|
137
|
+
const MAX_DELTA_UNITS = 65_536;
|
|
138
|
+
const MAX_SNAPSHOT_UNITS = 1_048_576;
|
|
139
|
+
const MAX_GENERATED_CODE_POINTS = 4_194_304;
|
|
140
|
+
const MAX_ESTIMATED_RESPONSES = 128;
|
|
141
|
+
const MAX_RUNS = 32;
|
|
142
|
+
const MAX_RESPONSE_IDS = 256;
|
|
143
|
+
const MAX_RESPONSE_ID_UNITS = 128;
|
|
144
|
+
const MAX_THOUGHT_INTERVALS = 256;
|
|
145
|
+
const HEAD_UNITS = 64;
|
|
146
|
+
const TAIL_UNITS = 64;
|
|
147
|
+
const HASH_OFFSET = 0x811c9dc5;
|
|
148
|
+
|
|
149
|
+
function isNonnegativeSafeInteger(value: unknown): value is number {
|
|
150
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function safeAdd(left: number, right: number): number {
|
|
154
|
+
return Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function estimatedTokens(codePoints: number): number {
|
|
158
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(codePoints / 4));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function addTokens(
|
|
162
|
+
left: InteractionTokens,
|
|
163
|
+
right: InteractionTokens,
|
|
164
|
+
): InteractionTokens {
|
|
165
|
+
return {
|
|
166
|
+
input: safeAdd(left.input, right.input),
|
|
167
|
+
output: safeAdd(left.output, right.output),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type InteractionMessage = {
|
|
172
|
+
role?: string;
|
|
173
|
+
usage?: unknown;
|
|
174
|
+
responseId?: unknown;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function parseAssistantMessageTokens(
|
|
178
|
+
message: InteractionMessage,
|
|
179
|
+
): InteractionTokens | undefined {
|
|
180
|
+
if (message.role !== "assistant") return undefined;
|
|
181
|
+
const usage = (message as unknown as AssistantMessage).usage;
|
|
182
|
+
return isNonnegativeSafeInteger(usage?.input) &&
|
|
183
|
+
isNonnegativeSafeInteger(usage?.output)
|
|
184
|
+
? { input: usage.input, output: usage.output }
|
|
185
|
+
: undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function responseIdKey(message: InteractionMessage): string | undefined {
|
|
189
|
+
if (typeof message.responseId !== "string") return undefined;
|
|
190
|
+
const value = message.responseId;
|
|
191
|
+
if (value.length > MAX_RESPONSE_ID_UNITS) return undefined;
|
|
192
|
+
const normalized = value.trim();
|
|
193
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function safeTime(value: number): number {
|
|
197
|
+
if (!Number.isFinite(value)) return value > 0 ? Number.MAX_SAFE_INTEGER : 0;
|
|
198
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function durationMs(startedAt: number, now: number): number {
|
|
202
|
+
const elapsed = Math.floor(now - startedAt);
|
|
203
|
+
if (Number.isNaN(elapsed) || elapsed <= 0) return 0;
|
|
204
|
+
return Math.min(Number.MAX_SAFE_INTEGER, elapsed);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function unionIntervals(intervals: readonly Interval[]): Interval[] {
|
|
208
|
+
const sorted = intervals
|
|
209
|
+
.filter(({ start, end }) => end > start)
|
|
210
|
+
.map(({ start, end }) => ({ start: safeTime(start), end: safeTime(end) }))
|
|
211
|
+
.filter(({ start, end }) => end > start)
|
|
212
|
+
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
213
|
+
const union: Interval[] = [];
|
|
214
|
+
for (const interval of sorted) {
|
|
215
|
+
const previous = union.at(-1);
|
|
216
|
+
if (!previous || interval.start > previous.end) union.push({ ...interval });
|
|
217
|
+
else previous.end = Math.max(previous.end, interval.end);
|
|
218
|
+
}
|
|
219
|
+
return union;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function intervalDuration(intervals: readonly Interval[]): number {
|
|
223
|
+
return unionIntervals(intervals).reduce(
|
|
224
|
+
(total, interval) => safeAdd(total, interval.end - interval.start),
|
|
225
|
+
0,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sameSources(
|
|
230
|
+
left: readonly number[],
|
|
231
|
+
right: readonly number[],
|
|
232
|
+
): boolean {
|
|
233
|
+
return (
|
|
234
|
+
left.length === right.length &&
|
|
235
|
+
left.every((source, index) => source === right[index])
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeTaggedIntervals(
|
|
240
|
+
intervals: readonly TaggedInterval[],
|
|
241
|
+
): TaggedInterval[] {
|
|
242
|
+
const sanitized = intervals
|
|
243
|
+
.map(({ start, end, sources }) => ({
|
|
244
|
+
start: safeTime(start),
|
|
245
|
+
end: safeTime(end),
|
|
246
|
+
sources: [...new Set(sources)].sort((left, right) => left - right),
|
|
247
|
+
}))
|
|
248
|
+
.filter(({ start, end, sources }) => end > start && sources.length > 0);
|
|
249
|
+
const boundaries = [
|
|
250
|
+
...new Set(sanitized.flatMap(({ start, end }) => [start, end])),
|
|
251
|
+
].sort((left, right) => left - right);
|
|
252
|
+
const result: TaggedInterval[] = [];
|
|
253
|
+
for (let index = 0; index + 1 < boundaries.length; index++) {
|
|
254
|
+
const start = boundaries[index];
|
|
255
|
+
const end = boundaries[index + 1];
|
|
256
|
+
if (start === undefined || end === undefined || end <= start) continue;
|
|
257
|
+
const sources = [
|
|
258
|
+
...new Set(
|
|
259
|
+
sanitized
|
|
260
|
+
.filter((interval) => interval.start < end && interval.end > start)
|
|
261
|
+
.flatMap((interval) => interval.sources),
|
|
262
|
+
),
|
|
263
|
+
].sort((left, right) => left - right);
|
|
264
|
+
if (sources.length === 0) continue;
|
|
265
|
+
const previous = result.at(-1);
|
|
266
|
+
if (
|
|
267
|
+
previous &&
|
|
268
|
+
previous.end === start &&
|
|
269
|
+
sameSources(previous.sources, sources)
|
|
270
|
+
) {
|
|
271
|
+
previous.end = end;
|
|
272
|
+
} else result.push({ start, end, sources });
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function addThoughtInterval(
|
|
278
|
+
coverage: ThoughtCoverage,
|
|
279
|
+
interval: Interval,
|
|
280
|
+
source: number,
|
|
281
|
+
): boolean {
|
|
282
|
+
if (coverage.incomplete) return false;
|
|
283
|
+
const candidate = normalizeTaggedIntervals([
|
|
284
|
+
...coverage.intervals,
|
|
285
|
+
{ ...interval, sources: [source] },
|
|
286
|
+
]);
|
|
287
|
+
if (candidate.length > MAX_THOUGHT_INTERVALS) {
|
|
288
|
+
coverage.incomplete = true;
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
coverage.intervals = candidate;
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function subtractThoughtIntervalsWithinCap(
|
|
296
|
+
source: readonly Interval[],
|
|
297
|
+
removed: readonly Interval[],
|
|
298
|
+
): { intervals: Interval[]; incomplete: boolean } {
|
|
299
|
+
let result = unionIntervals(source);
|
|
300
|
+
for (const cut of unionIntervals(removed)) {
|
|
301
|
+
result = result.flatMap((part) => {
|
|
302
|
+
if (cut.end <= part.start || cut.start >= part.end) return [part];
|
|
303
|
+
return [
|
|
304
|
+
...(cut.start > part.start
|
|
305
|
+
? [{ start: part.start, end: cut.start }]
|
|
306
|
+
: []),
|
|
307
|
+
...(cut.end < part.end ? [{ start: cut.end, end: part.end }] : []),
|
|
308
|
+
];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
intervals: result.slice(0, MAX_THOUGHT_INTERVALS),
|
|
313
|
+
incomplete: result.length > MAX_THOUGHT_INTERVALS,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function cappedTaggedDifference(
|
|
318
|
+
source: readonly TaggedInterval[],
|
|
319
|
+
removed: readonly Interval[],
|
|
320
|
+
): { intervals: TaggedInterval[]; incomplete: boolean } {
|
|
321
|
+
let incomplete = false;
|
|
322
|
+
const fragments = source.flatMap((interval) => {
|
|
323
|
+
const difference = subtractThoughtIntervalsWithinCap([interval], removed);
|
|
324
|
+
incomplete ||= difference.incomplete;
|
|
325
|
+
return difference.intervals.map((fragment) => ({
|
|
326
|
+
...fragment,
|
|
327
|
+
sources: interval.sources,
|
|
328
|
+
}));
|
|
329
|
+
});
|
|
330
|
+
const normalized = normalizeTaggedIntervals(fragments);
|
|
331
|
+
return {
|
|
332
|
+
intervals: normalized.slice(0, MAX_THOUGHT_INTERVALS),
|
|
333
|
+
incomplete: incomplete || normalized.length > MAX_THOUGHT_INTERVALS,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hashUnits(
|
|
338
|
+
value: string,
|
|
339
|
+
start: number,
|
|
340
|
+
end: number,
|
|
341
|
+
seed = HASH_OFFSET,
|
|
342
|
+
): number {
|
|
343
|
+
let hash = seed;
|
|
344
|
+
for (let index = start; index < end; index++)
|
|
345
|
+
hash = Math.imul(hash ^ value.charCodeAt(index), 0x01000193) >>> 0;
|
|
346
|
+
return hash;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function appendCodePoints(
|
|
350
|
+
value: string,
|
|
351
|
+
pendingHighSurrogate: boolean,
|
|
352
|
+
): { codePoints: number; pendingHighSurrogate: boolean } {
|
|
353
|
+
let codePoints = 0;
|
|
354
|
+
let index = 0;
|
|
355
|
+
if (pendingHighSurrogate) {
|
|
356
|
+
if (value.length === 0) return { codePoints, pendingHighSurrogate };
|
|
357
|
+
const first = value.charCodeAt(0);
|
|
358
|
+
codePoints++;
|
|
359
|
+
if (first >= 0xdc00 && first <= 0xdfff) index = 1;
|
|
360
|
+
}
|
|
361
|
+
for (; index < value.length; index++) {
|
|
362
|
+
const unit = value.charCodeAt(index);
|
|
363
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
364
|
+
if (index + 1 === value.length)
|
|
365
|
+
return { codePoints, pendingHighSurrogate: true };
|
|
366
|
+
const next = value.charCodeAt(index + 1);
|
|
367
|
+
codePoints++;
|
|
368
|
+
if (next >= 0xdc00 && next <= 0xdfff) index++;
|
|
369
|
+
} else codePoints++;
|
|
370
|
+
}
|
|
371
|
+
return { codePoints, pendingHighSurrogate: false };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function contentFingerprintMatches(
|
|
375
|
+
block: ContentBlock,
|
|
376
|
+
snapshot: string,
|
|
377
|
+
): boolean {
|
|
378
|
+
if (block.headUnits > snapshot.length) return false;
|
|
379
|
+
if (hashUnits(snapshot, 0, block.headUnits) !== block.headHash) return false;
|
|
380
|
+
const tailStart = block.acceptedLength - block.tail.length;
|
|
381
|
+
return snapshot.slice(tailStart, block.acceptedLength) === block.tail;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
type ValidatedEvent = {
|
|
385
|
+
type:
|
|
386
|
+
| "text_delta"
|
|
387
|
+
| "thinking_delta"
|
|
388
|
+
| "toolcall_delta"
|
|
389
|
+
| "thinking_start"
|
|
390
|
+
| "thinking_end";
|
|
391
|
+
contentIndex: number;
|
|
392
|
+
delta?: string;
|
|
393
|
+
cumulative?: string;
|
|
394
|
+
oversizedDelta: boolean;
|
|
395
|
+
oversizedSnapshot: boolean;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
function validateEvent(
|
|
399
|
+
event: AssistantMessageEvent | undefined,
|
|
400
|
+
): ValidatedEvent | undefined | false {
|
|
401
|
+
if (!event) return undefined;
|
|
402
|
+
if (
|
|
403
|
+
event.type !== "text_delta" &&
|
|
404
|
+
event.type !== "thinking_delta" &&
|
|
405
|
+
event.type !== "toolcall_delta" &&
|
|
406
|
+
event.type !== "thinking_start" &&
|
|
407
|
+
event.type !== "thinking_end"
|
|
408
|
+
)
|
|
409
|
+
return undefined;
|
|
410
|
+
const record = event as unknown as Record<string, unknown>;
|
|
411
|
+
if (
|
|
412
|
+
!Number.isSafeInteger(record.contentIndex) ||
|
|
413
|
+
(record.contentIndex as number) < 0 ||
|
|
414
|
+
(record.contentIndex as number) > MAX_CONTENT_INDEX
|
|
415
|
+
)
|
|
416
|
+
return false;
|
|
417
|
+
const partial = record.partial;
|
|
418
|
+
if (!partial || typeof partial !== "object" || Array.isArray(partial))
|
|
419
|
+
return false;
|
|
420
|
+
const content = (partial as Record<string, unknown>).content;
|
|
421
|
+
if (!Array.isArray(content)) return false;
|
|
422
|
+
const contentIndex = record.contentIndex as number;
|
|
423
|
+
const isDelta =
|
|
424
|
+
event.type === "text_delta" ||
|
|
425
|
+
event.type === "thinking_delta" ||
|
|
426
|
+
event.type === "toolcall_delta";
|
|
427
|
+
if (isDelta && typeof record.delta !== "string") return false;
|
|
428
|
+
const expectedType =
|
|
429
|
+
event.type === "text_delta"
|
|
430
|
+
? "text"
|
|
431
|
+
: event.type === "toolcall_delta"
|
|
432
|
+
? "toolCall"
|
|
433
|
+
: "thinking";
|
|
434
|
+
let cumulative: string | undefined;
|
|
435
|
+
if (event.type === "toolcall_delta" && !(contentIndex in content))
|
|
436
|
+
return false;
|
|
437
|
+
if (contentIndex in content) {
|
|
438
|
+
const item = content[contentIndex];
|
|
439
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
440
|
+
const itemRecord = item as Record<string, unknown>;
|
|
441
|
+
if (itemRecord.type !== expectedType) return false;
|
|
442
|
+
if (expectedType === "toolCall") {
|
|
443
|
+
if (
|
|
444
|
+
typeof itemRecord.id !== "string" ||
|
|
445
|
+
typeof itemRecord.name !== "string" ||
|
|
446
|
+
!itemRecord.arguments ||
|
|
447
|
+
typeof itemRecord.arguments !== "object" ||
|
|
448
|
+
Array.isArray(itemRecord.arguments)
|
|
449
|
+
)
|
|
450
|
+
return false;
|
|
451
|
+
} else {
|
|
452
|
+
const field = expectedType === "text" ? "text" : "thinking";
|
|
453
|
+
if (typeof itemRecord[field] !== "string") return false;
|
|
454
|
+
cumulative = itemRecord[field] as string;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const delta = isDelta ? (record.delta as string) : undefined;
|
|
458
|
+
return {
|
|
459
|
+
type: event.type,
|
|
460
|
+
contentIndex,
|
|
461
|
+
delta,
|
|
462
|
+
cumulative,
|
|
463
|
+
oversizedDelta: (delta?.length ?? 0) > MAX_DELTA_UNITS,
|
|
464
|
+
oversizedSnapshot: (cumulative?.length ?? 0) > MAX_SNAPSHOT_UNITS,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Owns provider-usage snapshots and thought-time partitioning for one settled interaction. */
|
|
469
|
+
export class InteractionMetricsTracker {
|
|
470
|
+
private interaction?: InteractionState;
|
|
471
|
+
|
|
472
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
473
|
+
|
|
474
|
+
agentStart(now = this.now()): { interactionStarted: boolean } {
|
|
475
|
+
const at = safeTime(now);
|
|
476
|
+
const interactionStarted = this.interaction === undefined;
|
|
477
|
+
if (!this.interaction) {
|
|
478
|
+
this.interaction = {
|
|
479
|
+
startedAt: at,
|
|
480
|
+
runOrdinal: 0,
|
|
481
|
+
runs: [],
|
|
482
|
+
foldedCommitted: { input: 0, output: 0 },
|
|
483
|
+
thoughtCoverage: { intervals: [], incomplete: false },
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const previous = this.interaction.activeRun;
|
|
487
|
+
if (previous && !previous.ended) {
|
|
488
|
+
if (previous.response) {
|
|
489
|
+
this.commitSnapshot(previous, previous.response);
|
|
490
|
+
this.closeResponse(previous, previous.response, at);
|
|
491
|
+
}
|
|
492
|
+
previous.ended = true;
|
|
493
|
+
}
|
|
494
|
+
this.makeRunCapacity(this.interaction);
|
|
495
|
+
const ordinal = safeAdd(this.interaction.runOrdinal, 1);
|
|
496
|
+
this.interaction.runOrdinal = ordinal;
|
|
497
|
+
const run: RunState = {
|
|
498
|
+
ordinal,
|
|
499
|
+
startedAt: at,
|
|
500
|
+
ended: false,
|
|
501
|
+
committed: { input: 0, output: 0 },
|
|
502
|
+
responseOrdinal: 0,
|
|
503
|
+
committedResponseIds: new Set(),
|
|
504
|
+
responseLessClosed: false,
|
|
505
|
+
};
|
|
506
|
+
this.interaction.runs.push(run);
|
|
507
|
+
this.interaction.activeRun = run;
|
|
508
|
+
return { interactionStarted };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
turnStart(now = this.now()): void {
|
|
512
|
+
const run = this.activeRun();
|
|
513
|
+
if (!run) return;
|
|
514
|
+
if (run.response) {
|
|
515
|
+
this.commitSnapshot(run, run.response);
|
|
516
|
+
this.closeResponse(run, run.response, now);
|
|
517
|
+
}
|
|
518
|
+
this.openResponse(run);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
messageUpdate(
|
|
522
|
+
message: InteractionMessage,
|
|
523
|
+
event?: AssistantMessageEvent,
|
|
524
|
+
now = this.now(),
|
|
525
|
+
): MetricsUpdateResult {
|
|
526
|
+
const unchanged: MetricsUpdateResult = {
|
|
527
|
+
displayTokens: this.currentDisplayTokens(),
|
|
528
|
+
usageChanged: false,
|
|
529
|
+
thoughtChanged: false,
|
|
530
|
+
};
|
|
531
|
+
const run = this.activeRun();
|
|
532
|
+
if (!run || message.role !== "assistant") return unchanged;
|
|
533
|
+
const validated = validateEvent(event);
|
|
534
|
+
if (validated === false) return unchanged;
|
|
535
|
+
const tokens = parseAssistantMessageTokens(message);
|
|
536
|
+
if (!tokens && !validated) return unchanged;
|
|
537
|
+
const response = this.responseForUpdate(run, message);
|
|
538
|
+
if (!response) return unchanged;
|
|
539
|
+
let thoughtChanged = false;
|
|
540
|
+
let contentChanged = false;
|
|
541
|
+
if (validated) {
|
|
542
|
+
if (validated.type.startsWith("thinking"))
|
|
543
|
+
thoughtChanged = this.applyThoughtEvent(run, response, validated, now);
|
|
544
|
+
if (
|
|
545
|
+
validated.type === "text_delta" ||
|
|
546
|
+
validated.type === "thinking_delta" ||
|
|
547
|
+
validated.type === "toolcall_delta"
|
|
548
|
+
)
|
|
549
|
+
contentChanged = this.applyContentEvent(response, validated);
|
|
550
|
+
}
|
|
551
|
+
const usageChanged = tokens ? this.applyLiveUsage(response, tokens) : false;
|
|
552
|
+
const display = this.liveDisplay(response);
|
|
553
|
+
const displayChanged = !this.sameDisplay(response.lastDisplay, display);
|
|
554
|
+
response.lastDisplay = display;
|
|
555
|
+
return {
|
|
556
|
+
displayTokens: display,
|
|
557
|
+
usageChanged: usageChanged || contentChanged || displayChanged,
|
|
558
|
+
thoughtChanged,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
messageEnd(message: InteractionMessage, now = this.now()): MessageEndResult {
|
|
563
|
+
const run = this.activeRun();
|
|
564
|
+
if (!run || message.role !== "assistant") return { status: "rejected" };
|
|
565
|
+
const tokens = parseAssistantMessageTokens(message);
|
|
566
|
+
const key = responseIdKey(message);
|
|
567
|
+
const response = run.response;
|
|
568
|
+
if (!response?.authorized) return { status: "rejected" };
|
|
569
|
+
if (key && this.isDuplicateId(run, key)) return { status: "duplicate" };
|
|
570
|
+
if (response.responseIdKey && key && response.responseIdKey !== key)
|
|
571
|
+
return { status: "rejected" };
|
|
572
|
+
if (!response.responseIdKey && key) response.responseIdKey = key;
|
|
573
|
+
const preCloseDisplay = this.liveDisplay(response);
|
|
574
|
+
let source: "final" | "last-snapshot" | "no-snapshot";
|
|
575
|
+
if (tokens) {
|
|
576
|
+
response.snapshot = tokens;
|
|
577
|
+
response.hasSnapshot = true;
|
|
578
|
+
source = "final";
|
|
579
|
+
} else source = response.hasSnapshot ? "last-snapshot" : "no-snapshot";
|
|
580
|
+
this.commitSnapshot(run, response);
|
|
581
|
+
const committed = this.totalTokens();
|
|
582
|
+
const preserveEstimate =
|
|
583
|
+
source !== "final" && response.lastDisplay?.outputApproximate === true;
|
|
584
|
+
const displayTokens = preserveEstimate
|
|
585
|
+
? { ...preCloseDisplay, outputApproximate: true }
|
|
586
|
+
: { ...committed, outputApproximate: false };
|
|
587
|
+
run.lastClosedDisplay = preserveEstimate ? displayTokens : undefined;
|
|
588
|
+
this.closeResponse(run, response, now);
|
|
589
|
+
return { status: "accepted", tokens: committed, displayTokens, source };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
agentEnd(now = this.now()): void {
|
|
593
|
+
const interaction = this.interaction;
|
|
594
|
+
const run = interaction?.activeRun;
|
|
595
|
+
if (!interaction || !run) return;
|
|
596
|
+
if (run.response) {
|
|
597
|
+
this.commitSnapshot(run, run.response);
|
|
598
|
+
this.closeResponse(run, run.response, now);
|
|
599
|
+
}
|
|
600
|
+
run.ended = true;
|
|
601
|
+
interaction.activeRun = undefined;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
currentTokens(): InteractionTokens | undefined {
|
|
605
|
+
return this.interaction ? this.totalTokens() : undefined;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
currentDisplayTokens(): LiveTokenDisplay | undefined {
|
|
609
|
+
if (!this.interaction) return undefined;
|
|
610
|
+
const run = this.interaction.activeRun;
|
|
611
|
+
if (run?.response) return this.liveDisplay(run.response);
|
|
612
|
+
return (
|
|
613
|
+
run?.lastClosedDisplay ?? {
|
|
614
|
+
...this.totalTokens(),
|
|
615
|
+
outputApproximate: false,
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
currentThought(now = this.now()): ThoughtSnapshot | undefined {
|
|
621
|
+
if (!this.interaction) return undefined;
|
|
622
|
+
return this.thoughtSnapshot(this.interaction, now);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
settle(idle: boolean, now = this.now()): SettlementResult | undefined {
|
|
626
|
+
const interaction = this.interaction;
|
|
627
|
+
if (!interaction) return undefined;
|
|
628
|
+
const at = safeTime(now);
|
|
629
|
+
const splitIndex = idle
|
|
630
|
+
? interaction.runs.length
|
|
631
|
+
: interaction.runs.findIndex((run) => !run.ended);
|
|
632
|
+
const settledCount = splitIndex < 0 ? interaction.runs.length : splitIndex;
|
|
633
|
+
const hasFolded =
|
|
634
|
+
interaction.foldedCommitted.input > 0 ||
|
|
635
|
+
interaction.foldedCommitted.output > 0 ||
|
|
636
|
+
interaction.thoughtCoverage.intervals.some((interval) =>
|
|
637
|
+
interval.sources.includes(0),
|
|
638
|
+
);
|
|
639
|
+
if (settledCount === 0 && !hasFolded) return undefined;
|
|
640
|
+
const settledRuns = interaction.runs.slice(0, settledCount);
|
|
641
|
+
for (const run of interaction.runs) this.retainOpenThought(run, at);
|
|
642
|
+
for (const run of settledRuns) {
|
|
643
|
+
if (run.response) this.closeResponse(run, run.response, at);
|
|
644
|
+
}
|
|
645
|
+
const settledSources = new Set([
|
|
646
|
+
0,
|
|
647
|
+
...settledRuns.map((run) => run.ordinal),
|
|
648
|
+
]);
|
|
649
|
+
const settledIntervals = interaction.thoughtCoverage.intervals.filter(
|
|
650
|
+
(interval) =>
|
|
651
|
+
interval.sources.some((source) => settledSources.has(source)),
|
|
652
|
+
);
|
|
653
|
+
let tokens = interaction.foldedCommitted;
|
|
654
|
+
for (const run of settledRuns)
|
|
655
|
+
tokens = addTokens(tokens, this.runTokens(run));
|
|
656
|
+
const summary: TurnSummaryMetrics = {
|
|
657
|
+
durationMs: durationMs(interaction.startedAt, now),
|
|
658
|
+
thoughtDurationMs: intervalDuration(settledIntervals),
|
|
659
|
+
input: tokens.input,
|
|
660
|
+
output: tokens.output,
|
|
661
|
+
};
|
|
662
|
+
const remaining = interaction.runs.slice(settledCount);
|
|
663
|
+
if (remaining.length === 0) {
|
|
664
|
+
this.interaction = undefined;
|
|
665
|
+
return { summary };
|
|
666
|
+
}
|
|
667
|
+
const remainingSources = new Set(remaining.map((run) => run.ordinal));
|
|
668
|
+
const remainingCoverage = interaction.thoughtCoverage.intervals
|
|
669
|
+
.map((interval) => ({
|
|
670
|
+
...interval,
|
|
671
|
+
sources: interval.sources.filter((source) =>
|
|
672
|
+
remainingSources.has(source),
|
|
673
|
+
),
|
|
674
|
+
}))
|
|
675
|
+
.filter((interval) => interval.sources.length > 0);
|
|
676
|
+
const difference = cappedTaggedDifference(
|
|
677
|
+
remainingCoverage,
|
|
678
|
+
settledIntervals,
|
|
679
|
+
);
|
|
680
|
+
for (const run of remaining) {
|
|
681
|
+
for (const block of run.response?.thoughtBlocks.values() ?? []) {
|
|
682
|
+
if (block.startedAt !== undefined && !block.closed)
|
|
683
|
+
block.startedAt = at;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const nextStartedAt = remaining[0]?.startedAt ?? at;
|
|
687
|
+
this.interaction = {
|
|
688
|
+
startedAt: nextStartedAt,
|
|
689
|
+
runOrdinal: interaction.runOrdinal,
|
|
690
|
+
runs: remaining,
|
|
691
|
+
activeRun: remaining.find((run) => !run.ended),
|
|
692
|
+
foldedCommitted: { input: 0, output: 0 },
|
|
693
|
+
thoughtCoverage: {
|
|
694
|
+
intervals: difference.intervals,
|
|
695
|
+
incomplete:
|
|
696
|
+
interaction.thoughtCoverage.incomplete || difference.incomplete,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
return {
|
|
700
|
+
summary,
|
|
701
|
+
nextStartedAt,
|
|
702
|
+
nextTokens: this.currentDisplayTokens(),
|
|
703
|
+
nextThought: this.currentThought(at),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
shutdown(): void {
|
|
708
|
+
this.interaction = undefined;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
diagnostics(): InteractionMetricsDiagnostics {
|
|
712
|
+
const interaction = this.interaction;
|
|
713
|
+
const responses =
|
|
714
|
+
interaction?.runs.flatMap((run) =>
|
|
715
|
+
run.response ? [run.response] : [],
|
|
716
|
+
) ?? [];
|
|
717
|
+
const blocks = responses.flatMap((response) => [
|
|
718
|
+
...response.contentBlocks.values(),
|
|
719
|
+
]);
|
|
720
|
+
return {
|
|
721
|
+
runs: interaction?.runs.length ?? 0,
|
|
722
|
+
responseIds:
|
|
723
|
+
interaction?.runs.reduce(
|
|
724
|
+
(total, run) => total + run.committedResponseIds.size,
|
|
725
|
+
0,
|
|
726
|
+
) ?? 0,
|
|
727
|
+
activeResponses: responses.length,
|
|
728
|
+
contentBlocks: blocks.length,
|
|
729
|
+
thoughtBlocks: responses.reduce(
|
|
730
|
+
(total, response) => total + response.thoughtBlocks.size,
|
|
731
|
+
0,
|
|
732
|
+
),
|
|
733
|
+
thoughtIntervals: interaction?.thoughtCoverage.intervals.length ?? 0,
|
|
734
|
+
maxContentTailUnits: blocks.reduce(
|
|
735
|
+
(maximum, block) => Math.max(maximum, block.tail.length),
|
|
736
|
+
0,
|
|
737
|
+
),
|
|
738
|
+
retainedContentUnits: blocks.reduce(
|
|
739
|
+
(total, block) => total + block.tail.length,
|
|
740
|
+
0,
|
|
741
|
+
),
|
|
742
|
+
estimateIncomplete: responses.some(
|
|
743
|
+
(response) => response.estimateIncomplete,
|
|
744
|
+
),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private activeRun(): RunState | undefined {
|
|
749
|
+
const run = this.interaction?.activeRun;
|
|
750
|
+
return run && !run.ended ? run : undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private makeRunCapacity(interaction: InteractionState): void {
|
|
754
|
+
if (interaction.runs.length < MAX_RUNS) return;
|
|
755
|
+
const index = interaction.runs.findIndex((run) => run.ended);
|
|
756
|
+
if (index < 0) return;
|
|
757
|
+
const [run] = interaction.runs.splice(index, 1);
|
|
758
|
+
if (!run) return;
|
|
759
|
+
interaction.foldedCommitted = addTokens(
|
|
760
|
+
interaction.foldedCommitted,
|
|
761
|
+
this.runTokens(run),
|
|
762
|
+
);
|
|
763
|
+
interaction.thoughtCoverage.intervals = normalizeTaggedIntervals(
|
|
764
|
+
interaction.thoughtCoverage.intervals.map((interval) => ({
|
|
765
|
+
...interval,
|
|
766
|
+
sources: interval.sources.map((source) =>
|
|
767
|
+
source === run.ordinal ? 0 : source,
|
|
768
|
+
),
|
|
769
|
+
})),
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private openResponse(run: RunState, key?: string): ResponseState {
|
|
774
|
+
run.responseOrdinal = safeAdd(run.responseOrdinal, 1);
|
|
775
|
+
run.lastClosedDisplay = undefined;
|
|
776
|
+
const estimateEnabled =
|
|
777
|
+
run.ordinal <= MAX_RUNS && run.responseOrdinal <= MAX_ESTIMATED_RESPONSES;
|
|
778
|
+
const response: ResponseState = {
|
|
779
|
+
ordinal: run.responseOrdinal,
|
|
780
|
+
authorized: true,
|
|
781
|
+
responseIdKey: key,
|
|
782
|
+
snapshot: { input: 0, output: 0 },
|
|
783
|
+
hasSnapshot: false,
|
|
784
|
+
thoughtBlocks: new Map(),
|
|
785
|
+
contentBlocks: new Map(),
|
|
786
|
+
generatedCodePoints: 0,
|
|
787
|
+
outputAnchor: 0,
|
|
788
|
+
outputAnchorCodePoints: 0,
|
|
789
|
+
liveOutputFloor: 0,
|
|
790
|
+
estimateIncomplete: !estimateEnabled,
|
|
791
|
+
estimateEnabled,
|
|
792
|
+
};
|
|
793
|
+
run.response = response;
|
|
794
|
+
run.responseLessClosed = false;
|
|
795
|
+
return response;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private responseForUpdate(
|
|
799
|
+
run: RunState,
|
|
800
|
+
message: InteractionMessage,
|
|
801
|
+
): ResponseState | undefined {
|
|
802
|
+
const key = responseIdKey(message);
|
|
803
|
+
const response = run.response;
|
|
804
|
+
if (!response?.authorized || (key && this.isDuplicateId(run, key)))
|
|
805
|
+
return undefined;
|
|
806
|
+
if (response.responseIdKey && key && response.responseIdKey !== key)
|
|
807
|
+
return undefined;
|
|
808
|
+
if (!response.responseIdKey && key) response.responseIdKey = key;
|
|
809
|
+
return response;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private isDuplicateId(run: RunState, key: string): boolean {
|
|
813
|
+
return (
|
|
814
|
+
run.committedResponseIds.has(key) || run.lastClosedResponseId === key
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private applyContentEvent(
|
|
819
|
+
response: ResponseState,
|
|
820
|
+
event: ValidatedEvent,
|
|
821
|
+
): boolean {
|
|
822
|
+
if (!response.estimateEnabled) {
|
|
823
|
+
response.estimateIncomplete = true;
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (event.oversizedDelta || event.oversizedSnapshot) {
|
|
827
|
+
response.estimateIncomplete = true;
|
|
828
|
+
const existing = response.contentBlocks.get(
|
|
829
|
+
`${event.type === "text_delta" ? "text" : event.type === "toolcall_delta" ? "toolcall" : "thinking"}:${event.contentIndex}`,
|
|
830
|
+
);
|
|
831
|
+
if (existing) existing.frozen = true;
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const contentType =
|
|
835
|
+
event.type === "text_delta"
|
|
836
|
+
? "text"
|
|
837
|
+
: event.type === "toolcall_delta"
|
|
838
|
+
? "toolcall"
|
|
839
|
+
: "thinking";
|
|
840
|
+
const key = `${contentType}:${event.contentIndex}`;
|
|
841
|
+
let block = response.contentBlocks.get(key);
|
|
842
|
+
if (!block) {
|
|
843
|
+
if (response.contentBlocks.size >= MAX_CONTENT_BLOCKS) {
|
|
844
|
+
response.estimateIncomplete = true;
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
block = {
|
|
848
|
+
acceptedLength: 0,
|
|
849
|
+
codePoints: 0,
|
|
850
|
+
pendingHighSurrogate: false,
|
|
851
|
+
headHash: HASH_OFFSET,
|
|
852
|
+
headUnits: 0,
|
|
853
|
+
tail: "",
|
|
854
|
+
frozen: false,
|
|
855
|
+
};
|
|
856
|
+
response.contentBlocks.set(key, block);
|
|
857
|
+
}
|
|
858
|
+
if (block.frozen) return false;
|
|
859
|
+
let suffix = event.delta ?? "";
|
|
860
|
+
if (event.cumulative !== undefined) {
|
|
861
|
+
const snapshot = event.cumulative;
|
|
862
|
+
if (snapshot.length < block.acceptedLength) return false;
|
|
863
|
+
if (!contentFingerprintMatches(block, snapshot)) {
|
|
864
|
+
block.frozen = true;
|
|
865
|
+
response.estimateIncomplete = true;
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
if (snapshot.length === block.acceptedLength) return false;
|
|
869
|
+
if (snapshot.length - block.acceptedLength > MAX_DELTA_UNITS) {
|
|
870
|
+
block.frozen = true;
|
|
871
|
+
response.estimateIncomplete = true;
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
suffix = snapshot.slice(block.acceptedLength);
|
|
875
|
+
}
|
|
876
|
+
if (suffix.length === 0) return false;
|
|
877
|
+
if (block.acceptedLength + suffix.length > MAX_SNAPSHOT_UNITS) {
|
|
878
|
+
block.frozen = true;
|
|
879
|
+
response.estimateIncomplete = true;
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
const counted = appendCodePoints(suffix, block.pendingHighSurrogate);
|
|
883
|
+
if (
|
|
884
|
+
response.generatedCodePoints + counted.codePoints >
|
|
885
|
+
MAX_GENERATED_CODE_POINTS
|
|
886
|
+
) {
|
|
887
|
+
block.frozen = true;
|
|
888
|
+
response.estimateIncomplete = true;
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
if (block.headUnits < HEAD_UNITS) {
|
|
892
|
+
const taken = Math.min(HEAD_UNITS - block.headUnits, suffix.length);
|
|
893
|
+
block.headHash = hashUnits(suffix, 0, taken, block.headHash);
|
|
894
|
+
block.headUnits += taken;
|
|
895
|
+
}
|
|
896
|
+
block.tail =
|
|
897
|
+
suffix.length >= TAIL_UNITS
|
|
898
|
+
? suffix.slice(-TAIL_UNITS)
|
|
899
|
+
: `${block.tail}${suffix}`.slice(-TAIL_UNITS);
|
|
900
|
+
block.acceptedLength += suffix.length;
|
|
901
|
+
block.codePoints += counted.codePoints;
|
|
902
|
+
block.pendingHighSurrogate = counted.pendingHighSurrogate;
|
|
903
|
+
response.generatedCodePoints += counted.codePoints;
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private applyThoughtEvent(
|
|
908
|
+
run: RunState,
|
|
909
|
+
response: ResponseState,
|
|
910
|
+
event: ValidatedEvent,
|
|
911
|
+
now: number,
|
|
912
|
+
): boolean {
|
|
913
|
+
const at = safeTime(now);
|
|
914
|
+
let block = response.thoughtBlocks.get(event.contentIndex);
|
|
915
|
+
if (!block) {
|
|
916
|
+
if (response.thoughtBlocks.size >= MAX_THOUGHT_INTERVALS) return false;
|
|
917
|
+
block = { closed: false };
|
|
918
|
+
response.thoughtBlocks.set(event.contentIndex, block);
|
|
919
|
+
}
|
|
920
|
+
if (event.type === "thinking_end") {
|
|
921
|
+
if (block.closed || block.startedAt === undefined) {
|
|
922
|
+
block.closed = true;
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
this.closeBlock(run, block, at);
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
if (block.closed || block.startedAt !== undefined) return false;
|
|
929
|
+
block.startedAt = at;
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private closeBlock(run: RunState, block: ThoughtBlock, now: number): void {
|
|
934
|
+
if (block.closed) return;
|
|
935
|
+
block.closed = true;
|
|
936
|
+
if (block.startedAt === undefined) return;
|
|
937
|
+
const end = safeTime(now);
|
|
938
|
+
if (end <= block.startedAt || !this.interaction) return;
|
|
939
|
+
addThoughtInterval(
|
|
940
|
+
this.interaction.thoughtCoverage,
|
|
941
|
+
{ start: block.startedAt, end },
|
|
942
|
+
run.ordinal,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private closeResponse(
|
|
947
|
+
run: RunState,
|
|
948
|
+
response: ResponseState,
|
|
949
|
+
now: number,
|
|
950
|
+
): void {
|
|
951
|
+
if (run.response !== response) return;
|
|
952
|
+
for (const block of response.thoughtBlocks.values())
|
|
953
|
+
this.closeBlock(run, block, now);
|
|
954
|
+
if (response.responseIdKey) {
|
|
955
|
+
if (run.committedResponseIds.size < MAX_RESPONSE_IDS)
|
|
956
|
+
run.committedResponseIds.add(response.responseIdKey);
|
|
957
|
+
run.lastClosedResponseId = response.responseIdKey;
|
|
958
|
+
} else run.responseLessClosed = true;
|
|
959
|
+
response.authorized = false;
|
|
960
|
+
response.contentBlocks.clear();
|
|
961
|
+
response.thoughtBlocks.clear();
|
|
962
|
+
run.response = undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private commitSnapshot(run: RunState, response: ResponseState): void {
|
|
966
|
+
if (!response.hasSnapshot) return;
|
|
967
|
+
run.committed = addTokens(run.committed, response.snapshot);
|
|
968
|
+
response.hasSnapshot = false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private applyLiveUsage(
|
|
972
|
+
response: ResponseState,
|
|
973
|
+
tokens: InteractionTokens,
|
|
974
|
+
): boolean {
|
|
975
|
+
if (tokens.input === 0 && tokens.output === 0) return false;
|
|
976
|
+
if (
|
|
977
|
+
response.hasSnapshot &&
|
|
978
|
+
(tokens.input < response.snapshot.input ||
|
|
979
|
+
tokens.output < response.snapshot.output)
|
|
980
|
+
)
|
|
981
|
+
return false;
|
|
982
|
+
if (
|
|
983
|
+
response.hasSnapshot &&
|
|
984
|
+
tokens.input === response.snapshot.input &&
|
|
985
|
+
tokens.output === response.snapshot.output
|
|
986
|
+
)
|
|
987
|
+
return false;
|
|
988
|
+
const outputAdvanced =
|
|
989
|
+
!response.hasSnapshot || tokens.output > response.snapshot.output;
|
|
990
|
+
response.snapshot = tokens;
|
|
991
|
+
response.hasSnapshot = true;
|
|
992
|
+
if (outputAdvanced) {
|
|
993
|
+
response.outputAnchor = tokens.output;
|
|
994
|
+
response.outputAnchorCodePoints = response.generatedCodePoints;
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private liveDisplay(response: ResponseState): LiveTokenDisplay {
|
|
1000
|
+
const committed = this.committedTokens();
|
|
1001
|
+
const postAnchorCodePoints = Math.max(
|
|
1002
|
+
0,
|
|
1003
|
+
response.generatedCodePoints - response.outputAnchorCodePoints,
|
|
1004
|
+
);
|
|
1005
|
+
const candidate = safeAdd(
|
|
1006
|
+
response.outputAnchor,
|
|
1007
|
+
estimatedTokens(postAnchorCodePoints),
|
|
1008
|
+
);
|
|
1009
|
+
const responseOutput = Math.max(response.liveOutputFloor, candidate);
|
|
1010
|
+
const exactCoverage =
|
|
1011
|
+
response.hasSnapshot &&
|
|
1012
|
+
postAnchorCodePoints === 0 &&
|
|
1013
|
+
responseOutput === response.outputAnchor &&
|
|
1014
|
+
!response.estimateIncomplete;
|
|
1015
|
+
response.liveOutputFloor = responseOutput;
|
|
1016
|
+
return {
|
|
1017
|
+
input: safeAdd(
|
|
1018
|
+
committed.input,
|
|
1019
|
+
response.hasSnapshot ? response.snapshot.input : 0,
|
|
1020
|
+
),
|
|
1021
|
+
output: safeAdd(committed.output, responseOutput),
|
|
1022
|
+
outputApproximate: !exactCoverage,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private runTokens(run: RunState): InteractionTokens {
|
|
1027
|
+
return run.response?.hasSnapshot
|
|
1028
|
+
? addTokens(run.committed, run.response.snapshot)
|
|
1029
|
+
: run.committed;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private committedTokens(): InteractionTokens {
|
|
1033
|
+
const interaction = this.interaction;
|
|
1034
|
+
if (!interaction) return { input: 0, output: 0 };
|
|
1035
|
+
let total = interaction.foldedCommitted;
|
|
1036
|
+
for (const run of interaction.runs) total = addTokens(total, run.committed);
|
|
1037
|
+
return total;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private totalTokens(): InteractionTokens {
|
|
1041
|
+
const interaction = this.interaction;
|
|
1042
|
+
if (!interaction) return { input: 0, output: 0 };
|
|
1043
|
+
let total = interaction.foldedCommitted;
|
|
1044
|
+
for (const run of interaction.runs)
|
|
1045
|
+
total = addTokens(total, this.runTokens(run));
|
|
1046
|
+
return total;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private retainOpenThought(run: RunState, now: number): void {
|
|
1050
|
+
const end = safeTime(now);
|
|
1051
|
+
for (const block of run.response?.thoughtBlocks.values() ?? []) {
|
|
1052
|
+
if (
|
|
1053
|
+
block.startedAt === undefined ||
|
|
1054
|
+
block.closed ||
|
|
1055
|
+
end <= block.startedAt
|
|
1056
|
+
)
|
|
1057
|
+
continue;
|
|
1058
|
+
if (this.interaction) {
|
|
1059
|
+
addThoughtInterval(
|
|
1060
|
+
this.interaction.thoughtCoverage,
|
|
1061
|
+
{ start: block.startedAt, end },
|
|
1062
|
+
run.ordinal,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
block.startedAt = end;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private thoughtSnapshot(
|
|
1070
|
+
interaction: InteractionState,
|
|
1071
|
+
now: number,
|
|
1072
|
+
): ThoughtSnapshot {
|
|
1073
|
+
const coverage: ThoughtCoverage = {
|
|
1074
|
+
intervals: interaction.thoughtCoverage.intervals.map((interval) => ({
|
|
1075
|
+
...interval,
|
|
1076
|
+
sources: [...interval.sources],
|
|
1077
|
+
})),
|
|
1078
|
+
incomplete: interaction.thoughtCoverage.incomplete,
|
|
1079
|
+
};
|
|
1080
|
+
for (const run of interaction.runs) {
|
|
1081
|
+
for (const block of run.response?.thoughtBlocks.values() ?? []) {
|
|
1082
|
+
if (block.startedAt === undefined || block.closed) continue;
|
|
1083
|
+
addThoughtInterval(
|
|
1084
|
+
coverage,
|
|
1085
|
+
{ start: block.startedAt, end: safeTime(now) },
|
|
1086
|
+
run.ordinal,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
durationMs: intervalDuration(coverage.intervals),
|
|
1092
|
+
active: interaction.runs.some((run) =>
|
|
1093
|
+
[...(run.response?.thoughtBlocks.values() ?? [])].some(
|
|
1094
|
+
(block) => block.startedAt !== undefined && !block.closed,
|
|
1095
|
+
),
|
|
1096
|
+
),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private sameDisplay(
|
|
1101
|
+
left: LiveTokenDisplay | undefined,
|
|
1102
|
+
right: LiveTokenDisplay | undefined,
|
|
1103
|
+
): boolean {
|
|
1104
|
+
return (
|
|
1105
|
+
left?.input === right?.input &&
|
|
1106
|
+
left?.output === right?.output &&
|
|
1107
|
+
left?.outputApproximate === right?.outputApproximate
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function hasExactKeys(
|
|
1113
|
+
record: Record<string, unknown>,
|
|
1114
|
+
expected: readonly string[],
|
|
1115
|
+
): boolean {
|
|
1116
|
+
return (
|
|
1117
|
+
Object.keys(record).sort().join("\0") === [...expected].sort().join("\0")
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function isTurnSummaryData(value: unknown): value is TurnSummaryData {
|
|
1122
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1123
|
+
return false;
|
|
1124
|
+
const record = value as Record<string, unknown>;
|
|
1125
|
+
if (
|
|
1126
|
+
!isNonnegativeSafeInteger(record.durationMs) ||
|
|
1127
|
+
!isNonnegativeSafeInteger(record.input) ||
|
|
1128
|
+
!isNonnegativeSafeInteger(record.output)
|
|
1129
|
+
)
|
|
1130
|
+
return false;
|
|
1131
|
+
if (record.version === 1) {
|
|
1132
|
+
return hasExactKeys(record, ["version", "durationMs", "input", "output"]);
|
|
1133
|
+
}
|
|
1134
|
+
if (record.version === 2) {
|
|
1135
|
+
return (
|
|
1136
|
+
hasExactKeys(record, [
|
|
1137
|
+
"version",
|
|
1138
|
+
"durationMs",
|
|
1139
|
+
"input",
|
|
1140
|
+
"output",
|
|
1141
|
+
"stylePrefix",
|
|
1142
|
+
]) && isSafeSgrStylePrefix(record.stylePrefix)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return (
|
|
1146
|
+
record.version === TURN_SUMMARY_VERSION &&
|
|
1147
|
+
hasExactKeys(record, [
|
|
1148
|
+
"version",
|
|
1149
|
+
"durationMs",
|
|
1150
|
+
"thoughtDurationMs",
|
|
1151
|
+
"input",
|
|
1152
|
+
"output",
|
|
1153
|
+
"stylePrefix",
|
|
1154
|
+
]) &&
|
|
1155
|
+
isNonnegativeSafeInteger(record.thoughtDurationMs) &&
|
|
1156
|
+
isSafeSgrStylePrefix(record.stylePrefix)
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export function formatTurnSummary(
|
|
1161
|
+
data: TurnSummaryData | TurnSummaryMetrics,
|
|
1162
|
+
): string {
|
|
1163
|
+
const thought =
|
|
1164
|
+
"thoughtDurationMs" in data && data.thoughtDurationMs > 0
|
|
1165
|
+
? ` · thought for ${formatElapsedDuration(data.thoughtDurationMs)}`
|
|
1166
|
+
: "";
|
|
1167
|
+
return ` Turn took ${formatElapsedDuration(data.durationMs)}${thought} · ↑${formatCount(data.input)} ↓${formatCount(data.output)}`;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function renderTurnSummaryEntry(
|
|
1171
|
+
entry: { data?: unknown },
|
|
1172
|
+
themeOptions: unknown,
|
|
1173
|
+
theme: Pick<Theme, "fg">,
|
|
1174
|
+
): Text | undefined {
|
|
1175
|
+
if (!isTurnSummaryData(entry.data)) return undefined;
|
|
1176
|
+
const text = formatTurnSummary(entry.data);
|
|
1177
|
+
if (entry.data.version === 2 || entry.data.version === 3) {
|
|
1178
|
+
return new Text(`${entry.data.stylePrefix}${text}${SGR_RESET}`, 0, 0);
|
|
1179
|
+
}
|
|
1180
|
+
const options = (themeOptions ?? {}) as {
|
|
1181
|
+
colorSource?: ColorSource;
|
|
1182
|
+
workingLineHigh?: ColorSpec;
|
|
1183
|
+
};
|
|
1184
|
+
return new Text(
|
|
1185
|
+
`${renderWorkingLineHigh(
|
|
1186
|
+
theme,
|
|
1187
|
+
options.colorSource === "terminal" ? "terminal" : "theme",
|
|
1188
|
+
options.workingLineHigh,
|
|
1189
|
+
text,
|
|
1190
|
+
)}${SGR_RESET}`,
|
|
1191
|
+
0,
|
|
1192
|
+
0,
|
|
1193
|
+
);
|
|
1194
|
+
}
|