@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.
- package/README.md +47 -1
- package/dist/A2AClient.d.ts +43 -0
- package/dist/A2AClient.d.ts.map +1 -0
- package/dist/A2AClient.js +358 -0
- package/dist/A2AClient.js.map +1 -0
- package/dist/A2AThreadRuntimeCore.d.ts +75 -0
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -0
- package/dist/A2AThreadRuntimeCore.js +483 -0
- package/dist/A2AThreadRuntimeCore.js.map +1 -0
- package/dist/conversions.d.ts +14 -0
- package/dist/conversions.d.ts.map +1 -0
- package/dist/conversions.js +92 -0
- package/dist/conversions.js.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +228 -84
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -9
- package/dist/types.js.map +1 -1
- package/dist/useA2ARuntime.d.ts +35 -48
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +126 -172
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/A2AClient.test.ts +773 -0
- package/src/A2AClient.ts +519 -0
- package/src/A2AThreadRuntimeCore.test.ts +692 -0
- package/src/A2AThreadRuntimeCore.ts +633 -0
- package/src/conversions.test.ts +276 -0
- package/src/conversions.ts +115 -0
- package/src/index.ts +66 -6
- package/src/types.ts +276 -95
- package/src/useA2ARuntime.ts +204 -296
- package/dist/A2AMessageAccumulator.d.ts +0 -16
- package/dist/A2AMessageAccumulator.d.ts.map +0 -1
- package/dist/A2AMessageAccumulator.js +0 -29
- package/dist/A2AMessageAccumulator.js.map +0 -1
- package/dist/appendA2AChunk.d.ts +0 -3
- package/dist/appendA2AChunk.d.ts.map +0 -1
- package/dist/appendA2AChunk.js +0 -110
- package/dist/appendA2AChunk.js.map +0 -1
- package/dist/convertA2AMessages.d.ts +0 -64
- package/dist/convertA2AMessages.d.ts.map +0 -1
- package/dist/convertA2AMessages.js +0 -90
- package/dist/convertA2AMessages.js.map +0 -1
- package/dist/testUtils.d.ts +0 -4
- package/dist/testUtils.d.ts.map +0 -1
- package/dist/testUtils.js +0 -6
- package/dist/testUtils.js.map +0 -1
- package/dist/useA2AMessages.d.ts +0 -25
- package/dist/useA2AMessages.d.ts.map +0 -1
- package/dist/useA2AMessages.js +0 -122
- package/dist/useA2AMessages.js.map +0 -1
- package/src/A2AMessageAccumulator.ts +0 -48
- package/src/appendA2AChunk.ts +0 -121
- package/src/convertA2AMessages.ts +0 -108
- package/src/testUtils.ts +0 -11
- 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
|
+
}
|