@assistant-ui/react-ag-ui 0.0.44 → 0.0.46
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/agUiExtras.d.ts +0 -1
- package/dist/agUiExtras.d.ts.map +1 -1
- package/dist/hooks.d.ts +15 -9
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +17 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +28 -4
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +379 -114
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +0 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +12 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +48 -11
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -1
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/event-parser.d.ts +0 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +23 -11
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/logger.d.ts.map +1 -1
- package/dist/runtime/mcp-tool-result.d.ts +11 -0
- package/dist/runtime/mcp-tool-result.d.ts.map +1 -0
- package/dist/runtime/mcp-tool-result.js +20 -0
- package/dist/runtime/mcp-tool-result.js.map +1 -0
- package/dist/runtime/types.d.ts +15 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts +0 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +8 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -7
- package/src/hooks.ts +29 -7
- package/src/index.ts +2 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +547 -147
- package/src/runtime/adapter/conversions.ts +26 -17
- package/src/runtime/adapter/run-aggregator.ts +84 -7
- package/src/runtime/event-parser.ts +4 -1
- package/src/runtime/mcp-tool-result.ts +26 -0
- package/src/runtime/types.ts +19 -0
- package/src/useAgUiRuntime.ts +8 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
generateId,
|
|
5
|
+
fromThreadMessageLike,
|
|
6
|
+
isMcpAppUri,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
4
8
|
import type {
|
|
5
9
|
AddToolResultOptions,
|
|
6
10
|
AppendMessage,
|
|
@@ -8,12 +12,14 @@ import type {
|
|
|
8
12
|
AssistantRuntime,
|
|
9
13
|
ChatModelRunOptions,
|
|
10
14
|
ChatModelRunResult,
|
|
15
|
+
ExportedMessageRepository,
|
|
11
16
|
MessageStatus,
|
|
12
17
|
ThreadAssistantMessage,
|
|
13
18
|
ThreadHistoryAdapter,
|
|
14
19
|
ThreadMessage,
|
|
15
20
|
ToolCallMessagePart,
|
|
16
21
|
} from "@assistant-ui/core";
|
|
22
|
+
import { MessageRepository } from "@assistant-ui/core/internal";
|
|
17
23
|
import type { AbstractAgent } from "@ag-ui/client";
|
|
18
24
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
19
25
|
import type { Logger } from "./logger";
|
|
@@ -22,6 +28,8 @@ import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
|
22
28
|
import {
|
|
23
29
|
AG_UI_METADATA_NAMESPACE,
|
|
24
30
|
type AgUiCustomMetadata,
|
|
31
|
+
isPlainObject,
|
|
32
|
+
MCP_APPS_ACTIVITY_TYPE,
|
|
25
33
|
RunAggregator,
|
|
26
34
|
tryParseJSON,
|
|
27
35
|
} from "./adapter/run-aggregator";
|
|
@@ -36,6 +44,11 @@ const optimisticPrefix = "__optimistic__";
|
|
|
36
44
|
const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
|
|
37
45
|
const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);
|
|
38
46
|
|
|
47
|
+
const isResolvedToolCall = (
|
|
48
|
+
part: ThreadAssistantMessage["content"][number],
|
|
49
|
+
): boolean =>
|
|
50
|
+
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
51
|
+
|
|
39
52
|
const symbolResumeShim = Symbol("agui-resume-shim");
|
|
40
53
|
|
|
41
54
|
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
@@ -53,6 +66,7 @@ type CoreOptions = {
|
|
|
53
66
|
agent: AbstractAgent;
|
|
54
67
|
logger: Logger;
|
|
55
68
|
showThinking: boolean;
|
|
69
|
+
autoCancelPendingToolCalls?: boolean | undefined;
|
|
56
70
|
onError?: (error: Error) => void;
|
|
57
71
|
onCancel?: () => void;
|
|
58
72
|
history?: ThreadHistoryAdapter;
|
|
@@ -65,12 +79,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
65
79
|
private agent: AbstractAgent;
|
|
66
80
|
private logger: Logger;
|
|
67
81
|
private showThinking: boolean;
|
|
82
|
+
private autoCancelPendingToolCalls: boolean | undefined;
|
|
68
83
|
private onError: ((error: Error) => void) | undefined;
|
|
69
84
|
private onCancel: (() => void) | undefined;
|
|
70
85
|
private readonly notifyUpdate: () => void;
|
|
71
86
|
|
|
72
87
|
private runtime: AssistantRuntime | undefined;
|
|
73
|
-
private
|
|
88
|
+
private readonly repository = new MessageRepository();
|
|
89
|
+
private exportedRepository: ExportedMessageRepository | undefined;
|
|
74
90
|
private isRunningFlag = false;
|
|
75
91
|
private abortController: AbortController | null = null;
|
|
76
92
|
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
@@ -87,6 +103,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
87
103
|
this.agent = options.agent;
|
|
88
104
|
this.logger = options.logger;
|
|
89
105
|
this.showThinking = options.showThinking;
|
|
106
|
+
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
90
107
|
this.onError = options.onError;
|
|
91
108
|
this.onCancel = options.onCancel;
|
|
92
109
|
this.history = options.history;
|
|
@@ -98,6 +115,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
98
115
|
this.agent = options.agent;
|
|
99
116
|
this.logger = options.logger;
|
|
100
117
|
this.showThinking = options.showThinking;
|
|
118
|
+
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
101
119
|
this.onError = options.onError;
|
|
102
120
|
this.onCancel = options.onCancel;
|
|
103
121
|
this.history = options.history;
|
|
@@ -113,7 +131,97 @@ export class AgUiThreadRuntimeCore {
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
getMessages(): readonly ThreadMessage[] {
|
|
116
|
-
return this.
|
|
134
|
+
return this.repository.getMessages();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getMessageRepository(): ExportedMessageRepository {
|
|
138
|
+
if (this.exportedRepository) return this.exportedRepository;
|
|
139
|
+
|
|
140
|
+
const exportedRepository = this.repository.export();
|
|
141
|
+
let parentId: string | null = null;
|
|
142
|
+
for (const message of this.repository.getMessages()) {
|
|
143
|
+
if (message.metadata.isOptimistic) {
|
|
144
|
+
exportedRepository.messages.push({ parentId, message });
|
|
145
|
+
}
|
|
146
|
+
parentId = message.id;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.exportedRepository = {
|
|
150
|
+
...exportedRepository,
|
|
151
|
+
headId: this.repository.headId,
|
|
152
|
+
};
|
|
153
|
+
return this.exportedRepository;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private tryGetMessage(messageId: string) {
|
|
157
|
+
try {
|
|
158
|
+
return this.repository.getMessage(messageId);
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private tryGetMessages(
|
|
165
|
+
messageId: string,
|
|
166
|
+
): readonly ThreadMessage[] | undefined {
|
|
167
|
+
try {
|
|
168
|
+
return this.repository.getMessages(messageId);
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private hasMessage(messageId: string): boolean {
|
|
175
|
+
return this.tryGetMessage(messageId) !== undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private addOrUpdateMessage(
|
|
179
|
+
parentId: string | null,
|
|
180
|
+
message: ThreadMessage,
|
|
181
|
+
): void {
|
|
182
|
+
this.repository.addOrUpdateMessage(parentId, message);
|
|
183
|
+
this.exportedRepository = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private deleteMessage(
|
|
187
|
+
messageId: string,
|
|
188
|
+
replacementId?: string | null,
|
|
189
|
+
): void {
|
|
190
|
+
this.repository.deleteMessage(messageId, replacementId);
|
|
191
|
+
this.exportedRepository = undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private tryDeleteMessage(messageId: string): boolean {
|
|
195
|
+
if (!this.hasMessage(messageId)) return false;
|
|
196
|
+
this.deleteMessage(messageId);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private switchToBranch(messageId: string): void {
|
|
201
|
+
this.repository.switchToBranch(messageId);
|
|
202
|
+
this.exportedRepository = undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private resetRepositoryHead(messageId: string | null): void {
|
|
206
|
+
this.repository.resetHead(messageId);
|
|
207
|
+
this.exportedRepository = undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private clearRepository(): void {
|
|
211
|
+
this.repository.clear();
|
|
212
|
+
this.exportedRepository = undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private updateMessage(
|
|
216
|
+
messageId: string,
|
|
217
|
+
updater: (message: ThreadMessage) => ThreadMessage,
|
|
218
|
+
): boolean {
|
|
219
|
+
const item = this.tryGetMessage(messageId);
|
|
220
|
+
if (!item) return false;
|
|
221
|
+
const message = updater(item.message);
|
|
222
|
+
if (message === item.message) return false;
|
|
223
|
+
this.addOrUpdateMessage(item.parentId, message);
|
|
224
|
+
return true;
|
|
117
225
|
}
|
|
118
226
|
|
|
119
227
|
getState(): ReadonlyJSONValue | undefined {
|
|
@@ -139,15 +247,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
139
247
|
.then(async (repo) => {
|
|
140
248
|
if (!repo) return;
|
|
141
249
|
|
|
142
|
-
|
|
143
|
-
this.applyExternalMessages(messages);
|
|
250
|
+
this.applyExternalMessageRepository(repo);
|
|
144
251
|
|
|
145
252
|
if (repo.state !== undefined) {
|
|
146
253
|
this.loadExternalState(repo.state);
|
|
147
254
|
}
|
|
148
255
|
|
|
149
256
|
if (repo.unstable_resume) {
|
|
150
|
-
const parentId = repo.headId ??
|
|
257
|
+
const parentId = repo.headId ?? this.repository.headId;
|
|
151
258
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
152
259
|
await this.startRun(
|
|
153
260
|
parentId,
|
|
@@ -174,24 +281,37 @@ export class AgUiThreadRuntimeCore {
|
|
|
174
281
|
|
|
175
282
|
async append(message: AppendMessage): Promise<void> {
|
|
176
283
|
const startRun = message.startRun ?? message.role === "user";
|
|
177
|
-
if (startRun)
|
|
284
|
+
if (startRun) {
|
|
285
|
+
this.assertNoPendingInterrupts();
|
|
286
|
+
this.maybeAutoCancelPendingToolCalls();
|
|
287
|
+
}
|
|
178
288
|
const threadMessageId = this.appendEntry(message);
|
|
179
289
|
if (!startRun) return;
|
|
180
290
|
await this.startRun(threadMessageId, message.runConfig);
|
|
181
291
|
}
|
|
182
292
|
|
|
293
|
+
private maybeAutoCancelPendingToolCalls(): void {
|
|
294
|
+
if (this.autoCancelPendingToolCalls === false) return;
|
|
295
|
+
const pending = this.getPendingToolCalls();
|
|
296
|
+
if (!pending) return;
|
|
297
|
+
this.cancelUnresolvedToolCalls(pending.messageId);
|
|
298
|
+
this.maybeCompleteAfterToolResults(pending.messageId);
|
|
299
|
+
}
|
|
300
|
+
|
|
183
301
|
private appendEntry(message: AppendMessage): string {
|
|
184
|
-
if (message.sourceId)
|
|
185
|
-
this.messages = this.messages.filter(
|
|
186
|
-
(entry) => entry.id !== message.sourceId,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
this.resetHead(message.parentId);
|
|
302
|
+
if (message.sourceId) this.tryDeleteMessage(message.sourceId);
|
|
190
303
|
|
|
191
304
|
const threadMessage = this.toThreadMessage(message);
|
|
192
|
-
|
|
305
|
+
const parentId =
|
|
306
|
+
message.parentId === null
|
|
307
|
+
? null
|
|
308
|
+
: message.parentId && this.hasMessage(message.parentId)
|
|
309
|
+
? message.parentId
|
|
310
|
+
: this.repository.headId;
|
|
311
|
+
this.addOrUpdateMessage(parentId, threadMessage);
|
|
312
|
+
this.switchToBranch(threadMessage.id);
|
|
193
313
|
this.notifyUpdate();
|
|
194
|
-
this.recordHistoryEntry(
|
|
314
|
+
this.recordHistoryEntry(parentId, threadMessage);
|
|
195
315
|
return threadMessage.id;
|
|
196
316
|
}
|
|
197
317
|
|
|
@@ -204,8 +324,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
204
324
|
config: { runConfig?: RunConfig } = {},
|
|
205
325
|
): Promise<void> {
|
|
206
326
|
this.assertNoPendingInterrupts();
|
|
207
|
-
this.
|
|
208
|
-
this.notifyUpdate();
|
|
327
|
+
this.maybeAutoCancelPendingToolCalls();
|
|
209
328
|
await this.startRun(parentId, config.runConfig);
|
|
210
329
|
}
|
|
211
330
|
|
|
@@ -255,20 +374,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
255
374
|
);
|
|
256
375
|
}
|
|
257
376
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
| undefined;
|
|
377
|
+
private findRequiresActionAssistant(
|
|
378
|
+
reason: "interrupt" | "tool-calls",
|
|
379
|
+
): ThreadAssistantMessage | null {
|
|
380
|
+
const assistant = this.getMessages().findLast(
|
|
381
|
+
(message) => message.role === "assistant",
|
|
382
|
+
) as ThreadAssistantMessage | undefined;
|
|
265
383
|
if (
|
|
266
384
|
!assistant ||
|
|
267
385
|
assistant.status?.type !== "requires-action" ||
|
|
268
|
-
assistant.status.reason !==
|
|
386
|
+
assistant.status.reason !== reason
|
|
269
387
|
) {
|
|
270
388
|
return null;
|
|
271
389
|
}
|
|
390
|
+
return assistant;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
getPendingInterrupts(): {
|
|
394
|
+
messageId: string;
|
|
395
|
+
interrupts: readonly AgUiInterrupt[];
|
|
396
|
+
} | null {
|
|
397
|
+
const assistant = this.findRequiresActionAssistant("interrupt");
|
|
398
|
+
if (!assistant) return null;
|
|
272
399
|
const stored = (
|
|
273
400
|
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
274
401
|
| AgUiCustomMetadata
|
|
@@ -278,6 +405,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
278
405
|
return { messageId: assistant.id, interrupts: stored };
|
|
279
406
|
}
|
|
280
407
|
|
|
408
|
+
getPendingToolCalls(): {
|
|
409
|
+
messageId: string;
|
|
410
|
+
toolCallIds: string[];
|
|
411
|
+
} | null {
|
|
412
|
+
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
413
|
+
if (!assistant) return null;
|
|
414
|
+
const toolCallIds: string[] = [];
|
|
415
|
+
for (const part of assistant.content) {
|
|
416
|
+
if (part.type !== "tool-call") continue;
|
|
417
|
+
if (isResolvedToolCall(part)) continue;
|
|
418
|
+
toolCallIds.push(part.toolCallId);
|
|
419
|
+
}
|
|
420
|
+
if (toolCallIds.length === 0) return null;
|
|
421
|
+
return { messageId: assistant.id, toolCallIds };
|
|
422
|
+
}
|
|
423
|
+
|
|
281
424
|
async submitInterruptResponses(
|
|
282
425
|
responses: readonly AgUiResumeEntry[],
|
|
283
426
|
): Promise<void> {
|
|
@@ -288,25 +431,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
288
431
|
);
|
|
289
432
|
}
|
|
290
433
|
|
|
291
|
-
const responsesById =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
if (entry.status !== "resolved" && entry.status !== "cancelled") {
|
|
299
|
-
throw new Error(
|
|
300
|
-
`[agui] submitInterruptResponses: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
if (responsesById.has(entry.interruptId)) {
|
|
304
|
-
throw new Error(
|
|
305
|
-
`[agui] submitInterruptResponses: duplicate response for interrupt ${entry.interruptId}`,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
responsesById.set(entry.interruptId, entry);
|
|
309
|
-
}
|
|
434
|
+
const responsesById = this.collectInterruptResponses(
|
|
435
|
+
"submitInterruptResponses",
|
|
436
|
+
pending.interrupts,
|
|
437
|
+
responses,
|
|
438
|
+
);
|
|
310
439
|
|
|
311
440
|
const openIds = pending.interrupts.map((i) => i.id);
|
|
312
441
|
const missing = openIds.filter((id) => !responsesById.has(id));
|
|
@@ -315,13 +444,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
315
444
|
`[agui] submitInterruptResponses: missing responses for open interrupts: ${missing.join(", ")}`,
|
|
316
445
|
);
|
|
317
446
|
}
|
|
318
|
-
const known = new Set(openIds);
|
|
319
|
-
const unknownIds = [...responsesById.keys()].filter((id) => !known.has(id));
|
|
320
|
-
if (unknownIds.length > 0) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`[agui] submitInterruptResponses: unknown interrupt ids: ${unknownIds.join(", ")}`,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
447
|
|
|
326
448
|
const now = Date.now();
|
|
327
449
|
for (const interrupt of pending.interrupts) {
|
|
@@ -339,8 +461,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
339
461
|
}
|
|
340
462
|
}
|
|
341
463
|
|
|
342
|
-
const resume: AgUiResumeEntry[] = openIds.map(
|
|
343
|
-
|
|
464
|
+
const resume: AgUiResumeEntry[] = openIds.map((id) =>
|
|
465
|
+
responsesById.get(id)!,
|
|
344
466
|
);
|
|
345
467
|
|
|
346
468
|
if (this.isRunningFlag) {
|
|
@@ -359,6 +481,23 @@ export class AgUiThreadRuntimeCore {
|
|
|
359
481
|
): Promise<void> {
|
|
360
482
|
const pending = this.getPendingInterrupts();
|
|
361
483
|
if (!pending) {
|
|
484
|
+
const pendingTools = this.getPendingToolCalls();
|
|
485
|
+
if (pendingTools) {
|
|
486
|
+
if (responses?.length) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
"[agui] steerAway: responses are only valid for pending interrupts",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (this.isRunningFlag) {
|
|
492
|
+
throw new Error("[agui] steerAway: a run is already in progress");
|
|
493
|
+
}
|
|
494
|
+
this.cancelUnresolvedToolCalls(pendingTools.messageId);
|
|
495
|
+
this.maybeCompleteAfterToolResults(pendingTools.messageId);
|
|
496
|
+
const normalized = this.toAppendMessage(message);
|
|
497
|
+
const threadMessageId = this.appendEntry(normalized);
|
|
498
|
+
await this.startRun(threadMessageId, normalized.runConfig);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
362
501
|
if (responses?.length) {
|
|
363
502
|
throw new Error(
|
|
364
503
|
"[agui] steerAway: no pending interrupts on this thread",
|
|
@@ -375,9 +514,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
375
514
|
}
|
|
376
515
|
|
|
377
516
|
const normalized = this.toAppendMessage(message);
|
|
378
|
-
// Clear before appendEntry so the interrupted assistant is still in
|
|
379
|
-
// this.messages when its status is flipped; a parentId that points at an
|
|
380
|
-
// ancestor would otherwise truncate it away before it can be cleared.
|
|
381
517
|
this.clearPendingInterrupts(pending.messageId);
|
|
382
518
|
const threadMessageId = this.appendEntry(normalized);
|
|
383
519
|
await this.startRun(threadMessageId, normalized.runConfig, resume);
|
|
@@ -387,42 +523,58 @@ export class AgUiThreadRuntimeCore {
|
|
|
387
523
|
interrupts: readonly AgUiInterrupt[],
|
|
388
524
|
responses: readonly AgUiResumeEntry[] | undefined,
|
|
389
525
|
): AgUiResumeEntry[] {
|
|
390
|
-
const
|
|
391
|
-
|
|
526
|
+
const responsesById = this.collectInterruptResponses(
|
|
527
|
+
"steerAway",
|
|
528
|
+
interrupts,
|
|
529
|
+
responses ?? [],
|
|
530
|
+
);
|
|
531
|
+
return interrupts.map(
|
|
532
|
+
(interrupt) =>
|
|
533
|
+
responsesById.get(interrupt.id) ?? {
|
|
534
|
+
interruptId: interrupt.id,
|
|
535
|
+
status: "cancelled",
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private collectInterruptResponses(
|
|
541
|
+
method: "submitInterruptResponses" | "steerAway",
|
|
542
|
+
interrupts: readonly AgUiInterrupt[],
|
|
543
|
+
responses: readonly AgUiResumeEntry[],
|
|
544
|
+
): Map<string, AgUiResumeEntry> {
|
|
545
|
+
const known = new Set(interrupts.map((interrupt) => interrupt.id));
|
|
392
546
|
const responsesById = new Map<string, AgUiResumeEntry>();
|
|
393
|
-
for (const entry of responses
|
|
547
|
+
for (const entry of responses) {
|
|
394
548
|
if (!entry || typeof entry.interruptId !== "string") {
|
|
395
549
|
throw new Error(
|
|
396
|
-
|
|
550
|
+
`[agui] ${method}: every entry must have an interruptId`,
|
|
397
551
|
);
|
|
398
552
|
}
|
|
399
553
|
if (entry.status !== "resolved" && entry.status !== "cancelled") {
|
|
400
554
|
throw new Error(
|
|
401
|
-
`[agui]
|
|
555
|
+
`[agui] ${method}: invalid status "${entry.status}" for interrupt ${entry.interruptId}`,
|
|
402
556
|
);
|
|
403
557
|
}
|
|
404
558
|
if (!known.has(entry.interruptId)) {
|
|
405
559
|
throw new Error(
|
|
406
|
-
`[agui]
|
|
560
|
+
`[agui] ${method}: unknown interrupt id ${entry.interruptId}`,
|
|
407
561
|
);
|
|
408
562
|
}
|
|
409
563
|
if (responsesById.has(entry.interruptId)) {
|
|
410
564
|
throw new Error(
|
|
411
|
-
`[agui]
|
|
565
|
+
`[agui] ${method}: duplicate response for interrupt ${entry.interruptId}`,
|
|
412
566
|
);
|
|
413
567
|
}
|
|
414
568
|
responsesById.set(entry.interruptId, entry);
|
|
415
569
|
}
|
|
416
|
-
return
|
|
417
|
-
(id) => responsesById.get(id) ?? { interruptId: id, status: "cancelled" },
|
|
418
|
-
);
|
|
570
|
+
return responsesById;
|
|
419
571
|
}
|
|
420
572
|
|
|
421
573
|
private toAppendMessage(message: CreateAppendMessage): AppendMessage {
|
|
422
574
|
if (typeof message === "string") {
|
|
423
575
|
return {
|
|
424
576
|
createdAt: new Date(),
|
|
425
|
-
parentId: this.
|
|
577
|
+
parentId: this.repository.headId,
|
|
426
578
|
sourceId: null,
|
|
427
579
|
runConfig: {},
|
|
428
580
|
role: "user",
|
|
@@ -433,7 +585,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
433
585
|
}
|
|
434
586
|
return {
|
|
435
587
|
createdAt: message.createdAt ?? new Date(),
|
|
436
|
-
parentId: message.parentId ?? this.
|
|
588
|
+
parentId: message.parentId ?? this.repository.headId,
|
|
437
589
|
sourceId: message.sourceId ?? null,
|
|
438
590
|
role: message.role ?? "user",
|
|
439
591
|
content: message.content,
|
|
@@ -445,10 +597,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
445
597
|
}
|
|
446
598
|
|
|
447
599
|
private clearPendingInterrupts(messageId: string): void {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (message.id !== messageId || message.role !== "assistant")
|
|
451
|
-
return message;
|
|
600
|
+
const touched = this.updateMessage(messageId, (message) => {
|
|
601
|
+
if (message.role !== "assistant") return message;
|
|
452
602
|
const assistant = message as ThreadAssistantMessage;
|
|
453
603
|
if (
|
|
454
604
|
assistant.status?.type !== "requires-action" ||
|
|
@@ -456,7 +606,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
456
606
|
) {
|
|
457
607
|
return assistant;
|
|
458
608
|
}
|
|
459
|
-
touched = true;
|
|
460
609
|
const aguiMeta = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
461
610
|
| AgUiCustomMetadata
|
|
462
611
|
| undefined;
|
|
@@ -473,18 +622,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
473
622
|
metadata: { ...assistant.metadata, custom: newCustom },
|
|
474
623
|
};
|
|
475
624
|
});
|
|
476
|
-
if (touched)
|
|
625
|
+
if (touched) {
|
|
626
|
+
this.notifyUpdate();
|
|
627
|
+
}
|
|
477
628
|
}
|
|
478
629
|
|
|
479
630
|
findMessageIdForToolCall(toolCallId: string): string | undefined {
|
|
480
631
|
let fallbackMessageId: string | undefined;
|
|
481
|
-
|
|
482
|
-
|
|
632
|
+
const messages = this.getMessages();
|
|
633
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
634
|
+
const message = messages[index];
|
|
483
635
|
if (!message || message.role !== "assistant") continue;
|
|
484
636
|
for (const part of message.content) {
|
|
485
637
|
if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
|
|
486
638
|
continue;
|
|
487
|
-
if (!(
|
|
639
|
+
if (!isResolvedToolCall(part)) {
|
|
488
640
|
return message.id;
|
|
489
641
|
}
|
|
490
642
|
fallbackMessageId ??= message.id;
|
|
@@ -493,11 +645,26 @@ export class AgUiThreadRuntimeCore {
|
|
|
493
645
|
return fallbackMessageId;
|
|
494
646
|
}
|
|
495
647
|
|
|
648
|
+
private cancelUnresolvedToolCalls(messageId: string): void {
|
|
649
|
+
const updated = this.updateMessage(messageId, (message) => {
|
|
650
|
+
if (message.role !== "assistant") return message;
|
|
651
|
+
const assistant = message as ThreadAssistantMessage;
|
|
652
|
+
const content = assistant.content.map((part) => {
|
|
653
|
+
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
654
|
+
return {
|
|
655
|
+
...part,
|
|
656
|
+
result: { error: "Tool call cancelled by user" },
|
|
657
|
+
isError: true,
|
|
658
|
+
};
|
|
659
|
+
});
|
|
660
|
+
return { ...assistant, content };
|
|
661
|
+
});
|
|
662
|
+
if (updated) this.notifyUpdate();
|
|
663
|
+
}
|
|
664
|
+
|
|
496
665
|
addToolResult(options: AddToolResultOptions): void {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (message.id !== options.messageId || message.role !== "assistant")
|
|
500
|
-
return message;
|
|
666
|
+
const updated = this.updateMessage(options.messageId, (message) => {
|
|
667
|
+
if (message.role !== "assistant") return message;
|
|
501
668
|
const assistant = message as ThreadAssistantMessage;
|
|
502
669
|
let matchedToolCall = false;
|
|
503
670
|
const content = assistant.content.map((part) => {
|
|
@@ -512,7 +679,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
512
679
|
};
|
|
513
680
|
});
|
|
514
681
|
if (!matchedToolCall) return message;
|
|
515
|
-
updated = true;
|
|
516
682
|
return { ...assistant, content };
|
|
517
683
|
});
|
|
518
684
|
|
|
@@ -537,7 +703,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
537
703
|
}
|
|
538
704
|
|
|
539
705
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
540
|
-
const message = this.
|
|
706
|
+
const message = this.tryGetMessage(messageId)?.message;
|
|
541
707
|
if (!message || message.role !== "assistant") return false;
|
|
542
708
|
const assistant = message as ThreadAssistantMessage;
|
|
543
709
|
if (
|
|
@@ -547,20 +713,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
547
713
|
return false;
|
|
548
714
|
}
|
|
549
715
|
const allResolved = assistant.content.every(
|
|
550
|
-
(part) =>
|
|
551
|
-
part.type !== "tool-call" ||
|
|
552
|
-
("result" in part && part.result !== undefined),
|
|
716
|
+
(part) => part.type !== "tool-call" || isResolvedToolCall(part),
|
|
553
717
|
);
|
|
554
718
|
if (!allResolved) return false;
|
|
555
719
|
|
|
556
|
-
|
|
557
|
-
|
|
720
|
+
const updated = this.updateMessage(messageId, (current) =>
|
|
721
|
+
current.role === "assistant"
|
|
558
722
|
? {
|
|
559
|
-
...(
|
|
723
|
+
...(current as ThreadAssistantMessage),
|
|
560
724
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
561
725
|
}
|
|
562
|
-
:
|
|
726
|
+
: current,
|
|
563
727
|
);
|
|
728
|
+
if (!updated) return false;
|
|
564
729
|
this.notifyUpdate();
|
|
565
730
|
this.persistAssistantHistory(messageId);
|
|
566
731
|
return true;
|
|
@@ -574,9 +739,111 @@ export class AgUiThreadRuntimeCore {
|
|
|
574
739
|
|
|
575
740
|
applyExternalMessages(messages: readonly ThreadMessage[]): void {
|
|
576
741
|
this.assistantHistoryParents.clear();
|
|
577
|
-
|
|
742
|
+
|
|
743
|
+
if (messages.length === 0) {
|
|
744
|
+
this.clearRepository();
|
|
745
|
+
} else {
|
|
746
|
+
let expectedParentId: string | null = null;
|
|
747
|
+
let lastAppliedId: string | null = null;
|
|
748
|
+
let hardReplace = false;
|
|
749
|
+
const seen = new Set<string>();
|
|
750
|
+
|
|
751
|
+
for (const message of messages) {
|
|
752
|
+
if (seen.has(message.id)) continue;
|
|
753
|
+
seen.add(message.id);
|
|
754
|
+
const existing = this.tryGetMessage(message.id);
|
|
755
|
+
if (existing && existing.parentId !== expectedParentId) {
|
|
756
|
+
hardReplace = true;
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
this.addOrUpdateMessage(expectedParentId, message);
|
|
760
|
+
expectedParentId = message.id;
|
|
761
|
+
lastAppliedId = message.id;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (hardReplace) {
|
|
765
|
+
this.clearRepository();
|
|
766
|
+
expectedParentId = null;
|
|
767
|
+
lastAppliedId = null;
|
|
768
|
+
seen.clear();
|
|
769
|
+
for (const message of messages) {
|
|
770
|
+
if (seen.has(message.id)) continue;
|
|
771
|
+
seen.add(message.id);
|
|
772
|
+
this.addOrUpdateMessage(expectedParentId, message);
|
|
773
|
+
expectedParentId = message.id;
|
|
774
|
+
lastAppliedId = message.id;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
this.resetRepositoryHead(lastAppliedId);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
this.recordedHistoryIds.clear();
|
|
782
|
+
for (const { message } of this.getMessageRepository().messages) {
|
|
783
|
+
this.recordedHistoryIds.add(message.id);
|
|
784
|
+
}
|
|
785
|
+
this.notifyUpdate();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private applyExternalMessageRepository(
|
|
789
|
+
loaded: ExportedMessageRepository,
|
|
790
|
+
): void {
|
|
791
|
+
const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
|
|
792
|
+
const ids = new Set<string>();
|
|
793
|
+
let degenerate = false;
|
|
794
|
+
for (const { message } of loaded.messages) {
|
|
795
|
+
if (ids.has(message.id)) {
|
|
796
|
+
degenerate = true;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
ids.add(message.id);
|
|
800
|
+
}
|
|
801
|
+
if (headId !== null && !ids.has(headId)) degenerate = true;
|
|
802
|
+
|
|
803
|
+
if (!degenerate) {
|
|
804
|
+
this.clearRepository();
|
|
805
|
+
let pending = [...loaded.messages];
|
|
806
|
+
const importedIds = new Set<string>();
|
|
807
|
+
|
|
808
|
+
while (pending.length > 0) {
|
|
809
|
+
const unresolved: typeof pending = [];
|
|
810
|
+
let progressed = false;
|
|
811
|
+
for (const item of pending) {
|
|
812
|
+
if (item.parentId !== null && !importedIds.has(item.parentId)) {
|
|
813
|
+
unresolved.push(item);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
this.addOrUpdateMessage(item.parentId, item.message);
|
|
817
|
+
importedIds.add(item.message.id);
|
|
818
|
+
progressed = true;
|
|
819
|
+
}
|
|
820
|
+
if (!progressed) {
|
|
821
|
+
degenerate = true;
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
pending = unresolved;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (degenerate) {
|
|
829
|
+
this.clearRepository();
|
|
830
|
+
let previousId: string | null = null;
|
|
831
|
+
for (const { message } of loaded.messages) {
|
|
832
|
+
const existing = this.tryGetMessage(message.id);
|
|
833
|
+
this.addOrUpdateMessage(
|
|
834
|
+
existing ? existing.parentId : previousId,
|
|
835
|
+
message,
|
|
836
|
+
);
|
|
837
|
+
previousId = message.id;
|
|
838
|
+
}
|
|
839
|
+
this.resetRepositoryHead(previousId);
|
|
840
|
+
} else {
|
|
841
|
+
this.resetRepositoryHead(headId);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
this.assistantHistoryParents.clear();
|
|
578
845
|
this.recordedHistoryIds.clear();
|
|
579
|
-
for (const message of
|
|
846
|
+
for (const { message } of loaded.messages) {
|
|
580
847
|
this.recordedHistoryIds.add(message.id);
|
|
581
848
|
}
|
|
582
849
|
this.notifyUpdate();
|
|
@@ -587,6 +854,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
587
854
|
this.notifyUpdate();
|
|
588
855
|
}
|
|
589
856
|
|
|
857
|
+
setState(
|
|
858
|
+
next:
|
|
859
|
+
| ReadonlyJSONValue
|
|
860
|
+
| ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
|
|
861
|
+
): void {
|
|
862
|
+
this.stateSnapshot =
|
|
863
|
+
typeof next === "function" ? next(this.stateSnapshot) : next;
|
|
864
|
+
this.notifyUpdate();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
resetState(): void {
|
|
868
|
+
this.stateSnapshot = undefined;
|
|
869
|
+
this.notifyUpdate();
|
|
870
|
+
}
|
|
871
|
+
|
|
590
872
|
private async startRun(
|
|
591
873
|
parentId: string | null,
|
|
592
874
|
runConfig?: RunConfig,
|
|
@@ -595,20 +877,32 @@ export class AgUiThreadRuntimeCore {
|
|
|
595
877
|
): Promise<void> {
|
|
596
878
|
const normalizedRunConfig = runConfig ?? {};
|
|
597
879
|
this.lastRunConfig = normalizedRunConfig;
|
|
598
|
-
this.
|
|
599
|
-
const
|
|
880
|
+
const parent = parentId === null ? undefined : this.tryGetMessage(parentId);
|
|
881
|
+
const shouldEagerlyInsertAssistant =
|
|
882
|
+
parentId !== null &&
|
|
883
|
+
parent !== undefined &&
|
|
884
|
+
parentId !== this.repository.headId;
|
|
885
|
+
const historicalMessages = [
|
|
886
|
+
...(shouldEagerlyInsertAssistant
|
|
887
|
+
? (this.tryGetMessages(parentId) ?? this.repository.getMessages())
|
|
888
|
+
: this.repository.getMessages()),
|
|
889
|
+
];
|
|
600
890
|
|
|
601
891
|
this.pendingError = null;
|
|
602
|
-
const assistantParentId = parentId
|
|
892
|
+
const assistantParentId = parent ? parentId : this.repository.headId;
|
|
603
893
|
let assistantMessageId: string | undefined;
|
|
604
894
|
const ensureAssistant = () => {
|
|
605
895
|
if (assistantMessageId) return assistantMessageId;
|
|
606
|
-
const created = this.insertAssistantPlaceholder(
|
|
896
|
+
const created = this.insertAssistantPlaceholder(
|
|
897
|
+
assistantParentId ?? null,
|
|
898
|
+
);
|
|
607
899
|
assistantMessageId = created;
|
|
608
900
|
this.markPendingAssistantHistory(created, assistantParentId ?? null);
|
|
609
901
|
return created;
|
|
610
902
|
};
|
|
611
903
|
|
|
904
|
+
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
905
|
+
|
|
612
906
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
613
907
|
const resolved = this.updateAssistantMessage(ensureAssistant(), update);
|
|
614
908
|
if (resolved !== assistantMessageId) {
|
|
@@ -623,8 +917,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
623
917
|
onServerMessageId: (serverId) => {
|
|
624
918
|
const placeholder = ensureAssistant();
|
|
625
919
|
if (placeholder === serverId) return;
|
|
626
|
-
this.reassignAssistantId(placeholder, serverId)
|
|
627
|
-
|
|
920
|
+
if (this.reassignAssistantId(placeholder, serverId)) {
|
|
921
|
+
assistantMessageId = serverId;
|
|
922
|
+
}
|
|
628
923
|
},
|
|
629
924
|
});
|
|
630
925
|
const dispatch = (event: AgUiEvent) => this.handleEvent(aggregator, event);
|
|
@@ -744,7 +1039,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
744
1039
|
unstable_threadId: ctx.threadId,
|
|
745
1040
|
unstable_parentId: ctx.parentId,
|
|
746
1041
|
unstable_getMessage: () => {
|
|
747
|
-
const message = this.
|
|
1042
|
+
const message = this.tryGetMessage(currentId())?.message;
|
|
748
1043
|
if (!message) {
|
|
749
1044
|
throw new Error(
|
|
750
1045
|
"[agui] resume stream requested the assistant message before it existed",
|
|
@@ -771,7 +1066,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
771
1066
|
}
|
|
772
1067
|
|
|
773
1068
|
if (ctx.abortSignal.aborted) return;
|
|
774
|
-
const current = this.
|
|
1069
|
+
const current = this.tryGetMessage(currentId())?.message;
|
|
775
1070
|
if (!current || current.status?.type === "running") {
|
|
776
1071
|
ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
777
1072
|
}
|
|
@@ -784,7 +1079,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
784
1079
|
resume?: AgUiResumeEntry[],
|
|
785
1080
|
) {
|
|
786
1081
|
const threadId = this.agent.threadId || "main";
|
|
787
|
-
const messages = toAgUiMessages(
|
|
1082
|
+
const messages = toAgUiMessages(
|
|
1083
|
+
historyMessages ?? this.repository.getMessages(),
|
|
1084
|
+
);
|
|
788
1085
|
const context = this.runtime?.thread.getModelContext();
|
|
789
1086
|
return {
|
|
790
1087
|
threadId,
|
|
@@ -837,7 +1134,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
837
1134
|
this.setRunning(false);
|
|
838
1135
|
}
|
|
839
1136
|
|
|
840
|
-
private insertAssistantPlaceholder(): string {
|
|
1137
|
+
private insertAssistantPlaceholder(parentId: string | null): string {
|
|
841
1138
|
const id = generateOptimisticId();
|
|
842
1139
|
const assistant: ThreadAssistantMessage = {
|
|
843
1140
|
id,
|
|
@@ -854,56 +1151,62 @@ export class AgUiThreadRuntimeCore {
|
|
|
854
1151
|
custom: {},
|
|
855
1152
|
},
|
|
856
1153
|
};
|
|
857
|
-
this.
|
|
1154
|
+
this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
|
|
1155
|
+
this.switchToBranch(id);
|
|
858
1156
|
this.notifyUpdate();
|
|
859
1157
|
return id;
|
|
860
1158
|
}
|
|
861
1159
|
|
|
862
|
-
private reassignAssistantId(oldId: string, newId: string):
|
|
863
|
-
if (oldId === newId) return;
|
|
1160
|
+
private reassignAssistantId(oldId: string, newId: string): boolean {
|
|
1161
|
+
if (oldId === newId) return true;
|
|
1162
|
+
const oldItem = this.tryGetMessage(oldId);
|
|
1163
|
+
if (!oldItem) return false;
|
|
864
1164
|
|
|
865
|
-
const collidesWithExisting = this.
|
|
1165
|
+
const collidesWithExisting = this.hasMessage(newId);
|
|
866
1166
|
|
|
867
1167
|
if (collidesWithExisting) {
|
|
868
1168
|
this.logger.debug?.(
|
|
869
|
-
"[agui] reassignAssistantId: server id already present in messages, dropping placeholder",
|
|
1169
|
+
"[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder",
|
|
870
1170
|
{ oldId, newId },
|
|
871
1171
|
);
|
|
872
|
-
this.
|
|
1172
|
+
this.deleteMessage(oldId, oldItem.parentId ?? null);
|
|
873
1173
|
} else {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1174
|
+
const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
|
|
1175
|
+
this.addOrUpdateMessage(oldItem.parentId, {
|
|
1176
|
+
...oldItem.message,
|
|
1177
|
+
id: newId,
|
|
1178
|
+
metadata,
|
|
1179
|
+
} as ThreadMessage);
|
|
1180
|
+
this.switchToBranch(newId);
|
|
1181
|
+
this.tryDeleteMessage(oldId);
|
|
879
1182
|
}
|
|
880
1183
|
|
|
881
1184
|
const pendingParent = this.assistantHistoryParents.get(oldId);
|
|
882
1185
|
if (pendingParent !== undefined) {
|
|
883
1186
|
this.assistantHistoryParents.delete(oldId);
|
|
884
|
-
if (!this.assistantHistoryParents.has(newId)) {
|
|
1187
|
+
if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) {
|
|
885
1188
|
this.assistantHistoryParents.set(newId, pendingParent);
|
|
886
1189
|
}
|
|
887
1190
|
}
|
|
888
1191
|
|
|
889
1192
|
if (this.recordedHistoryIds.has(oldId)) {
|
|
890
1193
|
this.recordedHistoryIds.delete(oldId);
|
|
891
|
-
|
|
1194
|
+
if (!collidesWithExisting) {
|
|
1195
|
+
this.recordedHistoryIds.add(newId);
|
|
1196
|
+
}
|
|
892
1197
|
}
|
|
893
1198
|
|
|
894
1199
|
this.notifyUpdate();
|
|
1200
|
+
return !collidesWithExisting;
|
|
895
1201
|
}
|
|
896
1202
|
|
|
897
1203
|
private updateAssistantMessage(
|
|
898
1204
|
messageId: string,
|
|
899
1205
|
update: ChatModelRunResult,
|
|
900
1206
|
): string {
|
|
901
|
-
let touched = false;
|
|
902
1207
|
let latestStatus: MessageStatus | undefined;
|
|
903
|
-
|
|
904
|
-
if (message.
|
|
905
|
-
return message;
|
|
906
|
-
touched = true;
|
|
1208
|
+
const touched = this.updateMessage(messageId, (message) => {
|
|
1209
|
+
if (message.role !== "assistant") return message;
|
|
907
1210
|
const assistant = message as ThreadAssistantMessage;
|
|
908
1211
|
const metadata = update.metadata
|
|
909
1212
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
@@ -952,11 +1255,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
952
1255
|
): ThreadAssistantMessage["content"] {
|
|
953
1256
|
const resolved = new Map<string, ToolCallMessagePart>();
|
|
954
1257
|
for (const part of previous) {
|
|
955
|
-
if (
|
|
956
|
-
part.type === "tool-call" &&
|
|
957
|
-
"result" in part &&
|
|
958
|
-
part.result !== undefined
|
|
959
|
-
) {
|
|
1258
|
+
if (part.type === "tool-call" && isResolvedToolCall(part)) {
|
|
960
1259
|
resolved.set(part.toolCallId, part);
|
|
961
1260
|
}
|
|
962
1261
|
}
|
|
@@ -964,8 +1263,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
964
1263
|
|
|
965
1264
|
let changed = false;
|
|
966
1265
|
const merged = next.map((part) => {
|
|
967
|
-
if (part.type !== "tool-call") return part;
|
|
968
|
-
if ("result" in part && part.result !== undefined) return part;
|
|
1266
|
+
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
969
1267
|
const prior = resolved.get(part.toolCallId);
|
|
970
1268
|
if (!prior) return part;
|
|
971
1269
|
changed = true;
|
|
@@ -1047,6 +1345,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
1047
1345
|
aggregator.handle(event);
|
|
1048
1346
|
return;
|
|
1049
1347
|
}
|
|
1348
|
+
case "ACTIVITY_SNAPSHOT": {
|
|
1349
|
+
const toolCallId = event.content["toolCallId"];
|
|
1350
|
+
if (
|
|
1351
|
+
event.activityType === MCP_APPS_ACTIVITY_TYPE &&
|
|
1352
|
+
typeof toolCallId === "string" &&
|
|
1353
|
+
!aggregator.hasToolCall(toolCallId)
|
|
1354
|
+
) {
|
|
1355
|
+
const messageId = this.findMessageIdForToolCall(toolCallId);
|
|
1356
|
+
if (messageId !== undefined) {
|
|
1357
|
+
this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
aggregator.handle(event);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1050
1364
|
default:
|
|
1051
1365
|
aggregator.handle(event);
|
|
1052
1366
|
}
|
|
@@ -1056,27 +1370,63 @@ export class AgUiThreadRuntimeCore {
|
|
|
1056
1370
|
messageId: string,
|
|
1057
1371
|
event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
|
|
1058
1372
|
): void {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (message.id !== messageId || message.role !== "assistant")
|
|
1062
|
-
return message;
|
|
1373
|
+
const updated = this.updateMessage(messageId, (message) => {
|
|
1374
|
+
if (message.role !== "assistant") return message;
|
|
1063
1375
|
const assistant = message as ThreadAssistantMessage;
|
|
1064
1376
|
let matchedToolCall = false;
|
|
1065
1377
|
const content = assistant.content.map((part) => {
|
|
1066
1378
|
if (part.type !== "tool-call" || part.toolCallId !== event.toolCallId)
|
|
1067
1379
|
return part;
|
|
1068
1380
|
matchedToolCall = true;
|
|
1381
|
+
// An applied activity snapshot owns part.result; a later result only
|
|
1382
|
+
// fills what is missing, mirroring the aggregator's finishToolCall.
|
|
1383
|
+
// The aggregator's flag is set only when the snapshot carried a
|
|
1384
|
+
// result, so app presence alone is not enough: the current result
|
|
1385
|
+
// must be CallToolResult-shaped (a required content array).
|
|
1386
|
+
const snapshotResultApplied =
|
|
1387
|
+
part.mcp?.app !== undefined &&
|
|
1388
|
+
isPlainObject(part.result) &&
|
|
1389
|
+
Array.isArray((part.result as Record<string, unknown>)["content"]);
|
|
1390
|
+
if (snapshotResultApplied) {
|
|
1391
|
+
return {
|
|
1392
|
+
...part,
|
|
1393
|
+
...(part.modelContent === undefined && event.content
|
|
1394
|
+
? {
|
|
1395
|
+
modelContent: [
|
|
1396
|
+
{ type: "text" as const, text: event.content },
|
|
1397
|
+
],
|
|
1398
|
+
}
|
|
1399
|
+
: {}),
|
|
1400
|
+
...(part.isError === undefined
|
|
1401
|
+
? typeof event.mcpResult?.isError === "boolean"
|
|
1402
|
+
? { isError: event.mcpResult.isError }
|
|
1403
|
+
: event.role === "tool"
|
|
1404
|
+
? { isError: false }
|
|
1405
|
+
: {}
|
|
1406
|
+
: {}),
|
|
1407
|
+
...(event.messageId
|
|
1408
|
+
? { unstable_toolMessageId: event.messageId }
|
|
1409
|
+
: {}),
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1069
1412
|
return {
|
|
1070
1413
|
...part,
|
|
1071
|
-
result:
|
|
1072
|
-
|
|
1414
|
+
result: (event.mcpResult ??
|
|
1415
|
+
tryParseJSON(event.content ?? "")) as ReadonlyJSONValue,
|
|
1416
|
+
...(event.mcpResult !== undefined
|
|
1417
|
+
? { modelContent: [{ type: "text" as const, text: event.content }] }
|
|
1418
|
+
: {}),
|
|
1419
|
+
...(typeof event.mcpResult?.isError === "boolean"
|
|
1420
|
+
? { isError: event.mcpResult.isError }
|
|
1421
|
+
: event.role === "tool"
|
|
1422
|
+
? { isError: false }
|
|
1423
|
+
: {}),
|
|
1073
1424
|
...(event.messageId
|
|
1074
1425
|
? { unstable_toolMessageId: event.messageId }
|
|
1075
1426
|
: {}),
|
|
1076
1427
|
};
|
|
1077
1428
|
});
|
|
1078
1429
|
if (!matchedToolCall) return message;
|
|
1079
|
-
updated = true;
|
|
1080
1430
|
return { ...assistant, content };
|
|
1081
1431
|
});
|
|
1082
1432
|
|
|
@@ -1087,6 +1437,68 @@ export class AgUiThreadRuntimeCore {
|
|
|
1087
1437
|
this.maybeCompleteAfterToolResults(messageId);
|
|
1088
1438
|
}
|
|
1089
1439
|
|
|
1440
|
+
private applyCrossRunActivitySnapshot(
|
|
1441
|
+
messageId: string,
|
|
1442
|
+
toolCallId: string,
|
|
1443
|
+
event: Extract<AgUiEvent, { type: "ACTIVITY_SNAPSHOT" }>,
|
|
1444
|
+
): void {
|
|
1445
|
+
const resourceUri = event.content["resourceUri"];
|
|
1446
|
+
if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) return;
|
|
1447
|
+
const serverId = event.content["serverId"];
|
|
1448
|
+
const serverHash = event.content["serverHash"];
|
|
1449
|
+
const appServerId =
|
|
1450
|
+
typeof serverId === "string" && serverId.length > 0
|
|
1451
|
+
? serverId
|
|
1452
|
+
: typeof serverHash === "string" && serverHash.length > 0
|
|
1453
|
+
? serverHash
|
|
1454
|
+
: undefined;
|
|
1455
|
+
const result = event.content["result"];
|
|
1456
|
+
const updated = this.updateMessage(messageId, (message) => {
|
|
1457
|
+
if (message.role !== "assistant") return message;
|
|
1458
|
+
const assistant = message as ThreadAssistantMessage;
|
|
1459
|
+
let matchedToolCall = false;
|
|
1460
|
+
const content = assistant.content.map((part) => {
|
|
1461
|
+
if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
|
|
1462
|
+
return part;
|
|
1463
|
+
matchedToolCall = true;
|
|
1464
|
+
return {
|
|
1465
|
+
...part,
|
|
1466
|
+
mcp: {
|
|
1467
|
+
app: {
|
|
1468
|
+
resourceUri,
|
|
1469
|
+
...(appServerId ? { serverId: appServerId } : {}),
|
|
1470
|
+
},
|
|
1471
|
+
},
|
|
1472
|
+
...(isPlainObject(result)
|
|
1473
|
+
? {
|
|
1474
|
+
...(part.result !== undefined && part.modelContent === undefined
|
|
1475
|
+
? {
|
|
1476
|
+
modelContent: [
|
|
1477
|
+
{
|
|
1478
|
+
type: "text" as const,
|
|
1479
|
+
text:
|
|
1480
|
+
typeof part.result === "string"
|
|
1481
|
+
? part.result
|
|
1482
|
+
: JSON.stringify(part.result),
|
|
1483
|
+
},
|
|
1484
|
+
],
|
|
1485
|
+
}
|
|
1486
|
+
: {}),
|
|
1487
|
+
result: result as ReadonlyJSONValue,
|
|
1488
|
+
isError: result["isError"] === true,
|
|
1489
|
+
}
|
|
1490
|
+
: {}),
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
if (!matchedToolCall) return message;
|
|
1494
|
+
return { ...assistant, content };
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
if (!updated) return;
|
|
1498
|
+
this.notifyUpdate();
|
|
1499
|
+
this.maybeCompleteAfterToolResults(messageId);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1090
1502
|
private importMessagesSnapshot(rawMessages: readonly unknown[]) {
|
|
1091
1503
|
try {
|
|
1092
1504
|
const normalized = fromAgUiMessages(rawMessages, {
|
|
@@ -1119,18 +1531,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1119
1531
|
);
|
|
1120
1532
|
}
|
|
1121
1533
|
|
|
1122
|
-
private resetHead(parentId: string | null | undefined) {
|
|
1123
|
-
if (!parentId) {
|
|
1124
|
-
if (this.messages.length) {
|
|
1125
|
-
this.messages = [];
|
|
1126
|
-
}
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
const idx = this.messages.findIndex((message) => message.id === parentId);
|
|
1130
|
-
if (idx === -1) return;
|
|
1131
|
-
this.messages = this.messages.slice(0, idx + 1);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
1534
|
private isTerminalStatus(status?: MessageStatus): boolean {
|
|
1135
1535
|
return status?.type === "complete" || status?.type === "incomplete";
|
|
1136
1536
|
}
|
|
@@ -1156,7 +1556,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1156
1556
|
if (!this.history) return;
|
|
1157
1557
|
const parentId = this.assistantHistoryParents.get(messageId);
|
|
1158
1558
|
if (parentId === undefined) return;
|
|
1159
|
-
const message = this.
|
|
1559
|
+
const message = this.tryGetMessage(messageId)?.message;
|
|
1160
1560
|
if (!message || message.role !== "assistant") return;
|
|
1161
1561
|
if (!this.isPersistableStatus(message.status)) return;
|
|
1162
1562
|
this.assistantHistoryParents.delete(messageId);
|