@assistant-ui/react-a2a 0.2.5 → 0.2.7

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 (60) hide show
  1. package/README.md +47 -1
  2. package/dist/A2AClient.d.ts +43 -0
  3. package/dist/A2AClient.d.ts.map +1 -0
  4. package/dist/A2AClient.js +358 -0
  5. package/dist/A2AClient.js.map +1 -0
  6. package/dist/A2AThreadRuntimeCore.d.ts +75 -0
  7. package/dist/A2AThreadRuntimeCore.d.ts.map +1 -0
  8. package/dist/A2AThreadRuntimeCore.js +483 -0
  9. package/dist/A2AThreadRuntimeCore.js.map +1 -0
  10. package/dist/conversions.d.ts +14 -0
  11. package/dist/conversions.d.ts.map +1 -0
  12. package/dist/conversions.js +92 -0
  13. package/dist/conversions.js.map +1 -0
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/index.js.map +1 -1
  18. package/dist/types.d.ts +228 -84
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/types.js +4 -9
  21. package/dist/types.js.map +1 -1
  22. package/dist/useA2ARuntime.d.ts +35 -48
  23. package/dist/useA2ARuntime.d.ts.map +1 -1
  24. package/dist/useA2ARuntime.js +126 -172
  25. package/dist/useA2ARuntime.js.map +1 -1
  26. package/package.json +9 -9
  27. package/src/A2AClient.test.ts +773 -0
  28. package/src/A2AClient.ts +519 -0
  29. package/src/A2AThreadRuntimeCore.test.ts +692 -0
  30. package/src/A2AThreadRuntimeCore.ts +633 -0
  31. package/src/conversions.test.ts +276 -0
  32. package/src/conversions.ts +115 -0
  33. package/src/index.ts +66 -6
  34. package/src/types.ts +276 -95
  35. package/src/useA2ARuntime.ts +204 -296
  36. package/dist/A2AMessageAccumulator.d.ts +0 -16
  37. package/dist/A2AMessageAccumulator.d.ts.map +0 -1
  38. package/dist/A2AMessageAccumulator.js +0 -29
  39. package/dist/A2AMessageAccumulator.js.map +0 -1
  40. package/dist/appendA2AChunk.d.ts +0 -3
  41. package/dist/appendA2AChunk.d.ts.map +0 -1
  42. package/dist/appendA2AChunk.js +0 -110
  43. package/dist/appendA2AChunk.js.map +0 -1
  44. package/dist/convertA2AMessages.d.ts +0 -64
  45. package/dist/convertA2AMessages.d.ts.map +0 -1
  46. package/dist/convertA2AMessages.js +0 -90
  47. package/dist/convertA2AMessages.js.map +0 -1
  48. package/dist/testUtils.d.ts +0 -4
  49. package/dist/testUtils.d.ts.map +0 -1
  50. package/dist/testUtils.js +0 -6
  51. package/dist/testUtils.js.map +0 -1
  52. package/dist/useA2AMessages.d.ts +0 -25
  53. package/dist/useA2AMessages.d.ts.map +0 -1
  54. package/dist/useA2AMessages.js +0 -122
  55. package/dist/useA2AMessages.js.map +0 -1
  56. package/src/A2AMessageAccumulator.ts +0 -48
  57. package/src/appendA2AChunk.ts +0 -121
  58. package/src/convertA2AMessages.ts +0 -108
  59. package/src/testUtils.ts +0 -11
  60. package/src/useA2AMessages.ts +0 -180
