@khorsheed/dsh-context-lens 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +26953 -0
- package/lib/index.js +3943 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1569 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +1202 -0
- package/lib/types/changes.d.ts +72 -0
- package/lib/types/changes.js +328 -0
- package/lib/types/client/AgentTabs.d.ts +34 -0
- package/lib/types/client/AgentTabs.js +89 -0
- package/lib/types/client/ChangeList.d.ts +28 -0
- package/lib/types/client/ChangeList.js +109 -0
- package/lib/types/client/ConfigFacts.d.ts +21 -0
- package/lib/types/client/ConfigFacts.js +30 -0
- package/lib/types/client/HeaderUtility.d.ts +12 -0
- package/lib/types/client/HeaderUtility.js +8 -0
- package/lib/types/client/InspectorPanel.d.ts +5 -0
- package/lib/types/client/InspectorPanel.js +486 -0
- package/lib/types/client/RawView.d.ts +18 -0
- package/lib/types/client/RawView.js +8 -0
- package/lib/types/client/RequestFacts.d.ts +19 -0
- package/lib/types/client/RequestFacts.js +39 -0
- package/lib/types/client/RoomDecisionsView.d.ts +24 -0
- package/lib/types/client/RoomDecisionsView.js +52 -0
- package/lib/types/client/SchemaFields.d.ts +13 -0
- package/lib/types/client/SchemaFields.js +96 -0
- package/lib/types/client/SegCtl.d.ts +32 -0
- package/lib/types/client/SegCtl.js +12 -0
- package/lib/types/client/SegmentBlocks.d.ts +28 -0
- package/lib/types/client/SegmentBlocks.js +225 -0
- package/lib/types/client/TimingView.d.ts +17 -0
- package/lib/types/client/TimingView.js +98 -0
- package/lib/types/client/TokenView.d.ts +21 -0
- package/lib/types/client/TokenView.js +22 -0
- package/lib/types/client/ToolCatalog.d.ts +9 -0
- package/lib/types/client/ToolCatalog.js +51 -0
- package/lib/types/client/Trend.d.ts +25 -0
- package/lib/types/client/Trend.js +100 -0
- package/lib/types/client/TurnAction.d.ts +14 -0
- package/lib/types/client/TurnAction.js +31 -0
- package/lib/types/client/TurnMenu.d.ts +36 -0
- package/lib/types/client/TurnMenu.js +50 -0
- package/lib/types/client/aggregate.d.ts +141 -0
- package/lib/types/client/aggregate.js +213 -0
- package/lib/types/client/contract.d.ts +58 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +47 -0
- package/lib/types/client/definition.js +25 -0
- package/lib/types/client/icons-local.d.ts +19 -0
- package/lib/types/client/icons-local.js +3 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +139 -0
- package/lib/types/client/locales.d.ts +365 -0
- package/lib/types/client/locales.js +705 -0
- package/lib/types/client/preset-visibility.d.ts +99 -0
- package/lib/types/client/preset-visibility.js +108 -0
- package/lib/types/client/scroll-sync.d.ts +54 -0
- package/lib/types/client/scroll-sync.js +118 -0
- package/lib/types/client/timing.d.ts +29 -0
- package/lib/types/client/timing.js +117 -0
- package/lib/types/client/wire.d.ts +13 -0
- package/lib/types/client/wire.js +11 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +25 -0
- package/lib/types/log-lane.d.ts +198 -0
- package/lib/types/log-lane.js +841 -0
- package/lib/types/messages.d.ts +96 -0
- package/lib/types/messages.js +254 -0
- package/lib/types/prefix-diff.d.ts +4 -0
- package/lib/types/prefix-diff.js +20 -0
- package/lib/types/remote.d.ts +45 -0
- package/lib/types/remote.js +99 -0
- package/lib/types/room.d.ts +66 -0
- package/lib/types/room.js +101 -0
- package/lib/types/segmenter.d.ts +100 -0
- package/lib/types/segmenter.js +184 -0
- package/lib/types/service.d.ts +145 -0
- package/lib/types/service.js +493 -0
- package/lib/types/store.d.ts +424 -0
- package/lib/types/store.js +1920 -0
- package/lib/types/tool-summary.d.ts +6 -0
- package/lib/types/tool-summary.js +7 -0
- package/lib/types/types.d.ts +703 -0
- package/lib/types/types.js +18 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +133 -3
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log lane: reconstructs requests from an on-disk session log, read through
|
|
3
|
+
* the host's `sessionPersistence` service (which owns the root layout, the
|
|
4
|
+
* zstd framing, and the format-version chain). Two drivers share one fold:
|
|
5
|
+
*
|
|
6
|
+
* - **Child scopes** — a `subagent_dsh` one-shot child runs in a NEW process,
|
|
7
|
+
* so the parent's `llm/stream` never sees its requests; the scope fills from
|
|
8
|
+
* the child's own log, incrementally (the cursor resumes at the consumed
|
|
9
|
+
* offset across panel queries).
|
|
10
|
+
* - **Main-session backfill** — the panel otherwise shows only turns captured
|
|
11
|
+
* live since mount; the session's own log reconstructs the pre-mount history
|
|
12
|
+
* at log fidelity ({@link reconstructSession}), once per session.
|
|
13
|
+
*
|
|
14
|
+
* Honesty contract (one-shot child logs lack two things by design): no
|
|
15
|
+
* `request/header` events means `config` stays `{}` until a header says
|
|
16
|
+
* otherwise, and a missing system prompt never lands in the log, so the system
|
|
17
|
+
* segment stays empty. Main-session logs carry both (`request/header` →
|
|
18
|
+
* config + tools; `system/message` → the rendered prompt as ONE block — the
|
|
19
|
+
* assemble-section attribution is the live lane's upgrade, the log keeps no
|
|
20
|
+
* section names). The lane never invents either.
|
|
21
|
+
*
|
|
22
|
+
* Each `assistant/message` (a settled, usage-carrying response) or
|
|
23
|
+
* `assistant/attempt` (a settled failure) completes one request row.
|
|
24
|
+
*
|
|
25
|
+
* @module @khorsheed/dsh-context-lens/log-lane
|
|
26
|
+
*/
|
|
27
|
+
import { countToolCalls } from "./tool-summary.js";
|
|
28
|
+
import { responseFromSpans } from "./store.js";
|
|
29
|
+
import { contentHash, splitRequest } from "./segmenter.js";
|
|
30
|
+
import { AMBIENT_SOURCES, fromLogMessage, lastParagraph, messageKey, previewLine, serializeMessage } from "./messages.js";
|
|
31
|
+
/** Structural check for the probed `sessionPersistence` service. */
|
|
32
|
+
export function isSessionLogReader(value) {
|
|
33
|
+
return typeof value === 'object' && value !== null
|
|
34
|
+
&& typeof value.open === 'function';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Subagent providers that run inside this host, so their requests pass
|
|
38
|
+
* through the live capture. Every other provider (claude-local, codex-local,
|
|
39
|
+
* kimi-cli, dsh-cli, …) is its own process, mirrored into its log after the
|
|
40
|
+
* fact.
|
|
41
|
+
*/
|
|
42
|
+
export const IN_PROCESS_PROVIDERS = new Set(['spawn', 'fork']);
|
|
43
|
+
/** A step/start this close to its answer was written after the call, not when it began. */
|
|
44
|
+
const MIRROR_LAG_MS = 50;
|
|
45
|
+
/** A fresh cursor for one log. */
|
|
46
|
+
export function openCursor() {
|
|
47
|
+
return {
|
|
48
|
+
consumed: 0,
|
|
49
|
+
numberedFromStart: false,
|
|
50
|
+
messages: [],
|
|
51
|
+
compactions: new Map(),
|
|
52
|
+
stepStartAt: undefined,
|
|
53
|
+
config: undefined,
|
|
54
|
+
tools: undefined,
|
|
55
|
+
callAt: new Map(),
|
|
56
|
+
callNames: new Map(),
|
|
57
|
+
callOwners: new Map(),
|
|
58
|
+
messageDurations: new Map(),
|
|
59
|
+
previous: undefined,
|
|
60
|
+
requests: [],
|
|
61
|
+
turnHost: undefined,
|
|
62
|
+
turnQuerySeen: false,
|
|
63
|
+
turnTriggerSeen: false,
|
|
64
|
+
pendingDelegations: new Map(),
|
|
65
|
+
broken: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fold new log rows into the cursor. Returns the requests completed by THIS
|
|
70
|
+
* batch (the child-scope driver's whole output); the sink receives those plus
|
|
71
|
+
* the turn/delegation structure (the backfill driver). Tool exec spans can
|
|
72
|
+
* open and close in later batches, so pairing data lives on the cursor.
|
|
73
|
+
*/
|
|
74
|
+
export function foldLogRows(cursor, rows, sink = {}) {
|
|
75
|
+
const built = [];
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
if (cursor.consumed === 0)
|
|
78
|
+
cursor.numberedFromStart = row.type === 'session' || row.seq === 0;
|
|
79
|
+
cursor.consumed += 1;
|
|
80
|
+
if (row.ignorable)
|
|
81
|
+
continue;
|
|
82
|
+
const data = (typeof row.data === 'object' && row.data !== null ? row.data : {});
|
|
83
|
+
switch (row.type) {
|
|
84
|
+
case 'turn/start': {
|
|
85
|
+
const hostTurn = typeof data['turn'] === 'number' ? data['turn'] : undefined;
|
|
86
|
+
cursor.turnHost = hostTurn;
|
|
87
|
+
cursor.turnQuerySeen = false;
|
|
88
|
+
cursor.turnTriggerSeen = false;
|
|
89
|
+
if (hostTurn !== undefined)
|
|
90
|
+
sink.turnStart?.(hostTurn, row.time, row.seq);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'turn/end': {
|
|
94
|
+
flushDeferred(cursor, built, sink);
|
|
95
|
+
const hostTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
96
|
+
const reason = typeof data['reason'] === 'object' && data['reason'] !== null
|
|
97
|
+
? data['reason']
|
|
98
|
+
: { kind: 'unknown' };
|
|
99
|
+
if (hostTurn !== undefined)
|
|
100
|
+
sink.turnEnd?.(hostTurn, reason, row.time);
|
|
101
|
+
cursor.turnHost = undefined;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'step/start':
|
|
105
|
+
cursor.stepStartAt = row.time;
|
|
106
|
+
if (cursor.lastEventAt === undefined)
|
|
107
|
+
delete cursor.stepPrevAt;
|
|
108
|
+
else
|
|
109
|
+
cursor.stepPrevAt = cursor.lastEventAt;
|
|
110
|
+
break;
|
|
111
|
+
case 'subagent/descriptor': {
|
|
112
|
+
const provider = data['provider'];
|
|
113
|
+
if (typeof provider === 'string' && !IN_PROCESS_PROVIDERS.has(provider))
|
|
114
|
+
cursor.external = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'request/header': {
|
|
118
|
+
const header = data['header'];
|
|
119
|
+
const config = header?.config;
|
|
120
|
+
if (config !== undefined) {
|
|
121
|
+
cursor.config = {
|
|
122
|
+
...typeof config['provider'] === 'string' ? { provider: config['provider'] } : {},
|
|
123
|
+
...typeof config['model'] === 'string' ? { model: config['model'] } : {},
|
|
124
|
+
...typeof config['reasoningEffort'] === 'string' ? { reasoningEffort: config['reasoningEffort'] } : {},
|
|
125
|
+
...typeof config['temperature'] === 'number' ? { temperature: config['temperature'] } : {},
|
|
126
|
+
...typeof config['maxTokens'] === 'number' ? { maxTokens: config['maxTokens'] } : {},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(header?.tools)) {
|
|
130
|
+
cursor.tools = header.tools.filter((tool) => typeof tool === 'object' && tool !== null
|
|
131
|
+
&& typeof tool.name === 'string'
|
|
132
|
+
&& typeof tool.description === 'string'
|
|
133
|
+
&& typeof tool.parameters === 'object');
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'system/message':
|
|
138
|
+
case 'developer/message': {
|
|
139
|
+
const message = fromLogMessage(data['message']);
|
|
140
|
+
if (message !== undefined)
|
|
141
|
+
message.sourceTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
142
|
+
if (message !== undefined)
|
|
143
|
+
placeMessage(cursor, row, message);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'user/message': {
|
|
147
|
+
const message = fromLogMessage(data);
|
|
148
|
+
if (message !== undefined)
|
|
149
|
+
message.sourceTurn = cursor.turnHost;
|
|
150
|
+
if (message !== undefined) {
|
|
151
|
+
placeMessage(cursor, row, message);
|
|
152
|
+
if (!cursor.turnQuerySeen && message.sourceKind === 'user') {
|
|
153
|
+
cursor.turnQuerySeen = true;
|
|
154
|
+
sink.queryText?.(cursor.turnHost, collapseQuery(serializeMessage(message)), row.time);
|
|
155
|
+
}
|
|
156
|
+
else if (!cursor.turnTriggerSeen && (row.surfaceOp === undefined || row.surfaceOp === 'append') && !AMBIENT_SOURCES.has(message.sourceKind)) {
|
|
157
|
+
cursor.turnTriggerSeen = true;
|
|
158
|
+
sink.triggerText?.(cursor.turnHost, message.sourceKind, lastParagraph(serializeMessage(message)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case 'assistant/message': {
|
|
164
|
+
const hostTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
165
|
+
if (cursor.deferred !== undefined && cursor.deferred.hostTurn !== hostTurn)
|
|
166
|
+
flushDeferred(cursor, built, sink);
|
|
167
|
+
const own = buildRequest(cursor, row, data, false);
|
|
168
|
+
const message = fromLogMessage(data['message']);
|
|
169
|
+
let request = own;
|
|
170
|
+
if (cursor.deferred !== undefined) {
|
|
171
|
+
// The rest of the deferred call: it joins the request the call began.
|
|
172
|
+
request = cursor.deferred.request;
|
|
173
|
+
const blocks = [...cursor.deferred.blocks, ...message?.content ?? []];
|
|
174
|
+
cursor.deferred.blocks = blocks;
|
|
175
|
+
joinCall(request, own, blocks);
|
|
176
|
+
}
|
|
177
|
+
if (cursor.external === true && own.usage === undefined && own.status !== 'failed') {
|
|
178
|
+
cursor.deferred ??= { request, hostTurn, blocks: [...message?.content ?? []] };
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
delete cursor.deferred;
|
|
182
|
+
built.push(request);
|
|
183
|
+
cursor.requests.push(request);
|
|
184
|
+
sink.request?.(hostTurn, request);
|
|
185
|
+
}
|
|
186
|
+
if (message !== undefined)
|
|
187
|
+
message.sourceTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
188
|
+
if (message !== undefined) {
|
|
189
|
+
placeMessage(cursor, row, message);
|
|
190
|
+
if (message.id !== undefined) {
|
|
191
|
+
if (request.settledAt !== undefined) {
|
|
192
|
+
cursor.messageDurations.set(message.id, request.settledAt - request.startedAt);
|
|
193
|
+
}
|
|
194
|
+
sink.closingMessage?.(hostTurn, message.id);
|
|
195
|
+
}
|
|
196
|
+
for (const block of message.content) {
|
|
197
|
+
if (block.type === 'tool-call' && block.id !== undefined) {
|
|
198
|
+
cursor.callOwners.set(block.id, request);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'assistant/attempt': {
|
|
205
|
+
const request = buildRequest(cursor, row, data, true);
|
|
206
|
+
built.push(request);
|
|
207
|
+
cursor.requests.push(request);
|
|
208
|
+
sink.request?.(typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost, request);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'tool/call': {
|
|
212
|
+
const callId = typeof data['callId'] === 'string' ? data['callId'] : undefined;
|
|
213
|
+
const name = typeof data['name'] === 'string' ? data['name'] : undefined;
|
|
214
|
+
if (callId !== undefined) {
|
|
215
|
+
cursor.callAt.set(callId, row.time ?? 0);
|
|
216
|
+
if (name !== undefined)
|
|
217
|
+
cursor.callNames.set(callId, name);
|
|
218
|
+
const owner = cursor.callOwners.get(callId);
|
|
219
|
+
if (owner !== undefined && name !== undefined) {
|
|
220
|
+
owner.execs.push({ callId, name, start: row.time ?? owner.settledAt ?? owner.startedAt });
|
|
221
|
+
}
|
|
222
|
+
if (name !== undefined && isDelegationTool(name)) {
|
|
223
|
+
const delegation = {
|
|
224
|
+
...typeof data['turn'] === 'number' ? { hostTurn: data['turn'] } : {},
|
|
225
|
+
callId,
|
|
226
|
+
name,
|
|
227
|
+
at: row.time ?? 0,
|
|
228
|
+
...delegationLabel(typeof data['arguments'] === 'string' ? data['arguments'] : ''),
|
|
229
|
+
};
|
|
230
|
+
cursor.pendingDelegations.set(callId, delegation);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'subagent/catalog': {
|
|
236
|
+
const childId = typeof data['childId'] === 'string' ? data['childId'] : undefined;
|
|
237
|
+
if (childId !== undefined && childId.length > 0) {
|
|
238
|
+
// FIFO bind: the catalog belongs to the oldest unbound delegation call.
|
|
239
|
+
let bound = false;
|
|
240
|
+
for (const pending of cursor.pendingDelegations.values()) {
|
|
241
|
+
if (pending.childId === undefined) {
|
|
242
|
+
pending.childId = childId;
|
|
243
|
+
if (typeof data['label'] === 'string' && data['label'].length > 0)
|
|
244
|
+
pending.catalogLabel = data['label'];
|
|
245
|
+
bound = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// A child no tool call started (an external agent a provider or the
|
|
250
|
+
// user brought in) still is a child of this turn: surface it as-is.
|
|
251
|
+
if (!bound) {
|
|
252
|
+
sink.delegation?.({
|
|
253
|
+
...cursor.turnHost === undefined ? {} : { hostTurn: cursor.turnHost },
|
|
254
|
+
callId: '',
|
|
255
|
+
name: 'subagent/catalog',
|
|
256
|
+
at: row.time ?? 0,
|
|
257
|
+
childId,
|
|
258
|
+
...typeof data['label'] === 'string' && data['label'].length > 0 ? { catalogLabel: data['label'] } : {},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case 'compaction/start': {
|
|
265
|
+
const id = data['compactionId'];
|
|
266
|
+
if (typeof id === 'string')
|
|
267
|
+
cursor.compactions.set(id, { startedAt: row.time ?? 0, ...typeof data['turn'] === 'number' ? { hostTurn: data['turn'] } : {} });
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case 'compaction/summary': {
|
|
271
|
+
const request = buildCompaction(cursor, row, data);
|
|
272
|
+
if (request) {
|
|
273
|
+
const id = request.compaction.id;
|
|
274
|
+
cursor.compactions.set(id, { ...cursor.compactions.get(id), startedAt: request.startedAt, request });
|
|
275
|
+
built.push(request);
|
|
276
|
+
sink.request?.(cursor.turnHost, request);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'compaction/end':
|
|
281
|
+
if (typeof data['compactionId'] === 'string')
|
|
282
|
+
cursor.compactions.delete(data['compactionId']);
|
|
283
|
+
break;
|
|
284
|
+
case 'tool/result': {
|
|
285
|
+
const message = fromLogMessage(data['message']);
|
|
286
|
+
if (message !== undefined)
|
|
287
|
+
message.sourceTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
288
|
+
if (message !== undefined)
|
|
289
|
+
placeMessage(cursor, row, message);
|
|
290
|
+
if (row.surfaceOp && row.surfaceOp !== 'append')
|
|
291
|
+
break;
|
|
292
|
+
const callId = message?.toolCallId
|
|
293
|
+
?? (typeof data['toolCallId'] === 'string' ? data['toolCallId'] : undefined);
|
|
294
|
+
if (callId !== undefined) {
|
|
295
|
+
const owner = cursor.callOwners.get(callId);
|
|
296
|
+
const span = owner?.execs.find(exec => exec.callId === callId && exec.end === undefined);
|
|
297
|
+
if (span !== undefined) {
|
|
298
|
+
if (row.time !== undefined)
|
|
299
|
+
span.end = row.time;
|
|
300
|
+
span.isError = message?.isError === true;
|
|
301
|
+
if (message !== undefined)
|
|
302
|
+
span.result = serializeMessage(message);
|
|
303
|
+
}
|
|
304
|
+
const delegation = cursor.pendingDelegations.get(callId);
|
|
305
|
+
if (delegation !== undefined) {
|
|
306
|
+
cursor.pendingDelegations.delete(callId);
|
|
307
|
+
if (row.time !== undefined)
|
|
308
|
+
delegation.resultAt = row.time;
|
|
309
|
+
delegation.isError = message?.isError === true;
|
|
310
|
+
if (delegation.isError && message !== undefined) {
|
|
311
|
+
delegation.errorPreview = previewLine(serializeMessage(message));
|
|
312
|
+
}
|
|
313
|
+
sink.delegation?.(delegation);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
// Unknown / log-only rows carry no request information; ignorable
|
|
320
|
+
// plugin rows (e.g. local-agent/stream) land here by design.
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (row.time !== undefined && row.type !== 'local-agent/stream')
|
|
324
|
+
cursor.lastEventAt = row.time;
|
|
325
|
+
}
|
|
326
|
+
return built;
|
|
327
|
+
}
|
|
328
|
+
/** Apply inclusive endpoints in CURRENT surface order (seq order may differ). */
|
|
329
|
+
function placeMessage(cursor, row, message) {
|
|
330
|
+
const node = { seq: row.seq ?? cursor.consumed, message };
|
|
331
|
+
const op = row.surfaceOp;
|
|
332
|
+
// Markerless legacy fixtures behave as append; native decoded logs carry the marker.
|
|
333
|
+
if (op === undefined || op === 'append') {
|
|
334
|
+
cursor.messages.push(node);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const start = cursor.messages.findIndex(item => item.seq === op.startSeq);
|
|
338
|
+
const end = cursor.messages.findIndex(item => item.seq === op.endSeq);
|
|
339
|
+
if (op.op !== 'replace' || start < 0 || end < start)
|
|
340
|
+
throw new Error(`Invalid context surface replacement at event ${row.seq}`);
|
|
341
|
+
if (row.type === 'tool/result') {
|
|
342
|
+
const original = cursor.messages[start].message;
|
|
343
|
+
const beforeChars = original.pruning?.beforeChars ?? serializeMessage(original).length;
|
|
344
|
+
const afterChars = serializeMessage(message).length;
|
|
345
|
+
if (afterChars < beforeChars)
|
|
346
|
+
message.pruning = { beforeChars, afterChars };
|
|
347
|
+
}
|
|
348
|
+
cursor.messages.splice(start, end - start + 1, node);
|
|
349
|
+
const compression = message.compactionId === undefined ? undefined : cursor.compactions.get(message.compactionId)?.request?.compaction;
|
|
350
|
+
if (compression)
|
|
351
|
+
compression.applied = true;
|
|
352
|
+
}
|
|
353
|
+
/** Recover recorded model-backed summaries without inventing the missing instruction. */
|
|
354
|
+
function buildCompaction(cursor, row, data) {
|
|
355
|
+
const id = data['compactionId'];
|
|
356
|
+
if (typeof id !== 'string' || (data['llmStreamCall'] !== true && data['usage'] === undefined))
|
|
357
|
+
return undefined;
|
|
358
|
+
const start = cursor.compactions.get(id);
|
|
359
|
+
const seqs = Array.isArray(data['shadowedSeqs']) ? data['shadowedSeqs'] : [];
|
|
360
|
+
const selected = new Set(seqs);
|
|
361
|
+
const region = cursor.messages.filter(item => selected.has(item.seq));
|
|
362
|
+
const summary = fromLogMessage({ role: 'assistant', content: data['summary'] });
|
|
363
|
+
if (!summary)
|
|
364
|
+
return undefined;
|
|
365
|
+
const temp = openCursor();
|
|
366
|
+
if (cursor.materialize !== undefined)
|
|
367
|
+
temp.materialize = cursor.materialize;
|
|
368
|
+
temp.turnHost = start?.hostTurn;
|
|
369
|
+
// The summarize call replays the conversation prefix: the system head at
|
|
370
|
+
// surface node 0, the header's tools, then the region (compaction-basic
|
|
371
|
+
// buildSummarizationInput). Only its trailing instruction lives in host
|
|
372
|
+
// code rather than the log, so the request stays inputPartial.
|
|
373
|
+
const head = cursor.messages[0];
|
|
374
|
+
temp.messages = head !== undefined && head.message.role === 'system' && !selected.has(head.seq) ? [head, ...region] : region;
|
|
375
|
+
if (cursor.tools !== undefined)
|
|
376
|
+
temp.tools = cursor.tools;
|
|
377
|
+
temp.stepStartAt = start?.startedAt ?? row.time;
|
|
378
|
+
temp.config = {
|
|
379
|
+
...typeof data['provider'] === 'string' ? { provider: data['provider'] } : {},
|
|
380
|
+
...typeof data['model'] === 'string' ? { model: data['model'] } : {},
|
|
381
|
+
...typeof data['maxTokens'] === 'number' ? { maxTokens: data['maxTokens'] } : {},
|
|
382
|
+
};
|
|
383
|
+
const result = buildRequest(temp, row, { message: { role: 'assistant', content: data['summary'] }, usage: data['usage'] }, false);
|
|
384
|
+
return { ...result, responseSummary: { kind: 'compaction' }, purpose: 'compaction', compaction: {
|
|
385
|
+
id, applied: false, ...start?.hostTurn === undefined ? {} : { hostTurn: start.hostTurn },
|
|
386
|
+
...Array.isArray(data['shadowedSeqs']) && region.length === seqs.length ? { replacedCount: region.length, replacedChars: region.reduce((n, item) => n + serializeMessage(item.message).length, 0) } : {}, inputPartial: true,
|
|
387
|
+
} };
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* One-shot reconstruction of a whole session log into turns (the main-session
|
|
391
|
+
* backfill). Requests group by their host turn number (step-scoped events
|
|
392
|
+
* carry it; a torn bracket falls back to the fold's open-turn state). Turns
|
|
393
|
+
* never closed by a turn/end (a crash) come out with `endReasonKind` unset.
|
|
394
|
+
*/
|
|
395
|
+
export function reconstructSession(rows, materialize) {
|
|
396
|
+
const cursor = openCursor();
|
|
397
|
+
if (materialize !== undefined)
|
|
398
|
+
cursor.materialize = materialize;
|
|
399
|
+
const turns = [];
|
|
400
|
+
const byHostTurn = new Map();
|
|
401
|
+
// Children catalogued outside any turn (a room member run started by a
|
|
402
|
+
// human dispatch) gather in one holder the store files as room members.
|
|
403
|
+
const orphans = [];
|
|
404
|
+
const place = (delegation) => {
|
|
405
|
+
const turn = turnFor(delegation.hostTurn, delegation.at);
|
|
406
|
+
if (turn !== undefined)
|
|
407
|
+
turn.delegations.push(delegation);
|
|
408
|
+
else if (delegation.childId !== undefined)
|
|
409
|
+
orphans.push(delegation);
|
|
410
|
+
};
|
|
411
|
+
const turnFor = (hostTurn, at) => {
|
|
412
|
+
if (hostTurn === undefined)
|
|
413
|
+
return undefined;
|
|
414
|
+
let turn = byHostTurn.get(hostTurn);
|
|
415
|
+
if (turn === undefined) {
|
|
416
|
+
turn = {
|
|
417
|
+
hostTurn,
|
|
418
|
+
query: '',
|
|
419
|
+
...at === undefined ? { startedAt: 0 } : { startedAt: at },
|
|
420
|
+
requests: [],
|
|
421
|
+
delegations: [],
|
|
422
|
+
};
|
|
423
|
+
byHostTurn.set(hostTurn, turn);
|
|
424
|
+
turns.push(turn);
|
|
425
|
+
}
|
|
426
|
+
else if (turn.startedAt === 0 && at !== undefined) {
|
|
427
|
+
turn.startedAt = at;
|
|
428
|
+
}
|
|
429
|
+
return turn;
|
|
430
|
+
};
|
|
431
|
+
const sink = {
|
|
432
|
+
turnStart: (hostTurn, at, seq) => {
|
|
433
|
+
const turn = turnFor(hostTurn, at);
|
|
434
|
+
if (turn !== undefined && seq !== undefined && turn.startSeq === undefined)
|
|
435
|
+
turn.startSeq = seq;
|
|
436
|
+
},
|
|
437
|
+
turnEnd: (hostTurn, reason, at) => {
|
|
438
|
+
const turn = turnFor(hostTurn, at);
|
|
439
|
+
if (turn === undefined)
|
|
440
|
+
return;
|
|
441
|
+
if (at !== undefined)
|
|
442
|
+
turn.endedAt = at;
|
|
443
|
+
turn.endReasonKind = reason.kind;
|
|
444
|
+
if (reason.kind === 'error' && reason.error?.message !== undefined)
|
|
445
|
+
turn.endError = reason.error.message;
|
|
446
|
+
},
|
|
447
|
+
queryText: (hostTurn, text, at) => {
|
|
448
|
+
const turn = turnFor(hostTurn, at);
|
|
449
|
+
if (turn !== undefined && turn.query === '')
|
|
450
|
+
turn.query = text;
|
|
451
|
+
},
|
|
452
|
+
triggerText: (hostTurn, source, text) => {
|
|
453
|
+
const turn = turnFor(hostTurn, undefined);
|
|
454
|
+
if (turn !== undefined && turn.trigger === undefined)
|
|
455
|
+
turn.trigger = { source, text };
|
|
456
|
+
},
|
|
457
|
+
closingMessage: (hostTurn, messageId) => {
|
|
458
|
+
const turn = turnFor(hostTurn, undefined);
|
|
459
|
+
if (turn !== undefined)
|
|
460
|
+
turn.closingMessageId = messageId;
|
|
461
|
+
},
|
|
462
|
+
request: (hostTurn, request) => {
|
|
463
|
+
if (request.compaction) {
|
|
464
|
+
turns.push({ hostTurn: -turns.length - 1, purpose: 'compaction', query: 'compaction', startedAt: request.startedAt, endedAt: request.settledAt ?? request.startedAt, endReasonKind: 'completed', requests: [request], delegations: [] });
|
|
465
|
+
}
|
|
466
|
+
else
|
|
467
|
+
turnFor(hostTurn, request.startedAt)?.requests.push(request);
|
|
468
|
+
},
|
|
469
|
+
delegation: (delegation) => {
|
|
470
|
+
place(delegation);
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
foldLogRows(cursor, rows, sink);
|
|
474
|
+
// An external call still in progress shows the steps it wrote so far.
|
|
475
|
+
flushDeferred(cursor, [], sink);
|
|
476
|
+
// A delegation whose result never landed (a crashed turn) still surfaces.
|
|
477
|
+
for (const pending of cursor.pendingDelegations.values())
|
|
478
|
+
place(pending);
|
|
479
|
+
if (orphans.length > 0) {
|
|
480
|
+
turns.push({ hostTurn: -1, purpose: 'room-members', query: '', startedAt: orphans[0].at, endedAt: orphans.at(-1).at, endReasonKind: 'completed', requests: [], delegations: orphans });
|
|
481
|
+
}
|
|
482
|
+
return turns;
|
|
483
|
+
}
|
|
484
|
+
/** The delegation tool family whose calls pair with `subagent/catalog` events. */
|
|
485
|
+
function isDelegationTool(name) {
|
|
486
|
+
return /^subagent[_-]/.test(name);
|
|
487
|
+
}
|
|
488
|
+
/** The delegation label out of the call arguments (`description` is the subagent_dsh convention). */
|
|
489
|
+
function delegationLabel(args) {
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(args);
|
|
492
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
493
|
+
const description = parsed.description;
|
|
494
|
+
if (typeof description === 'string' && description.length > 0)
|
|
495
|
+
return { label: description };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// Unparseable model arguments simply carry no label.
|
|
500
|
+
}
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
/** The turn-list query preview: one collapsed line, capped. */
|
|
504
|
+
function collapseQuery(text) {
|
|
505
|
+
return text.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* A log without request headers (an external agent's child: its CLI owns the
|
|
509
|
+
* request) still names who answered on the assistant message's source; the
|
|
510
|
+
* rest of the request config was never recorded and stays absent.
|
|
511
|
+
*/
|
|
512
|
+
function configFromSource(raw) {
|
|
513
|
+
const source = (typeof raw === 'object' && raw !== null ? raw['source'] : undefined);
|
|
514
|
+
return {
|
|
515
|
+
...typeof source?.['provider'] === 'string' ? { provider: source['provider'] } : {},
|
|
516
|
+
...typeof source?.['model'] === 'string' ? { model: source['model'] } : {},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/** One settled request out of its assistant event: stream timing, usage, and the segment split. */
|
|
520
|
+
function buildRequest(cursor, row, data, failed) {
|
|
521
|
+
const written = cursor.stepStartAt ?? row.time ?? 0;
|
|
522
|
+
// Written with its answer: the step opened after the call was over.
|
|
523
|
+
const late = cursor.external === true && row.time !== undefined && row.time - written < MIRROR_LAG_MS && cursor.stepPrevAt !== undefined;
|
|
524
|
+
const startedAt = late ? cursor.stepPrevAt : written;
|
|
525
|
+
const settledAt = row.time ?? startedAt;
|
|
526
|
+
const stream = Array.isArray(data['stream']) ? data['stream'] : [];
|
|
527
|
+
const streamFacts = readStream(stream);
|
|
528
|
+
const usage = readUsage(data['usage']);
|
|
529
|
+
const hostTurn = typeof data['turn'] === 'number' ? data['turn'] : cursor.turnHost;
|
|
530
|
+
const messages = cursor.messages.map(folded => folded.message);
|
|
531
|
+
let segments;
|
|
532
|
+
if (cursor.materialize?.(hostTurn) === false) {
|
|
533
|
+
// Outside the window: keep the structure, not the copied context.
|
|
534
|
+
cursor.previousMessages = messages;
|
|
535
|
+
cursor.previousToolNames = (cursor.tools ?? []).map(tool => tool.name);
|
|
536
|
+
cursor.previous = undefined;
|
|
537
|
+
segments = { system: [], tools: [], history: [], added: [], totalChars: 0, fingerprint: { messageKeys: [], toolNames: [] } };
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
if (cursor.previous === undefined && cursor.previousMessages !== undefined) {
|
|
541
|
+
cursor.previous = { messageKeys: cursor.previousMessages.map(message => messageKey(message, contentHash)), toolNames: cursor.previousToolNames ?? [] };
|
|
542
|
+
}
|
|
543
|
+
delete cursor.previousMessages;
|
|
544
|
+
segments = splitRequest({
|
|
545
|
+
messages,
|
|
546
|
+
...cursor.tools === undefined ? {} : { tools: cursor.tools },
|
|
547
|
+
context: {
|
|
548
|
+
...cursor.previous === undefined ? {} : { previous: cursor.previous },
|
|
549
|
+
assemblies: [],
|
|
550
|
+
resolveToolName: callId => cursor.callNames.get(callId),
|
|
551
|
+
durationFor: messageId => cursor.messageDurations.get(messageId),
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
cursor.previous = segments.fingerprint;
|
|
555
|
+
}
|
|
556
|
+
const message = failed ? undefined : fromLogMessage(data['message']);
|
|
557
|
+
const blocks = message !== undefined
|
|
558
|
+
? spansFromMessage(message, streamFacts, startedAt)
|
|
559
|
+
: streamFacts.blocks;
|
|
560
|
+
// The response's own content: the committed message is authoritative; an
|
|
561
|
+
// attempt (failed) keeps whatever its stream delivered.
|
|
562
|
+
const response = message !== undefined
|
|
563
|
+
? responseFromMessage(message)
|
|
564
|
+
: responseFromSpans(streamFacts.blocks);
|
|
565
|
+
const finish = streamFacts.finish;
|
|
566
|
+
const error = failed
|
|
567
|
+
? (finish?.failure ?? 'attempt settled without a committed message')
|
|
568
|
+
: undefined;
|
|
569
|
+
const finishKind = finish?.kind;
|
|
570
|
+
return {
|
|
571
|
+
...cursor.numberedFromStart ? { requestNumber: cursor.requests.length + 1 } : {},
|
|
572
|
+
...hostTurn === undefined ? {} : { hostTurn },
|
|
573
|
+
startedAt,
|
|
574
|
+
status: failed ? 'failed' : 'ok',
|
|
575
|
+
...error === undefined ? {} : { error },
|
|
576
|
+
config: cursor.config ?? configFromSource(data['message']),
|
|
577
|
+
...usage === undefined ? {} : { usage },
|
|
578
|
+
responseSummary: message !== undefined ? summarizeMessage(message) : summarizeBlocks(streamFacts.blocks),
|
|
579
|
+
...response === undefined ? {} : { response },
|
|
580
|
+
segments: {
|
|
581
|
+
system: segments.system,
|
|
582
|
+
tools: segments.tools,
|
|
583
|
+
history: segments.history,
|
|
584
|
+
added: segments.added,
|
|
585
|
+
},
|
|
586
|
+
totalChars: segments.totalChars,
|
|
587
|
+
...streamFacts.firstChunkAt === undefined ? {} : { firstChunkAt: streamFacts.firstChunkAt },
|
|
588
|
+
settledAt,
|
|
589
|
+
...finishKind === undefined ? {} : { finishKind },
|
|
590
|
+
blocks,
|
|
591
|
+
execs: [],
|
|
592
|
+
fingerprint: segments.fingerprint,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/** Block spans for a committed response: stream timing with the message's tool-call identity. */
|
|
596
|
+
function spansFromMessage(message, facts, fallbackStart) {
|
|
597
|
+
const calls = message.content.filter(block => block.type === 'tool-call');
|
|
598
|
+
const spans = facts.blocks.map(span => ({ ...span }));
|
|
599
|
+
for (const span of spans) {
|
|
600
|
+
if (span.kind !== 'tool-call')
|
|
601
|
+
continue;
|
|
602
|
+
// The stream's block-end carries the assembled block on the in-process
|
|
603
|
+
// lane; the log's compact records do too (raw chunk records), so the span
|
|
604
|
+
// may already know its call. Match by stream order otherwise.
|
|
605
|
+
if (span.callId !== undefined)
|
|
606
|
+
continue;
|
|
607
|
+
const call = calls.find(block => block.id !== undefined
|
|
608
|
+
&& !spans.some(other => other !== span && other.callId === block.id));
|
|
609
|
+
if (call?.id !== undefined) {
|
|
610
|
+
span.callId = call.id;
|
|
611
|
+
if (call.name !== undefined)
|
|
612
|
+
span.toolName = call.name;
|
|
613
|
+
if (call.arguments !== undefined)
|
|
614
|
+
span.args = call.arguments;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (spans.length === 0 && message.content.length > 0) {
|
|
618
|
+
// A response with no surviving block records (compact runs only): one
|
|
619
|
+
// coarse span keeps the lane honest about when the response was live.
|
|
620
|
+
return [{ kind: 'text', index: 0, start: facts.firstChunkAt ?? fallbackStart }];
|
|
621
|
+
}
|
|
622
|
+
return spans;
|
|
623
|
+
}
|
|
624
|
+
/** A deferred call's step that never got its usage step still counts as a request. */
|
|
625
|
+
function flushDeferred(cursor, built, sink) {
|
|
626
|
+
const deferred = cursor.deferred;
|
|
627
|
+
if (deferred === undefined)
|
|
628
|
+
return;
|
|
629
|
+
delete cursor.deferred;
|
|
630
|
+
built.push(deferred.request);
|
|
631
|
+
cursor.requests.push(deferred.request);
|
|
632
|
+
sink.request?.(deferred.hostTurn, deferred.request);
|
|
633
|
+
}
|
|
634
|
+
/** Fold one more step of a call into the request the call began (its input stays the first step's). */
|
|
635
|
+
function joinCall(request, step, blocks) {
|
|
636
|
+
const message = { role: 'assistant', content: blocks };
|
|
637
|
+
const response = responseFromMessage(message);
|
|
638
|
+
if (response !== undefined)
|
|
639
|
+
request.response = response;
|
|
640
|
+
request.responseSummary = summarizeMessage(message);
|
|
641
|
+
request.status = step.status;
|
|
642
|
+
if (step.error !== undefined)
|
|
643
|
+
request.error = step.error;
|
|
644
|
+
if (step.usage !== undefined)
|
|
645
|
+
request.usage = step.usage;
|
|
646
|
+
if (step.settledAt !== undefined)
|
|
647
|
+
request.settledAt = step.settledAt;
|
|
648
|
+
if (step.finishKind !== undefined)
|
|
649
|
+
request.finishKind = step.finishKind;
|
|
650
|
+
request.blocks.push(...step.blocks);
|
|
651
|
+
request.execs.push(...step.execs);
|
|
652
|
+
}
|
|
653
|
+
/** The response's own content from a committed assistant message (authoritative). */
|
|
654
|
+
function responseFromMessage(message) {
|
|
655
|
+
const reasoning = message.content
|
|
656
|
+
.filter(block => block.type === 'reasoning')
|
|
657
|
+
.map(block => block.text ?? '')
|
|
658
|
+
.join('');
|
|
659
|
+
const text = message.content
|
|
660
|
+
.filter(block => block.type === 'text')
|
|
661
|
+
.map(block => block.text ?? '')
|
|
662
|
+
.join('');
|
|
663
|
+
const toolCalls = message.content
|
|
664
|
+
.filter(block => block.type === 'tool-call' && block.name !== undefined)
|
|
665
|
+
.map(block => ({ ...block.id === undefined ? {} : { callId: block.id }, name: block.name, arguments: block.arguments ?? '' }));
|
|
666
|
+
if (reasoning === '' && text === '' && toolCalls.length === 0)
|
|
667
|
+
return undefined;
|
|
668
|
+
return {
|
|
669
|
+
...reasoning === '' ? {} : { reasoning },
|
|
670
|
+
...text === '' ? {} : { text },
|
|
671
|
+
toolCalls,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/** The response hint from a committed message's blocks (structured 明细头锚点). */
|
|
675
|
+
function summarizeMessage(message) {
|
|
676
|
+
const calls = message.content.filter(block => block.type === 'tool-call' && block.name !== undefined);
|
|
677
|
+
if (calls.length > 0) {
|
|
678
|
+
const first = calls[0];
|
|
679
|
+
const hint = hintFromArgs(first.arguments);
|
|
680
|
+
return {
|
|
681
|
+
kind: 'tool',
|
|
682
|
+
name: first.name,
|
|
683
|
+
...hint === undefined ? {} : { hint },
|
|
684
|
+
...calls.length > 1 ? { more: calls.length - 1, toolCounts: countToolCalls(calls.map(call => call.name)) } : {},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const hasText = message.content.some(block => block.type === 'text' && (block.text ?? '').trim().length > 0);
|
|
688
|
+
if (hasText)
|
|
689
|
+
return { kind: 'text' };
|
|
690
|
+
const hasReasoning = message.content.some(block => block.type === 'reasoning' && (block.text ?? '').trim().length > 0);
|
|
691
|
+
return hasReasoning ? { kind: 'reasoning' } : { kind: 'none' };
|
|
692
|
+
}
|
|
693
|
+
/** The response summary when only stream spans survived (an attempt). */
|
|
694
|
+
function summarizeBlocks(blocks) {
|
|
695
|
+
const calls = blocks.filter(block => block.kind === 'tool-call' && block.toolName !== undefined);
|
|
696
|
+
if (calls.length === 0) {
|
|
697
|
+
if (blocks.some(block => block.kind === 'text'))
|
|
698
|
+
return { kind: 'text' };
|
|
699
|
+
return blocks.some(block => block.kind === 'reasoning') ? { kind: 'reasoning' } : { kind: 'none' };
|
|
700
|
+
}
|
|
701
|
+
const first = calls[0];
|
|
702
|
+
const hint = hintFromArgs(first.args);
|
|
703
|
+
return {
|
|
704
|
+
kind: 'tool',
|
|
705
|
+
name: first.toolName,
|
|
706
|
+
...hint === undefined ? {} : { hint },
|
|
707
|
+
...calls.length > 1 ? { more: calls.length - 1, toolCounts: countToolCalls(calls.map(call => call.toolName)) } : {},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
/** The one-line argument hint — the reference reconstruction's key order. */
|
|
711
|
+
function hintFromArgs(args) {
|
|
712
|
+
if (args === undefined)
|
|
713
|
+
return undefined;
|
|
714
|
+
let parsed;
|
|
715
|
+
try {
|
|
716
|
+
parsed = JSON.parse(args);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
722
|
+
return undefined;
|
|
723
|
+
const value = parsed;
|
|
724
|
+
const what = value['what'];
|
|
725
|
+
if (typeof what === 'string' && what.length > 0)
|
|
726
|
+
return previewLine(what, 40);
|
|
727
|
+
const file = value['file_path'] ?? value['path'] ?? value['filePath'];
|
|
728
|
+
if (typeof file === 'string' && file.length > 0)
|
|
729
|
+
return previewLine(file.split('/').pop() ?? file, 40);
|
|
730
|
+
const purpose = value['for'];
|
|
731
|
+
if (typeof purpose === 'string' && purpose.length > 0)
|
|
732
|
+
return previewLine(purpose, 40);
|
|
733
|
+
const description = value['description'];
|
|
734
|
+
if (typeof description === 'string' && description.length > 0)
|
|
735
|
+
return previewLine(description, 40);
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Block timing out of one compact stream record list (`AssistantStreamRecord`):
|
|
740
|
+
* block-start/block-end ride as raw `chunk` records, deltas pack into runs
|
|
741
|
+
* whose `time0` still counts toward the first-chunk time.
|
|
742
|
+
*/
|
|
743
|
+
function readStream(records) {
|
|
744
|
+
const blocks = [];
|
|
745
|
+
let firstChunkAt;
|
|
746
|
+
let finish;
|
|
747
|
+
const note = (time) => {
|
|
748
|
+
if (typeof time !== 'number')
|
|
749
|
+
return;
|
|
750
|
+
if (firstChunkAt === undefined || time < firstChunkAt)
|
|
751
|
+
firstChunkAt = time;
|
|
752
|
+
};
|
|
753
|
+
for (const raw of records) {
|
|
754
|
+
if (typeof raw !== 'object' || raw === null)
|
|
755
|
+
continue;
|
|
756
|
+
const record = raw;
|
|
757
|
+
if (record['type'] === 'chunk') {
|
|
758
|
+
const time = record['time'];
|
|
759
|
+
const chunk = record['chunk'];
|
|
760
|
+
if (typeof time !== 'number' || chunk === undefined)
|
|
761
|
+
continue;
|
|
762
|
+
note(time);
|
|
763
|
+
switch (chunk['type']) {
|
|
764
|
+
case 'block-start':
|
|
765
|
+
blocks.push({
|
|
766
|
+
kind: typeof chunk['blockType'] === 'string' ? chunk['blockType'] : 'unknown',
|
|
767
|
+
index: typeof chunk['index'] === 'number' ? chunk['index'] : blocks.length,
|
|
768
|
+
start: time,
|
|
769
|
+
});
|
|
770
|
+
break;
|
|
771
|
+
case 'block-end': {
|
|
772
|
+
const index = chunk['index'];
|
|
773
|
+
const span = blocks.find(block => block.index === index && block.end === undefined);
|
|
774
|
+
if (span !== undefined) {
|
|
775
|
+
span.end = time;
|
|
776
|
+
const block = chunk['block'];
|
|
777
|
+
// The assembled block is authoritative over the accumulated runs.
|
|
778
|
+
if (typeof block?.['text'] === 'string')
|
|
779
|
+
span.text = block['text'];
|
|
780
|
+
if (block?.['type'] === 'tool-call') {
|
|
781
|
+
if (typeof block['id'] === 'string')
|
|
782
|
+
span.callId = block['id'];
|
|
783
|
+
if (typeof block['name'] === 'string')
|
|
784
|
+
span.toolName = block['name'];
|
|
785
|
+
if (typeof block['arguments'] === 'string')
|
|
786
|
+
span.args = block['arguments'];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case 'finish': {
|
|
792
|
+
const reason = chunk['reason'];
|
|
793
|
+
const kind = typeof reason?.['kind'] === 'string' ? reason['kind'] : undefined;
|
|
794
|
+
const failure = reason?.['failure'];
|
|
795
|
+
finish = {
|
|
796
|
+
kind: kind ?? 'unknown',
|
|
797
|
+
...typeof failure?.['message'] === 'string' ? { failure: failure['message'] } : {},
|
|
798
|
+
};
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
default:
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// Packed delta runs: their time0 is the run's first chunk's time. Their
|
|
807
|
+
// fragments accumulate onto the open span so a torn stream keeps the
|
|
808
|
+
// partial response (the attempt path reads it).
|
|
809
|
+
note(record['time0']);
|
|
810
|
+
const span = blocks.find(block => block.index === record['index'] && block.end === undefined);
|
|
811
|
+
if (span !== undefined) {
|
|
812
|
+
if (Array.isArray(record['texts'])) {
|
|
813
|
+
span.text = (span.text ?? '') + record['texts'].join('');
|
|
814
|
+
}
|
|
815
|
+
else if (Array.isArray(record['args'])) {
|
|
816
|
+
span.args = (span.args ?? '') + record['args'].join('');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
...firstChunkAt === undefined ? {} : { firstChunkAt },
|
|
823
|
+
blocks,
|
|
824
|
+
...finish === undefined ? {} : { finish },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/** Structural read of one usage object (provider figures, cache fields optional). */
|
|
828
|
+
function readUsage(raw) {
|
|
829
|
+
if (typeof raw !== 'object' || raw === null)
|
|
830
|
+
return undefined;
|
|
831
|
+
const value = raw;
|
|
832
|
+
if (typeof value['inputTokens'] !== 'number' || typeof value['outputTokens'] !== 'number')
|
|
833
|
+
return undefined;
|
|
834
|
+
return {
|
|
835
|
+
inputTokens: value['inputTokens'],
|
|
836
|
+
outputTokens: value['outputTokens'],
|
|
837
|
+
...typeof value['totalTokens'] === 'number' ? { totalTokens: value['totalTokens'] } : {},
|
|
838
|
+
...typeof value['cacheReadTokens'] === 'number' ? { cacheReadTokens: value['cacheReadTokens'] } : {},
|
|
839
|
+
...typeof value['cacheWriteTokens'] === 'number' ? { cacheWriteTokens: value['cacheWriteTokens'] } : {},
|
|
840
|
+
};
|
|
841
|
+
}
|