@assistant-ui/react-ag-ui 0.1.1 → 0.1.2

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 (41) hide show
  1. package/README.md +2 -0
  2. package/dist/runtime/AgUiThreadController.d.ts +11 -0
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadController.js +212 -22
  5. package/dist/runtime/AgUiThreadController.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts +9 -1
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +184 -9
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +34 -12
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +1 -8
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/event-parser.d.ts.map +1 -1
  19. package/dist/runtime/event-parser.js +1 -0
  20. package/dist/runtime/event-parser.js.map +1 -1
  21. package/dist/runtime/types.d.ts +2 -0
  22. package/dist/runtime/types.d.ts.map +1 -1
  23. package/dist/runtime/types.js +2 -1
  24. package/dist/runtime/types.js.map +1 -1
  25. package/package.json +5 -5
  26. package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
  27. package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
  28. package/src/runtime/AgUiThreadController.replay.test.ts +8 -1
  29. package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +167 -0
  30. package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
  31. package/src/runtime/AgUiThreadController.test.ts +40 -0
  32. package/src/runtime/AgUiThreadController.ts +459 -27
  33. package/src/runtime/adapter/conversions.content.test.ts +129 -0
  34. package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
  35. package/src/runtime/adapter/conversions.ts +331 -20
  36. package/src/runtime/adapter/run-aggregator.metadata.test.ts +151 -0
  37. package/src/runtime/adapter/run-aggregator.ts +52 -9
  38. package/src/runtime/adapter/subscriber.test.ts +1 -1
  39. package/src/runtime/adapter/subscriber.ts +1 -6
  40. package/src/runtime/event-parser.ts +3 -0
  41. package/src/runtime/types.ts +5 -0
