@assistant-ui/react-a2a 0.2.35 → 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 +45 -48
- 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 +100 -87
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +218 -20
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/a2aExtras.d.ts +2 -5
- package/dist/a2aExtras.d.ts.map +1 -1
- package/dist/conversions.d.ts +26 -26
- 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 -7
- 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 +276 -279
- package/dist/types.d.ts.map +1 -1
- package/dist/useA2ARuntime.d.ts +3 -6
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +8 -0
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +12 -10
- package/src/A2AClient.test.ts +41 -0
- package/src/A2AClient.ts +50 -6
- package/src/A2AThreadRuntimeCore.test.ts +1134 -10
- package/src/A2AThreadRuntimeCore.ts +372 -21
- package/src/conversions.test.ts +116 -0
- package/src/conversions.ts +48 -1
- package/src/useA2ARuntime.test.tsx +336 -1
- 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;
|
|
@@ -83,18 +103,23 @@ export class A2AThreadRuntimeCore {
|
|
|
83
103
|
private readonly session = createMessageRepositorySession();
|
|
84
104
|
private isRunningFlag = false;
|
|
85
105
|
private abortController: AbortController | null = null;
|
|
106
|
+
private runGeneration = 0;
|
|
86
107
|
private pendingError: Error | null = null;
|
|
87
108
|
|
|
88
109
|
// A2A-specific state
|
|
89
110
|
private currentTask: A2ATask | undefined;
|
|
90
111
|
private currentArtifacts: A2AArtifact[] = [];
|
|
112
|
+
private a2uiState: A2uiState = new Map();
|
|
113
|
+
private readonly a2uiMessageIds = new Set<string>();
|
|
91
114
|
private agentCardValue: A2AAgentCard | undefined;
|
|
92
115
|
|
|
93
116
|
// History tracking
|
|
94
117
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
95
118
|
private readonly recordedHistoryIds = new Set<string>();
|
|
119
|
+
private readonly historyWrites = new Map<string, Promise<void>>();
|
|
96
120
|
private _isLoading = false;
|
|
97
121
|
private _loadPromise: Promise<void> | undefined;
|
|
122
|
+
private _historyLoadGeneration = 0;
|
|
98
123
|
private _loadRequested = false;
|
|
99
124
|
private _agentCardPromise: Promise<void> | undefined;
|
|
100
125
|
private _agentCardRetryAfter = 0;
|
|
@@ -146,6 +171,8 @@ export class A2AThreadRuntimeCore {
|
|
|
146
171
|
/** Thread-boundary reset: applyExternalMessages alone also serves branch
|
|
147
172
|
* switches, deletes, and cancel resyncs, which must keep the live context. */
|
|
148
173
|
resetContext(): void {
|
|
174
|
+
this._historyLoadGeneration++;
|
|
175
|
+
this._isLoading = false;
|
|
149
176
|
// Restore the seed before aborting: an onCancel callback that starts a
|
|
150
177
|
// new run must not pick up the old thread's context, and its controller
|
|
151
178
|
// must not be discarded.
|
|
@@ -184,6 +211,54 @@ export class A2AThreadRuntimeCore {
|
|
|
184
211
|
return this.session.export();
|
|
185
212
|
}
|
|
186
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
|
+
|
|
187
262
|
getTask(): A2ATask | undefined {
|
|
188
263
|
return this.currentTask;
|
|
189
264
|
}
|
|
@@ -257,16 +332,19 @@ export class A2AThreadRuntimeCore {
|
|
|
257
332
|
|
|
258
333
|
this._isLoading = true;
|
|
259
334
|
|
|
335
|
+
const generation = this._historyLoadGeneration;
|
|
260
336
|
const historyPromise = this.history.load();
|
|
261
337
|
|
|
262
|
-
this._loadPromise =
|
|
263
|
-
.then((
|
|
338
|
+
this._loadPromise = historyPromise
|
|
339
|
+
.then((repo) => {
|
|
340
|
+
if (generation !== this._historyLoadGeneration) return;
|
|
264
341
|
if (repo) {
|
|
265
342
|
this.session.applyExternalMessageRepository(repo);
|
|
266
343
|
this.finalizeExternalApply();
|
|
267
344
|
}
|
|
268
345
|
})
|
|
269
346
|
.catch((error) => {
|
|
347
|
+
if (generation !== this._historyLoadGeneration) return;
|
|
270
348
|
invokeRuntimeCallback(
|
|
271
349
|
"onError",
|
|
272
350
|
this.onError,
|
|
@@ -274,6 +352,7 @@ export class A2AThreadRuntimeCore {
|
|
|
274
352
|
);
|
|
275
353
|
})
|
|
276
354
|
.finally(() => {
|
|
355
|
+
if (generation !== this._historyLoadGeneration) return;
|
|
277
356
|
this._isLoading = false;
|
|
278
357
|
this.notifyUpdate();
|
|
279
358
|
});
|
|
@@ -305,6 +384,14 @@ export class A2AThreadRuntimeCore {
|
|
|
305
384
|
await this.startRun(threadMessage);
|
|
306
385
|
}
|
|
307
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
|
+
|
|
308
395
|
async edit(message: AppendMessage): Promise<void> {
|
|
309
396
|
await this.append(message);
|
|
310
397
|
}
|
|
@@ -328,14 +415,26 @@ export class A2AThreadRuntimeCore {
|
|
|
328
415
|
async cancel(): Promise<void> {
|
|
329
416
|
if (!this.abortController) return;
|
|
330
417
|
|
|
418
|
+
// Read the server target before aborting: the abort listener runs the
|
|
419
|
+
// onCancel callback synchronously, which may clear the thread and with it
|
|
420
|
+
// the task this cancellation is for, or start a new run.
|
|
421
|
+
const task = this.currentTask;
|
|
422
|
+
const generation = this.runGeneration;
|
|
423
|
+
|
|
331
424
|
// Abort locally first so the stream stops immediately
|
|
332
425
|
this.abortController.abort();
|
|
333
426
|
|
|
334
427
|
// Then try to cancel the task on the server
|
|
335
|
-
if (
|
|
428
|
+
if (task?.id) {
|
|
336
429
|
try {
|
|
337
|
-
const updated = await this.client.cancelTask(
|
|
338
|
-
|
|
430
|
+
const updated = await this.client.cancelTask(task.id);
|
|
431
|
+
// Only apply the response while nothing newer exists. A newer snapshot
|
|
432
|
+
// or a cleared thread replaces the task object; a follow-up run that
|
|
433
|
+
// has not emitted yet keeps it, so the run generation is what rules
|
|
434
|
+
// that case out.
|
|
435
|
+
if (this.currentTask === task && this.runGeneration === generation) {
|
|
436
|
+
this.currentTask = updated;
|
|
437
|
+
}
|
|
339
438
|
} catch {
|
|
340
439
|
// Server cancel failed; local abort already handled
|
|
341
440
|
}
|
|
@@ -366,6 +465,8 @@ export class A2AThreadRuntimeCore {
|
|
|
366
465
|
}
|
|
367
466
|
this.currentTask = undefined;
|
|
368
467
|
this.currentArtifacts = [];
|
|
468
|
+
this.a2uiState = new Map();
|
|
469
|
+
this.a2uiMessageIds.clear();
|
|
369
470
|
this.notifyUpdate();
|
|
370
471
|
}
|
|
371
472
|
|
|
@@ -405,6 +506,8 @@ export class A2AThreadRuntimeCore {
|
|
|
405
506
|
// --- Run logic ---
|
|
406
507
|
|
|
407
508
|
private async startRun(userThreadMessage: ThreadMessage): Promise<void> {
|
|
509
|
+
this.runGeneration++;
|
|
510
|
+
|
|
408
511
|
// Cancel any in-progress run before starting a new one
|
|
409
512
|
if (this.abortController) {
|
|
410
513
|
this.abortController.abort();
|
|
@@ -429,6 +532,8 @@ export class A2AThreadRuntimeCore {
|
|
|
429
532
|
}
|
|
430
533
|
|
|
431
534
|
this.currentArtifacts = [];
|
|
535
|
+
this.a2uiState = new Map();
|
|
536
|
+
this.a2uiMessageIds.clear();
|
|
432
537
|
|
|
433
538
|
const assistantParentId = userThreadMessage.id;
|
|
434
539
|
const assistantId = this.insertAssistantPlaceholder(assistantParentId);
|
|
@@ -561,7 +666,7 @@ export class A2AThreadRuntimeCore {
|
|
|
561
666
|
this.handleStatusUpdate(assistantId, event.event);
|
|
562
667
|
break;
|
|
563
668
|
case "artifactUpdate":
|
|
564
|
-
this.handleArtifactUpdate(event.event);
|
|
669
|
+
this.handleArtifactUpdate(assistantId, event.event);
|
|
565
670
|
break;
|
|
566
671
|
case "message":
|
|
567
672
|
this.handleMessage(assistantId, event.message);
|
|
@@ -591,6 +696,7 @@ export class A2AThreadRuntimeCore {
|
|
|
591
696
|
}
|
|
592
697
|
|
|
593
698
|
if (event.status.message) {
|
|
699
|
+
this.applyA2uiMessage(event.status.message);
|
|
594
700
|
const content = a2aMessageToContent(event.status.message);
|
|
595
701
|
this.updateAssistantContent(assistantId, content);
|
|
596
702
|
}
|
|
@@ -601,7 +707,10 @@ export class A2AThreadRuntimeCore {
|
|
|
601
707
|
this.notifyUpdate();
|
|
602
708
|
}
|
|
603
709
|
|
|
604
|
-
private handleArtifactUpdate(
|
|
710
|
+
private handleArtifactUpdate(
|
|
711
|
+
assistantId: string,
|
|
712
|
+
event: A2ATaskArtifactUpdateEvent,
|
|
713
|
+
) {
|
|
605
714
|
const { append, lastChunk } = event;
|
|
606
715
|
const artifact = normalizeArtifact(event.artifact);
|
|
607
716
|
const existingIdx = this.currentArtifacts.findIndex(
|
|
@@ -632,6 +741,10 @@ export class A2AThreadRuntimeCore {
|
|
|
632
741
|
this.currentArtifacts = [...this.currentArtifacts, updated];
|
|
633
742
|
}
|
|
634
743
|
|
|
744
|
+
this.applyA2uiParts(artifact.parts);
|
|
745
|
+
this.updateAssistantArtifacts(assistantId);
|
|
746
|
+
this.rebuildAssistantA2uiSurfaces(assistantId);
|
|
747
|
+
|
|
635
748
|
if (lastChunk) {
|
|
636
749
|
invokeRuntimeCallback(
|
|
637
750
|
"onArtifactComplete",
|
|
@@ -646,6 +759,7 @@ export class A2AThreadRuntimeCore {
|
|
|
646
759
|
private handleMessage(assistantId: string, message: A2AMessage) {
|
|
647
760
|
if (message.role !== "agent") return;
|
|
648
761
|
|
|
762
|
+
this.applyA2uiMessage(message);
|
|
649
763
|
const content = a2aMessageToContent(message);
|
|
650
764
|
this.updateAssistantContent(assistantId, content);
|
|
651
765
|
this.notifyUpdate();
|
|
@@ -673,13 +787,43 @@ export class A2AThreadRuntimeCore {
|
|
|
673
787
|
if (task.contextId) {
|
|
674
788
|
this.contextId = task.contextId;
|
|
675
789
|
}
|
|
676
|
-
|
|
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) {
|
|
677
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);
|
|
678
820
|
}
|
|
679
821
|
|
|
680
822
|
if (task.status.message) {
|
|
681
823
|
const content = a2aMessageToContent(task.status.message);
|
|
682
824
|
this.updateAssistantContent(assistantId, content);
|
|
825
|
+
} else {
|
|
826
|
+
this.rebuildAssistantA2uiSurfaces(assistantId);
|
|
683
827
|
}
|
|
684
828
|
|
|
685
829
|
const status = taskStateToMessageStatus(task.status.state);
|
|
@@ -718,10 +862,124 @@ export class A2AThreadRuntimeCore {
|
|
|
718
862
|
) {
|
|
719
863
|
this.session.updateMessage(messageId, (message) => {
|
|
720
864
|
if (message.role !== "assistant") return message;
|
|
721
|
-
return {
|
|
865
|
+
return {
|
|
866
|
+
...message,
|
|
867
|
+
content: this.withA2uiSurfaces(content, message.content),
|
|
868
|
+
};
|
|
722
869
|
});
|
|
723
870
|
}
|
|
724
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
|
+
};
|
|
979
|
+
});
|
|
980
|
+
if (touched) this.notifyUpdate();
|
|
981
|
+
}
|
|
982
|
+
|
|
725
983
|
private updateAssistantStatus(messageId: string, status: MessageStatus) {
|
|
726
984
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
727
985
|
if (message.role !== "assistant") return message;
|
|
@@ -729,7 +987,7 @@ export class A2AThreadRuntimeCore {
|
|
|
729
987
|
});
|
|
730
988
|
if (touched) {
|
|
731
989
|
this.notifyUpdate();
|
|
732
|
-
if (
|
|
990
|
+
if (this.isPersistableAssistantStatus(status)) {
|
|
733
991
|
this.persistAssistantHistory(messageId);
|
|
734
992
|
}
|
|
735
993
|
}
|
|
@@ -741,6 +999,14 @@ export class A2AThreadRuntimeCore {
|
|
|
741
999
|
return msg.status;
|
|
742
1000
|
}
|
|
743
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
|
+
|
|
744
1010
|
// --- Lifecycle helpers ---
|
|
745
1011
|
|
|
746
1012
|
private setRunning(running: boolean) {
|
|
@@ -757,7 +1023,9 @@ export class A2AThreadRuntimeCore {
|
|
|
757
1023
|
// --- History persistence ---
|
|
758
1024
|
|
|
759
1025
|
private recordHistoryEntry(parentId: string | null, message: ThreadMessage) {
|
|
760
|
-
this.appendHistoryItem(parentId, message)
|
|
1026
|
+
void this.appendHistoryItem(parentId, message)?.catch((error) => {
|
|
1027
|
+
console.error("[react-a2a] failed to append history entry", error);
|
|
1028
|
+
});
|
|
761
1029
|
}
|
|
762
1030
|
|
|
763
1031
|
private markPendingAssistantHistory(
|
|
@@ -770,24 +1038,107 @@ export class A2AThreadRuntimeCore {
|
|
|
770
1038
|
|
|
771
1039
|
private persistAssistantHistory(messageId: string) {
|
|
772
1040
|
if (!this.history) return;
|
|
1041
|
+
const messageData = this.session.tryGetMessage(messageId);
|
|
773
1042
|
const parentId = this.assistantHistoryParents.get(messageId);
|
|
774
|
-
if (parentId === undefined)
|
|
775
|
-
|
|
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;
|
|
776
1050
|
if (!message || message.role !== "assistant") return;
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
)
|
|
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
|
+
}
|
|
781
1082
|
return;
|
|
782
|
-
|
|
783
|
-
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
|
+
});
|
|
784
1096
|
}
|
|
785
1097
|
|
|
786
|
-
private appendHistoryItem(
|
|
1098
|
+
private appendHistoryItem(
|
|
1099
|
+
parentId: string | null,
|
|
1100
|
+
message: ThreadMessage,
|
|
1101
|
+
): Promise<void> | undefined {
|
|
787
1102
|
if (!this.history || this.recordedHistoryIds.has(message.id)) return;
|
|
788
1103
|
this.recordedHistoryIds.add(message.id);
|
|
789
|
-
|
|
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(() => {
|
|
790
1109
|
this.recordedHistoryIds.delete(message.id);
|
|
791
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;
|
|
792
1143
|
}
|
|
793
1144
|
}
|