@assistant-ui/react-a2a 0.2.36 → 0.2.37

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.
@@ -7,13 +7,24 @@ import type {
7
7
  ExportedMessageRepository,
8
8
  MessageStatus,
9
9
  ThreadAssistantMessage,
10
+ ThreadAssistantMessagePart,
10
11
  ThreadHistoryAdapter,
11
12
  ThreadMessage,
13
+ ToolCallMessagePart,
14
+ Unstable_RecordToolInteractionOptions,
12
15
  } from "@assistant-ui/core";
13
16
  import {
17
+ appendToolInteraction,
14
18
  createMessageRepositorySession,
15
19
  invokeUserCallback,
16
20
  } from "@assistant-ui/core/internal";
21
+ import type { ReadonlyJSONObject } from "assistant-stream/utils";
22
+ import {
23
+ applyA2uiOperations,
24
+ convertSurfaceToUISpec,
25
+ surfaceToOperations,
26
+ type A2uiState,
27
+ } from "@assistant-ui/react-generative-ui/a2ui";
17
28
  import type { A2AClient } from "./A2AClient";
18
29
  import type {
19
30
  A2AArtifact,
@@ -28,6 +39,7 @@ import type {
28
39
 
29
40
  import {
30
41
  a2aMessageToContent,
42
+ a2uiPartsToOperations,
31
43
  isTerminalTaskState,
32
44
  threadMessageToA2AMessage,
33
45
  taskStateToMessageStatus,
@@ -36,6 +48,14 @@ import {
36
48
  const INITIAL_AGENT_CARD_RETRY_DELAY_MS = 5_000;
37
49
  const MAX_AGENT_CARD_RETRY_DELAY_MS = 5 * 60_000;
38
50
 
51
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
52
+ typeof value === "object" && value !== null && !Array.isArray(value);
53
+
54
+ const storedArtifact = (artifact: A2AArtifact): A2AArtifact => ({
55
+ ...artifact,
56
+ parts: artifact.parts.map(({ raw: _raw, ...part }) => part),
57
+ });
58
+
39
59
  export type A2AThreadRuntimeCoreOptions = {
40
60
  client: A2AClient;
41
61
  contextId?: string | undefined;
@@ -89,11 +109,14 @@ export class A2AThreadRuntimeCore {
89
109
  // A2A-specific state
90
110
  private currentTask: A2ATask | undefined;
91
111
  private currentArtifacts: A2AArtifact[] = [];
112
+ private a2uiState: A2uiState = new Map();
113
+ private readonly a2uiMessageIds = new Set<string>();
92
114
  private agentCardValue: A2AAgentCard | undefined;
93
115
 
94
116
  // History tracking
95
117
  private readonly assistantHistoryParents = new Map<string, string | null>();
96
118
  private readonly recordedHistoryIds = new Set<string>();
119
+ private readonly historyWrites = new Map<string, Promise<void>>();
97
120
  private _isLoading = false;
98
121
  private _loadPromise: Promise<void> | undefined;
99
122
  private _historyLoadGeneration = 0;
@@ -188,6 +211,54 @@ export class A2AThreadRuntimeCore {
188
211
  return this.session.export();
189
212
  }
190
213
 
214
+ public async recordToolInteraction({
215
+ messageId,
216
+ toolCallId,
217
+ interaction,
218
+ }: Unstable_RecordToolInteractionOptions): Promise<void> {
219
+ const message = this.session.tryGetMessage(messageId)?.message;
220
+ if (!message) {
221
+ throw new Error(
222
+ "Tried to record a tool interaction on a non-existing message",
223
+ );
224
+ }
225
+ if (message.role !== "assistant") {
226
+ throw new Error(
227
+ "Tried to record a tool interaction on a non-assistant message",
228
+ );
229
+ }
230
+ const target = message.content.find(
231
+ (part) => part.type === "tool-call" && part.toolCallId === toolCallId,
232
+ );
233
+ if (!target || target.type !== "tool-call") {
234
+ throw new Error(
235
+ "Tried to record a tool interaction on a non-existing tool call",
236
+ );
237
+ }
238
+
239
+ const touched = this.session.updateMessage(messageId, (current) => {
240
+ if (current.role !== "assistant") return current;
241
+ return {
242
+ ...current,
243
+ content: current.content.map((part) =>
244
+ part.type === "tool-call" && part.toolCallId === toolCallId
245
+ ? {
246
+ ...part,
247
+ unstable_interactions: appendToolInteraction(
248
+ part.unstable_interactions,
249
+ interaction,
250
+ ),
251
+ }
252
+ : part,
253
+ ),
254
+ };
255
+ });
256
+ if (touched) {
257
+ this.notifyUpdate();
258
+ this.persistAssistantHistory(messageId);
259
+ }
260
+ }
261
+
191
262
  getTask(): A2ATask | undefined {
192
263
  return this.currentTask;
193
264
  }
@@ -264,8 +335,8 @@ export class A2AThreadRuntimeCore {
264
335
  const generation = this._historyLoadGeneration;
265
336
  const historyPromise = this.history.load();
266
337
 
267
- this._loadPromise = Promise.all([historyPromise, agentCardPromise])
268
- .then(([repo]) => {
338
+ this._loadPromise = historyPromise
339
+ .then((repo) => {
269
340
  if (generation !== this._historyLoadGeneration) return;
270
341
  if (repo) {
271
342
  this.session.applyExternalMessageRepository(repo);
@@ -313,6 +384,14 @@ export class A2AThreadRuntimeCore {
313
384
  await this.startRun(threadMessage);
314
385
  }
315
386
 
387
+ appendVoiceTranscript(message: ThreadMessage): void {
388
+ const parentId = this.session.headId;
389
+ this.session.addOrUpdateMessage(parentId, message);
390
+ this.session.switchToBranch(message.id);
391
+ this.notifyUpdate();
392
+ this.recordHistoryEntry(parentId, message);
393
+ }
394
+
316
395
  async edit(message: AppendMessage): Promise<void> {
317
396
  await this.append(message);
318
397
  }
@@ -386,6 +465,8 @@ export class A2AThreadRuntimeCore {
386
465
  }
387
466
  this.currentTask = undefined;
388
467
  this.currentArtifacts = [];
468
+ this.a2uiState = new Map();
469
+ this.a2uiMessageIds.clear();
389
470
  this.notifyUpdate();
390
471
  }
391
472
 
@@ -451,6 +532,8 @@ export class A2AThreadRuntimeCore {
451
532
  }
452
533
 
453
534
  this.currentArtifacts = [];
535
+ this.a2uiState = new Map();
536
+ this.a2uiMessageIds.clear();
454
537
 
455
538
  const assistantParentId = userThreadMessage.id;
456
539
  const assistantId = this.insertAssistantPlaceholder(assistantParentId);
@@ -583,7 +666,7 @@ export class A2AThreadRuntimeCore {
583
666
  this.handleStatusUpdate(assistantId, event.event);
584
667
  break;
585
668
  case "artifactUpdate":
586
- this.handleArtifactUpdate(event.event);
669
+ this.handleArtifactUpdate(assistantId, event.event);
587
670
  break;
588
671
  case "message":
589
672
  this.handleMessage(assistantId, event.message);
@@ -613,6 +696,7 @@ export class A2AThreadRuntimeCore {
613
696
  }
614
697
 
615
698
  if (event.status.message) {
699
+ this.applyA2uiMessage(event.status.message);
616
700
  const content = a2aMessageToContent(event.status.message);
617
701
  this.updateAssistantContent(assistantId, content);
618
702
  }
@@ -623,7 +707,10 @@ export class A2AThreadRuntimeCore {
623
707
  this.notifyUpdate();
624
708
  }
625
709
 
626
- private handleArtifactUpdate(event: A2ATaskArtifactUpdateEvent) {
710
+ private handleArtifactUpdate(
711
+ assistantId: string,
712
+ event: A2ATaskArtifactUpdateEvent,
713
+ ) {
627
714
  const { append, lastChunk } = event;
628
715
  const artifact = normalizeArtifact(event.artifact);
629
716
  const existingIdx = this.currentArtifacts.findIndex(
@@ -654,6 +741,10 @@ export class A2AThreadRuntimeCore {
654
741
  this.currentArtifacts = [...this.currentArtifacts, updated];
655
742
  }
656
743
 
744
+ this.applyA2uiParts(artifact.parts);
745
+ this.updateAssistantArtifacts(assistantId);
746
+ this.rebuildAssistantA2uiSurfaces(assistantId);
747
+
657
748
  if (lastChunk) {
658
749
  invokeRuntimeCallback(
659
750
  "onArtifactComplete",
@@ -668,6 +759,7 @@ export class A2AThreadRuntimeCore {
668
759
  private handleMessage(assistantId: string, message: A2AMessage) {
669
760
  if (message.role !== "agent") return;
670
761
 
762
+ this.applyA2uiMessage(message);
671
763
  const content = a2aMessageToContent(message);
672
764
  this.updateAssistantContent(assistantId, content);
673
765
  this.notifyUpdate();
@@ -695,13 +787,43 @@ export class A2AThreadRuntimeCore {
695
787
  if (task.contextId) {
696
788
  this.contextId = task.contextId;
697
789
  }
698
- if (artifacts) {
790
+ const isCompleteSnapshot = history !== undefined && artifacts !== undefined;
791
+ const artifactsToApply = isCompleteSnapshot
792
+ ? artifacts
793
+ : artifacts?.filter(
794
+ (artifact) =>
795
+ !this.currentArtifacts.some(
796
+ ({ artifactId }) => artifactId === artifact.artifactId,
797
+ ),
798
+ );
799
+
800
+ if (isCompleteSnapshot) {
699
801
  this.currentArtifacts = artifacts;
802
+ this.a2uiState = new Map();
803
+ this.a2uiMessageIds.clear();
804
+ } else if (artifactsToApply) {
805
+ this.currentArtifacts = [...this.currentArtifacts, ...artifactsToApply];
806
+ }
807
+
808
+ for (const message of history ?? []) {
809
+ if (message.role === "agent") this.applyA2uiMessage(message, true);
810
+ }
811
+ for (const artifact of artifactsToApply ?? []) {
812
+ this.applyA2uiParts(artifact.parts);
813
+ }
814
+ if (task.status.message) {
815
+ this.applyA2uiMessage(task.status.message, true);
816
+ }
817
+
818
+ if (isCompleteSnapshot || artifactsToApply?.length) {
819
+ this.updateAssistantArtifacts(assistantId);
700
820
  }
701
821
 
702
822
  if (task.status.message) {
703
823
  const content = a2aMessageToContent(task.status.message);
704
824
  this.updateAssistantContent(assistantId, content);
825
+ } else {
826
+ this.rebuildAssistantA2uiSurfaces(assistantId);
705
827
  }
706
828
 
707
829
  const status = taskStateToMessageStatus(task.status.state);
@@ -740,8 +862,122 @@ export class A2AThreadRuntimeCore {
740
862
  ) {
741
863
  this.session.updateMessage(messageId, (message) => {
742
864
  if (message.role !== "assistant") return message;
743
- return { ...message, content };
865
+ return {
866
+ ...message,
867
+ content: this.withA2uiSurfaces(content, message.content),
868
+ };
869
+ });
870
+ }
871
+
872
+ private applyA2uiMessage(message: A2AMessage, replay = false) {
873
+ if (message.messageId) {
874
+ if (replay && this.a2uiMessageIds.has(message.messageId)) return;
875
+ this.a2uiMessageIds.add(message.messageId);
876
+ }
877
+ this.applyA2uiParts(message.parts);
878
+ }
879
+
880
+ private applyA2uiParts(parts: readonly A2AMessage["parts"][number][]) {
881
+ const operations = a2uiPartsToOperations(parts);
882
+ if (operations.length === 0) return;
883
+ this.a2uiState = applyA2uiOperations(this.a2uiState, operations).state;
884
+ }
885
+
886
+ private a2uiSurfaceParts(): ThreadAssistantMessagePart[] {
887
+ const parts: ThreadAssistantMessagePart[] = [];
888
+ for (const [surfaceId, surface] of this.a2uiState) {
889
+ const { spec } = convertSurfaceToUISpec(surface);
890
+ if (!spec) continue;
891
+ parts.push({
892
+ type: "tool-call",
893
+ toolCallId: `a2ui:${surfaceId}`,
894
+ toolName: "present",
895
+ args: spec as unknown as ReadonlyJSONObject,
896
+ argsText: JSON.stringify(spec),
897
+ result: {},
898
+ artifact: { a2ui: surfaceToOperations(surface) },
899
+ });
900
+ }
901
+ return parts;
902
+ }
903
+
904
+ private withA2uiSurfaces(
905
+ content: ThreadAssistantMessage["content"],
906
+ previousContent: ThreadAssistantMessage["content"] = content,
907
+ ): ThreadAssistantMessage["content"] {
908
+ const preserved = content.filter(
909
+ (part) =>
910
+ !(part.type === "tool-call" && part.toolCallId.startsWith("a2ui:")),
911
+ );
912
+ const interactionSource = [...previousContent, ...content];
913
+ return this.withPreservedToolInteractions(interactionSource, [
914
+ ...preserved,
915
+ ...this.a2uiSurfaceParts(),
916
+ ]);
917
+ }
918
+
919
+ private withPreservedToolInteractions(
920
+ previousContent: ThreadAssistantMessage["content"],
921
+ content: ThreadAssistantMessage["content"],
922
+ ): ThreadAssistantMessage["content"] {
923
+ const interactions = new Map<
924
+ string,
925
+ ToolCallMessagePart["unstable_interactions"]
926
+ >();
927
+ for (const part of previousContent) {
928
+ if (
929
+ part.type === "tool-call" &&
930
+ part.unstable_interactions !== undefined
931
+ ) {
932
+ interactions.set(part.toolCallId, part.unstable_interactions);
933
+ }
934
+ }
935
+ if (interactions.size === 0) return content;
936
+ return content.map((part) => {
937
+ if (
938
+ part.type !== "tool-call" ||
939
+ part.unstable_interactions !== undefined
940
+ ) {
941
+ return part;
942
+ }
943
+ const unstable_interactions = interactions.get(part.toolCallId);
944
+ return unstable_interactions === undefined
945
+ ? part
946
+ : { ...part, unstable_interactions };
947
+ });
948
+ }
949
+
950
+ private rebuildAssistantA2uiSurfaces(messageId: string) {
951
+ const touched = this.session.updateMessage(messageId, (message) => {
952
+ if (message.role !== "assistant") return message;
953
+ return {
954
+ ...message,
955
+ content: this.withA2uiSurfaces(message.content),
956
+ };
957
+ });
958
+ if (touched) this.notifyUpdate();
959
+ }
960
+
961
+ private updateAssistantArtifacts(messageId: string) {
962
+ const touched = this.session.updateMessage(messageId, (message) => {
963
+ if (message.role !== "assistant") return message;
964
+ const custom = message.metadata.custom;
965
+ const a2a = isRecord(custom.a2a) ? custom.a2a : {};
966
+ return {
967
+ ...message,
968
+ metadata: {
969
+ ...message.metadata,
970
+ custom: {
971
+ ...custom,
972
+ a2a: {
973
+ ...a2a,
974
+ artifacts: this.currentArtifacts.map(storedArtifact),
975
+ },
976
+ },
977
+ },
978
+ };
744
979
  });
980
+ if (touched) this.notifyUpdate();
745
981
  }
746
982
 
747
983
  private updateAssistantStatus(messageId: string, status: MessageStatus) {
@@ -751,7 +987,7 @@ export class A2AThreadRuntimeCore {
751
987
  });
752
988
  if (touched) {
753
989
  this.notifyUpdate();
754
- if (status.type === "complete" || status.type === "incomplete") {
990
+ if (this.isPersistableAssistantStatus(status)) {
755
991
  this.persistAssistantHistory(messageId);
756
992
  }
757
993
  }
@@ -763,6 +999,14 @@ export class A2AThreadRuntimeCore {
763
999
  return msg.status;
764
1000
  }
765
1001
 
1002
+ private isPersistableAssistantStatus(status: MessageStatus): boolean {
1003
+ return (
1004
+ status.type === "complete" ||
1005
+ status.type === "incomplete" ||
1006
+ (status.type === "requires-action" && status.reason === "interrupt")
1007
+ );
1008
+ }
1009
+
766
1010
  // --- Lifecycle helpers ---
767
1011
 
768
1012
  private setRunning(running: boolean) {
@@ -779,7 +1023,9 @@ export class A2AThreadRuntimeCore {
779
1023
  // --- History persistence ---
780
1024
 
781
1025
  private recordHistoryEntry(parentId: string | null, message: ThreadMessage) {
782
- this.appendHistoryItem(parentId, message);
1026
+ void this.appendHistoryItem(parentId, message)?.catch((error) => {
1027
+ console.error("[react-a2a] failed to append history entry", error);
1028
+ });
783
1029
  }
784
1030
 
785
1031
  private markPendingAssistantHistory(
@@ -792,24 +1038,107 @@ export class A2AThreadRuntimeCore {
792
1038
 
793
1039
  private persistAssistantHistory(messageId: string) {
794
1040
  if (!this.history) return;
1041
+ const messageData = this.session.tryGetMessage(messageId);
795
1042
  const parentId = this.assistantHistoryParents.get(messageId);
796
- if (parentId === undefined) return;
797
- const message = this.session.tryGetMessage(messageId)?.message;
1043
+ if (parentId === undefined && !this.recordedHistoryIds.has(messageId)) {
1044
+ return;
1045
+ }
1046
+ const resolvedParentId =
1047
+ parentId === undefined ? messageData?.parentId : parentId;
1048
+ if (resolvedParentId === undefined) return;
1049
+ const message = messageData?.message;
798
1050
  if (!message || message.role !== "assistant") return;
799
- if (
800
- message.status?.type !== "complete" &&
801
- message.status?.type !== "incomplete"
802
- )
1051
+ if (!this.isPersistableAssistantStatus(message.status)) return;
1052
+ const isPausing = message.status.type === "requires-action";
1053
+
1054
+ if (isPausing && !this.history.update) return;
1055
+
1056
+ if (this.recordedHistoryIds.has(messageId)) {
1057
+ if (this.history.update) {
1058
+ const update = this.history.update.bind(this.history);
1059
+ const write = this.chainHistoryWrite(messageId, () =>
1060
+ update({ parentId: resolvedParentId, message }),
1061
+ );
1062
+ if (!isPausing) {
1063
+ this.assistantHistoryParents.delete(messageId);
1064
+ }
1065
+ void write.then(
1066
+ () => {
1067
+ this.recordedHistoryIds.add(messageId);
1068
+ },
1069
+ (error) => {
1070
+ const pending = this.historyWrites.get(messageId);
1071
+ if (pending === undefined || pending === write) {
1072
+ this.assistantHistoryParents.set(messageId, resolvedParentId);
1073
+ }
1074
+ console.error("[react-a2a] failed to update history entry", error);
1075
+ },
1076
+ );
1077
+ return;
1078
+ }
1079
+ if (!isPausing) {
1080
+ this.assistantHistoryParents.delete(messageId);
1081
+ }
803
1082
  return;
804
- this.assistantHistoryParents.delete(messageId);
805
- this.appendHistoryItem(parentId, message);
1083
+ }
1084
+ const write = this.appendHistoryItem(resolvedParentId, message);
1085
+ if (!write) return;
1086
+ if (!isPausing) {
1087
+ this.assistantHistoryParents.delete(messageId);
1088
+ }
1089
+ void write.catch((error) => {
1090
+ const pending = this.historyWrites.get(messageId);
1091
+ if (pending === undefined || pending === write) {
1092
+ this.assistantHistoryParents.set(messageId, resolvedParentId);
1093
+ }
1094
+ console.error("[react-a2a] failed to append history entry", error);
1095
+ });
806
1096
  }
807
1097
 
808
- private appendHistoryItem(parentId: string | null, message: ThreadMessage) {
1098
+ private appendHistoryItem(
1099
+ parentId: string | null,
1100
+ message: ThreadMessage,
1101
+ ): Promise<void> | undefined {
809
1102
  if (!this.history || this.recordedHistoryIds.has(message.id)) return;
810
1103
  this.recordedHistoryIds.add(message.id);
811
- void this.history.append({ parentId, message }).catch(() => {
1104
+ const append = this.history.append.bind(this.history);
1105
+ const write = this.chainHistoryWrite(message.id, () =>
1106
+ append({ parentId, message }),
1107
+ );
1108
+ void write.catch(() => {
812
1109
  this.recordedHistoryIds.delete(message.id);
813
1110
  });
1111
+ return write;
1112
+ }
1113
+
1114
+ private chainHistoryWrite(
1115
+ id: string,
1116
+ write: () => Promise<void>,
1117
+ ): Promise<void> {
1118
+ const pending = this.historyWrites.get(id);
1119
+ let next: Promise<void>;
1120
+ if (pending) {
1121
+ next = pending.then(write, write);
1122
+ } else {
1123
+ try {
1124
+ next = Promise.resolve(write());
1125
+ } catch (error) {
1126
+ next = Promise.reject(error);
1127
+ }
1128
+ }
1129
+ this.historyWrites.set(id, next);
1130
+ void next.then(
1131
+ () => {
1132
+ if (this.historyWrites.get(id) === next) {
1133
+ this.historyWrites.delete(id);
1134
+ }
1135
+ },
1136
+ () => {
1137
+ if (this.historyWrites.get(id) === next) {
1138
+ this.historyWrites.delete(id);
1139
+ }
1140
+ },
1141
+ );
1142
+ return next;
814
1143
  }
815
1144
  }
@@ -3,6 +3,7 @@ import {
3
3
  a2aPartToContent,
4
4
  a2aPartsToContent,
5
5
  a2aMessageToContent,
6
+ isA2uiDataPart,
6
7
  taskStateToMessageStatus,
7
8
  contentPartsToA2AParts,
8
9
  isTerminalTaskState,
@@ -157,6 +158,82 @@ describe("a2aPartToContent", () => {
157
158
  });
158
159
  });
159
160
 
161
+ describe("A2UI data parts", () => {
162
+ it.each([
163
+ [
164
+ "media type",
165
+ {
166
+ mediaType: "application/vnd.A2UI+json",
167
+ data: { value: "from media type" },
168
+ },
169
+ ],
170
+ [
171
+ "metadata",
172
+ {
173
+ metadata: { mimeType: "application/a2ui+json" },
174
+ data: { value: "from metadata" },
175
+ },
176
+ ],
177
+ [
178
+ "metadata media type",
179
+ {
180
+ metadata: { mediaType: "application/a2ui+json" },
181
+ data: { value: "from metadata media type" },
182
+ },
183
+ ],
184
+ [
185
+ "operation shape",
186
+ {
187
+ data: {
188
+ version: "v0.9",
189
+ createSurface: { surfaceId: "surface" },
190
+ },
191
+ },
192
+ ],
193
+ [
194
+ "operation array",
195
+ {
196
+ data: [
197
+ {
198
+ version: "v0.9",
199
+ createSurface: { surfaceId: "surface" },
200
+ },
201
+ {
202
+ version: "v0.9",
203
+ deleteSurface: { surfaceId: "surface" },
204
+ },
205
+ ],
206
+ },
207
+ ],
208
+ ] as const)("detects A2UI by %s", (_source, part) => {
209
+ expect(isA2uiDataPart(part)).toBe(true);
210
+ expect(a2aPartsToContent([part])).toEqual([]);
211
+ });
212
+
213
+ it("does not detect an operation array containing a non-operation", () => {
214
+ const part = {
215
+ data: [
216
+ {
217
+ version: "v0.9",
218
+ createSurface: { surfaceId: "surface" },
219
+ },
220
+ { value: "not an operation" },
221
+ ],
222
+ };
223
+
224
+ expect(isA2uiDataPart(part)).toBe(false);
225
+ });
226
+
227
+ it("keeps ordinary data parts as JSON text", () => {
228
+ expect(a2aPartsToContent([{ data: { value: "plain" } }])).toEqual([
229
+ {
230
+ type: "text",
231
+ text: '{\n "value": "plain"\n}',
232
+ },
233
+ ]);
234
+ });
235
+ });
236
+
160
237
  describe("inbound file part round trip", () => {
161
238
  const restoreFilePart = (part: A2APart) => {
162
239
  const restored = a2aPartToContent(part);
@@ -690,4 +767,43 @@ describe("threadMessageToA2AMessage", () => {
690
767
  expect(result.contextId).toBeUndefined();
691
768
  expect(result.taskId).toBeUndefined();
692
769
  });
770
+
771
+ it("keeps tool interactions out of outbound messages", () => {
772
+ const result = threadMessageToA2AMessage({
773
+ ...userMessage,
774
+ role: "user",
775
+ attachments: [],
776
+ content: [
777
+ { type: "text", text: "hello" },
778
+ {
779
+ type: "file",
780
+ data: "ZmlsZQ==",
781
+ mimeType: "text/plain",
782
+ filename: "file.txt",
783
+ },
784
+ {
785
+ type: "tool-call",
786
+ toolCallId: "tool-1",
787
+ toolName: "present",
788
+ args: {},
789
+ argsText: "{}",
790
+ result: {},
791
+ unstable_interactions: {
792
+ entries: [
793
+ {
794
+ type: "action",
795
+ occurredAt: 1_700_000_000_000,
796
+ payload: { value: "selected" },
797
+ },
798
+ ],
799
+ },
800
+ },
801
+ ],
802
+ } as any);
803
+
804
+ expect(result.parts).toEqual([
805
+ { text: "hello" },
806
+ { raw: "ZmlsZQ==", mediaType: "text/plain", filename: "file.txt" },
807
+ ]);
808
+ });
693
809
  });