@@ -22,6 +22,7 @@ import {
22
22
  import jsonpatch, { type Operation } from "fast-json-patch";
23
23
  import { readMcpAppResourceUri, toolContentText } from "../mcp-tool-result";
24
24
  import { projectAgUiToolApprovals } from "./tool-approval";
25
+ import { withAgUiPartMessageMetadata } from "./conversions";
25
26
  import {
26
27
  AG_UI_METADATA_NAMESPACE,
27
28
  A2UI_SURFACE_ACTIVITY_TYPE,
@@ -30,15 +31,12 @@ import {
30
31
  type AgUiEvent,
31
32
  type AgUiInterrupt,
32
33
  type AgUiOpaqueReasoning,
34
+ MAX_SUBAGENT_DEPTH,
33
35
  } from "../types";
34
36
  import type { Logger } from "../logger";
35
37
 
36
38
  const ROOT_SCOPE = "";
37
39
 
38
- // Defensive guard against a malformed or cyclic parentToolCallId /
39
- // parentSubagentRunId chain, not a realistic nesting depth.
40
- const MAX_SUBAGENT_DEPTH = 16;
41
-
42
40
  type SubagentRunState = {
43
41
  subagentRunId: string;
44
42
  name: string;
@@ -93,6 +91,7 @@ type ToolCallState = {
93
91
  mcpAppServerId?: string;
94
92
  artifact?: unknown;
95
93
  modelContent?: ToolModelContentPart[];
94
+ resultMetadata?: Record<string, unknown>;
96
95
  snapshotResultApplied: boolean;
97
96
  subagentRunId?: string;
98
97
  };
@@ -209,6 +208,7 @@ export class RunAggregator {
209
208
  private interrupts: AgUiInterrupt[] | undefined;
210
209
  private pendingToolCallIds: string[] | undefined;
211
210
  private readonly textParts = new Map<string, { buffer: string }>();
211
+ private readonly pendingSnapshotText = new Map<string, string>();
212
212
  private readonly activeTextMessageIdByScope = new Map<string, string>();
213
213
  private readonly reasoningParts = new Map<
214
214
  string,
@@ -282,6 +282,20 @@ export class RunAggregator {
282
282
  this.emit();
283
283
  break;
284
284
  }
285
+ case "MESSAGES_SNAPSHOT": {
286
+ this.pendingSnapshotText.clear();
287
+ for (const message of event.messages) {
288
+ if (
289
+ message.role !== "assistant" ||
290
+ typeof message.content !== "string"
291
+ )
292
+ continue;
293
+ const part = this.textParts.get(message.id);
294
+ if (part) part.buffer = message.content;
295
+ else this.pendingSnapshotText.set(message.id, message.content);
296
+ }
297
+ break;
298
+ }
285
299
  case "RUN_FINISHED": {
286
300
  if (event.outcome?.type === "cancelled") {
287
301
  this.interrupts = undefined;
@@ -368,7 +382,7 @@ export class RunAggregator {
368
382
  const incomingId = event.messageId;
369
383
  const scope = this.scopeOf(event);
370
384
  if (scope === ROOT_SCOPE) {
371
- this.beginDistinctTextMessage(incomingId);
385
+ this.beginDistinctTextMessage(incomingId, true);
372
386
  this.reportServerMessageId(incomingId);
373
387
  if (incomingId) this.lastTextMessageId = incomingId;
374
388
  }
@@ -494,6 +508,7 @@ export class RunAggregator {
494
508
  : undefined,
495
509
  event.messageId,
496
510
  event.mcpResult,
511
+ event.metadata,
497
512
  );
498
513
  this.emit();
499
514
  break;
@@ -793,6 +808,7 @@ export class RunAggregator {
793
808
 
794
809
  private resetMessageParts(): void {
795
810
  this.clearTextParts();
811
+ this.pendingSnapshotText.clear();
796
812
  this.reasoningParts.clear();
797
813
  this.reasoningSignatures.clear();
798
814
  this.reasoningSignatureIds.clear();
@@ -837,7 +853,10 @@ export class RunAggregator {
837
853
  return scope === ROOT_SCOPE ? id : `${scope}\u0000${id}`;
838
854
  }
839
855
 
840
- private beginDistinctTextMessage(messageId: string | undefined): void {
856
+ private beginDistinctTextMessage(
857
+ messageId: string | undefined,
858
+ preserveSnapshotText = false,
859
+ ): void {
841
860
  if (
842
861
  this.joinStrategy === "concat-content" ||
843
862
  !messageId ||
@@ -854,7 +873,13 @@ export class RunAggregator {
854
873
  const openToolCallIds = Array.from(this.toolCalls.values())
855
874
  .filter((call) => call.result === undefined)
856
875
  .map((call) => call.toolCallId);
876
+ const snapshotText = preserveSnapshotText
877
+ ? this.pendingSnapshotText.get(messageId)
878
+ : undefined;
857
879
  this.resetMessageParts();
880
+ if (snapshotText !== undefined) {
881
+ this.pendingSnapshotText.set(messageId, snapshotText);
882
+ }
858
883
  this.onTextMessageStart(messageId, openToolCallIds);
859
884
  this.restoreReasoning(carried);
860
885
  }
@@ -946,15 +971,21 @@ export class RunAggregator {
946
971
  private startTextMessage(scope: string, messageId?: string): string {
947
972
  const id = messageId ?? this.generateTextKey();
948
973
  const key = this.ensureTextPart(scope, id);
974
+ if (scope === ROOT_SCOPE && messageId) {
975
+ const snapshotText = this.pendingSnapshotText.get(messageId);
976
+ const part = this.textParts.get(key);
977
+ if (snapshotText !== undefined && part) {
978
+ part.buffer = snapshotText;
979
+ this.pendingSnapshotText.delete(messageId);
980
+ }
981
+ }
949
982
  this.activeTextMessageIdByScope.set(scope, key);
950
983
  return key;
951
984
  }
952
985
 
953
986
  private resolveTextMessageId(scope: string, messageId?: string): string {
954
987
  if (messageId) {
955
- const key = this.ensureTextPart(scope, messageId);
956
- this.activeTextMessageIdByScope.set(scope, key);
957
- return key;
988
+ return this.startTextMessage(scope, messageId);
958
989
  }
959
990
 
960
991
  const active = this.activeTextMessageIdByScope.get(scope);
@@ -1098,6 +1129,7 @@ export class RunAggregator {
1098
1129
  isError?: boolean,
1099
1130
  toolMessageId?: string,
1100
1131
  mcpResult?: Record<string, unknown>,
1132
+ metadata?: Record<string, unknown>,
1101
1133
  ) {
1102
1134
  if (!id) return;
1103
1135
  const modelText = toolContentText(content);
@@ -1149,6 +1181,9 @@ export class RunAggregator {
1149
1181
  if (toolMessageId) {
1150
1182
  entry.toolMessageId = toolMessageId;
1151
1183
  }
1184
+ if (metadata !== undefined) {
1185
+ entry.resultMetadata = { ...entry.resultMetadata, ...metadata };
1186
+ }
1152
1187
  this.lastResolvedToolCallIdByScope.set(scope, id);
1153
1188
  }
1154
1189
 
@@ -1299,6 +1334,14 @@ export class RunAggregator {
1299
1334
  ...(entry.toolMessageId
1300
1335
  ? { unstable_toolMessageId: entry.toolMessageId }
1301
1336
  : {}),
1337
+ ...(entry.resultMetadata !== undefined
1338
+ ? {
1339
+ providerMetadata: withAgUiPartMessageMetadata(
1340
+ undefined,
1341
+ entry.resultMetadata,
1342
+ ),
1343
+ }
1344
+ : {}),
1302
1345
  ...(nestedMessages.length > 0 ? { messages: nestedMessages } : {}),
1303
1346
  } as ToolCallMessagePart & { unstable_toolMessageId?: string };
1304
1347
  snapshot.push(toolPart);
@@ -4,7 +4,7 @@ import { createAgUiSubscriber } from "./subscriber";
4
4
  const createSubscriber = () => {
5
5
  const dispatch = vi.fn();
6
6
  const logger = { debug: vi.fn(), error: vi.fn() };
7
- const subscriber = createAgUiSubscriber({ dispatch, runId: "run-1", logger });
7
+ const subscriber = createAgUiSubscriber({ dispatch, logger });
8
8
  return { subscriber, dispatch, logger };
9
9
  };
10
10
 
@@ -60,7 +60,6 @@ const ensureEvent = (
60
60
 
61
61
  type SubscriberOptions = {
62
62
  dispatch: Dispatch;
63
- runId: string;
64
63
  logger?: Logger;
65
64
  onRunFailed?: (error: Error) => void;
66
65
  };
@@ -68,7 +67,7 @@ type SubscriberOptions = {
68
67
  export const createAgUiSubscriber = (
69
68
  options: SubscriberOptions,
70
69
  ): Subscriber => {
71
- const { dispatch, runId, logger, onRunFailed } = options;
70
+ const { dispatch, logger, onRunFailed } = options;
72
71
  let runFinishedDispatched = false;
73
72
  const dispatchIfValid = (raw: unknown, type: AgUiEvent["type"]) => {
74
73
  const event = ensureEvent(raw, type, logger);
@@ -140,10 +139,6 @@ export const createAgUiSubscriber = (
140
139
  logger?.debug?.("[agui] step boundary ignored", event),
141
140
  onStepFinishedEvent: ({ event }) =>
142
141
  logger?.debug?.("[agui] step boundary ignored", event),
143
- onRunFinalized: () => {
144
- if (runFinishedDispatched) return;
145
- dispatch({ type: "RUN_FINISHED", runId });
146
- },
147
142
  onRunFailed: ({ error }) => {
148
143
  if (runFinishedDispatched) return;
149
144
  runFinishedDispatched = true;
@@ -344,6 +344,9 @@ export const parseAgUiEvent = (
344
344
  messageId: getString("messageId"),
345
345
  role: payload.role === "tool" ? "tool" : undefined,
346
346
  mcpResult: parseMcpToolCallResult(payload, content),
347
+ metadata: isPlainObject(payload.metadata)
348
+ ? payload.metadata
349
+ : undefined,
347
350
  subagentRunId: getString("subagentRunId"),
348
351
  },
349
352
  );
@@ -26,6 +26,10 @@ export const AG_UI_METADATA_NAMESPACE = "agui";
26
26
  export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
27
27
  export const A2UI_SURFACE_ACTIVITY_TYPE = "a2ui-surface";
28
28
 
29
+ // Defensive guard against a malformed or cyclic parentToolCallId /
30
+ // parentSubagentRunId chain, not a realistic nesting depth.
31
+ export const MAX_SUBAGENT_DEPTH = 16;
32
+
29
33
  export type AgUiOpaqueReasoning = {
30
34
  id: string;
31
35
  encryptedValue: string;
@@ -278,6 +282,7 @@ export type AgUiEvent =
278
282
  content: string | ContentPart[];
279
283
  role?: "tool";
280
284
  mcpResult?: McpToolCallResult;
285
+ metadata?: Record<string, unknown>;
281
286
  subagentRunId?: string;
282
287
  }
283
288
  | {