@assistant-ui/react-ag-ui 0.0.54 → 0.0.57
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/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +25 -15
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +257 -210
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +13 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +77 -39
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +23 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +155 -41
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +3 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +9 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +56 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
- package/dist/runtime/adapter/tool-approval.js +244 -0
- package/dist/runtime/adapter/tool-approval.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -1
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.d.ts +8 -0
- package/dist/runtime/interrupt-internals.d.ts.map +1 -0
- package/dist/runtime/interrupt-internals.js +25 -0
- package/dist/runtime/interrupt-internals.js.map +1 -0
- package/dist/useAgUiRuntime.js +14 -9
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +436 -272
- package/src/runtime/adapter/conversions.content.test.ts +61 -3
- package/src/runtime/adapter/conversions.ts +123 -57
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +234 -47
- package/src/runtime/adapter/subscriber.ts +14 -0
- package/src/runtime/adapter/tool-approval.test.ts +681 -0
- package/src/runtime/adapter/tool-approval.ts +366 -0
- package/src/runtime/event-parser.ts +10 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.history.test.tsx +76 -0
- package/src/useAgUiRuntime.ts +15 -9
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type A2uiSurfaceState,
|
|
16
16
|
} from "@assistant-ui/react-generative-ui/a2ui";
|
|
17
17
|
import { readMcpAppResourceUri } from "../mcp-tool-result";
|
|
18
|
+
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
18
19
|
import type { AgUiEvent, AgUiInterrupt } from "../types";
|
|
19
20
|
import type { Logger } from "../logger";
|
|
20
21
|
|
|
@@ -27,6 +28,9 @@ export type AgUiOpaqueReasoning = {
|
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
export type AgUiCustomMetadata = {
|
|
31
|
+
/** Wire role restored on export for messages the internal model cannot
|
|
32
|
+
* represent (a developer record rides as a system message). */
|
|
33
|
+
role?: "developer";
|
|
30
34
|
interrupts?: AgUiInterrupt[];
|
|
31
35
|
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
32
36
|
};
|
|
@@ -62,29 +66,47 @@ export type RunAggregatorOptions = {
|
|
|
62
66
|
logger: Logger;
|
|
63
67
|
emit: Emit;
|
|
64
68
|
onServerMessageId?: (messageId: string) => void;
|
|
69
|
+
onTextMessageStart?: (messageId: string) => void;
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
73
|
* Collects AG-UI events into assistant-ui run snapshots that can be yielded from a ChatModelAdapter.
|
|
69
74
|
*
|
|
70
75
|
* The aggregator keeps a single assistant message worth of parts. Each incoming event updates the parts and
|
|
71
|
-
* emits a fresh snapshot through the provided `emit` callback.
|
|
76
|
+
* emits a fresh snapshot through the provided `emit` callback. `CUSTOM` events
|
|
77
|
+
* become canonical data parts; integration plumbing names such as
|
|
78
|
+
* `on_interrupt`, `PredictState`, `Exit`, `hook_error`, `state_update_error`,
|
|
79
|
+
* `system:*`, and `MultiAgentHandoff` are forwarded the same way, so apps only
|
|
80
|
+
* need data renderers for names they own.
|
|
72
81
|
*/
|
|
73
82
|
export class RunAggregator {
|
|
74
83
|
private readonly emitUpdate: Emit;
|
|
75
84
|
private readonly showThinking: boolean;
|
|
76
85
|
private readonly logger: Logger;
|
|
77
86
|
private readonly onServerMessageId: ((messageId: string) => void) | undefined;
|
|
87
|
+
private readonly onTextMessageStart:
|
|
88
|
+
| ((messageId: string) => void)
|
|
89
|
+
| undefined;
|
|
78
90
|
|
|
79
91
|
private status: ChatModelRunResult["status"] | undefined;
|
|
80
92
|
private interrupts: AgUiInterrupt[] | undefined;
|
|
81
|
-
private readonly textParts = new Map<
|
|
82
|
-
string,
|
|
83
|
-
{ buffer: string; touched: boolean }
|
|
84
|
-
>();
|
|
93
|
+
private readonly textParts = new Map<string, { buffer: string }>();
|
|
85
94
|
private activeTextMessageId: string | undefined;
|
|
86
95
|
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
87
96
|
private readonly reasoningSignatures = new Map<string, string>();
|
|
97
|
+
private readonly reasoningSignatureIds = new Map<string, string>();
|
|
98
|
+
// Signatures captured while thinking is hidden have no block to live on;
|
|
99
|
+
// they are transport state that must survive to the next run input. The
|
|
100
|
+
// claim rules mirror the visible path: an id-carrying block, or an open
|
|
101
|
+
// anonymous one.
|
|
102
|
+
private readonly hiddenSignatures = new Map<string, string>();
|
|
103
|
+
private readonly hiddenSignatureAnchors = new Map<string, number>();
|
|
104
|
+
private readonly hiddenBlockAnchors = new Map<string, number>();
|
|
105
|
+
private hiddenAnonymousAnchor: number | undefined;
|
|
106
|
+
private readonly hiddenReasoningIds = new Set<string>();
|
|
107
|
+
private hiddenActiveReasoning: "none" | "anonymous" | "identified" = "none";
|
|
108
|
+
private hasEmittedOpaqueReasoning = false;
|
|
109
|
+
private readonly loggedDroppedOpaqueIds = new Set<string>();
|
|
88
110
|
private readonly reasoningMessageIds = new Map<string, string>();
|
|
89
111
|
private readonly anonymousReasoningKeys = new Set<string>();
|
|
90
112
|
private activeReasoningKey: string | undefined;
|
|
@@ -97,9 +119,12 @@ export class RunAggregator {
|
|
|
97
119
|
| { kind: "text"; key: string }
|
|
98
120
|
| { kind: "reasoning"; key: string }
|
|
99
121
|
| { kind: "tool-call"; toolCallId: string }
|
|
122
|
+
| { kind: "data"; name: string; value: unknown }
|
|
100
123
|
)[] = [];
|
|
101
124
|
private textPartCounter = 0;
|
|
102
125
|
private serverMessageIdReported = false;
|
|
126
|
+
private reportedServerMessageId: string | undefined;
|
|
127
|
+
private lastTextMessageId: string | undefined;
|
|
103
128
|
|
|
104
129
|
private streamStartTime: number | undefined;
|
|
105
130
|
private firstTokenTime: number | undefined;
|
|
@@ -110,6 +135,7 @@ export class RunAggregator {
|
|
|
110
135
|
this.showThinking = options.showThinking;
|
|
111
136
|
this.logger = options.logger;
|
|
112
137
|
this.onServerMessageId = options.onServerMessageId;
|
|
138
|
+
this.onTextMessageStart = options.onTextMessageStart;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
hasToolCall(toolCallId: string): boolean {
|
|
@@ -119,22 +145,11 @@ export class RunAggregator {
|
|
|
119
145
|
handle(event: AgUiEvent): void {
|
|
120
146
|
switch (event.type) {
|
|
121
147
|
case "RUN_STARTED": {
|
|
122
|
-
this.
|
|
123
|
-
this.reasoningParts.clear();
|
|
124
|
-
this.reasoningSignatures.clear();
|
|
125
|
-
this.reasoningMessageIds.clear();
|
|
126
|
-
this.anonymousReasoningKeys.clear();
|
|
127
|
-
this.activeReasoningKey = undefined;
|
|
128
|
-
this.reasoningPartCounter = 0;
|
|
129
|
-
this.toolCalls.clear();
|
|
130
|
-
this.a2uiBuckets.clear();
|
|
131
|
-
this.a2uiToolCallIds.clear();
|
|
132
|
-
this.lastResolvedToolCallId = undefined;
|
|
133
|
-
this.partOrder.length = 0;
|
|
134
|
-
this.textPartCounter = 0;
|
|
135
|
-
this.activeTextMessageId = undefined;
|
|
148
|
+
this.resetMessageParts();
|
|
136
149
|
this.interrupts = undefined;
|
|
137
150
|
this.serverMessageIdReported = false;
|
|
151
|
+
this.lastTextMessageId = undefined;
|
|
152
|
+
this.reportedServerMessageId = undefined;
|
|
138
153
|
this.streamStartTime = Date.now();
|
|
139
154
|
this.firstTokenTime = undefined;
|
|
140
155
|
this.totalChunks = 0;
|
|
@@ -178,18 +193,19 @@ export class RunAggregator {
|
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
case "TEXT_MESSAGE_START": {
|
|
196
|
+
this.beginDistinctTextMessage(event.messageId);
|
|
181
197
|
this.reportServerMessageId(event.messageId);
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
this.markTextPartTouched(id);
|
|
185
|
-
}
|
|
198
|
+
this.startTextMessage(event.messageId);
|
|
199
|
+
if (event.messageId) this.lastTextMessageId = event.messageId;
|
|
186
200
|
this.emit();
|
|
187
201
|
break;
|
|
188
202
|
}
|
|
189
203
|
case "TEXT_MESSAGE_CONTENT":
|
|
190
204
|
case "TEXT_MESSAGE_CHUNK": {
|
|
191
205
|
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
206
|
+
this.beginDistinctTextMessage(incomingId);
|
|
192
207
|
this.reportServerMessageId(incomingId);
|
|
208
|
+
if (incomingId) this.lastTextMessageId = incomingId;
|
|
193
209
|
if (!event.delta) break;
|
|
194
210
|
this.recordFirstToken();
|
|
195
211
|
const id = this.resolveTextMessageId(incomingId);
|
|
@@ -207,6 +223,17 @@ export class RunAggregator {
|
|
|
207
223
|
break;
|
|
208
224
|
}
|
|
209
225
|
|
|
226
|
+
case "CUSTOM": {
|
|
227
|
+
this.activeTextMessageId = undefined;
|
|
228
|
+
this.partOrder.push({
|
|
229
|
+
kind: "data",
|
|
230
|
+
name: event.name,
|
|
231
|
+
value: event.value,
|
|
232
|
+
});
|
|
233
|
+
this.emit();
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
case "THINKING_START":
|
|
211
238
|
case "THINKING_TEXT_MESSAGE_START":
|
|
212
239
|
case "REASONING_START":
|
|
@@ -221,13 +248,31 @@ export class RunAggregator {
|
|
|
221
248
|
// entityId names any message, not necessarily a reasoning one, so an
|
|
222
249
|
// unmatched id may only claim a block that has no id to contradict it.
|
|
223
250
|
const active = this.activeReasoningKey;
|
|
224
|
-
const key = this.
|
|
225
|
-
? event.entityId
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
251
|
+
const key = this.showThinking
|
|
252
|
+
? this.reasoningParts.has(event.entityId)
|
|
253
|
+
? event.entityId
|
|
254
|
+
: active !== undefined && this.anonymousReasoningKeys.has(active)
|
|
255
|
+
? active
|
|
256
|
+
: undefined
|
|
257
|
+
: undefined;
|
|
229
258
|
if (key !== undefined) {
|
|
230
259
|
this.reasoningSignatures.set(key, event.encryptedValue);
|
|
260
|
+
this.reasoningSignatureIds.set(key, event.entityId);
|
|
261
|
+
this.emit();
|
|
262
|
+
} else if (
|
|
263
|
+
!this.showThinking &&
|
|
264
|
+
event.entityId.trim().length > 0 &&
|
|
265
|
+
event.encryptedValue.trim().length > 0 &&
|
|
266
|
+
(this.hiddenReasoningIds.has(event.entityId) ||
|
|
267
|
+
this.hiddenActiveReasoning === "anonymous")
|
|
268
|
+
) {
|
|
269
|
+
this.hiddenSignatures.set(event.entityId, event.encryptedValue);
|
|
270
|
+
this.hiddenSignatureAnchors.set(
|
|
271
|
+
event.entityId,
|
|
272
|
+
this.hiddenBlockAnchors.get(event.entityId) ??
|
|
273
|
+
this.hiddenAnonymousAnchor ??
|
|
274
|
+
this.partOrder.length,
|
|
275
|
+
);
|
|
231
276
|
this.emit();
|
|
232
277
|
}
|
|
233
278
|
}
|
|
@@ -419,8 +464,13 @@ export class RunAggregator {
|
|
|
419
464
|
}
|
|
420
465
|
|
|
421
466
|
private reportServerMessageId(messageId: string | undefined): void {
|
|
422
|
-
if (
|
|
467
|
+
if (!messageId) return;
|
|
468
|
+
if (this.lastTextMessageId === undefined) {
|
|
469
|
+
this.lastTextMessageId = messageId;
|
|
470
|
+
}
|
|
471
|
+
if (this.serverMessageIdReported) return;
|
|
423
472
|
this.serverMessageIdReported = true;
|
|
473
|
+
this.reportedServerMessageId = messageId;
|
|
424
474
|
this.onServerMessageId?.(messageId);
|
|
425
475
|
}
|
|
426
476
|
|
|
@@ -428,6 +478,45 @@ export class RunAggregator {
|
|
|
428
478
|
this.textParts.clear();
|
|
429
479
|
}
|
|
430
480
|
|
|
481
|
+
private resetMessageParts(): void {
|
|
482
|
+
this.clearTextParts();
|
|
483
|
+
this.reasoningParts.clear();
|
|
484
|
+
this.reasoningSignatures.clear();
|
|
485
|
+
this.reasoningSignatureIds.clear();
|
|
486
|
+
this.reasoningMessageIds.clear();
|
|
487
|
+
this.hiddenSignatures.clear();
|
|
488
|
+
this.hiddenSignatureAnchors.clear();
|
|
489
|
+
this.hiddenBlockAnchors.clear();
|
|
490
|
+
this.hiddenAnonymousAnchor = undefined;
|
|
491
|
+
this.hiddenReasoningIds.clear();
|
|
492
|
+
this.hiddenActiveReasoning = "none";
|
|
493
|
+
this.loggedDroppedOpaqueIds.clear();
|
|
494
|
+
this.anonymousReasoningKeys.clear();
|
|
495
|
+
this.activeReasoningKey = undefined;
|
|
496
|
+
this.reasoningPartCounter = 0;
|
|
497
|
+
this.toolCalls.clear();
|
|
498
|
+
this.a2uiBuckets.clear();
|
|
499
|
+
this.a2uiToolCallIds.clear();
|
|
500
|
+
this.lastResolvedToolCallId = undefined;
|
|
501
|
+
this.partOrder.length = 0;
|
|
502
|
+
this.textPartCounter = 0;
|
|
503
|
+
this.activeTextMessageId = undefined;
|
|
504
|
+
this.reportedServerMessageId = undefined;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private beginDistinctTextMessage(messageId: string | undefined): void {
|
|
508
|
+
if (
|
|
509
|
+
!messageId ||
|
|
510
|
+
this.lastTextMessageId === undefined ||
|
|
511
|
+
this.lastTextMessageId === messageId ||
|
|
512
|
+
!this.onTextMessageStart
|
|
513
|
+
) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.resetMessageParts();
|
|
517
|
+
this.onTextMessageStart(messageId);
|
|
518
|
+
}
|
|
519
|
+
|
|
431
520
|
private generateTextKey(): string {
|
|
432
521
|
this.textPartCounter += 1;
|
|
433
522
|
return `text-${this.textPartCounter}`;
|
|
@@ -459,7 +548,7 @@ export class RunAggregator {
|
|
|
459
548
|
|
|
460
549
|
private ensureTextPart(id: string): void {
|
|
461
550
|
if (!this.textParts.has(id)) {
|
|
462
|
-
this.textParts.set(id, { buffer: ""
|
|
551
|
+
this.textParts.set(id, { buffer: "" });
|
|
463
552
|
if (
|
|
464
553
|
!this.partOrder.some((part) => part.kind === "text" && part.key === id)
|
|
465
554
|
) {
|
|
@@ -468,18 +557,11 @@ export class RunAggregator {
|
|
|
468
557
|
}
|
|
469
558
|
}
|
|
470
559
|
|
|
471
|
-
private markTextPartTouched(id: string): void {
|
|
472
|
-
const entry = this.textParts.get(id);
|
|
473
|
-
if (!entry) return;
|
|
474
|
-
entry.touched = true;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
560
|
private appendText(id: string, delta: string): void {
|
|
478
561
|
this.ensureTextPart(id);
|
|
479
562
|
const entry = this.textParts.get(id);
|
|
480
563
|
if (!entry) return;
|
|
481
564
|
entry.buffer += delta;
|
|
482
|
-
entry.touched = true;
|
|
483
565
|
}
|
|
484
566
|
|
|
485
567
|
private startToolCall(
|
|
@@ -614,8 +696,31 @@ export class RunAggregator {
|
|
|
614
696
|
|
|
615
697
|
private emit(): void {
|
|
616
698
|
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
699
|
+
// A run that ended incomplete can no longer be resumed, so a gate left over
|
|
700
|
+
// from an earlier interrupt outcome is unanswerable and must not stay
|
|
701
|
+
// projected. The interrupts themselves are kept on the message, since the
|
|
702
|
+
// bespoke hooks read that payload.
|
|
703
|
+
const approvals = projectAgUiToolApprovals(
|
|
704
|
+
this.status?.type === "requires-action" ? this.interrupts : undefined,
|
|
705
|
+
new Set(this.toolCalls.keys()),
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
const opaqueCandidates: (AgUiOpaqueReasoning & { anchor: number })[] =
|
|
709
|
+
Array.from(this.hiddenSignatures, ([id, encryptedValue]) => ({
|
|
710
|
+
id,
|
|
711
|
+
encryptedValue,
|
|
712
|
+
anchor: this.hiddenSignatureAnchors.get(id)!,
|
|
713
|
+
}));
|
|
714
|
+
|
|
715
|
+
let currentIndex = -1;
|
|
716
|
+
let lastMaterializedIndex = -1;
|
|
717
|
+
const pushSnapshotPart = (part: (typeof snapshot)[number]) => {
|
|
718
|
+
snapshot.push(part);
|
|
719
|
+
lastMaterializedIndex = currentIndex;
|
|
720
|
+
};
|
|
617
721
|
|
|
618
|
-
for (const part of this.partOrder) {
|
|
722
|
+
for (const [index, part] of this.partOrder.entries()) {
|
|
723
|
+
currentIndex = index;
|
|
619
724
|
if (part.kind === "reasoning") {
|
|
620
725
|
if (this.showThinking) {
|
|
621
726
|
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
@@ -626,13 +731,22 @@ export class RunAggregator {
|
|
|
626
731
|
...(reasoningId !== undefined ? { reasoningId } : {}),
|
|
627
732
|
...(encryptedValue !== undefined ? { encryptedValue } : {}),
|
|
628
733
|
};
|
|
629
|
-
|
|
734
|
+
pushSnapshotPart({
|
|
630
735
|
type: "reasoning",
|
|
631
736
|
text: buffer,
|
|
632
737
|
...(Object.keys(meta).length > 0
|
|
633
738
|
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
634
739
|
: {}),
|
|
635
740
|
} as const);
|
|
741
|
+
} else {
|
|
742
|
+
// A retracted empty block still carries transport state: without
|
|
743
|
+
// a part to live on, its signature rides the message metadata,
|
|
744
|
+
// matching the shape a snapshot reload produces.
|
|
745
|
+
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
746
|
+
const id = this.reasoningSignatureIds.get(part.key);
|
|
747
|
+
if (id?.trim() && encryptedValue?.trim()) {
|
|
748
|
+
opaqueCandidates.push({ id, encryptedValue, anchor: index + 1 });
|
|
749
|
+
}
|
|
636
750
|
}
|
|
637
751
|
}
|
|
638
752
|
continue;
|
|
@@ -640,20 +754,31 @@ export class RunAggregator {
|
|
|
640
754
|
|
|
641
755
|
if (part.kind === "text") {
|
|
642
756
|
const entry = this.textParts.get(part.key);
|
|
643
|
-
if (entry
|
|
644
|
-
|
|
757
|
+
if (entry && entry.buffer.trim().length > 0) {
|
|
758
|
+
pushSnapshotPart({ type: "text", text: entry.buffer } as const);
|
|
645
759
|
}
|
|
646
760
|
continue;
|
|
647
761
|
}
|
|
648
762
|
|
|
763
|
+
if (part.kind === "data") {
|
|
764
|
+
pushSnapshotPart({
|
|
765
|
+
type: "data",
|
|
766
|
+
name: part.name,
|
|
767
|
+
data: part.value,
|
|
768
|
+
});
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
|
|
649
772
|
const entry = this.toolCalls.get(part.toolCallId);
|
|
650
773
|
if (!entry) continue;
|
|
774
|
+
const approval = approvals.get(entry.toolCallId);
|
|
651
775
|
const toolPart: ToolCallMessagePart = {
|
|
652
776
|
type: "tool-call",
|
|
653
777
|
toolCallId: entry.toolCallId,
|
|
654
778
|
toolName: entry.toolCallName,
|
|
655
779
|
args: (entry.parsedArgs ?? {}) as any,
|
|
656
780
|
argsText: entry.argsText,
|
|
781
|
+
...(approval ? { approval } : {}),
|
|
657
782
|
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
658
783
|
...(entry.modelContent !== undefined
|
|
659
784
|
? { modelContent: entry.modelContent }
|
|
@@ -676,17 +801,57 @@ export class RunAggregator {
|
|
|
676
801
|
? { unstable_toolMessageId: entry.toolMessageId }
|
|
677
802
|
: {}),
|
|
678
803
|
} as ToolCallMessagePart & { unstable_toolMessageId?: string };
|
|
679
|
-
|
|
804
|
+
pushSnapshotPart(toolPart);
|
|
680
805
|
}
|
|
681
806
|
|
|
807
|
+
// An anonymous claim promotes the signature's entityId to a wire message
|
|
808
|
+
// id; when that id actually names a text message or the adopted assistant
|
|
809
|
+
// message id (which can also arrive via TOOL_CALL_START.parentMessageId),
|
|
810
|
+
// replaying it as its own record would put two wire records under one id,
|
|
811
|
+
// so such entries are dropped instead — the same outcome the visible
|
|
812
|
+
// path's claim guard produces for a signature that is "not for this
|
|
813
|
+
// block". Entries with no materialized part after their anchor trail the
|
|
814
|
+
// assistant record, matching the snapshot path's anchor/after
|
|
815
|
+
// bookkeeping.
|
|
816
|
+
const toolMessageIds = new Set<string>();
|
|
817
|
+
for (const call of this.toolCalls.values()) {
|
|
818
|
+
if (call.toolMessageId) toolMessageIds.add(call.toolMessageId);
|
|
819
|
+
}
|
|
820
|
+
opaqueCandidates.sort((a, b) => a.anchor - b.anchor);
|
|
821
|
+
const publishableOpaqueReasoning = opaqueCandidates
|
|
822
|
+
.filter((entry) => {
|
|
823
|
+
const collides =
|
|
824
|
+
this.textParts.has(entry.id) ||
|
|
825
|
+
entry.id === this.reportedServerMessageId ||
|
|
826
|
+
toolMessageIds.has(entry.id);
|
|
827
|
+
if (collides && !this.loggedDroppedOpaqueIds.has(entry.id)) {
|
|
828
|
+
this.loggedDroppedOpaqueIds.add(entry.id);
|
|
829
|
+
this.logger.debug?.(
|
|
830
|
+
"[agui] aggregator dropped opaque reasoning signature: id collides with a message id",
|
|
831
|
+
entry.id,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return !collides;
|
|
835
|
+
})
|
|
836
|
+
.map(({ anchor, ...entry }) =>
|
|
837
|
+
lastMaterializedIndex < anchor ? { ...entry, after: true } : entry,
|
|
838
|
+
);
|
|
839
|
+
if (publishableOpaqueReasoning.length > 0)
|
|
840
|
+
this.hasEmittedOpaqueReasoning = true;
|
|
841
|
+
// Once an opaque entry has been published this run, the key keeps being
|
|
842
|
+
// emitted (empty when withdrawn) so the namespace merge can retract it.
|
|
843
|
+
const includeOpaque = this.hasEmittedOpaqueReasoning;
|
|
682
844
|
const timing = this.getTiming();
|
|
683
845
|
const metadata = {
|
|
684
846
|
...(timing ? { timing } : {}),
|
|
685
|
-
...(this.interrupts
|
|
847
|
+
...(this.interrupts || includeOpaque
|
|
686
848
|
? {
|
|
687
849
|
custom: {
|
|
688
850
|
[AG_UI_METADATA_NAMESPACE]: {
|
|
689
|
-
interrupts: this.interrupts,
|
|
851
|
+
...(this.interrupts ? { interrupts: this.interrupts } : {}),
|
|
852
|
+
...(includeOpaque
|
|
853
|
+
? { opaqueReasoning: publishableOpaqueReasoning }
|
|
854
|
+
: {}),
|
|
690
855
|
} satisfies AgUiCustomMetadata,
|
|
691
856
|
},
|
|
692
857
|
}
|
|
@@ -742,7 +907,18 @@ export class RunAggregator {
|
|
|
742
907
|
}
|
|
743
908
|
|
|
744
909
|
private handleReasoningStart(messageId?: string, isMessageId = false): void {
|
|
745
|
-
if (!this.showThinking)
|
|
910
|
+
if (!this.showThinking) {
|
|
911
|
+
const anchor = this.partOrder.length;
|
|
912
|
+
if (messageId === undefined) {
|
|
913
|
+
this.hiddenActiveReasoning = "anonymous";
|
|
914
|
+
this.hiddenAnonymousAnchor = anchor;
|
|
915
|
+
} else {
|
|
916
|
+
this.hiddenReasoningIds.add(messageId);
|
|
917
|
+
this.hiddenBlockAnchors.set(messageId, anchor);
|
|
918
|
+
this.hiddenActiveReasoning = "identified";
|
|
919
|
+
}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
746
922
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
747
923
|
// should be a new part, not appended to any pre-reasoning text.
|
|
748
924
|
this.activeTextMessageId = undefined;
|
|
@@ -768,7 +944,15 @@ export class RunAggregator {
|
|
|
768
944
|
messageId?: string,
|
|
769
945
|
isMessageId = false,
|
|
770
946
|
): void {
|
|
771
|
-
if (!
|
|
947
|
+
if (!delta) return;
|
|
948
|
+
if (!this.showThinking) {
|
|
949
|
+
// Content without a preceding START still names the block (the visible
|
|
950
|
+
// path opens it lazily); register it so a signature can claim it.
|
|
951
|
+
if (this.hiddenActiveReasoning === "none") {
|
|
952
|
+
this.handleReasoningStart(messageId, isMessageId);
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
772
956
|
if (!this.activeReasoningKey) {
|
|
773
957
|
// Content arrived without a preceding START — create the slot lazily.
|
|
774
958
|
this.handleReasoningStart(messageId, isMessageId);
|
|
@@ -780,7 +964,10 @@ export class RunAggregator {
|
|
|
780
964
|
}
|
|
781
965
|
|
|
782
966
|
private handleReasoningEnd(): void {
|
|
783
|
-
if (!this.showThinking)
|
|
967
|
+
if (!this.showThinking) {
|
|
968
|
+
this.hiddenActiveReasoning = "none";
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
784
971
|
this.activeReasoningKey = undefined;
|
|
785
972
|
this.emit();
|
|
786
973
|
}
|
|
@@ -35,6 +35,7 @@ type Subscriber = {
|
|
|
35
35
|
onCustomEvent?: (payload: { event: unknown }) => void;
|
|
36
36
|
onRawEvent?: (payload: { event: unknown }) => void;
|
|
37
37
|
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
38
|
+
onRunErrorEvent?: (payload: { event: unknown }) => void;
|
|
38
39
|
onRunFinalized?: () => void;
|
|
39
40
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
40
41
|
};
|
|
@@ -146,11 +147,24 @@ export const createAgUiSubscriber = (
|
|
|
146
147
|
onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
|
|
147
148
|
onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
|
|
148
149
|
onRunFinishedEvent: ({ event }) => {
|
|
150
|
+
if (runFinishedDispatched) return;
|
|
149
151
|
const parsed = ensureEvent(event, "RUN_FINISHED", logger);
|
|
150
152
|
if (!parsed) return;
|
|
151
153
|
runFinishedDispatched = true;
|
|
152
154
|
dispatch(parsed);
|
|
153
155
|
},
|
|
156
|
+
onRunErrorEvent: ({ event }) => {
|
|
157
|
+
const parsed = ensureEvent(event, "RUN_ERROR", logger);
|
|
158
|
+
if (!parsed || parsed.type !== "RUN_ERROR") return;
|
|
159
|
+
runFinishedDispatched = true;
|
|
160
|
+
dispatch(parsed);
|
|
161
|
+
|
|
162
|
+
const error = Object.assign(
|
|
163
|
+
new Error(parsed.message ?? "Run failed"),
|
|
164
|
+
parsed.code === undefined ? {} : { code: parsed.code },
|
|
165
|
+
);
|
|
166
|
+
onRunFailed?.(error);
|
|
167
|
+
},
|
|
154
168
|
onRunFinalized: () => {
|
|
155
169
|
if (runFinishedDispatched) return;
|
|
156
170
|
dispatch({ type: "RUN_FINISHED", runId });
|