@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.
- package/dist/A2AClient.d.ts +42 -44
- package/dist/A2AClient.d.ts.map +1 -1
- package/dist/A2AClient.js +43 -7
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts +98 -86
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +204 -17
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/a2aExtras.d.ts +1 -3
- package/dist/a2aExtras.d.ts.map +1 -1
- package/dist/conversions.d.ts +18 -17
- package/dist/conversions.d.ts.map +1 -1
- package/dist/conversions.js +24 -2
- package/dist/conversions.js.map +1 -1
- package/dist/hooks.d.ts +3 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -0
- package/dist/types.d.ts +207 -209
- package/dist/types.d.ts.map +1 -1
- package/dist/useA2ARuntime.d.ts +2 -4
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +8 -0
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/A2AClient.test.ts +41 -0
- package/src/A2AClient.ts +50 -6
- package/src/A2AThreadRuntimeCore.test.ts +887 -9
- package/src/A2AThreadRuntimeCore.ts +347 -18
- package/src/conversions.test.ts +116 -0
- package/src/conversions.ts +48 -1
- package/src/useA2ARuntime.test.tsx +245 -0
- package/src/useA2ARuntime.ts +14 -0
- package/src/useA2ARuntime.voice.test.tsx +370 -0
|
@@ -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 =
|
|
268
|
-
.then((
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
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)
|
|
797
|
-
|
|
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
|
-
|
|
801
|
-
|
|
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
|
-
|
|
805
|
-
this.appendHistoryItem(
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/src/conversions.test.ts
CHANGED
|
@@ -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
|
});
|