@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.
Files changed (49) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +23 -2
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  5. package/dist/runtime/AgUiThreadRuntimeCore.js +183 -39
  6. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  7. package/dist/runtime/adapter/conversions.d.ts +19 -3
  8. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  9. package/dist/runtime/adapter/conversions.js +197 -36
  10. package/dist/runtime/adapter/conversions.js.map +1 -1
  11. package/dist/runtime/adapter/run-aggregator.d.ts +15 -2
  12. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  13. package/dist/runtime/adapter/run-aggregator.js +62 -10
  14. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  15. package/dist/runtime/adapter/subscriber.d.ts +6 -0
  16. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  17. package/dist/runtime/adapter/subscriber.js +10 -0
  18. package/dist/runtime/adapter/subscriber.js.map +1 -1
  19. package/dist/runtime/adapter/tool-approval.d.ts +56 -0
  20. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
  21. package/dist/runtime/adapter/tool-approval.js +244 -0
  22. package/dist/runtime/adapter/tool-approval.js.map +1 -0
  23. package/dist/runtime/event-parser.d.ts.map +1 -1
  24. package/dist/runtime/event-parser.js +15 -1
  25. package/dist/runtime/event-parser.js.map +1 -1
  26. package/dist/runtime/interrupt-internals.d.ts +8 -0
  27. package/dist/runtime/interrupt-internals.d.ts.map +1 -0
  28. package/dist/runtime/interrupt-internals.js +25 -0
  29. package/dist/runtime/interrupt-internals.js.map +1 -0
  30. package/dist/runtime/types.d.ts +5 -0
  31. package/dist/runtime/types.d.ts.map +1 -1
  32. package/dist/useAgUiRuntime.js +4 -1
  33. package/dist/useAgUiRuntime.js.map +1 -1
  34. package/package.json +7 -7
  35. package/src/index.ts +8 -2
  36. package/src/runtime/AgUiThreadRuntimeCore.ts +335 -42
  37. package/src/runtime/adapter/conversions.content.test.ts +278 -0
  38. package/src/runtime/adapter/conversions.ts +298 -50
  39. package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
  40. package/src/runtime/adapter/run-aggregator.ts +96 -5
  41. package/src/runtime/adapter/subscriber.ts +17 -0
  42. package/src/runtime/adapter/tool-approval.test.ts +681 -0
  43. package/src/runtime/adapter/tool-approval.ts +366 -0
  44. package/src/runtime/event-parser.ts +23 -1
  45. package/src/runtime/interrupt-internals.ts +35 -0
  46. package/src/runtime/types.ts +6 -0
  47. package/src/useAgUiRuntime.approval.test.tsx +885 -0
  48. package/src/useAgUiRuntime.queue.test.tsx +68 -1
  49. 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
- snapshot.push({ type: "reasoning", text: buffer } as const);
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(delta: string, messageId?: string): void {
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 });