@assistant-ui/react-ag-ui 0.0.53 → 0.0.56
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 +23 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +183 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +19 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +197 -36
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +15 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +62 -10
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +6 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +10 -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 +15 -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/runtime/types.d.ts +5 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +4 -1
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.ts +335 -42
- package/src/runtime/adapter/conversions.content.test.ts +278 -0
- package/src/runtime/adapter/conversions.ts +298 -50
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +96 -5
- package/src/runtime/adapter/subscriber.ts +17 -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 +23 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/runtime/types.ts +6 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.queue.test.tsx +68 -1
- package/src/useAgUiRuntime.ts +5 -1
|
@@ -15,13 +15,21 @@ 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
|
|
|
21
22
|
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
22
23
|
|
|
24
|
+
export type AgUiOpaqueReasoning = {
|
|
25
|
+
id: string;
|
|
26
|
+
encryptedValue: string;
|
|
27
|
+
after?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
23
30
|
export type AgUiCustomMetadata = {
|
|
24
31
|
interrupts?: AgUiInterrupt[];
|
|
32
|
+
opaqueReasoning?: AgUiOpaqueReasoning[];
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
type Emit = (update: ChatModelRunResult) => void;
|
|
@@ -61,7 +69,11 @@ export type RunAggregatorOptions = {
|
|
|
61
69
|
* Collects AG-UI events into assistant-ui run snapshots that can be yielded from a ChatModelAdapter.
|
|
62
70
|
*
|
|
63
71
|
* The aggregator keeps a single assistant message worth of parts. Each incoming event updates the parts and
|
|
64
|
-
* emits a fresh snapshot through the provided `emit` callback.
|
|
72
|
+
* emits a fresh snapshot through the provided `emit` callback. `CUSTOM` events
|
|
73
|
+
* become canonical data parts; integration plumbing names such as
|
|
74
|
+
* `on_interrupt`, `PredictState`, `Exit`, `hook_error`, `state_update_error`,
|
|
75
|
+
* `system:*`, and `MultiAgentHandoff` are forwarded the same way, so apps only
|
|
76
|
+
* need data renderers for names they own.
|
|
65
77
|
*/
|
|
66
78
|
export class RunAggregator {
|
|
67
79
|
private readonly emitUpdate: Emit;
|
|
@@ -77,6 +89,9 @@ export class RunAggregator {
|
|
|
77
89
|
>();
|
|
78
90
|
private activeTextMessageId: string | undefined;
|
|
79
91
|
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
92
|
+
private readonly reasoningSignatures = new Map<string, string>();
|
|
93
|
+
private readonly reasoningMessageIds = new Map<string, string>();
|
|
94
|
+
private readonly anonymousReasoningKeys = new Set<string>();
|
|
80
95
|
private activeReasoningKey: string | undefined;
|
|
81
96
|
private reasoningPartCounter = 0;
|
|
82
97
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
@@ -87,6 +102,7 @@ export class RunAggregator {
|
|
|
87
102
|
| { kind: "text"; key: string }
|
|
88
103
|
| { kind: "reasoning"; key: string }
|
|
89
104
|
| { kind: "tool-call"; toolCallId: string }
|
|
105
|
+
| { kind: "data"; name: string; value: unknown }
|
|
90
106
|
)[] = [];
|
|
91
107
|
private textPartCounter = 0;
|
|
92
108
|
private serverMessageIdReported = false;
|
|
@@ -111,6 +127,9 @@ export class RunAggregator {
|
|
|
111
127
|
case "RUN_STARTED": {
|
|
112
128
|
this.clearTextParts();
|
|
113
129
|
this.reasoningParts.clear();
|
|
130
|
+
this.reasoningSignatures.clear();
|
|
131
|
+
this.reasoningMessageIds.clear();
|
|
132
|
+
this.anonymousReasoningKeys.clear();
|
|
114
133
|
this.activeReasoningKey = undefined;
|
|
115
134
|
this.reasoningPartCounter = 0;
|
|
116
135
|
this.toolCalls.clear();
|
|
@@ -194,14 +213,42 @@ export class RunAggregator {
|
|
|
194
213
|
break;
|
|
195
214
|
}
|
|
196
215
|
|
|
216
|
+
case "CUSTOM": {
|
|
217
|
+
this.activeTextMessageId = undefined;
|
|
218
|
+
this.partOrder.push({
|
|
219
|
+
kind: "data",
|
|
220
|
+
name: event.name,
|
|
221
|
+
value: event.value,
|
|
222
|
+
});
|
|
223
|
+
this.emit();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
197
227
|
case "THINKING_START":
|
|
198
228
|
case "THINKING_TEXT_MESSAGE_START":
|
|
199
229
|
case "REASONING_START":
|
|
200
230
|
case "REASONING_MESSAGE_START":
|
|
201
231
|
this.handleReasoningStart(
|
|
202
232
|
"messageId" in event ? event.messageId : undefined,
|
|
233
|
+
event.type === "REASONING_MESSAGE_START",
|
|
203
234
|
);
|
|
204
235
|
break;
|
|
236
|
+
case "REASONING_ENCRYPTED_VALUE":
|
|
237
|
+
if (event.subtype === "message") {
|
|
238
|
+
// entityId names any message, not necessarily a reasoning one, so an
|
|
239
|
+
// unmatched id may only claim a block that has no id to contradict it.
|
|
240
|
+
const active = this.activeReasoningKey;
|
|
241
|
+
const key = this.reasoningParts.has(event.entityId)
|
|
242
|
+
? event.entityId
|
|
243
|
+
: active !== undefined && this.anonymousReasoningKeys.has(active)
|
|
244
|
+
? active
|
|
245
|
+
: undefined;
|
|
246
|
+
if (key !== undefined) {
|
|
247
|
+
this.reasoningSignatures.set(key, event.encryptedValue);
|
|
248
|
+
this.emit();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
205
252
|
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
206
253
|
this.handleReasoningContent(event.delta);
|
|
207
254
|
this.totalChunks++;
|
|
@@ -211,6 +258,7 @@ export class RunAggregator {
|
|
|
211
258
|
this.handleReasoningContent(
|
|
212
259
|
event.delta,
|
|
213
260
|
"messageId" in event ? event.messageId : undefined,
|
|
261
|
+
true,
|
|
214
262
|
);
|
|
215
263
|
this.totalChunks++;
|
|
216
264
|
this.recordFirstToken();
|
|
@@ -583,13 +631,33 @@ export class RunAggregator {
|
|
|
583
631
|
|
|
584
632
|
private emit(): void {
|
|
585
633
|
const snapshot: ThreadAssistantMessagePart[] = [];
|
|
634
|
+
// A run that ended incomplete can no longer be resumed, so a gate left over
|
|
635
|
+
// from an earlier interrupt outcome is unanswerable and must not stay
|
|
636
|
+
// projected. The interrupts themselves are kept on the message, since the
|
|
637
|
+
// bespoke hooks read that payload.
|
|
638
|
+
const approvals = projectAgUiToolApprovals(
|
|
639
|
+
this.status?.type === "requires-action" ? this.interrupts : undefined,
|
|
640
|
+
new Set(this.toolCalls.keys()),
|
|
641
|
+
);
|
|
586
642
|
|
|
587
643
|
for (const part of this.partOrder) {
|
|
588
644
|
if (part.kind === "reasoning") {
|
|
589
645
|
if (this.showThinking) {
|
|
590
646
|
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
591
647
|
if (buffer.length > 0 || this.activeReasoningKey === part.key) {
|
|
592
|
-
|
|
648
|
+
const encryptedValue = this.reasoningSignatures.get(part.key);
|
|
649
|
+
const reasoningId = this.reasoningMessageIds.get(part.key);
|
|
650
|
+
const meta = {
|
|
651
|
+
...(reasoningId !== undefined ? { reasoningId } : {}),
|
|
652
|
+
...(encryptedValue !== undefined ? { encryptedValue } : {}),
|
|
653
|
+
};
|
|
654
|
+
snapshot.push({
|
|
655
|
+
type: "reasoning",
|
|
656
|
+
text: buffer,
|
|
657
|
+
...(Object.keys(meta).length > 0
|
|
658
|
+
? { providerMetadata: { [AG_UI_METADATA_NAMESPACE]: meta } }
|
|
659
|
+
: {}),
|
|
660
|
+
} as const);
|
|
593
661
|
}
|
|
594
662
|
}
|
|
595
663
|
continue;
|
|
@@ -603,14 +671,25 @@ export class RunAggregator {
|
|
|
603
671
|
continue;
|
|
604
672
|
}
|
|
605
673
|
|
|
674
|
+
if (part.kind === "data") {
|
|
675
|
+
snapshot.push({
|
|
676
|
+
type: "data",
|
|
677
|
+
name: part.name,
|
|
678
|
+
data: part.value,
|
|
679
|
+
});
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
|
|
606
683
|
const entry = this.toolCalls.get(part.toolCallId);
|
|
607
684
|
if (!entry) continue;
|
|
685
|
+
const approval = approvals.get(entry.toolCallId);
|
|
608
686
|
const toolPart: ToolCallMessagePart = {
|
|
609
687
|
type: "tool-call",
|
|
610
688
|
toolCallId: entry.toolCallId,
|
|
611
689
|
toolName: entry.toolCallName,
|
|
612
690
|
args: (entry.parsedArgs ?? {}) as any,
|
|
613
691
|
argsText: entry.argsText,
|
|
692
|
+
...(approval ? { approval } : {}),
|
|
614
693
|
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
615
694
|
...(entry.modelContent !== undefined
|
|
616
695
|
? { modelContent: entry.modelContent }
|
|
@@ -698,12 +777,20 @@ export class RunAggregator {
|
|
|
698
777
|
};
|
|
699
778
|
}
|
|
700
779
|
|
|
701
|
-
private handleReasoningStart(messageId?: string): void {
|
|
780
|
+
private handleReasoningStart(messageId?: string, isMessageId = false): void {
|
|
702
781
|
if (!this.showThinking) return;
|
|
703
782
|
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
704
783
|
// should be a new part, not appended to any pre-reasoning text.
|
|
705
784
|
this.activeTextMessageId = undefined;
|
|
706
785
|
const key = messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`;
|
|
786
|
+
// Two different questions: which id may be replayed as a ReasoningMessage.id
|
|
787
|
+
// (only the message-scoped aliases carry one), and which block an unmatched
|
|
788
|
+
// signature may claim (any block opened without an id at all).
|
|
789
|
+
if (messageId === undefined) {
|
|
790
|
+
this.anonymousReasoningKeys.add(key);
|
|
791
|
+
} else if (isMessageId) {
|
|
792
|
+
this.reasoningMessageIds.set(key, messageId);
|
|
793
|
+
}
|
|
707
794
|
if (!this.reasoningParts.has(key)) {
|
|
708
795
|
this.reasoningParts.set(key, "");
|
|
709
796
|
this.partOrder.push({ kind: "reasoning", key });
|
|
@@ -712,11 +799,15 @@ export class RunAggregator {
|
|
|
712
799
|
this.emit();
|
|
713
800
|
}
|
|
714
801
|
|
|
715
|
-
private handleReasoningContent(
|
|
802
|
+
private handleReasoningContent(
|
|
803
|
+
delta: string,
|
|
804
|
+
messageId?: string,
|
|
805
|
+
isMessageId = false,
|
|
806
|
+
): void {
|
|
716
807
|
if (!this.showThinking || !delta) return;
|
|
717
808
|
if (!this.activeReasoningKey) {
|
|
718
809
|
// Content arrived without a preceding START — create the slot lazily.
|
|
719
|
-
this.handleReasoningStart(messageId);
|
|
810
|
+
this.handleReasoningStart(messageId, isMessageId);
|
|
720
811
|
}
|
|
721
812
|
const key = this.activeReasoningKey;
|
|
722
813
|
if (!key) return;
|
|
@@ -22,6 +22,7 @@ type Subscriber = {
|
|
|
22
22
|
onReasoningMessageStartEvent?: (payload: { event: unknown }) => void;
|
|
23
23
|
onReasoningMessageContentEvent?: (payload: { event: unknown }) => void;
|
|
24
24
|
onReasoningMessageEndEvent?: (payload: { event: unknown }) => void;
|
|
25
|
+
onReasoningEncryptedValueEvent?: (payload: { event: unknown }) => void;
|
|
25
26
|
onToolCallStartEvent?: (payload: { event: unknown }) => void;
|
|
26
27
|
onToolCallArgsEvent?: (payload: { event: unknown }) => void;
|
|
27
28
|
onToolCallEndEvent?: (payload: { event: unknown }) => void;
|
|
@@ -34,6 +35,7 @@ type Subscriber = {
|
|
|
34
35
|
onCustomEvent?: (payload: { event: unknown }) => void;
|
|
35
36
|
onRawEvent?: (payload: { event: unknown }) => void;
|
|
36
37
|
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
38
|
+
onRunErrorEvent?: (payload: { event: unknown }) => void;
|
|
37
39
|
onRunFinalized?: () => void;
|
|
38
40
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
39
41
|
};
|
|
@@ -124,6 +126,8 @@ export const createAgUiSubscriber = (
|
|
|
124
126
|
dispatchIfValid(event, "REASONING_MESSAGE_CONTENT"),
|
|
125
127
|
onReasoningMessageEndEvent: ({ event }) =>
|
|
126
128
|
dispatchIfValid(event, "REASONING_MESSAGE_END"),
|
|
129
|
+
onReasoningEncryptedValueEvent: ({ event }) =>
|
|
130
|
+
dispatchIfValid(event, "REASONING_ENCRYPTED_VALUE"),
|
|
127
131
|
onToolCallStartEvent: ({ event }) =>
|
|
128
132
|
dispatchIfValid(event, "TOOL_CALL_START"),
|
|
129
133
|
onToolCallArgsEvent: ({ event }) =>
|
|
@@ -143,11 +147,24 @@ export const createAgUiSubscriber = (
|
|
|
143
147
|
onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
|
|
144
148
|
onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
|
|
145
149
|
onRunFinishedEvent: ({ event }) => {
|
|
150
|
+
if (runFinishedDispatched) return;
|
|
146
151
|
const parsed = ensureEvent(event, "RUN_FINISHED", logger);
|
|
147
152
|
if (!parsed) return;
|
|
148
153
|
runFinishedDispatched = true;
|
|
149
154
|
dispatch(parsed);
|
|
150
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
|
+
},
|
|
151
168
|
onRunFinalized: () => {
|
|
152
169
|
if (runFinishedDispatched) return;
|
|
153
170
|
dispatch({ type: "RUN_FINISHED", runId });
|