@assistant-ui/react-ag-ui 0.1.5 → 0.1.7
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/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +80 -13
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +37 -31
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +363 -218
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/package.json +2 -2
- package/src/runtime/AgUiThreadController.disconnect.test.ts +198 -3
- package/src/runtime/AgUiThreadController.replay.test.ts +57 -0
- package/src/runtime/AgUiThreadController.snapshot-head-history.test.ts +34 -2
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +510 -0
- package/src/runtime/AgUiThreadController.ts +179 -28
- package/src/runtime/adapter/run-aggregator.routing.test.ts +359 -0
- package/src/runtime/adapter/run-aggregator.ts +539 -258
|
@@ -175,20 +175,110 @@ export const isPlainObject = (
|
|
|
175
175
|
): value is Record<string, unknown> =>
|
|
176
176
|
!!value && typeof value === "object" && !Array.isArray(value);
|
|
177
177
|
|
|
178
|
+
// One assistant message worth of parts. A run that moves on to another
|
|
179
|
+
// message keeps the previous buffer, so later events naming it still land.
|
|
180
|
+
type MessageBuffer = {
|
|
181
|
+
id: string | undefined;
|
|
182
|
+
textParts: Map<string, { buffer: string }>;
|
|
183
|
+
openTextFrames: Set<string>;
|
|
184
|
+
activeTextMessageIdByScope: Map<string, string>;
|
|
185
|
+
reasoningParts: Map<string, { buffer: string; timing?: MessagePartTiming }>;
|
|
186
|
+
reasoningSignatures: Map<string, string>;
|
|
187
|
+
reasoningSignatureIds: Map<string, string>;
|
|
188
|
+
hiddenSignatures: Map<string, string>;
|
|
189
|
+
hiddenSignatureAnchors: Map<string, number>;
|
|
190
|
+
hiddenBlockAnchors: Map<string, number>;
|
|
191
|
+
hiddenReasoningIds: Set<string>;
|
|
192
|
+
hiddenActiveReasoningId: string | undefined;
|
|
193
|
+
hasEmittedOpaqueReasoning: boolean;
|
|
194
|
+
loggedDroppedOpaqueIds: Set<string>;
|
|
195
|
+
reasoningMessageIds: Map<string, string>;
|
|
196
|
+
activeReasoningKeyByScope: Map<string, string>;
|
|
197
|
+
subagentRuns: Map<string, SubagentRunState>;
|
|
198
|
+
// The nesting graph only moves when a run is announced or a tool call is
|
|
199
|
+
// created, which is rare next to emit(), so it is derived once per change
|
|
200
|
+
// rather than per streamed delta.
|
|
201
|
+
nestingMemo:
|
|
202
|
+
| {
|
|
203
|
+
subagentsByParentToolCallId: Map<string, string[]>;
|
|
204
|
+
reachable: Set<string>;
|
|
205
|
+
}
|
|
206
|
+
| undefined;
|
|
207
|
+
toolCalls: Map<string, ToolCallState>;
|
|
208
|
+
a2uiBuckets: Map<string, A2uiState>;
|
|
209
|
+
a2uiToolCallIds: Set<string>;
|
|
210
|
+
activityParts: Map<
|
|
211
|
+
string,
|
|
212
|
+
{ kind: "data"; name: string; value: unknown; subagentRunId?: string }
|
|
213
|
+
>;
|
|
214
|
+
lastResolvedToolCallIdByScope: Map<string, string>;
|
|
215
|
+
partOrder: PartOrderEntry[];
|
|
216
|
+
lastReasoningBeforeToolCall: PartOrderEntry | undefined;
|
|
217
|
+
textPartCounter: number;
|
|
218
|
+
reportedServerMessageId: string | undefined;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const createMessageBuffer = (id?: string): MessageBuffer => ({
|
|
222
|
+
id,
|
|
223
|
+
textParts: new Map(),
|
|
224
|
+
openTextFrames: new Set(),
|
|
225
|
+
activeTextMessageIdByScope: new Map(),
|
|
226
|
+
reasoningParts: new Map(),
|
|
227
|
+
reasoningSignatures: new Map(),
|
|
228
|
+
reasoningSignatureIds: new Map(),
|
|
229
|
+
hiddenSignatures: new Map(),
|
|
230
|
+
hiddenSignatureAnchors: new Map(),
|
|
231
|
+
hiddenBlockAnchors: new Map(),
|
|
232
|
+
hiddenReasoningIds: new Set(),
|
|
233
|
+
hiddenActiveReasoningId: undefined,
|
|
234
|
+
hasEmittedOpaqueReasoning: false,
|
|
235
|
+
loggedDroppedOpaqueIds: new Set(),
|
|
236
|
+
reasoningMessageIds: new Map(),
|
|
237
|
+
activeReasoningKeyByScope: new Map(),
|
|
238
|
+
subagentRuns: new Map(),
|
|
239
|
+
nestingMemo: undefined,
|
|
240
|
+
toolCalls: new Map(),
|
|
241
|
+
a2uiBuckets: new Map(),
|
|
242
|
+
a2uiToolCallIds: new Set(),
|
|
243
|
+
activityParts: new Map(),
|
|
244
|
+
lastResolvedToolCallIdByScope: new Map(),
|
|
245
|
+
partOrder: [],
|
|
246
|
+
lastReasoningBeforeToolCall: undefined,
|
|
247
|
+
textPartCounter: 0,
|
|
248
|
+
reportedServerMessageId: undefined,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export type RetainedMessageState = {
|
|
252
|
+
messageId: string;
|
|
253
|
+
/** A root text frame or a sub-agent run is still open. */
|
|
254
|
+
streaming: boolean;
|
|
255
|
+
openToolCallIds: string[];
|
|
256
|
+
};
|
|
257
|
+
|
|
178
258
|
export type RunAggregatorOptions = {
|
|
179
259
|
showThinking: boolean;
|
|
180
260
|
joinStrategy?: JoinStrategy | undefined;
|
|
181
261
|
logger: Logger;
|
|
182
262
|
emit: Emit;
|
|
263
|
+
onRetainedMessage?: (
|
|
264
|
+
update: ChatModelRunResult,
|
|
265
|
+
state: RetainedMessageState,
|
|
266
|
+
) => void;
|
|
267
|
+
canRetainMessage?: (messageId: string) => boolean;
|
|
183
268
|
onServerMessageId?: (messageId: string) => void;
|
|
184
|
-
|
|
269
|
+
/** Returns the id the outgoing message lives under, when it may keep receiving events. */
|
|
270
|
+
onTextMessageStart?: (
|
|
271
|
+
messageId: string,
|
|
272
|
+
openToolCallIds: string[],
|
|
273
|
+
) => string | void;
|
|
185
274
|
};
|
|
186
275
|
|
|
187
276
|
/**
|
|
188
277
|
* Collects AG-UI events into assistant-ui run snapshots that can be yielded from a ChatModelAdapter.
|
|
189
278
|
*
|
|
190
|
-
*
|
|
191
|
-
* emits a fresh snapshot through
|
|
279
|
+
* Each incoming event updates the parts of the assistant message it names and
|
|
280
|
+
* emits a fresh snapshot: through `emit` for the message the run streams into,
|
|
281
|
+
* and through `onRetainedMessage` for one it moved on from. `CUSTOM` events
|
|
192
282
|
* become canonical data parts; integration plumbing names such as
|
|
193
283
|
* `on_interrupt`, `PredictState`, `Exit`, `hook_error`, `state_update_error`,
|
|
194
284
|
* `system:*`, and `MultiAgentHandoff` are forwarded the same way, so apps only
|
|
@@ -200,54 +290,21 @@ export class RunAggregator {
|
|
|
200
290
|
private readonly joinStrategy: JoinStrategy | undefined;
|
|
201
291
|
private readonly logger: Logger;
|
|
202
292
|
private readonly onServerMessageId: ((messageId: string) => void) | undefined;
|
|
203
|
-
private readonly onTextMessageStart:
|
|
204
|
-
|
|
205
|
-
|
|
293
|
+
private readonly onTextMessageStart: RunAggregatorOptions["onTextMessageStart"];
|
|
294
|
+
private readonly onRetainedMessage: RunAggregatorOptions["onRetainedMessage"];
|
|
295
|
+
private readonly canRetainMessage: RunAggregatorOptions["canRetainMessage"];
|
|
206
296
|
|
|
207
297
|
private status: ChatModelRunResult["status"] | undefined;
|
|
208
298
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
209
299
|
private pendingToolCallIds: string[] | undefined;
|
|
210
|
-
private readonly textParts = new Map<string, { buffer: string }>();
|
|
211
300
|
private readonly pendingSnapshotText = new Map<string, string>();
|
|
212
|
-
private
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>();
|
|
217
|
-
private
|
|
218
|
-
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
219
|
-
private readonly hiddenSignatures = new Map<string, string>();
|
|
220
|
-
private readonly hiddenSignatureAnchors = new Map<string, number>();
|
|
221
|
-
private readonly hiddenBlockAnchors = new Map<string, number>();
|
|
222
|
-
private readonly hiddenReasoningIds = new Set<string>();
|
|
223
|
-
private hiddenActiveReasoningId: string | undefined;
|
|
224
|
-
private hasEmittedOpaqueReasoning = false;
|
|
225
|
-
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
226
|
-
private readonly reasoningMessageIds = new Map<string, string>();
|
|
227
|
-
private readonly activeReasoningKeyByScope = new Map<string, string>();
|
|
228
|
-
private readonly subagentRuns = new Map<string, SubagentRunState>();
|
|
229
|
-
// The nesting graph only moves when a run is announced or a tool call is
|
|
230
|
-
// created, which is rare next to emit(), so it is derived once per change
|
|
231
|
-
// rather than per streamed delta.
|
|
232
|
-
private nestingMemo:
|
|
233
|
-
| {
|
|
234
|
-
subagentsByParentToolCallId: Map<string, string[]>;
|
|
235
|
-
reachable: Set<string>;
|
|
236
|
-
}
|
|
237
|
-
| undefined;
|
|
238
|
-
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
239
|
-
private readonly a2uiBuckets = new Map<string, A2uiState>();
|
|
240
|
-
private readonly a2uiToolCallIds = new Set<string>();
|
|
241
|
-
private readonly activityParts = new Map<
|
|
242
|
-
string,
|
|
243
|
-
{ kind: "data"; name: string; value: unknown; subagentRunId?: string }
|
|
244
|
-
>();
|
|
245
|
-
private readonly lastResolvedToolCallIdByScope = new Map<string, string>();
|
|
246
|
-
private readonly partOrder: PartOrderEntry[] = [];
|
|
247
|
-
private lastReasoningBeforeToolCall: PartOrderEntry | undefined;
|
|
248
|
-
private textPartCounter = 0;
|
|
301
|
+
private current: MessageBuffer = createMessageBuffer();
|
|
302
|
+
// The buffer the event being handled routes to; equal to `current` except
|
|
303
|
+
// while an event naming a moved-off message is applied.
|
|
304
|
+
private buf: MessageBuffer = this.current;
|
|
305
|
+
private readonly retained = new Map<string, MessageBuffer>();
|
|
306
|
+
private unannounced: MessageBuffer | undefined;
|
|
249
307
|
private serverMessageIdReported = false;
|
|
250
|
-
private reportedServerMessageId: string | undefined;
|
|
251
308
|
private lastTextMessageId: string | undefined;
|
|
252
309
|
|
|
253
310
|
private streamStartTime: number | undefined;
|
|
@@ -261,31 +318,229 @@ export class RunAggregator {
|
|
|
261
318
|
this.logger = options.logger;
|
|
262
319
|
this.onServerMessageId = options.onServerMessageId;
|
|
263
320
|
this.onTextMessageStart = options.onTextMessageStart;
|
|
321
|
+
this.onRetainedMessage = options.onRetainedMessage;
|
|
322
|
+
this.canRetainMessage = options.canRetainMessage;
|
|
264
323
|
}
|
|
265
324
|
|
|
266
325
|
hasToolCall(toolCallId: string): boolean {
|
|
267
|
-
return this.toolCalls.has(toolCallId);
|
|
326
|
+
return this.buffers().some((buffer) => buffer.toolCalls.has(toolCallId));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
isRetained(messageId: string): boolean {
|
|
330
|
+
return this.retained.has(messageId);
|
|
268
331
|
}
|
|
269
332
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Makes `messageId` the message the run streams into. The previous message
|
|
335
|
+
* keeps its parts under `previousId`, and returns true when it does; call
|
|
336
|
+
* `announceRetained` to publish its state once the caller can apply it.
|
|
337
|
+
*/
|
|
338
|
+
rebaseMessage(messageId: string, previousId?: string): boolean {
|
|
339
|
+
const retained = this.openMessage(messageId, previousId);
|
|
273
340
|
this.lastTextMessageId = messageId;
|
|
274
|
-
this.reportedServerMessageId = messageId;
|
|
275
|
-
if (snapshotText !== undefined) {
|
|
276
|
-
this.pendingSnapshotText.set(messageId, snapshotText);
|
|
277
|
-
}
|
|
341
|
+
this.current.reportedServerMessageId = messageId;
|
|
278
342
|
this.startTextMessage(ROOT_SCOPE, messageId);
|
|
343
|
+
if (retained) this.unannounced = retained;
|
|
344
|
+
return retained !== undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
announceRetained(): void {
|
|
348
|
+
const buffer = this.unannounced;
|
|
349
|
+
this.unannounced = undefined;
|
|
350
|
+
if (buffer && this.retained.get(buffer.id!) === buffer)
|
|
351
|
+
this.withBuffer(buffer, () => this.emit());
|
|
279
352
|
}
|
|
280
353
|
|
|
281
354
|
handle(event: AgUiEvent): void {
|
|
355
|
+
const owner = this.ownerOf(event);
|
|
356
|
+
if (owner === this.current) this.apply(event);
|
|
357
|
+
else this.withBuffer(owner, () => this.apply(event));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private buffers(): MessageBuffer[] {
|
|
361
|
+
return [this.current, ...this.retained.values()];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private withBuffer(buffer: MessageBuffer, fn: () => void): void {
|
|
365
|
+
const previous = this.buf;
|
|
366
|
+
this.buf = buffer;
|
|
367
|
+
try {
|
|
368
|
+
fn();
|
|
369
|
+
} finally {
|
|
370
|
+
this.buf = previous;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Starts a fresh current message (or resumes a retained one) and keeps the
|
|
375
|
+
// outgoing one addressable by id, so events that still name it route back.
|
|
376
|
+
private openMessage(
|
|
377
|
+
messageId: string,
|
|
378
|
+
previousId: string | undefined,
|
|
379
|
+
): MessageBuffer | undefined {
|
|
380
|
+
const outgoing = this.current;
|
|
381
|
+
const next = this.retained.get(messageId) ?? createMessageBuffer(messageId);
|
|
382
|
+
this.retained.delete(messageId);
|
|
383
|
+
let retained: MessageBuffer | undefined;
|
|
384
|
+
if (previousId !== undefined && previousId !== messageId) {
|
|
385
|
+
outgoing.id = previousId;
|
|
386
|
+
this.retained.set(previousId, outgoing);
|
|
387
|
+
retained = outgoing;
|
|
388
|
+
}
|
|
389
|
+
this.current = next;
|
|
390
|
+
this.buf = next;
|
|
391
|
+
return retained;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private ownerForMessage(messageId: string | undefined): MessageBuffer {
|
|
395
|
+
if (!messageId || messageId === this.current.id) return this.current;
|
|
396
|
+
const retained = this.retained.get(messageId);
|
|
397
|
+
if (retained) return retained;
|
|
398
|
+
// A snapshot can deliver a message before the events that finish it, as
|
|
399
|
+
// a reconnect replay does.
|
|
400
|
+
if (this.canRetainMessage?.(messageId)) {
|
|
401
|
+
const buffer = createMessageBuffer(messageId);
|
|
402
|
+
this.retained.set(messageId, buffer);
|
|
403
|
+
if (this.pendingSnapshotText.has(messageId))
|
|
404
|
+
this.withBuffer(buffer, () =>
|
|
405
|
+
this.startTextMessage(ROOT_SCOPE, messageId),
|
|
406
|
+
);
|
|
407
|
+
return buffer;
|
|
408
|
+
}
|
|
409
|
+
return this.current;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private ownerForToolCall(toolCallId: unknown): MessageBuffer | undefined {
|
|
413
|
+
if (typeof toolCallId !== "string") return undefined;
|
|
414
|
+
return this.buffers().find((buffer) => buffer.toolCalls.has(toolCallId));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private ownerForSubagent(subagentRunId: string): MessageBuffer {
|
|
418
|
+
return (
|
|
419
|
+
this.buffers().find((buffer) => buffer.subagentRuns.has(subagentRunId)) ??
|
|
420
|
+
this.current
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private ownerForActivity(event: {
|
|
425
|
+
messageId?: string;
|
|
426
|
+
subagentRunId?: string;
|
|
427
|
+
}): MessageBuffer {
|
|
428
|
+
if (event.subagentRunId !== undefined)
|
|
429
|
+
return this.ownerForSubagent(event.subagentRunId);
|
|
430
|
+
if (event.messageId === undefined) return this.current;
|
|
431
|
+
const key = `message:${event.messageId}`;
|
|
432
|
+
return (
|
|
433
|
+
this.buffers().find((buffer) => buffer.activityParts.has(key)) ??
|
|
434
|
+
this.current
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private ownerOf(event: AgUiEvent): MessageBuffer {
|
|
439
|
+
if (this.joinStrategy === "concat-content") return this.current;
|
|
440
|
+
switch (event.type) {
|
|
441
|
+
case "TEXT_MESSAGE_START":
|
|
442
|
+
case "TEXT_MESSAGE_CONTENT":
|
|
443
|
+
case "TEXT_MESSAGE_END":
|
|
444
|
+
return event.subagentRunId !== undefined
|
|
445
|
+
? this.ownerForSubagent(event.subagentRunId)
|
|
446
|
+
: this.ownerForMessage(event.messageId);
|
|
447
|
+
case "TOOL_CALL_START":
|
|
448
|
+
return event.subagentRunId !== undefined
|
|
449
|
+
? this.ownerForSubagent(event.subagentRunId)
|
|
450
|
+
: this.ownerForMessage(event.parentMessageId);
|
|
451
|
+
case "TOOL_CALL_ARGS":
|
|
452
|
+
case "TOOL_CALL_END":
|
|
453
|
+
case "TOOL_CALL_RESULT":
|
|
454
|
+
return (
|
|
455
|
+
this.ownerForToolCall(event.toolCallId) ??
|
|
456
|
+
(event.subagentRunId !== undefined
|
|
457
|
+
? this.ownerForSubagent(event.subagentRunId)
|
|
458
|
+
: this.current)
|
|
459
|
+
);
|
|
460
|
+
case "SUBAGENT_STARTED": {
|
|
461
|
+
const existing = this.buffers().find((buffer) =>
|
|
462
|
+
buffer.subagentRuns.has(event.subagentRunId),
|
|
463
|
+
);
|
|
464
|
+
if (existing) return existing;
|
|
465
|
+
if (event.parentToolCallId !== undefined) {
|
|
466
|
+
const owner = this.ownerForToolCall(event.parentToolCallId);
|
|
467
|
+
if (owner) return owner;
|
|
468
|
+
}
|
|
469
|
+
if (event.parentSubagentRunId !== undefined)
|
|
470
|
+
return this.ownerForSubagent(event.parentSubagentRunId);
|
|
471
|
+
return this.ownerForMessage(event.parentMessageId);
|
|
472
|
+
}
|
|
473
|
+
case "SUBAGENT_FINISHED":
|
|
474
|
+
case "SUBAGENT_ERROR":
|
|
475
|
+
return this.ownerForSubagent(event.subagentRunId);
|
|
476
|
+
case "ACTIVITY_SNAPSHOT":
|
|
477
|
+
if (event.activityType === MCP_APPS_ACTIVITY_TYPE) {
|
|
478
|
+
const owner = this.ownerForToolCall(event.content.toolCallId);
|
|
479
|
+
if (owner) return owner;
|
|
480
|
+
}
|
|
481
|
+
if (event.activityType === A2UI_SURFACE_ACTIVITY_TYPE)
|
|
482
|
+
return this.current;
|
|
483
|
+
return this.ownerForActivity(event);
|
|
484
|
+
case "ACTIVITY_DELTA":
|
|
485
|
+
return this.ownerForActivity(event);
|
|
486
|
+
case "REASONING_START":
|
|
487
|
+
case "REASONING_MESSAGE_START":
|
|
488
|
+
case "REASONING_MESSAGE_CONTENT":
|
|
489
|
+
case "REASONING_MESSAGE_END":
|
|
490
|
+
case "REASONING_END":
|
|
491
|
+
case "REASONING_ENCRYPTED_VALUE": {
|
|
492
|
+
if (event.subagentRunId !== undefined)
|
|
493
|
+
return this.ownerForSubagent(event.subagentRunId);
|
|
494
|
+
const id =
|
|
495
|
+
event.type === "REASONING_ENCRYPTED_VALUE"
|
|
496
|
+
? event.entityId
|
|
497
|
+
: event.messageId;
|
|
498
|
+
return (
|
|
499
|
+
this.buffers().find(
|
|
500
|
+
(buffer) =>
|
|
501
|
+
buffer.reasoningParts.has(id) ||
|
|
502
|
+
buffer.hiddenReasoningIds.has(id),
|
|
503
|
+
) ?? this.current
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
default:
|
|
507
|
+
return this.current;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private retainedState(buffer: MessageBuffer): RetainedMessageState {
|
|
512
|
+
const openToolCallIds = Array.from(buffer.toolCalls.values())
|
|
513
|
+
.filter(
|
|
514
|
+
(call) =>
|
|
515
|
+
call.result === undefined &&
|
|
516
|
+
!buffer.a2uiToolCallIds.has(call.toolCallId),
|
|
517
|
+
)
|
|
518
|
+
.map((call) => call.toolCallId);
|
|
519
|
+
return {
|
|
520
|
+
messageId: buffer.id!,
|
|
521
|
+
openToolCallIds,
|
|
522
|
+
streaming:
|
|
523
|
+
buffer.openTextFrames.size > 0 ||
|
|
524
|
+
Array.from(buffer.subagentRuns.values()).some(
|
|
525
|
+
(run) => run.status.type === "running",
|
|
526
|
+
),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private apply(event: AgUiEvent): void {
|
|
282
531
|
switch (event.type) {
|
|
283
532
|
case "RUN_STARTED": {
|
|
284
|
-
this.
|
|
533
|
+
this.retained.clear();
|
|
534
|
+
this.unannounced = undefined;
|
|
535
|
+
const { hasEmittedOpaqueReasoning } = this.current;
|
|
536
|
+
this.current = createMessageBuffer();
|
|
537
|
+
// A resumed message must still retract what the previous run published.
|
|
538
|
+
this.current.hasEmittedOpaqueReasoning = hasEmittedOpaqueReasoning;
|
|
539
|
+
this.buf = this.current;
|
|
540
|
+
this.pendingSnapshotText.clear();
|
|
285
541
|
this.interrupts = undefined;
|
|
286
542
|
this.serverMessageIdReported = false;
|
|
287
543
|
this.lastTextMessageId = undefined;
|
|
288
|
-
this.reportedServerMessageId = undefined;
|
|
289
544
|
this.streamStartTime = Date.now();
|
|
290
545
|
this.firstTokenTime = undefined;
|
|
291
546
|
this.totalChunks = 0;
|
|
@@ -295,15 +550,33 @@ export class RunAggregator {
|
|
|
295
550
|
}
|
|
296
551
|
case "MESSAGES_SNAPSHOT": {
|
|
297
552
|
this.pendingSnapshotText.clear();
|
|
553
|
+
const buffers = this.buffers();
|
|
298
554
|
for (const message of event.messages) {
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
555
|
+
if (message?.role !== "assistant") continue;
|
|
556
|
+
if (typeof message.content === "string") {
|
|
557
|
+
const part = buffers
|
|
558
|
+
.map((buffer) => buffer.textParts.get(message.id))
|
|
559
|
+
.find((entry) => entry !== undefined);
|
|
560
|
+
if (part) part.buffer = message.content;
|
|
561
|
+
else this.pendingSnapshotText.set(message.id, message.content);
|
|
562
|
+
}
|
|
563
|
+
// A call the stream opened without args takes the record's, so the
|
|
564
|
+
// message it stays on matches what a reload would show.
|
|
565
|
+
for (const call of Array.isArray(message.toolCalls)
|
|
566
|
+
? message.toolCalls
|
|
567
|
+
: []) {
|
|
568
|
+
const args = call?.function?.arguments;
|
|
569
|
+
if (typeof args !== "string" || args.length === 0) continue;
|
|
570
|
+
for (const buffer of buffers) {
|
|
571
|
+
const entry = buffer.toolCalls.get(call.id);
|
|
572
|
+
if (entry === undefined || entry.argsText !== "") continue;
|
|
573
|
+
entry.argsText = args;
|
|
574
|
+
entry.argsTextScanner = createJSONContainerScanner();
|
|
575
|
+
scanJSONContainerDelta(entry.argsTextScanner, args);
|
|
576
|
+
const parsed = tryParseJSON(args);
|
|
577
|
+
entry.parsedArgs = isPlainObject(parsed) ? parsed : undefined;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
307
580
|
}
|
|
308
581
|
break;
|
|
309
582
|
}
|
|
@@ -334,10 +607,10 @@ export class RunAggregator {
|
|
|
334
607
|
const hasUnresolvedToolCalls =
|
|
335
608
|
pendingToolCallIds && pendingToolCallIds.length > 0
|
|
336
609
|
? pendingToolCallIds.some((id) => {
|
|
337
|
-
const call = this.toolCalls.get(id);
|
|
610
|
+
const call = this.buf.toolCalls.get(id);
|
|
338
611
|
return call !== undefined && call.result === undefined;
|
|
339
612
|
})
|
|
340
|
-
: Array.from(this.toolCalls.values()).some(
|
|
613
|
+
: Array.from(this.buf.toolCalls.values()).some(
|
|
341
614
|
(tc) => tc.result === undefined,
|
|
342
615
|
);
|
|
343
616
|
|
|
@@ -380,11 +653,13 @@ export class RunAggregator {
|
|
|
380
653
|
|
|
381
654
|
case "TEXT_MESSAGE_START": {
|
|
382
655
|
const scope = this.scopeOf(event);
|
|
383
|
-
if (scope === ROOT_SCOPE) {
|
|
656
|
+
if (scope === ROOT_SCOPE && this.buf === this.current) {
|
|
384
657
|
this.beginDistinctTextMessage(event.messageId);
|
|
385
658
|
this.reportServerMessageId(event.messageId);
|
|
386
659
|
if (event.messageId) this.lastTextMessageId = event.messageId;
|
|
387
660
|
}
|
|
661
|
+
if (scope === ROOT_SCOPE && event.messageId)
|
|
662
|
+
this.buf.openTextFrames.add(event.messageId);
|
|
388
663
|
this.startTextMessage(scope, event.messageId);
|
|
389
664
|
this.emit();
|
|
390
665
|
break;
|
|
@@ -392,11 +667,13 @@ export class RunAggregator {
|
|
|
392
667
|
case "TEXT_MESSAGE_CONTENT": {
|
|
393
668
|
const incomingId = event.messageId;
|
|
394
669
|
const scope = this.scopeOf(event);
|
|
395
|
-
if (scope === ROOT_SCOPE) {
|
|
670
|
+
if (scope === ROOT_SCOPE && this.buf === this.current) {
|
|
396
671
|
this.beginDistinctTextMessage(incomingId, true);
|
|
397
672
|
this.reportServerMessageId(incomingId);
|
|
398
673
|
if (incomingId) this.lastTextMessageId = incomingId;
|
|
399
674
|
}
|
|
675
|
+
if (scope === ROOT_SCOPE && incomingId)
|
|
676
|
+
this.buf.openTextFrames.add(incomingId);
|
|
400
677
|
if (!event.delta) break;
|
|
401
678
|
this.recordFirstToken();
|
|
402
679
|
const id = this.resolveTextMessageId(scope, incomingId);
|
|
@@ -407,21 +684,25 @@ export class RunAggregator {
|
|
|
407
684
|
}
|
|
408
685
|
case "TEXT_MESSAGE_END": {
|
|
409
686
|
const scope = this.scopeOf(event);
|
|
410
|
-
if (scope === ROOT_SCOPE
|
|
687
|
+
if (scope === ROOT_SCOPE && event.messageId) {
|
|
688
|
+
if (this.buf === this.current)
|
|
689
|
+
this.reportServerMessageId(event.messageId);
|
|
690
|
+
this.buf.openTextFrames.delete(event.messageId);
|
|
691
|
+
}
|
|
411
692
|
if (
|
|
412
693
|
event.messageId &&
|
|
413
|
-
this.activeTextMessageIdByScope.get(scope) ===
|
|
694
|
+
this.buf.activeTextMessageIdByScope.get(scope) ===
|
|
414
695
|
this.partKey(scope, event.messageId)
|
|
415
696
|
) {
|
|
416
|
-
this.activeTextMessageIdByScope.delete(scope);
|
|
697
|
+
this.buf.activeTextMessageIdByScope.delete(scope);
|
|
417
698
|
}
|
|
418
699
|
this.emit();
|
|
419
700
|
break;
|
|
420
701
|
}
|
|
421
702
|
|
|
422
703
|
case "CUSTOM": {
|
|
423
|
-
this.activeTextMessageIdByScope.delete(ROOT_SCOPE);
|
|
424
|
-
this.partOrder.push({
|
|
704
|
+
this.buf.activeTextMessageIdByScope.delete(ROOT_SCOPE);
|
|
705
|
+
this.buf.partOrder.push({
|
|
425
706
|
kind: "data",
|
|
426
707
|
name: event.name,
|
|
427
708
|
value: event.value,
|
|
@@ -444,25 +725,25 @@ export class RunAggregator {
|
|
|
444
725
|
const scope = this.scopeOf(event);
|
|
445
726
|
const entityKey = this.partKey(scope, event.entityId);
|
|
446
727
|
const key =
|
|
447
|
-
this.showThinking && this.reasoningParts.has(entityKey)
|
|
728
|
+
this.showThinking && this.buf.reasoningParts.has(entityKey)
|
|
448
729
|
? entityKey
|
|
449
730
|
: undefined;
|
|
450
731
|
if (key !== undefined) {
|
|
451
|
-
this.reasoningSignatures.set(key, event.encryptedValue);
|
|
452
|
-
this.reasoningSignatureIds.set(key, event.entityId);
|
|
732
|
+
this.buf.reasoningSignatures.set(key, event.encryptedValue);
|
|
733
|
+
this.buf.reasoningSignatureIds.set(key, event.entityId);
|
|
453
734
|
this.emit();
|
|
454
735
|
} else if (
|
|
455
736
|
!this.showThinking &&
|
|
456
737
|
scope === ROOT_SCOPE &&
|
|
457
738
|
event.entityId.trim().length > 0 &&
|
|
458
739
|
event.encryptedValue.trim().length > 0 &&
|
|
459
|
-
this.hiddenReasoningIds.has(event.entityId)
|
|
740
|
+
this.buf.hiddenReasoningIds.has(event.entityId)
|
|
460
741
|
) {
|
|
461
|
-
this.hiddenSignatures.set(event.entityId, event.encryptedValue);
|
|
462
|
-
this.hiddenSignatureAnchors.set(
|
|
742
|
+
this.buf.hiddenSignatures.set(event.entityId, event.encryptedValue);
|
|
743
|
+
this.buf.hiddenSignatureAnchors.set(
|
|
463
744
|
event.entityId,
|
|
464
|
-
this.hiddenBlockAnchors.get(event.entityId) ??
|
|
465
|
-
this.partOrder.length,
|
|
745
|
+
this.buf.hiddenBlockAnchors.get(event.entityId) ??
|
|
746
|
+
this.buf.partOrder.length,
|
|
466
747
|
);
|
|
467
748
|
this.emit();
|
|
468
749
|
}
|
|
@@ -485,7 +766,7 @@ export class RunAggregator {
|
|
|
485
766
|
|
|
486
767
|
case "TOOL_CALL_START": {
|
|
487
768
|
const scope = this.scopeOf(event);
|
|
488
|
-
if (scope === ROOT_SCOPE) {
|
|
769
|
+
if (scope === ROOT_SCOPE && this.buf === this.current) {
|
|
489
770
|
this.reportServerMessageId(event.parentMessageId);
|
|
490
771
|
}
|
|
491
772
|
this.startToolCall(
|
|
@@ -536,12 +817,12 @@ export class RunAggregator {
|
|
|
536
817
|
const activityScope = this.scopeOf(event);
|
|
537
818
|
const toolCallId = event.content.toolCallId;
|
|
538
819
|
const fallbackId =
|
|
539
|
-
this.lastResolvedToolCallIdByScope.get(activityScope);
|
|
820
|
+
this.buf.lastResolvedToolCallIdByScope.get(activityScope);
|
|
540
821
|
const entry =
|
|
541
822
|
typeof toolCallId === "string"
|
|
542
|
-
? this.toolCalls.get(toolCallId)
|
|
823
|
+
? this.buf.toolCalls.get(toolCallId)
|
|
543
824
|
: fallbackId
|
|
544
|
-
? this.toolCalls.get(fallbackId)
|
|
825
|
+
? this.buf.toolCalls.get(fallbackId)
|
|
545
826
|
: undefined;
|
|
546
827
|
const resourceUri = event.content.resourceUri;
|
|
547
828
|
if (
|
|
@@ -589,7 +870,7 @@ export class RunAggregator {
|
|
|
589
870
|
// a repeat is a continuation: it may refresh the descriptive fields
|
|
590
871
|
// and reopen the status, but re-parenting an established run would
|
|
591
872
|
// move its already-rendered output to a different tool call.
|
|
592
|
-
const existing = this.subagentRuns.get(event.subagentRunId);
|
|
873
|
+
const existing = this.buf.subagentRuns.get(event.subagentRunId);
|
|
593
874
|
if (existing) {
|
|
594
875
|
existing.name = event.name;
|
|
595
876
|
if (event.description !== undefined)
|
|
@@ -598,8 +879,8 @@ export class RunAggregator {
|
|
|
598
879
|
this.emit();
|
|
599
880
|
break;
|
|
600
881
|
}
|
|
601
|
-
this.nestingMemo = undefined;
|
|
602
|
-
this.subagentRuns.set(event.subagentRunId, {
|
|
882
|
+
this.buf.nestingMemo = undefined;
|
|
883
|
+
this.buf.subagentRuns.set(event.subagentRunId, {
|
|
603
884
|
subagentRunId: event.subagentRunId,
|
|
604
885
|
name: event.name,
|
|
605
886
|
...(event.description !== undefined
|
|
@@ -621,7 +902,7 @@ export class RunAggregator {
|
|
|
621
902
|
break;
|
|
622
903
|
}
|
|
623
904
|
case "SUBAGENT_FINISHED": {
|
|
624
|
-
const run = this.subagentRuns.get(event.subagentRunId);
|
|
905
|
+
const run = this.buf.subagentRuns.get(event.subagentRunId);
|
|
625
906
|
if (run) {
|
|
626
907
|
run.status =
|
|
627
908
|
event.outcome?.type === "suspended"
|
|
@@ -635,7 +916,7 @@ export class RunAggregator {
|
|
|
635
916
|
break;
|
|
636
917
|
}
|
|
637
918
|
case "SUBAGENT_ERROR": {
|
|
638
|
-
const run = this.subagentRuns.get(event.subagentRunId);
|
|
919
|
+
const run = this.buf.subagentRuns.get(event.subagentRunId);
|
|
639
920
|
if (run) {
|
|
640
921
|
run.status = {
|
|
641
922
|
type: "incomplete",
|
|
@@ -663,7 +944,7 @@ export class RunAggregator {
|
|
|
663
944
|
event: Extract<AgUiEvent, { type: "ACTIVITY_DELTA" }>,
|
|
664
945
|
): void {
|
|
665
946
|
const scope = this.scopeOf(event);
|
|
666
|
-
const existing = this.activityParts.get(
|
|
947
|
+
const existing = this.buf.activityParts.get(
|
|
667
948
|
this.partKey(scope, `message:${event.messageId}`),
|
|
668
949
|
);
|
|
669
950
|
if (!existing) {
|
|
@@ -700,14 +981,14 @@ export class RunAggregator {
|
|
|
700
981
|
if (!Array.isArray(operations)) return;
|
|
701
982
|
|
|
702
983
|
const messageId = event.messageId ?? "a2ui:anonymous";
|
|
703
|
-
if (event.replace === false && this.a2uiBuckets.has(messageId)) return;
|
|
984
|
+
if (event.replace === false && this.buf.a2uiBuckets.has(messageId)) return;
|
|
704
985
|
|
|
705
986
|
const { state, warnings } = applyA2uiOperations(new Map(), operations);
|
|
706
987
|
for (const warning of warnings) {
|
|
707
988
|
this.logger.debug("[agui] a2ui operation warning", warning);
|
|
708
989
|
}
|
|
709
|
-
this.a2uiBuckets.delete(messageId);
|
|
710
|
-
this.a2uiBuckets.set(messageId, state);
|
|
990
|
+
this.buf.a2uiBuckets.delete(messageId);
|
|
991
|
+
this.buf.a2uiBuckets.set(messageId, state);
|
|
711
992
|
this.synthesizeA2uiToolCalls();
|
|
712
993
|
this.emit();
|
|
713
994
|
}
|
|
@@ -722,7 +1003,7 @@ export class RunAggregator {
|
|
|
722
1003
|
? `message:${event.messageId}`
|
|
723
1004
|
: `type:${event.activityType}`,
|
|
724
1005
|
);
|
|
725
|
-
const existing = this.activityParts.get(key);
|
|
1006
|
+
const existing = this.buf.activityParts.get(key);
|
|
726
1007
|
if (existing) {
|
|
727
1008
|
if (event.replace === false) return;
|
|
728
1009
|
existing.name = `agui-activity/${event.activityType}`;
|
|
@@ -741,9 +1022,9 @@ export class RunAggregator {
|
|
|
741
1022
|
value: event.content,
|
|
742
1023
|
subagentRunId: scope,
|
|
743
1024
|
};
|
|
744
|
-
this.activityParts.set(key, part);
|
|
745
|
-
this.partOrder.push(part);
|
|
746
|
-
this.activeTextMessageIdByScope.delete(scope);
|
|
1025
|
+
this.buf.activityParts.set(key, part);
|
|
1026
|
+
this.buf.partOrder.push(part);
|
|
1027
|
+
this.buf.activeTextMessageIdByScope.delete(scope);
|
|
747
1028
|
}
|
|
748
1029
|
this.emit();
|
|
749
1030
|
}
|
|
@@ -751,7 +1032,7 @@ export class RunAggregator {
|
|
|
751
1032
|
private synthesizeA2uiToolCalls(): void {
|
|
752
1033
|
const surfaces = new Map<string, A2uiSurfaceState>();
|
|
753
1034
|
|
|
754
|
-
for (const bucket of this.a2uiBuckets.values()) {
|
|
1035
|
+
for (const bucket of this.buf.a2uiBuckets.values()) {
|
|
755
1036
|
for (const [surfaceId, surface] of bucket) {
|
|
756
1037
|
surfaces.set(surfaceId, surface);
|
|
757
1038
|
}
|
|
@@ -782,23 +1063,23 @@ export class RunAggregator {
|
|
|
782
1063
|
artifact: toolCall.artifact,
|
|
783
1064
|
snapshotResultApplied: false,
|
|
784
1065
|
};
|
|
785
|
-
if (!this.toolCalls.has(toolCallId)) {
|
|
786
|
-
this.partOrder.push({ kind: "tool-call", toolCallId });
|
|
1066
|
+
if (!this.buf.toolCalls.has(toolCallId)) {
|
|
1067
|
+
this.buf.partOrder.push({ kind: "tool-call", toolCallId });
|
|
787
1068
|
}
|
|
788
|
-
this.nestingMemo = undefined;
|
|
789
|
-
this.toolCalls.set(toolCallId, entry);
|
|
790
|
-
this.a2uiToolCallIds.add(toolCallId);
|
|
1069
|
+
this.buf.nestingMemo = undefined;
|
|
1070
|
+
this.buf.toolCalls.set(toolCallId, entry);
|
|
1071
|
+
this.buf.a2uiToolCallIds.add(toolCallId);
|
|
791
1072
|
}
|
|
792
1073
|
|
|
793
|
-
for (const toolCallId of this.a2uiToolCallIds) {
|
|
1074
|
+
for (const toolCallId of this.buf.a2uiToolCallIds) {
|
|
794
1075
|
if (activeToolCallIds.has(toolCallId)) continue;
|
|
795
|
-
this.nestingMemo = undefined;
|
|
796
|
-
this.toolCalls.delete(toolCallId);
|
|
797
|
-
const partIndex = this.partOrder.findIndex(
|
|
1076
|
+
this.buf.nestingMemo = undefined;
|
|
1077
|
+
this.buf.toolCalls.delete(toolCallId);
|
|
1078
|
+
const partIndex = this.buf.partOrder.findIndex(
|
|
798
1079
|
(part) => part.kind === "tool-call" && part.toolCallId === toolCallId,
|
|
799
1080
|
);
|
|
800
|
-
if (partIndex !== -1) this.partOrder.splice(partIndex, 1);
|
|
801
|
-
this.a2uiToolCallIds.delete(toolCallId);
|
|
1081
|
+
if (partIndex !== -1) this.buf.partOrder.splice(partIndex, 1);
|
|
1082
|
+
this.buf.a2uiToolCallIds.delete(toolCallId);
|
|
802
1083
|
}
|
|
803
1084
|
}
|
|
804
1085
|
|
|
@@ -809,47 +1090,24 @@ export class RunAggregator {
|
|
|
809
1090
|
}
|
|
810
1091
|
if (this.serverMessageIdReported) return;
|
|
811
1092
|
this.serverMessageIdReported = true;
|
|
812
|
-
this.reportedServerMessageId = messageId;
|
|
1093
|
+
this.buf.reportedServerMessageId = messageId;
|
|
813
1094
|
this.onServerMessageId?.(messageId);
|
|
814
1095
|
}
|
|
815
1096
|
|
|
816
|
-
private clearTextParts(): void {
|
|
817
|
-
this.textParts.clear();
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
private resetMessageParts(): void {
|
|
821
|
-
this.clearTextParts();
|
|
822
|
-
this.pendingSnapshotText.clear();
|
|
823
|
-
this.reasoningParts.clear();
|
|
824
|
-
this.reasoningSignatures.clear();
|
|
825
|
-
this.reasoningSignatureIds.clear();
|
|
826
|
-
this.reasoningMessageIds.clear();
|
|
827
|
-
this.hiddenSignatures.clear();
|
|
828
|
-
this.hiddenSignatureAnchors.clear();
|
|
829
|
-
this.hiddenBlockAnchors.clear();
|
|
830
|
-
this.hiddenReasoningIds.clear();
|
|
831
|
-
this.hiddenActiveReasoningId = undefined;
|
|
832
|
-
this.loggedDroppedOpaqueIds.clear();
|
|
833
|
-
this.activeReasoningKeyByScope.clear();
|
|
834
|
-
this.nestingMemo = undefined;
|
|
835
|
-
this.subagentRuns.clear();
|
|
836
|
-
this.toolCalls.clear();
|
|
837
|
-
this.a2uiBuckets.clear();
|
|
838
|
-
this.a2uiToolCallIds.clear();
|
|
839
|
-
this.activityParts.clear();
|
|
840
|
-
this.lastResolvedToolCallIdByScope.clear();
|
|
841
|
-
this.partOrder.length = 0;
|
|
842
|
-
this.lastReasoningBeforeToolCall = undefined;
|
|
843
|
-
this.textPartCounter = 0;
|
|
844
|
-
this.activeTextMessageIdByScope.clear();
|
|
845
|
-
this.reportedServerMessageId = undefined;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
1097
|
// A subagent only ever reports its own terminal, so a run that ends while
|
|
849
1098
|
// one is still streaming would leave that nested message spinning forever.
|
|
1099
|
+
// Moved-off messages publish first, so the current emit that follows sees
|
|
1100
|
+
// them settled.
|
|
850
1101
|
private closeOpenSubagentRuns(status: MessageStatus): void {
|
|
851
|
-
for (const
|
|
852
|
-
|
|
1102
|
+
for (const buffer of this.buffers()) {
|
|
1103
|
+
let closed = false;
|
|
1104
|
+
for (const run of buffer.subagentRuns.values()) {
|
|
1105
|
+
if (run.status.type !== "running") continue;
|
|
1106
|
+
run.status = status;
|
|
1107
|
+
closed = true;
|
|
1108
|
+
}
|
|
1109
|
+
if (closed && buffer !== this.current)
|
|
1110
|
+
this.withBuffer(buffer, () => this.emit());
|
|
853
1111
|
}
|
|
854
1112
|
}
|
|
855
1113
|
|
|
@@ -881,17 +1139,13 @@ export class RunAggregator {
|
|
|
881
1139
|
if (carried.parts.length > 0 || carried.hiddenSignatures.length > 0) {
|
|
882
1140
|
this.emit();
|
|
883
1141
|
}
|
|
884
|
-
const openToolCallIds = Array.from(this.toolCalls.values())
|
|
1142
|
+
const openToolCallIds = Array.from(this.buf.toolCalls.values())
|
|
885
1143
|
.filter((call) => call.result === undefined)
|
|
886
1144
|
.map((call) => call.toolCallId);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
this.
|
|
891
|
-
if (snapshotText !== undefined) {
|
|
892
|
-
this.pendingSnapshotText.set(messageId, snapshotText);
|
|
893
|
-
}
|
|
894
|
-
this.onTextMessageStart(messageId, openToolCallIds);
|
|
1145
|
+
if (!preserveSnapshotText) this.pendingSnapshotText.delete(messageId);
|
|
1146
|
+
const outgoingId =
|
|
1147
|
+
this.onTextMessageStart(messageId, openToolCallIds) || undefined;
|
|
1148
|
+
this.openMessage(messageId, outgoingId);
|
|
895
1149
|
this.restoreReasoning(carried);
|
|
896
1150
|
}
|
|
897
1151
|
|
|
@@ -903,37 +1157,37 @@ export class RunAggregator {
|
|
|
903
1157
|
signatureId: string | undefined;
|
|
904
1158
|
reasoningId: string | undefined;
|
|
905
1159
|
}[] = [];
|
|
906
|
-
let last = this.partOrder.at(-1);
|
|
1160
|
+
let last = this.buf.partOrder.at(-1);
|
|
907
1161
|
while (
|
|
908
1162
|
last?.kind === "reasoning" &&
|
|
909
1163
|
last.subagentRunId === undefined &&
|
|
910
|
-
last !== this.lastReasoningBeforeToolCall
|
|
1164
|
+
last !== this.buf.lastReasoningBeforeToolCall
|
|
911
1165
|
) {
|
|
912
|
-
this.partOrder.pop();
|
|
1166
|
+
this.buf.partOrder.pop();
|
|
913
1167
|
parts.unshift({
|
|
914
1168
|
key: last.key,
|
|
915
|
-
entry: this.reasoningParts.get(last.key) ?? { buffer: "" },
|
|
916
|
-
signature: this.reasoningSignatures.get(last.key),
|
|
917
|
-
signatureId: this.reasoningSignatureIds.get(last.key),
|
|
918
|
-
reasoningId: this.reasoningMessageIds.get(last.key),
|
|
1169
|
+
entry: this.buf.reasoningParts.get(last.key) ?? { buffer: "" },
|
|
1170
|
+
signature: this.buf.reasoningSignatures.get(last.key),
|
|
1171
|
+
signatureId: this.buf.reasoningSignatureIds.get(last.key),
|
|
1172
|
+
reasoningId: this.buf.reasoningMessageIds.get(last.key),
|
|
919
1173
|
});
|
|
920
|
-
last = this.partOrder.at(-1);
|
|
1174
|
+
last = this.buf.partOrder.at(-1);
|
|
921
1175
|
}
|
|
922
|
-
const cut = this.partOrder.length;
|
|
923
|
-
const hiddenSignatures = Array.from(this.hiddenSignatures).filter(
|
|
924
|
-
([id]) => this.hiddenSignatureAnchors.get(id)! >= cut,
|
|
1176
|
+
const cut = this.buf.partOrder.length;
|
|
1177
|
+
const hiddenSignatures = Array.from(this.buf.hiddenSignatures).filter(
|
|
1178
|
+
([id]) => this.buf.hiddenSignatureAnchors.get(id)! >= cut,
|
|
925
1179
|
);
|
|
926
1180
|
for (const [id] of hiddenSignatures) {
|
|
927
|
-
this.hiddenSignatures.delete(id);
|
|
928
|
-
this.hiddenSignatureAnchors.delete(id);
|
|
1181
|
+
this.buf.hiddenSignatures.delete(id);
|
|
1182
|
+
this.buf.hiddenSignatureAnchors.delete(id);
|
|
929
1183
|
}
|
|
930
|
-
const hiddenBlockIds = Array.from(this.hiddenBlockAnchors)
|
|
1184
|
+
const hiddenBlockIds = Array.from(this.buf.hiddenBlockAnchors)
|
|
931
1185
|
.filter(([, anchor]) => anchor >= cut)
|
|
932
1186
|
.map(([id]) => id);
|
|
933
|
-
const hiddenActiveReasoningId = this.hiddenActiveReasoningId;
|
|
1187
|
+
const hiddenActiveReasoningId = this.buf.hiddenActiveReasoningId;
|
|
934
1188
|
return {
|
|
935
1189
|
parts,
|
|
936
|
-
activeKey: this.activeReasoningKeyByScope.get(ROOT_SCOPE),
|
|
1190
|
+
activeKey: this.buf.activeReasoningKeyByScope.get(ROOT_SCOPE),
|
|
937
1191
|
hiddenSignatures,
|
|
938
1192
|
hiddenBlockIds,
|
|
939
1193
|
hiddenActiveReasoningId:
|
|
@@ -948,35 +1202,35 @@ export class RunAggregator {
|
|
|
948
1202
|
carried: ReturnType<RunAggregator["takeTrailingReasoning"]>,
|
|
949
1203
|
): void {
|
|
950
1204
|
for (const part of carried.parts) {
|
|
951
|
-
this.partOrder.push({ kind: "reasoning", key: part.key });
|
|
952
|
-
this.reasoningParts.set(part.key, part.entry);
|
|
1205
|
+
this.buf.partOrder.push({ kind: "reasoning", key: part.key });
|
|
1206
|
+
this.buf.reasoningParts.set(part.key, part.entry);
|
|
953
1207
|
if (part.signature !== undefined)
|
|
954
|
-
this.reasoningSignatures.set(part.key, part.signature);
|
|
1208
|
+
this.buf.reasoningSignatures.set(part.key, part.signature);
|
|
955
1209
|
if (part.signatureId !== undefined)
|
|
956
|
-
this.reasoningSignatureIds.set(part.key, part.signatureId);
|
|
1210
|
+
this.buf.reasoningSignatureIds.set(part.key, part.signatureId);
|
|
957
1211
|
if (part.reasoningId !== undefined)
|
|
958
|
-
this.reasoningMessageIds.set(part.key, part.reasoningId);
|
|
1212
|
+
this.buf.reasoningMessageIds.set(part.key, part.reasoningId);
|
|
959
1213
|
}
|
|
960
1214
|
if (
|
|
961
1215
|
carried.activeKey !== undefined &&
|
|
962
|
-
this.reasoningParts.has(carried.activeKey)
|
|
1216
|
+
this.buf.reasoningParts.has(carried.activeKey)
|
|
963
1217
|
) {
|
|
964
|
-
this.activeReasoningKeyByScope.set(ROOT_SCOPE, carried.activeKey);
|
|
1218
|
+
this.buf.activeReasoningKeyByScope.set(ROOT_SCOPE, carried.activeKey);
|
|
965
1219
|
}
|
|
966
1220
|
for (const [id, encryptedValue] of carried.hiddenSignatures) {
|
|
967
|
-
this.hiddenSignatures.set(id, encryptedValue);
|
|
968
|
-
this.hiddenSignatureAnchors.set(id, 0);
|
|
1221
|
+
this.buf.hiddenSignatures.set(id, encryptedValue);
|
|
1222
|
+
this.buf.hiddenSignatureAnchors.set(id, 0);
|
|
969
1223
|
}
|
|
970
1224
|
for (const id of carried.hiddenBlockIds) {
|
|
971
|
-
this.hiddenReasoningIds.add(id);
|
|
972
|
-
this.hiddenBlockAnchors.set(id, 0);
|
|
1225
|
+
this.buf.hiddenReasoningIds.add(id);
|
|
1226
|
+
this.buf.hiddenBlockAnchors.set(id, 0);
|
|
973
1227
|
}
|
|
974
|
-
this.hiddenActiveReasoningId = carried.hiddenActiveReasoningId;
|
|
1228
|
+
this.buf.hiddenActiveReasoningId = carried.hiddenActiveReasoningId;
|
|
975
1229
|
}
|
|
976
1230
|
|
|
977
1231
|
private generateTextKey(): string {
|
|
978
|
-
this.textPartCounter += 1;
|
|
979
|
-
return `text-${this.textPartCounter}`;
|
|
1232
|
+
this.buf.textPartCounter += 1;
|
|
1233
|
+
return `text-${this.buf.textPartCounter}`;
|
|
980
1234
|
}
|
|
981
1235
|
|
|
982
1236
|
private startTextMessage(scope: string, messageId?: string): string {
|
|
@@ -984,13 +1238,13 @@ export class RunAggregator {
|
|
|
984
1238
|
const key = this.ensureTextPart(scope, id);
|
|
985
1239
|
if (scope === ROOT_SCOPE && messageId) {
|
|
986
1240
|
const snapshotText = this.pendingSnapshotText.get(messageId);
|
|
987
|
-
const part = this.textParts.get(key);
|
|
1241
|
+
const part = this.buf.textParts.get(key);
|
|
988
1242
|
if (snapshotText !== undefined && part) {
|
|
989
1243
|
part.buffer = snapshotText;
|
|
990
1244
|
this.pendingSnapshotText.delete(messageId);
|
|
991
1245
|
}
|
|
992
1246
|
}
|
|
993
|
-
this.activeTextMessageIdByScope.set(scope, key);
|
|
1247
|
+
this.buf.activeTextMessageIdByScope.set(scope, key);
|
|
994
1248
|
return key;
|
|
995
1249
|
}
|
|
996
1250
|
|
|
@@ -999,24 +1253,26 @@ export class RunAggregator {
|
|
|
999
1253
|
return this.startTextMessage(scope, messageId);
|
|
1000
1254
|
}
|
|
1001
1255
|
|
|
1002
|
-
const active = this.activeTextMessageIdByScope.get(scope);
|
|
1256
|
+
const active = this.buf.activeTextMessageIdByScope.get(scope);
|
|
1003
1257
|
if (active) {
|
|
1004
1258
|
return active;
|
|
1005
1259
|
}
|
|
1006
1260
|
|
|
1007
1261
|
const key = this.ensureTextPart(scope, this.generateTextKey());
|
|
1008
|
-
this.activeTextMessageIdByScope.set(scope, key);
|
|
1262
|
+
this.buf.activeTextMessageIdByScope.set(scope, key);
|
|
1009
1263
|
return key;
|
|
1010
1264
|
}
|
|
1011
1265
|
|
|
1012
1266
|
private ensureTextPart(scope: string, id: string): string {
|
|
1013
1267
|
const key = this.partKey(scope, id);
|
|
1014
|
-
if (!this.textParts.has(key)) {
|
|
1015
|
-
this.textParts.set(key, { buffer: "" });
|
|
1268
|
+
if (!this.buf.textParts.has(key)) {
|
|
1269
|
+
this.buf.textParts.set(key, { buffer: "" });
|
|
1016
1270
|
if (
|
|
1017
|
-
!this.partOrder.some(
|
|
1271
|
+
!this.buf.partOrder.some(
|
|
1272
|
+
(part) => part.kind === "text" && part.key === key,
|
|
1273
|
+
)
|
|
1018
1274
|
) {
|
|
1019
|
-
this.partOrder.push(
|
|
1275
|
+
this.buf.partOrder.push(
|
|
1020
1276
|
scope === ROOT_SCOPE
|
|
1021
1277
|
? { kind: "text", key }
|
|
1022
1278
|
: { kind: "text", key, subagentRunId: scope },
|
|
@@ -1027,7 +1283,7 @@ export class RunAggregator {
|
|
|
1027
1283
|
}
|
|
1028
1284
|
|
|
1029
1285
|
private appendText(id: string, delta: string): void {
|
|
1030
|
-
const entry = this.textParts.get(id);
|
|
1286
|
+
const entry = this.buf.textParts.get(id);
|
|
1031
1287
|
if (!entry) return;
|
|
1032
1288
|
entry.buffer += delta;
|
|
1033
1289
|
}
|
|
@@ -1043,15 +1299,15 @@ export class RunAggregator {
|
|
|
1043
1299
|
// after it should be a new part, not appended to the pre-tool text —
|
|
1044
1300
|
// scoped, so a subagent's tool call doesn't break the root run's text
|
|
1045
1301
|
// and vice versa.
|
|
1046
|
-
this.activeTextMessageIdByScope.delete(scope);
|
|
1302
|
+
this.buf.activeTextMessageIdByScope.delete(scope);
|
|
1047
1303
|
if (
|
|
1048
|
-
!this.partOrder.some(
|
|
1304
|
+
!this.buf.partOrder.some(
|
|
1049
1305
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
1050
1306
|
)
|
|
1051
1307
|
) {
|
|
1052
1308
|
// Tool insertion can precede existing reasoning in display order.
|
|
1053
1309
|
if (scope === ROOT_SCOPE) {
|
|
1054
|
-
this.lastReasoningBeforeToolCall = this.partOrder.findLast(
|
|
1310
|
+
this.buf.lastReasoningBeforeToolCall = this.buf.partOrder.findLast(
|
|
1055
1311
|
(part) =>
|
|
1056
1312
|
part.kind === "reasoning" && part.subagentRunId === undefined,
|
|
1057
1313
|
);
|
|
@@ -1073,8 +1329,8 @@ export class RunAggregator {
|
|
|
1073
1329
|
if (scope !== ROOT_SCOPE) {
|
|
1074
1330
|
state.subagentRunId = scope;
|
|
1075
1331
|
}
|
|
1076
|
-
this.nestingMemo = undefined;
|
|
1077
|
-
this.toolCalls.set(id, state);
|
|
1332
|
+
this.buf.nestingMemo = undefined;
|
|
1333
|
+
this.buf.toolCalls.set(id, state);
|
|
1078
1334
|
}
|
|
1079
1335
|
|
|
1080
1336
|
// The message and tool-call channels are unordered on the wire, so anchor a
|
|
@@ -1083,16 +1339,16 @@ export class RunAggregator {
|
|
|
1083
1339
|
const entry = { kind: "tool-call", toolCallId: id } as const;
|
|
1084
1340
|
if (parentMessageId) {
|
|
1085
1341
|
const parentKey = this.partKey(scope, parentMessageId);
|
|
1086
|
-
const parentIndex = this.partOrder.findIndex(
|
|
1342
|
+
const parentIndex = this.buf.partOrder.findIndex(
|
|
1087
1343
|
(part) => part.kind === "text" && part.key === parentKey,
|
|
1088
1344
|
);
|
|
1089
1345
|
if (parentIndex !== -1) {
|
|
1090
1346
|
let insertAt = parentIndex + 1;
|
|
1091
|
-
while (insertAt < this.partOrder.length) {
|
|
1092
|
-
const part = this.partOrder[insertAt];
|
|
1347
|
+
while (insertAt < this.buf.partOrder.length) {
|
|
1348
|
+
const part = this.buf.partOrder[insertAt];
|
|
1093
1349
|
if (
|
|
1094
1350
|
part?.kind === "tool-call" &&
|
|
1095
|
-
this.toolCalls.get(part.toolCallId)?.parentMessageId ===
|
|
1351
|
+
this.buf.toolCalls.get(part.toolCallId)?.parentMessageId ===
|
|
1096
1352
|
parentMessageId
|
|
1097
1353
|
) {
|
|
1098
1354
|
insertAt += 1;
|
|
@@ -1100,15 +1356,15 @@ export class RunAggregator {
|
|
|
1100
1356
|
}
|
|
1101
1357
|
break;
|
|
1102
1358
|
}
|
|
1103
|
-
this.partOrder.splice(insertAt, 0, entry);
|
|
1359
|
+
this.buf.partOrder.splice(insertAt, 0, entry);
|
|
1104
1360
|
return;
|
|
1105
1361
|
}
|
|
1106
1362
|
}
|
|
1107
|
-
this.partOrder.push(entry);
|
|
1363
|
+
this.buf.partOrder.push(entry);
|
|
1108
1364
|
}
|
|
1109
1365
|
|
|
1110
1366
|
private appendToolArgs(id: string | undefined, delta: string) {
|
|
1111
|
-
const entry = id ? this.toolCalls.get(id) : undefined;
|
|
1367
|
+
const entry = id ? this.buf.toolCalls.get(id) : undefined;
|
|
1112
1368
|
if (!entry) return;
|
|
1113
1369
|
if (!entry.argsTextScanner) {
|
|
1114
1370
|
entry.argsTextScanner = createJSONContainerScanner();
|
|
@@ -1144,7 +1400,7 @@ export class RunAggregator {
|
|
|
1144
1400
|
) {
|
|
1145
1401
|
if (!id) return;
|
|
1146
1402
|
const modelText = toolContentText(content);
|
|
1147
|
-
let entry = this.toolCalls.get(id);
|
|
1403
|
+
let entry = this.buf.toolCalls.get(id);
|
|
1148
1404
|
if (!entry) {
|
|
1149
1405
|
entry = {
|
|
1150
1406
|
toolCallId: id,
|
|
@@ -1156,15 +1412,15 @@ export class RunAggregator {
|
|
|
1156
1412
|
snapshotResultApplied: false,
|
|
1157
1413
|
...(scope !== ROOT_SCOPE ? { subagentRunId: scope } : {}),
|
|
1158
1414
|
};
|
|
1159
|
-
this.nestingMemo = undefined;
|
|
1160
|
-
this.toolCalls.set(id, entry);
|
|
1415
|
+
this.buf.nestingMemo = undefined;
|
|
1416
|
+
this.buf.toolCalls.set(id, entry);
|
|
1161
1417
|
}
|
|
1162
1418
|
if (
|
|
1163
|
-
!this.partOrder.some(
|
|
1419
|
+
!this.buf.partOrder.some(
|
|
1164
1420
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
1165
1421
|
)
|
|
1166
1422
|
) {
|
|
1167
|
-
this.partOrder.push({ kind: "tool-call", toolCallId: id });
|
|
1423
|
+
this.buf.partOrder.push({ kind: "tool-call", toolCallId: id });
|
|
1168
1424
|
}
|
|
1169
1425
|
if (mcpResult !== undefined && !entry.snapshotResultApplied) {
|
|
1170
1426
|
entry.result = mcpResult;
|
|
@@ -1195,26 +1451,26 @@ export class RunAggregator {
|
|
|
1195
1451
|
if (metadata !== undefined) {
|
|
1196
1452
|
entry.resultMetadata = { ...entry.resultMetadata, ...metadata };
|
|
1197
1453
|
}
|
|
1198
|
-
this.lastResolvedToolCallIdByScope.set(scope, id);
|
|
1454
|
+
this.buf.lastResolvedToolCallIdByScope.set(scope, id);
|
|
1199
1455
|
}
|
|
1200
1456
|
|
|
1201
1457
|
private nesting(): {
|
|
1202
1458
|
subagentsByParentToolCallId: Map<string, string[]>;
|
|
1203
1459
|
reachable: Set<string>;
|
|
1204
1460
|
} {
|
|
1205
|
-
if (!this.nestingMemo) {
|
|
1461
|
+
if (!this.buf.nestingMemo) {
|
|
1206
1462
|
const subagentsByParentToolCallId = this.subagentsByParentToolCallId();
|
|
1207
|
-
this.nestingMemo = {
|
|
1463
|
+
this.buf.nestingMemo = {
|
|
1208
1464
|
subagentsByParentToolCallId,
|
|
1209
1465
|
reachable: this.reachableSubagentRunIds(subagentsByParentToolCallId),
|
|
1210
1466
|
};
|
|
1211
1467
|
}
|
|
1212
|
-
return this.nestingMemo;
|
|
1468
|
+
return this.buf.nestingMemo;
|
|
1213
1469
|
}
|
|
1214
1470
|
|
|
1215
1471
|
private subagentsByParentToolCallId(): Map<string, string[]> {
|
|
1216
1472
|
const map = new Map<string, string[]>();
|
|
1217
|
-
for (const run of this.subagentRuns.values()) {
|
|
1473
|
+
for (const run of this.buf.subagentRuns.values()) {
|
|
1218
1474
|
if (!run.parentToolCallId) continue;
|
|
1219
1475
|
const list = map.get(run.parentToolCallId) ?? [];
|
|
1220
1476
|
list.push(run.subagentRunId);
|
|
@@ -1234,7 +1490,7 @@ export class RunAggregator {
|
|
|
1234
1490
|
const reachable = new Set<string>();
|
|
1235
1491
|
const walk = (scope: string, depth: number): void => {
|
|
1236
1492
|
if (depth > MAX_SUBAGENT_DEPTH) return;
|
|
1237
|
-
for (const entry of this.toolCalls.values()) {
|
|
1493
|
+
for (const entry of this.buf.toolCalls.values()) {
|
|
1238
1494
|
if ((entry.subagentRunId ?? ROOT_SCOPE) !== scope) continue;
|
|
1239
1495
|
for (const id of subagentsByParentToolCallId.get(entry.toolCallId) ??
|
|
1240
1496
|
[]) {
|
|
@@ -1257,7 +1513,7 @@ export class RunAggregator {
|
|
|
1257
1513
|
depth: number,
|
|
1258
1514
|
ctx: BuildContext,
|
|
1259
1515
|
): ThreadMessage | undefined {
|
|
1260
|
-
const run = this.subagentRuns.get(subagentRunId);
|
|
1516
|
+
const run = this.buf.subagentRuns.get(subagentRunId);
|
|
1261
1517
|
if (!run || depth > MAX_SUBAGENT_DEPTH) return undefined;
|
|
1262
1518
|
const content = this.buildParts(subagentRunId, depth + 1, ctx);
|
|
1263
1519
|
return {
|
|
@@ -1307,7 +1563,7 @@ export class RunAggregator {
|
|
|
1307
1563
|
};
|
|
1308
1564
|
|
|
1309
1565
|
if (part.kind === "tool-call") {
|
|
1310
|
-
const entry = this.toolCalls.get(part.toolCallId);
|
|
1566
|
+
const entry = this.buf.toolCalls.get(part.toolCallId);
|
|
1311
1567
|
if (!entry) continue;
|
|
1312
1568
|
const approval = ctx.approvals.get(entry.toolCallId);
|
|
1313
1569
|
const nestedMessages = (
|
|
@@ -1367,13 +1623,13 @@ export class RunAggregator {
|
|
|
1367
1623
|
|
|
1368
1624
|
if (part.kind === "reasoning") {
|
|
1369
1625
|
if (this.showThinking) {
|
|
1370
|
-
const entry = this.reasoningParts.get(part.key);
|
|
1626
|
+
const entry = this.buf.reasoningParts.get(part.key);
|
|
1371
1627
|
const buffer = entry?.buffer ?? "";
|
|
1372
1628
|
const isActive =
|
|
1373
|
-
this.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1629
|
+
this.buf.activeReasoningKeyByScope.get(rawScope) === part.key;
|
|
1374
1630
|
if (buffer.length > 0 || isActive) {
|
|
1375
|
-
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
1376
|
-
const reasoningId = this.reasoningMessageIds.get(part.key);
|
|
1631
|
+
const encryptedValue = this.buf.reasoningSignatures.get(part.key);
|
|
1632
|
+
const reasoningId = this.buf.reasoningMessageIds.get(part.key);
|
|
1377
1633
|
const meta = {
|
|
1378
1634
|
...(reasoningId !== undefined ? { reasoningId } : {}),
|
|
1379
1635
|
...(encryptedValue !== undefined ? { encryptedValue } : {}),
|
|
@@ -1391,8 +1647,8 @@ export class RunAggregator {
|
|
|
1391
1647
|
// A retracted empty block still carries transport state: without
|
|
1392
1648
|
// a part to live on, its signature rides the message metadata,
|
|
1393
1649
|
// matching the shape a snapshot reload produces.
|
|
1394
|
-
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
1395
|
-
const id = this.reasoningSignatureIds.get(part.key);
|
|
1650
|
+
const encryptedValue = this.buf.reasoningSignatures.get(part.key);
|
|
1651
|
+
const id = this.buf.reasoningSignatureIds.get(part.key);
|
|
1396
1652
|
if (id?.trim() && encryptedValue?.trim()) {
|
|
1397
1653
|
ctx.root.opaqueCandidates.push({
|
|
1398
1654
|
id,
|
|
@@ -1406,7 +1662,7 @@ export class RunAggregator {
|
|
|
1406
1662
|
}
|
|
1407
1663
|
|
|
1408
1664
|
if (part.kind === "text") {
|
|
1409
|
-
const entry = this.textParts.get(part.key);
|
|
1665
|
+
const entry = this.buf.textParts.get(part.key);
|
|
1410
1666
|
if (entry && entry.buffer.trim().length > 0) {
|
|
1411
1667
|
snapshot.push({ type: "text", text: entry.buffer } as const);
|
|
1412
1668
|
materialized();
|
|
@@ -1432,11 +1688,11 @@ export class RunAggregator {
|
|
|
1432
1688
|
const { subagentsByParentToolCallId, reachable } = this.nesting();
|
|
1433
1689
|
const root = {
|
|
1434
1690
|
opaqueCandidates: Array.from(
|
|
1435
|
-
this.hiddenSignatures,
|
|
1691
|
+
this.buf.hiddenSignatures,
|
|
1436
1692
|
([id, encryptedValue]) => ({
|
|
1437
1693
|
id,
|
|
1438
1694
|
encryptedValue,
|
|
1439
|
-
anchor: this.hiddenSignatureAnchors.get(id)!,
|
|
1695
|
+
anchor: this.buf.hiddenSignatureAnchors.get(id)!,
|
|
1440
1696
|
}),
|
|
1441
1697
|
) as (AgUiOpaqueReasoning & { anchor: number })[],
|
|
1442
1698
|
lastMaterializedIndex: -1,
|
|
@@ -1452,10 +1708,11 @@ export class RunAggregator {
|
|
|
1452
1708
|
string,
|
|
1453
1709
|
{ index: number; part: PartOrderEntry }[]
|
|
1454
1710
|
>();
|
|
1455
|
-
for (const [index, part] of this.partOrder.entries()) {
|
|
1711
|
+
for (const [index, part] of this.buf.partOrder.entries()) {
|
|
1456
1712
|
const rawScope =
|
|
1457
1713
|
part.kind === "tool-call"
|
|
1458
|
-
? (this.toolCalls.get(part.toolCallId)?.subagentRunId ??
|
|
1714
|
+
? (this.buf.toolCalls.get(part.toolCallId)?.subagentRunId ??
|
|
1715
|
+
ROOT_SCOPE)
|
|
1459
1716
|
: "subagentRunId" in part
|
|
1460
1717
|
? (part.subagentRunId ?? ROOT_SCOPE)
|
|
1461
1718
|
: ROOT_SCOPE;
|
|
@@ -1472,9 +1729,13 @@ export class RunAggregator {
|
|
|
1472
1729
|
reachable,
|
|
1473
1730
|
partsByScope,
|
|
1474
1731
|
approvals: projectAgUiToolApprovals(
|
|
1475
|
-
this.status?.type === "requires-action"
|
|
1732
|
+
this.buf === this.current && this.status?.type === "requires-action"
|
|
1733
|
+
? this.interrupts
|
|
1734
|
+
: undefined,
|
|
1476
1735
|
new Set(
|
|
1477
|
-
Array.from(this.toolCalls.values()).map(
|
|
1736
|
+
Array.from(this.buf.toolCalls.values()).map(
|
|
1737
|
+
(entry) => entry.toolCallId,
|
|
1738
|
+
),
|
|
1478
1739
|
),
|
|
1479
1740
|
),
|
|
1480
1741
|
root,
|
|
@@ -1483,18 +1744,18 @@ export class RunAggregator {
|
|
|
1483
1744
|
const { opaqueCandidates, lastMaterializedIndex } = root;
|
|
1484
1745
|
|
|
1485
1746
|
const toolMessageIds = new Set<string>();
|
|
1486
|
-
for (const call of this.toolCalls.values()) {
|
|
1747
|
+
for (const call of this.buf.toolCalls.values()) {
|
|
1487
1748
|
if (call.toolMessageId) toolMessageIds.add(call.toolMessageId);
|
|
1488
1749
|
}
|
|
1489
1750
|
opaqueCandidates.sort((a, b) => a.anchor - b.anchor);
|
|
1490
1751
|
const publishableOpaqueReasoning = opaqueCandidates
|
|
1491
1752
|
.filter((entry) => {
|
|
1492
1753
|
const collides =
|
|
1493
|
-
this.textParts.has(entry.id) ||
|
|
1494
|
-
entry.id === this.reportedServerMessageId ||
|
|
1754
|
+
this.buf.textParts.has(entry.id) ||
|
|
1755
|
+
entry.id === this.buf.reportedServerMessageId ||
|
|
1495
1756
|
toolMessageIds.has(entry.id);
|
|
1496
|
-
if (collides && !this.loggedDroppedOpaqueIds.has(entry.id)) {
|
|
1497
|
-
this.loggedDroppedOpaqueIds.add(entry.id);
|
|
1757
|
+
if (collides && !this.buf.loggedDroppedOpaqueIds.has(entry.id)) {
|
|
1758
|
+
this.buf.loggedDroppedOpaqueIds.add(entry.id);
|
|
1498
1759
|
this.logger.debug?.(
|
|
1499
1760
|
"[agui] aggregator dropped opaque reasoning signature: id collides with a message id",
|
|
1500
1761
|
entry.id,
|
|
@@ -1506,10 +1767,30 @@ export class RunAggregator {
|
|
|
1506
1767
|
lastMaterializedIndex < anchor ? { ...entry, after: true } : entry,
|
|
1507
1768
|
);
|
|
1508
1769
|
if (publishableOpaqueReasoning.length > 0)
|
|
1509
|
-
this.hasEmittedOpaqueReasoning = true;
|
|
1770
|
+
this.buf.hasEmittedOpaqueReasoning = true;
|
|
1510
1771
|
// Once an opaque entry has been published this run, the key keeps being
|
|
1511
1772
|
// emitted (empty when withdrawn) so the namespace merge can retract it.
|
|
1512
|
-
const includeOpaque = this.hasEmittedOpaqueReasoning;
|
|
1773
|
+
const includeOpaque = this.buf.hasEmittedOpaqueReasoning;
|
|
1774
|
+
if (this.buf !== this.current) {
|
|
1775
|
+
this.onRetainedMessage?.(
|
|
1776
|
+
{
|
|
1777
|
+
content: snapshot,
|
|
1778
|
+
...(includeOpaque
|
|
1779
|
+
? {
|
|
1780
|
+
metadata: {
|
|
1781
|
+
custom: {
|
|
1782
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
1783
|
+
opaqueReasoning: publishableOpaqueReasoning,
|
|
1784
|
+
} satisfies AgUiCustomMetadata,
|
|
1785
|
+
},
|
|
1786
|
+
},
|
|
1787
|
+
}
|
|
1788
|
+
: {}),
|
|
1789
|
+
},
|
|
1790
|
+
this.retainedState(this.buf),
|
|
1791
|
+
);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1513
1794
|
const timing = this.getTiming();
|
|
1514
1795
|
const metadata = {
|
|
1515
1796
|
...(timing ? { timing } : {}),
|
|
@@ -1560,7 +1841,7 @@ export class RunAggregator {
|
|
|
1560
1841
|
const tokenCount =
|
|
1561
1842
|
this.totalChunks > 0
|
|
1562
1843
|
? Math.ceil(
|
|
1563
|
-
Array.from(this.textParts.values()).reduce(
|
|
1844
|
+
Array.from(this.buf.textParts.values()).reduce(
|
|
1564
1845
|
(sum, p) => sum + p.buffer.length,
|
|
1565
1846
|
0,
|
|
1566
1847
|
) / 4,
|
|
@@ -1580,7 +1861,7 @@ export class RunAggregator {
|
|
|
1580
1861
|
...(tokenCount !== undefined ? { tokenCount } : {}),
|
|
1581
1862
|
...(tokensPerSecond !== undefined ? { tokensPerSecond } : {}),
|
|
1582
1863
|
totalChunks: this.totalChunks,
|
|
1583
|
-
toolCallCount: this.toolCalls.size,
|
|
1864
|
+
toolCallCount: this.buf.toolCalls.size,
|
|
1584
1865
|
};
|
|
1585
1866
|
}
|
|
1586
1867
|
|
|
@@ -1592,28 +1873,28 @@ export class RunAggregator {
|
|
|
1592
1873
|
): void {
|
|
1593
1874
|
if (!this.showThinking) {
|
|
1594
1875
|
if (scope !== ROOT_SCOPE) return;
|
|
1595
|
-
const anchor = this.partOrder.length;
|
|
1596
|
-
this.hiddenReasoningIds.add(messageId);
|
|
1597
|
-
this.hiddenBlockAnchors.set(messageId, anchor);
|
|
1598
|
-
this.hiddenActiveReasoningId = messageId;
|
|
1876
|
+
const anchor = this.buf.partOrder.length;
|
|
1877
|
+
this.buf.hiddenReasoningIds.add(messageId);
|
|
1878
|
+
this.buf.hiddenBlockAnchors.set(messageId, anchor);
|
|
1879
|
+
this.buf.hiddenActiveReasoningId = messageId;
|
|
1599
1880
|
return;
|
|
1600
1881
|
}
|
|
1601
1882
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
1602
1883
|
// should be a new part, not appended to any pre-reasoning text — scoped.
|
|
1603
|
-
this.activeTextMessageIdByScope.delete(scope);
|
|
1884
|
+
this.buf.activeTextMessageIdByScope.delete(scope);
|
|
1604
1885
|
const key = this.partKey(scope, messageId);
|
|
1605
1886
|
if (isMessageId) {
|
|
1606
|
-
this.reasoningMessageIds.set(key, messageId);
|
|
1887
|
+
this.buf.reasoningMessageIds.set(key, messageId);
|
|
1607
1888
|
}
|
|
1608
|
-
if (!this.reasoningParts.has(key)) {
|
|
1609
|
-
this.reasoningParts.set(key, { buffer: "" });
|
|
1610
|
-
this.partOrder.push(
|
|
1889
|
+
if (!this.buf.reasoningParts.has(key)) {
|
|
1890
|
+
this.buf.reasoningParts.set(key, { buffer: "" });
|
|
1891
|
+
this.buf.partOrder.push(
|
|
1611
1892
|
scope === ROOT_SCOPE
|
|
1612
1893
|
? { kind: "reasoning", key }
|
|
1613
1894
|
: { kind: "reasoning", key, subagentRunId: scope },
|
|
1614
1895
|
);
|
|
1615
1896
|
}
|
|
1616
|
-
const entry = this.reasoningParts.get(key);
|
|
1897
|
+
const entry = this.buf.reasoningParts.get(key);
|
|
1617
1898
|
if (entry?.timing) {
|
|
1618
1899
|
entry.timing = { startedAt: entry.timing.startedAt };
|
|
1619
1900
|
} else if (
|
|
@@ -1623,7 +1904,7 @@ export class RunAggregator {
|
|
|
1623
1904
|
) {
|
|
1624
1905
|
entry.timing = { startedAt: timestamp };
|
|
1625
1906
|
}
|
|
1626
|
-
this.activeReasoningKeyByScope.set(scope, key);
|
|
1907
|
+
this.buf.activeReasoningKeyByScope.set(scope, key);
|
|
1627
1908
|
this.emit();
|
|
1628
1909
|
}
|
|
1629
1910
|
|
|
@@ -1638,29 +1919,29 @@ export class RunAggregator {
|
|
|
1638
1919
|
// Content without a preceding START still names the block (the visible
|
|
1639
1920
|
// path opens it lazily); register it so a signature can claim it.
|
|
1640
1921
|
if (scope !== ROOT_SCOPE) return;
|
|
1641
|
-
if (this.hiddenActiveReasoningId === undefined) {
|
|
1922
|
+
if (this.buf.hiddenActiveReasoningId === undefined) {
|
|
1642
1923
|
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
1643
1924
|
}
|
|
1644
1925
|
return;
|
|
1645
1926
|
}
|
|
1646
|
-
if (!this.activeReasoningKeyByScope.has(scope)) {
|
|
1927
|
+
if (!this.buf.activeReasoningKeyByScope.has(scope)) {
|
|
1647
1928
|
// Content arrived without a preceding START — create the slot lazily.
|
|
1648
1929
|
this.handleReasoningStart(scope, messageId, isMessageId);
|
|
1649
1930
|
}
|
|
1650
|
-
const key = this.activeReasoningKeyByScope.get(scope);
|
|
1931
|
+
const key = this.buf.activeReasoningKeyByScope.get(scope);
|
|
1651
1932
|
if (!key) return;
|
|
1652
|
-
const entry = this.reasoningParts.get(key);
|
|
1933
|
+
const entry = this.buf.reasoningParts.get(key);
|
|
1653
1934
|
if (entry) entry.buffer += delta;
|
|
1654
1935
|
this.emit();
|
|
1655
1936
|
}
|
|
1656
1937
|
|
|
1657
1938
|
private handleReasoningEnd(scope: string, timestamp?: number): void {
|
|
1658
1939
|
if (!this.showThinking) {
|
|
1659
|
-
if (scope === ROOT_SCOPE) this.hiddenActiveReasoningId = undefined;
|
|
1940
|
+
if (scope === ROOT_SCOPE) this.buf.hiddenActiveReasoningId = undefined;
|
|
1660
1941
|
return;
|
|
1661
1942
|
}
|
|
1662
|
-
const key = this.activeReasoningKeyByScope.get(scope);
|
|
1663
|
-
const entry = key ? this.reasoningParts.get(key) : undefined;
|
|
1943
|
+
const key = this.buf.activeReasoningKeyByScope.get(scope);
|
|
1944
|
+
const entry = key ? this.buf.reasoningParts.get(key) : undefined;
|
|
1664
1945
|
if (
|
|
1665
1946
|
entry?.timing &&
|
|
1666
1947
|
typeof timestamp === "number" &&
|
|
@@ -1668,7 +1949,7 @@ export class RunAggregator {
|
|
|
1668
1949
|
) {
|
|
1669
1950
|
entry.timing = { ...entry.timing, completedAt: timestamp };
|
|
1670
1951
|
}
|
|
1671
|
-
this.activeReasoningKeyByScope.delete(scope);
|
|
1952
|
+
this.buf.activeReasoningKeyByScope.delete(scope);
|
|
1672
1953
|
this.emit();
|
|
1673
1954
|
}
|
|
1674
1955
|
}
|