@@ -0,0 +1,633 @@
1
+ "use client";
2
+
3
+ import { generateId, fromThreadMessageLike } from "@assistant-ui/core/internal";
4
+ import type {
5
+ AppendMessage,
6
+ AssistantRuntime,
7
+ MessageStatus,
8
+ ThreadAssistantMessage,
9
+ ThreadHistoryAdapter,
10
+ ThreadMessage,
11
+ } from "@assistant-ui/core";
12
+ import type { A2AClient } from "./A2AClient";
13
+ import type {
14
+ A2AArtifact,
15
+ A2AAgentCard,
16
+ A2AMessage,
17
+ A2APart,
18
+ A2ASendMessageConfiguration,
19
+ A2AStreamEvent,
20
+ A2ATask,
21
+ A2ATaskArtifactUpdateEvent,
22
+ A2ATaskStatusUpdateEvent,
23
+ } from "./types";
24
+ import {
25
+ a2aMessageToContent,
26
+ isTerminalTaskState,
27
+ taskStateToMessageStatus,
28
+ } from "./conversions";
29
+
30
+ export type A2AThreadRuntimeCoreOptions = {
31
+ client: A2AClient;
32
+ contextId?: string | undefined;
33
+ configuration?: A2ASendMessageConfiguration | undefined;
34
+ onError?: ((error: Error) => void) | undefined;
35
+ onCancel?: (() => void) | undefined;
36
+ onArtifactComplete?: ((artifact: A2AArtifact) => void) | undefined;
37
+ history?: ThreadHistoryAdapter | undefined;
38
+ notifyUpdate: () => void;
39
+ };
40
+
41
+ const FALLBACK_USER_STATUS = {
42
+ type: "complete",
43
+ reason: "unknown",
44
+ } as const;
45
+
46
+ export class A2AThreadRuntimeCore {
47
+ private client: A2AClient;
48
+ private contextId: string | undefined;
49
+ private configuration: A2ASendMessageConfiguration | undefined;
50
+ private onError: ((error: Error) => void) | undefined;
51
+ private onCancel: (() => void) | undefined;
52
+ private onArtifactComplete: ((artifact: A2AArtifact) => void) | undefined;
53
+ private history: ThreadHistoryAdapter | undefined;
54
+ private readonly notifyUpdate: () => void;
55
+
56
+ private runtime: AssistantRuntime | undefined;
57
+ private messages: ThreadMessage[] = [];
58
+ private isRunningFlag = false;
59
+ private abortController: AbortController | null = null;
60
+ private pendingError: Error | null = null;
61
+
62
+ // A2A-specific state
63
+ private currentTask: A2ATask | undefined;
64
+ private currentArtifacts: A2AArtifact[] = [];
65
+ private agentCardValue: A2AAgentCard | undefined;
66
+
67
+ // History tracking
68
+ private readonly assistantHistoryParents = new Map<string, string | null>();
69
+ private readonly recordedHistoryIds = new Set<string>();
70
+ private _isLoading = false;
71
+ private _loadPromise: Promise<void> | undefined;
72
+
73
+ constructor(options: A2AThreadRuntimeCoreOptions) {
74
+ this.client = options.client;
75
+ this.contextId = options.contextId;
76
+ this.configuration = options.configuration;
77
+ this.onError = options.onError;
78
+ this.onCancel = options.onCancel;
79
+ this.onArtifactComplete = options.onArtifactComplete;
80
+ this.history = options.history;
81
+ this.notifyUpdate = options.notifyUpdate;
82
+ }
83
+
84
+ updateOptions(options: Omit<A2AThreadRuntimeCoreOptions, "notifyUpdate">) {
85
+ this.client = options.client;
86
+ this.contextId = options.contextId;
87
+ this.configuration = options.configuration;
88
+ this.onError = options.onError;
89
+ this.onCancel = options.onCancel;
90
+ this.onArtifactComplete = options.onArtifactComplete;
91
+ this.history = options.history;
92
+ }
93
+
94
+ attachRuntime(runtime: AssistantRuntime) {
95
+ this.runtime = runtime;
96
+ }
97
+
98
+ detachRuntime() {
99
+ this.runtime = undefined;
100
+ // Abort in-flight requests on unmount
101
+ if (this.abortController) {
102
+ this.abortController.abort();
103
+ this.abortController = null;
104
+ }
105
+ }
106
+
107
+ getRuntime(): AssistantRuntime | undefined {
108
+ return this.runtime;
109
+ }
110
+
111
+ getMessages(): readonly ThreadMessage[] {
112
+ return this.messages;
113
+ }
114
+
115
+ getTask(): A2ATask | undefined {
116
+ return this.currentTask;
117
+ }
118
+
119
+ getArtifacts(): readonly A2AArtifact[] {
120
+ return this.currentArtifacts;
121
+ }
122
+
123
+ getAgentCard(): A2AAgentCard | undefined {
124
+ return this.agentCardValue;
125
+ }
126
+
127
+ isRunning(): boolean {
128
+ return this.isRunningFlag;
129
+ }
130
+
131
+ get isLoading(): boolean {
132
+ return this._isLoading;
133
+ }
134
+
135
+ __internal_load(): Promise<void> {
136
+ if (this._loadPromise) return this._loadPromise;
137
+
138
+ this._isLoading = true;
139
+
140
+ const historyPromise = this.history?.load() ?? Promise.resolve(null);
141
+ const agentCardPromise = this.client.getAgentCard().catch(() => undefined);
142
+
143
+ this._loadPromise = Promise.all([historyPromise, agentCardPromise])
144
+ .then(([repo, agentCard]) => {
145
+ if (agentCard) {
146
+ this.agentCardValue = agentCard;
147
+ }
148
+ if (repo) {
149
+ const messages = repo.messages.map((item) => item.message);
150
+ this.applyExternalMessages(messages);
151
+ }
152
+ })
153
+ .catch((error) => {
154
+ this.onError?.(
155
+ error instanceof Error ? error : new Error(String(error)),
156
+ );
157
+ })
158
+ .finally(() => {
159
+ this._isLoading = false;
160
+ this.notifyUpdate();
161
+ });
162
+
163
+ this.notifyUpdate();
164
+ return this._loadPromise;
165
+ }
166
+
167
+ async append(message: AppendMessage): Promise<void> {
168
+ const startRun = message.startRun ?? message.role === "user";
169
+ if (message.sourceId) {
170
+ this.messages = this.messages.filter(
171
+ (entry) => entry.id !== message.sourceId,
172
+ );
173
+ }
174
+ this.resetHead(message.parentId);
175
+
176
+ const threadMessage = fromThreadMessageLike(
177
+ message as any,
178
+ generateId(),
179
+ FALLBACK_USER_STATUS,
180
+ );
181
+ this.messages = [...this.messages, threadMessage];
182
+ this.notifyUpdate();
183
+ this.recordHistoryEntry(message.parentId ?? null, threadMessage);
184
+
185
+ if (!startRun) return;
186
+ await this.startRun(threadMessage);
187
+ }
188
+
189
+ async edit(message: AppendMessage): Promise<void> {
190
+ await this.append(message);
191
+ }
192
+
193
+ async reload(
194
+ parentId: string | null,
195
+ _config: { runConfig?: Record<string, unknown> } = {},
196
+ ): Promise<void> {
197
+ this.resetHead(parentId);
198
+ this.notifyUpdate();
199
+
200
+ // Find the last user message to re-run
201
+ for (let i = this.messages.length - 1; i >= 0; i--) {
202
+ if (this.messages[i]!.role === "user") {
203
+ await this.startRun(this.messages[i]!);
204
+ return;
205
+ }
206
+ }
207
+ }
208
+
209
+ async cancel(): Promise<void> {
210
+ if (!this.abortController) return;
211
+
212
+ // Abort locally first so the stream stops immediately
213
+ this.abortController.abort();
214
+
215
+ // Then try to cancel the task on the server
216
+ if (this.currentTask?.id) {
217
+ try {
218
+ const updated = await this.client.cancelTask(this.currentTask.id);
219
+ this.currentTask = updated;
220
+ } catch {
221
+ // Server cancel failed; local abort already handled
222
+ }
223
+ }
224
+ }
225
+
226
+ applyExternalMessages(messages: readonly ThreadMessage[]): void {
227
+ this.assistantHistoryParents.clear();
228
+ this.messages = [...messages];
229
+ this.recordedHistoryIds.clear();
230
+ for (const message of this.messages) {
231
+ this.recordedHistoryIds.add(message.id);
232
+ }
233
+ // Reset task-specific state to prevent leaking into new thread
234
+ this.currentTask = undefined;
235
+ this.currentArtifacts = [];
236
+ this.notifyUpdate();
237
+ }
238
+
239
+ // --- Run logic ---
240
+
241
+ private async startRun(userThreadMessage: ThreadMessage): Promise<void> {
242
+ // Cancel any in-progress run before starting a new one
243
+ if (this.abortController) {
244
+ this.abortController.abort();
245
+ this.abortController = null;
246
+ }
247
+
248
+ const a2aMessage = this.threadMessageToA2AMessage(userThreadMessage);
249
+
250
+ // Clear task if previous task reached terminal state
251
+ if (
252
+ this.currentTask &&
253
+ isTerminalTaskState(this.currentTask.status.state)
254
+ ) {
255
+ this.currentTask = undefined;
256
+ }
257
+
258
+ this.currentArtifacts = [];
259
+
260
+ const assistantParentId = userThreadMessage.id;
261
+ const assistantId = this.insertAssistantPlaceholder();
262
+ this.markPendingAssistantHistory(assistantId, assistantParentId);
263
+
264
+ const abortController = new AbortController();
265
+ this.abortController = abortController;
266
+
267
+ abortController.signal.addEventListener(
268
+ "abort",
269
+ () => {
270
+ this.updateAssistantStatus(assistantId, {
271
+ type: "incomplete",
272
+ reason: "cancelled",
273
+ });
274
+ this.finishRun(abortController);
275
+ this.onCancel?.();
276
+ },
277
+ { once: true },
278
+ );
279
+
280
+ this.setRunning(true);
281
+
282
+ // Check if agent supports streaming; fall back to sync sendMessage if not
283
+ const supportsStreaming =
284
+ this.agentCardValue?.capabilities?.streaming !== false;
285
+
286
+ try {
287
+ if (supportsStreaming) {
288
+ await this.runStreaming(a2aMessage, assistantId, abortController);
289
+ } else {
290
+ await this.runSync(a2aMessage, assistantId, abortController);
291
+ }
292
+ } catch (error) {
293
+ if (!abortController.signal.aborted) {
294
+ const err = error instanceof Error ? error : new Error(String(error));
295
+ this.updateAssistantStatus(assistantId, {
296
+ type: "incomplete",
297
+ reason: "error",
298
+ });
299
+ this.onError?.(err);
300
+ this.pendingError = this.pendingError ?? err;
301
+ }
302
+ } finally {
303
+ this.finishRun(abortController);
304
+ }
305
+
306
+ if (this.pendingError) {
307
+ const err = this.pendingError;
308
+ this.pendingError = null;
309
+ throw err;
310
+ }
311
+ }
312
+
313
+ private async runStreaming(
314
+ a2aMessage: A2AMessage,
315
+ assistantId: string,
316
+ abortController: AbortController,
317
+ ): Promise<void> {
318
+ const stream = this.client.streamMessage(
319
+ a2aMessage,
320
+ this.configuration,
321
+ undefined, // metadata
322
+ abortController.signal,
323
+ );
324
+
325
+ for await (const event of stream) {
326
+ if (abortController.signal.aborted) break;
327
+ this.handleStreamEvent(assistantId, event);
328
+ }
329
+
330
+ if (!abortController.signal.aborted) {
331
+ const lastStatus = this.getAssistantStatus(assistantId);
332
+ if (lastStatus?.type === "running") {
333
+ this.updateAssistantStatus(assistantId, {
334
+ type: "complete",
335
+ reason: "stop",
336
+ });
337
+ }
338
+ }
339
+ }
340
+
341
+ private async runSync(
342
+ a2aMessage: A2AMessage,
343
+ assistantId: string,
344
+ abortController: AbortController,
345
+ ): Promise<void> {
346
+ const result = await this.client.sendMessage(
347
+ a2aMessage,
348
+ this.configuration,
349
+ undefined, // metadata
350
+ abortController.signal,
351
+ );
352
+
353
+ if (abortController.signal.aborted) return;
354
+
355
+ // Result is either A2ATask or A2AMessage
356
+ if ("id" in result && "status" in result) {
357
+ // It's a Task
358
+ this.handleTaskSnapshot(assistantId, result as A2ATask);
359
+ } else if ("messageId" in result && "parts" in result) {
360
+ // It's a Message
361
+ this.handleMessage(assistantId, result as A2AMessage);
362
+ this.updateAssistantStatus(assistantId, {
363
+ type: "complete",
364
+ reason: "stop",
365
+ });
366
+ }
367
+ }
368
+
369
+ private handleStreamEvent(assistantId: string, event: A2AStreamEvent) {
370
+ switch (event.type) {
371
+ case "statusUpdate":
372
+ this.handleStatusUpdate(assistantId, event.event);
373
+ break;
374
+ case "artifactUpdate":
375
+ this.handleArtifactUpdate(event.event);
376
+ break;
377
+ case "message":
378
+ this.handleMessage(assistantId, event.message);
379
+ break;
380
+ case "task":
381
+ this.handleTaskSnapshot(assistantId, event.task);
382
+ break;
383
+ }
384
+ }
385
+
386
+ private handleStatusUpdate(
387
+ assistantId: string,
388
+ event: A2ATaskStatusUpdateEvent,
389
+ ) {
390
+ if (!this.currentTask) {
391
+ this.currentTask = {
392
+ id: event.taskId,
393
+ contextId: event.contextId,
394
+ status: event.status,
395
+ };
396
+ } else {
397
+ this.currentTask = { ...this.currentTask, status: event.status };
398
+ }
399
+
400
+ if (event.contextId) {
401
+ this.contextId = event.contextId;
402
+ }
403
+
404
+ if (event.status.message) {
405
+ const content = a2aMessageToContent(event.status.message);
406
+ this.updateAssistantContent(assistantId, content);
407
+ }
408
+
409
+ const status = taskStateToMessageStatus(event.status.state);
410
+ this.updateAssistantStatus(assistantId, status);
411
+
412
+ this.notifyUpdate();
413
+ }
414
+
415
+ private handleArtifactUpdate(event: A2ATaskArtifactUpdateEvent) {
416
+ const { artifact, append, lastChunk } = event;
417
+ const existingIdx = this.currentArtifacts.findIndex(
418
+ (a) => a.artifactId === artifact.artifactId,
419
+ );
420
+
421
+ let updated: A2AArtifact;
422
+ if (existingIdx >= 0 && append) {
423
+ const existing = this.currentArtifacts[existingIdx]!;
424
+ updated = {
425
+ ...existing,
426
+ parts: [...existing.parts, ...artifact.parts],
427
+ };
428
+ this.currentArtifacts = [
429
+ ...this.currentArtifacts.slice(0, existingIdx),
430
+ updated,
431
+ ...this.currentArtifacts.slice(existingIdx + 1),
432
+ ];
433
+ } else if (existingIdx >= 0) {
434
+ updated = artifact;
435
+ this.currentArtifacts = [
436
+ ...this.currentArtifacts.slice(0, existingIdx),
437
+ updated,
438
+ ...this.currentArtifacts.slice(existingIdx + 1),
439
+ ];
440
+ } else {
441
+ updated = artifact;
442
+ this.currentArtifacts = [...this.currentArtifacts, updated];
443
+ }
444
+
445
+ if (lastChunk) {
446
+ this.onArtifactComplete?.(updated);
447
+ }
448
+
449
+ this.notifyUpdate();
450
+ }
451
+
452
+ private handleMessage(assistantId: string, message: A2AMessage) {
453
+ if (message.role !== "agent") return;
454
+
455
+ const content = a2aMessageToContent(message);
456
+ this.updateAssistantContent(assistantId, content);
457
+ this.notifyUpdate();
458
+ }
459
+
460
+ private handleTaskSnapshot(assistantId: string, task: A2ATask) {
461
+ this.currentTask = task;
462
+
463
+ if (task.contextId) {
464
+ this.contextId = task.contextId;
465
+ }
466
+ if (task.artifacts) {
467
+ this.currentArtifacts = task.artifacts;
468
+ }
469
+
470
+ if (task.status.message) {
471
+ const content = a2aMessageToContent(task.status.message);
472
+ this.updateAssistantContent(assistantId, content);
473
+ }
474
+
475
+ const status = taskStateToMessageStatus(task.status.state);
476
+ this.updateAssistantStatus(assistantId, status);
477
+
478
+ this.notifyUpdate();
479
+ }
480
+
481
+ // --- Message helpers ---
482
+
483
+ private threadMessageToA2AMessage(message: ThreadMessage): A2AMessage {
484
+ const parts: A2APart[] = [];
485
+
486
+ if (message.role === "user") {
487
+ for (const part of message.content) {
488
+ if (part.type === "text") {
489
+ parts.push({ text: part.text });
490
+ } else if (part.type === "image") {
491
+ parts.push({ url: part.image, mediaType: "image/*" });
492
+ }
493
+ }
494
+ }
495
+
496
+ const a2aMsg: A2AMessage = {
497
+ messageId: message.id,
498
+ role: "user",
499
+ parts,
500
+ };
501
+
502
+ if (this.contextId) {
503
+ a2aMsg.contextId = this.contextId;
504
+ }
505
+ // Only attach taskId if current task is NOT in terminal state
506
+ if (
507
+ this.currentTask?.id &&
508
+ !isTerminalTaskState(this.currentTask.status.state)
509
+ ) {
510
+ a2aMsg.taskId = this.currentTask.id;
511
+ }
512
+
513
+ return a2aMsg;
514
+ }
515
+
516
+ private insertAssistantPlaceholder(): string {
517
+ const id = generateId();
518
+ const assistant: ThreadAssistantMessage = {
519
+ id,
520
+ role: "assistant",
521
+ createdAt: new Date(),
522
+ status: { type: "running" },
523
+ content: [],
524
+ metadata: {
525
+ unstable_state: null,
526
+ unstable_annotations: [],
527
+ unstable_data: [],
528
+ steps: [],
529
+ custom: {},
530
+ },
531
+ };
532
+ this.messages = [...this.messages, assistant];
533
+ this.notifyUpdate();
534
+ return id;
535
+ }
536
+
537
+ private updateAssistantContent(
538
+ messageId: string,
539
+ content: ThreadAssistantMessage["content"],
540
+ ) {
541
+ this.messages = this.messages.map((message) => {
542
+ if (message.id !== messageId || message.role !== "assistant")
543
+ return message;
544
+ return { ...message, content };
545
+ });
546
+ }
547
+
548
+ private updateAssistantStatus(messageId: string, status: MessageStatus) {
549
+ let touched = false;
550
+ this.messages = this.messages.map((message) => {
551
+ if (message.id !== messageId || message.role !== "assistant")
552
+ return message;
553
+ touched = true;
554
+ return { ...message, status };
555
+ });
556
+ if (touched) {
557
+ this.notifyUpdate();
558
+ if (status.type === "complete" || status.type === "incomplete") {
559
+ this.persistAssistantHistory(messageId);
560
+ }
561
+ }
562
+ }
563
+
564
+ private getAssistantStatus(messageId: string): MessageStatus | undefined {
565
+ const msg = this.messages.find(
566
+ (m) => m.id === messageId && m.role === "assistant",
567
+ );
568
+ return msg?.status;
569
+ }
570
+
571
+ // --- Lifecycle helpers ---
572
+
573
+ private setRunning(running: boolean) {
574
+ this.isRunningFlag = running;
575
+ this.notifyUpdate();
576
+ }
577
+
578
+ private finishRun(controller: AbortController | null) {
579
+ if (this.abortController === controller) {
580
+ this.abortController = null;
581
+ }
582
+ this.setRunning(false);
583
+ }
584
+
585
+ private resetHead(parentId: string | null | undefined) {
586
+ if (!parentId) {
587
+ if (this.messages.length) {
588
+ this.messages = [];
589
+ }
590
+ return;
591
+ }
592
+ const idx = this.messages.findIndex((message) => message.id === parentId);
593
+ if (idx === -1) return;
594
+ this.messages = this.messages.slice(0, idx + 1);
595
+ }
596
+
597
+ // --- History persistence ---
598
+
599
+ private recordHistoryEntry(parentId: string | null, message: ThreadMessage) {
600
+ this.appendHistoryItem(parentId, message);
601
+ }
602
+
603
+ private markPendingAssistantHistory(
604
+ messageId: string,
605
+ parentId: string | null,
606
+ ) {
607
+ if (!this.history) return;
608
+ this.assistantHistoryParents.set(messageId, parentId);
609
+ }
610
+
611
+ private persistAssistantHistory(messageId: string) {
612
+ if (!this.history) return;
613
+ const parentId = this.assistantHistoryParents.get(messageId);
614
+ if (parentId === undefined) return;
615
+ const message = this.messages.find((m) => m.id === messageId);
616
+ if (!message || message.role !== "assistant") return;
617
+ if (
618
+ message.status?.type !== "complete" &&
619
+ message.status?.type !== "incomplete"
620
+ )
621
+ return;
622
+ this.assistantHistoryParents.delete(messageId);
623
+ this.appendHistoryItem(parentId, message);
624
+ }
625
+
626
+ private appendHistoryItem(parentId: string | null, message: ThreadMessage) {
627
+ if (!this.history || this.recordedHistoryIds.has(message.id)) return;
628
+ this.recordedHistoryIds.add(message.id);
629
+ void this.history.append({ parentId, message }).catch(() => {
630
+ this.recordedHistoryIds.delete(message.id);
631
+ });
632
+ }
633
+ }