@assistant-ui/react-ag-ui 0.0.43 → 0.0.45
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 +26 -4
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +296 -89
- 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 +24 -5
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +0 -1
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +36 -7
- 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 +18 -9
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/logger.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +13 -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 +6 -6
- package/src/hooks.ts +29 -7
- package/src/index.ts +2 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +390 -108
- package/src/runtime/adapter/conversions.ts +71 -15
- package/src/runtime/adapter/run-aggregator.ts +69 -5
- package/src/runtime/types.ts +17 -0
- package/src/useAgUiRuntime.ts +8 -1
|
@@ -8,12 +8,14 @@ import type {
|
|
|
8
8
|
AssistantRuntime,
|
|
9
9
|
ChatModelRunOptions,
|
|
10
10
|
ChatModelRunResult,
|
|
11
|
+
ExportedMessageRepository,
|
|
11
12
|
MessageStatus,
|
|
12
13
|
ThreadAssistantMessage,
|
|
13
14
|
ThreadHistoryAdapter,
|
|
14
15
|
ThreadMessage,
|
|
15
16
|
ToolCallMessagePart,
|
|
16
17
|
} from "@assistant-ui/core";
|
|
18
|
+
import { MessageRepository } from "@assistant-ui/core/internal";
|
|
17
19
|
import type { AbstractAgent } from "@ag-ui/client";
|
|
18
20
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
19
21
|
import type { Logger } from "./logger";
|
|
@@ -36,6 +38,11 @@ const optimisticPrefix = "__optimistic__";
|
|
|
36
38
|
const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
|
|
37
39
|
const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);
|
|
38
40
|
|
|
41
|
+
const isResolvedToolCall = (
|
|
42
|
+
part: ThreadAssistantMessage["content"][number],
|
|
43
|
+
): boolean =>
|
|
44
|
+
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
45
|
+
|
|
39
46
|
const symbolResumeShim = Symbol("agui-resume-shim");
|
|
40
47
|
|
|
41
48
|
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
@@ -53,6 +60,7 @@ type CoreOptions = {
|
|
|
53
60
|
agent: AbstractAgent;
|
|
54
61
|
logger: Logger;
|
|
55
62
|
showThinking: boolean;
|
|
63
|
+
autoCancelPendingToolCalls?: boolean | undefined;
|
|
56
64
|
onError?: (error: Error) => void;
|
|
57
65
|
onCancel?: () => void;
|
|
58
66
|
history?: ThreadHistoryAdapter;
|
|
@@ -65,12 +73,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
65
73
|
private agent: AbstractAgent;
|
|
66
74
|
private logger: Logger;
|
|
67
75
|
private showThinking: boolean;
|
|
76
|
+
private autoCancelPendingToolCalls: boolean | undefined;
|
|
68
77
|
private onError: ((error: Error) => void) | undefined;
|
|
69
78
|
private onCancel: (() => void) | undefined;
|
|
70
79
|
private readonly notifyUpdate: () => void;
|
|
71
80
|
|
|
72
81
|
private runtime: AssistantRuntime | undefined;
|
|
73
|
-
private
|
|
82
|
+
private readonly repository = new MessageRepository();
|
|
83
|
+
private exportedRepository: ExportedMessageRepository | undefined;
|
|
74
84
|
private isRunningFlag = false;
|
|
75
85
|
private abortController: AbortController | null = null;
|
|
76
86
|
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
@@ -87,6 +97,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
87
97
|
this.agent = options.agent;
|
|
88
98
|
this.logger = options.logger;
|
|
89
99
|
this.showThinking = options.showThinking;
|
|
100
|
+
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
90
101
|
this.onError = options.onError;
|
|
91
102
|
this.onCancel = options.onCancel;
|
|
92
103
|
this.history = options.history;
|
|
@@ -98,6 +109,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
98
109
|
this.agent = options.agent;
|
|
99
110
|
this.logger = options.logger;
|
|
100
111
|
this.showThinking = options.showThinking;
|
|
112
|
+
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
101
113
|
this.onError = options.onError;
|
|
102
114
|
this.onCancel = options.onCancel;
|
|
103
115
|
this.history = options.history;
|
|
@@ -113,7 +125,97 @@ export class AgUiThreadRuntimeCore {
|
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
getMessages(): readonly ThreadMessage[] {
|
|
116
|
-
return this.
|
|
128
|
+
return this.repository.getMessages();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getMessageRepository(): ExportedMessageRepository {
|
|
132
|
+
if (this.exportedRepository) return this.exportedRepository;
|
|
133
|
+
|
|
134
|
+
const exportedRepository = this.repository.export();
|
|
135
|
+
let parentId: string | null = null;
|
|
136
|
+
for (const message of this.repository.getMessages()) {
|
|
137
|
+
if (message.metadata.isOptimistic) {
|
|
138
|
+
exportedRepository.messages.push({ parentId, message });
|
|
139
|
+
}
|
|
140
|
+
parentId = message.id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.exportedRepository = {
|
|
144
|
+
...exportedRepository,
|
|
145
|
+
headId: this.repository.headId,
|
|
146
|
+
};
|
|
147
|
+
return this.exportedRepository;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private tryGetMessage(messageId: string) {
|
|
151
|
+
try {
|
|
152
|
+
return this.repository.getMessage(messageId);
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private tryGetMessages(
|
|
159
|
+
messageId: string,
|
|
160
|
+
): readonly ThreadMessage[] | undefined {
|
|
161
|
+
try {
|
|
162
|
+
return this.repository.getMessages(messageId);
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private hasMessage(messageId: string): boolean {
|
|
169
|
+
return this.tryGetMessage(messageId) !== undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private addOrUpdateMessage(
|
|
173
|
+
parentId: string | null,
|
|
174
|
+
message: ThreadMessage,
|
|
175
|
+
): void {
|
|
176
|
+
this.repository.addOrUpdateMessage(parentId, message);
|
|
177
|
+
this.exportedRepository = undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private deleteMessage(
|
|
181
|
+
messageId: string,
|
|
182
|
+
replacementId?: string | null,
|
|
183
|
+
): void {
|
|
184
|
+
this.repository.deleteMessage(messageId, replacementId);
|
|
185
|
+
this.exportedRepository = undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private tryDeleteMessage(messageId: string): boolean {
|
|
189
|
+
if (!this.hasMessage(messageId)) return false;
|
|
190
|
+
this.deleteMessage(messageId);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private switchToBranch(messageId: string): void {
|
|
195
|
+
this.repository.switchToBranch(messageId);
|
|
196
|
+
this.exportedRepository = undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private resetRepositoryHead(messageId: string | null): void {
|
|
200
|
+
this.repository.resetHead(messageId);
|
|
201
|
+
this.exportedRepository = undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private clearRepository(): void {
|
|
205
|
+
this.repository.clear();
|
|
206
|
+
this.exportedRepository = undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private updateMessage(
|
|
210
|
+
messageId: string,
|
|
211
|
+
updater: (message: ThreadMessage) => ThreadMessage,
|
|
212
|
+
): boolean {
|
|
213
|
+
const item = this.tryGetMessage(messageId);
|
|
214
|
+
if (!item) return false;
|
|
215
|
+
const message = updater(item.message);
|
|
216
|
+
if (message === item.message) return false;
|
|
217
|
+
this.addOrUpdateMessage(item.parentId, message);
|
|
218
|
+
return true;
|
|
117
219
|
}
|
|
118
220
|
|
|
119
221
|
getState(): ReadonlyJSONValue | undefined {
|
|
@@ -139,15 +241,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
139
241
|
.then(async (repo) => {
|
|
140
242
|
if (!repo) return;
|
|
141
243
|
|
|
142
|
-
|
|
143
|
-
this.applyExternalMessages(messages);
|
|
244
|
+
this.applyExternalMessageRepository(repo);
|
|
144
245
|
|
|
145
246
|
if (repo.state !== undefined) {
|
|
146
247
|
this.loadExternalState(repo.state);
|
|
147
248
|
}
|
|
148
249
|
|
|
149
250
|
if (repo.unstable_resume) {
|
|
150
|
-
const parentId = repo.headId ??
|
|
251
|
+
const parentId = repo.headId ?? this.repository.headId;
|
|
151
252
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
152
253
|
await this.startRun(
|
|
153
254
|
parentId,
|
|
@@ -174,24 +275,37 @@ export class AgUiThreadRuntimeCore {
|
|
|
174
275
|
|
|
175
276
|
async append(message: AppendMessage): Promise<void> {
|
|
176
277
|
const startRun = message.startRun ?? message.role === "user";
|
|
177
|
-
if (startRun)
|
|
278
|
+
if (startRun) {
|
|
279
|
+
this.assertNoPendingInterrupts();
|
|
280
|
+
this.maybeAutoCancelPendingToolCalls();
|
|
281
|
+
}
|
|
178
282
|
const threadMessageId = this.appendEntry(message);
|
|
179
283
|
if (!startRun) return;
|
|
180
284
|
await this.startRun(threadMessageId, message.runConfig);
|
|
181
285
|
}
|
|
182
286
|
|
|
287
|
+
private maybeAutoCancelPendingToolCalls(): void {
|
|
288
|
+
if (this.autoCancelPendingToolCalls === false) return;
|
|
289
|
+
const pending = this.getPendingToolCalls();
|
|
290
|
+
if (!pending) return;
|
|
291
|
+
this.cancelUnresolvedToolCalls(pending.messageId);
|
|
292
|
+
this.maybeCompleteAfterToolResults(pending.messageId);
|
|
293
|
+
}
|
|
294
|
+
|
|
183
295
|
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);
|
|
296
|
+
if (message.sourceId) this.tryDeleteMessage(message.sourceId);
|
|
190
297
|
|
|
191
298
|
const threadMessage = this.toThreadMessage(message);
|
|
192
|
-
|
|
299
|
+
const parentId =
|
|
300
|
+
message.parentId === null
|
|
301
|
+
? null
|
|
302
|
+
: message.parentId && this.hasMessage(message.parentId)
|
|
303
|
+
? message.parentId
|
|
304
|
+
: this.repository.headId;
|
|
305
|
+
this.addOrUpdateMessage(parentId, threadMessage);
|
|
306
|
+
this.switchToBranch(threadMessage.id);
|
|
193
307
|
this.notifyUpdate();
|
|
194
|
-
this.recordHistoryEntry(
|
|
308
|
+
this.recordHistoryEntry(parentId, threadMessage);
|
|
195
309
|
return threadMessage.id;
|
|
196
310
|
}
|
|
197
311
|
|
|
@@ -204,8 +318,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
204
318
|
config: { runConfig?: RunConfig } = {},
|
|
205
319
|
): Promise<void> {
|
|
206
320
|
this.assertNoPendingInterrupts();
|
|
207
|
-
this.
|
|
208
|
-
this.notifyUpdate();
|
|
321
|
+
this.maybeAutoCancelPendingToolCalls();
|
|
209
322
|
await this.startRun(parentId, config.runConfig);
|
|
210
323
|
}
|
|
211
324
|
|
|
@@ -255,20 +368,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
255
368
|
);
|
|
256
369
|
}
|
|
257
370
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
| undefined;
|
|
371
|
+
private findRequiresActionAssistant(
|
|
372
|
+
reason: "interrupt" | "tool-calls",
|
|
373
|
+
): ThreadAssistantMessage | null {
|
|
374
|
+
const assistant = this.getMessages().findLast(
|
|
375
|
+
(message) => message.role === "assistant",
|
|
376
|
+
) as ThreadAssistantMessage | undefined;
|
|
265
377
|
if (
|
|
266
378
|
!assistant ||
|
|
267
379
|
assistant.status?.type !== "requires-action" ||
|
|
268
|
-
assistant.status.reason !==
|
|
380
|
+
assistant.status.reason !== reason
|
|
269
381
|
) {
|
|
270
382
|
return null;
|
|
271
383
|
}
|
|
384
|
+
return assistant;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getPendingInterrupts(): {
|
|
388
|
+
messageId: string;
|
|
389
|
+
interrupts: readonly AgUiInterrupt[];
|
|
390
|
+
} | null {
|
|
391
|
+
const assistant = this.findRequiresActionAssistant("interrupt");
|
|
392
|
+
if (!assistant) return null;
|
|
272
393
|
const stored = (
|
|
273
394
|
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
274
395
|
| AgUiCustomMetadata
|
|
@@ -278,6 +399,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
278
399
|
return { messageId: assistant.id, interrupts: stored };
|
|
279
400
|
}
|
|
280
401
|
|
|
402
|
+
getPendingToolCalls(): {
|
|
403
|
+
messageId: string;
|
|
404
|
+
toolCallIds: string[];
|
|
405
|
+
} | null {
|
|
406
|
+
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
407
|
+
if (!assistant) return null;
|
|
408
|
+
const toolCallIds: string[] = [];
|
|
409
|
+
for (const part of assistant.content) {
|
|
410
|
+
if (part.type !== "tool-call") continue;
|
|
411
|
+
if (isResolvedToolCall(part)) continue;
|
|
412
|
+
toolCallIds.push(part.toolCallId);
|
|
413
|
+
}
|
|
414
|
+
if (toolCallIds.length === 0) return null;
|
|
415
|
+
return { messageId: assistant.id, toolCallIds };
|
|
416
|
+
}
|
|
417
|
+
|
|
281
418
|
async submitInterruptResponses(
|
|
282
419
|
responses: readonly AgUiResumeEntry[],
|
|
283
420
|
): Promise<void> {
|
|
@@ -339,8 +476,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
339
476
|
}
|
|
340
477
|
}
|
|
341
478
|
|
|
342
|
-
const resume: AgUiResumeEntry[] = openIds.map(
|
|
343
|
-
|
|
479
|
+
const resume: AgUiResumeEntry[] = openIds.map((id) =>
|
|
480
|
+
responsesById.get(id)!,
|
|
344
481
|
);
|
|
345
482
|
|
|
346
483
|
if (this.isRunningFlag) {
|
|
@@ -359,6 +496,23 @@ export class AgUiThreadRuntimeCore {
|
|
|
359
496
|
): Promise<void> {
|
|
360
497
|
const pending = this.getPendingInterrupts();
|
|
361
498
|
if (!pending) {
|
|
499
|
+
const pendingTools = this.getPendingToolCalls();
|
|
500
|
+
if (pendingTools) {
|
|
501
|
+
if (responses?.length) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"[agui] steerAway: responses are only valid for pending interrupts",
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (this.isRunningFlag) {
|
|
507
|
+
throw new Error("[agui] steerAway: a run is already in progress");
|
|
508
|
+
}
|
|
509
|
+
this.cancelUnresolvedToolCalls(pendingTools.messageId);
|
|
510
|
+
this.maybeCompleteAfterToolResults(pendingTools.messageId);
|
|
511
|
+
const normalized = this.toAppendMessage(message);
|
|
512
|
+
const threadMessageId = this.appendEntry(normalized);
|
|
513
|
+
await this.startRun(threadMessageId, normalized.runConfig);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
362
516
|
if (responses?.length) {
|
|
363
517
|
throw new Error(
|
|
364
518
|
"[agui] steerAway: no pending interrupts on this thread",
|
|
@@ -375,9 +529,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
375
529
|
}
|
|
376
530
|
|
|
377
531
|
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
532
|
this.clearPendingInterrupts(pending.messageId);
|
|
382
533
|
const threadMessageId = this.appendEntry(normalized);
|
|
383
534
|
await this.startRun(threadMessageId, normalized.runConfig, resume);
|
|
@@ -422,7 +573,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
422
573
|
if (typeof message === "string") {
|
|
423
574
|
return {
|
|
424
575
|
createdAt: new Date(),
|
|
425
|
-
parentId: this.
|
|
576
|
+
parentId: this.repository.headId,
|
|
426
577
|
sourceId: null,
|
|
427
578
|
runConfig: {},
|
|
428
579
|
role: "user",
|
|
@@ -433,7 +584,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
433
584
|
}
|
|
434
585
|
return {
|
|
435
586
|
createdAt: message.createdAt ?? new Date(),
|
|
436
|
-
parentId: message.parentId ?? this.
|
|
587
|
+
parentId: message.parentId ?? this.repository.headId,
|
|
437
588
|
sourceId: message.sourceId ?? null,
|
|
438
589
|
role: message.role ?? "user",
|
|
439
590
|
content: message.content,
|
|
@@ -445,10 +596,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
445
596
|
}
|
|
446
597
|
|
|
447
598
|
private clearPendingInterrupts(messageId: string): void {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (message.id !== messageId || message.role !== "assistant")
|
|
451
|
-
return message;
|
|
599
|
+
const touched = this.updateMessage(messageId, (message) => {
|
|
600
|
+
if (message.role !== "assistant") return message;
|
|
452
601
|
const assistant = message as ThreadAssistantMessage;
|
|
453
602
|
if (
|
|
454
603
|
assistant.status?.type !== "requires-action" ||
|
|
@@ -456,7 +605,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
456
605
|
) {
|
|
457
606
|
return assistant;
|
|
458
607
|
}
|
|
459
|
-
touched = true;
|
|
460
608
|
const aguiMeta = assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
461
609
|
| AgUiCustomMetadata
|
|
462
610
|
| undefined;
|
|
@@ -473,18 +621,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
473
621
|
metadata: { ...assistant.metadata, custom: newCustom },
|
|
474
622
|
};
|
|
475
623
|
});
|
|
476
|
-
if (touched)
|
|
624
|
+
if (touched) {
|
|
625
|
+
this.notifyUpdate();
|
|
626
|
+
}
|
|
477
627
|
}
|
|
478
628
|
|
|
479
629
|
findMessageIdForToolCall(toolCallId: string): string | undefined {
|
|
480
630
|
let fallbackMessageId: string | undefined;
|
|
481
|
-
|
|
482
|
-
|
|
631
|
+
const messages = this.getMessages();
|
|
632
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
633
|
+
const message = messages[index];
|
|
483
634
|
if (!message || message.role !== "assistant") continue;
|
|
484
635
|
for (const part of message.content) {
|
|
485
636
|
if (part.type !== "tool-call" || part.toolCallId !== toolCallId)
|
|
486
637
|
continue;
|
|
487
|
-
if (!(
|
|
638
|
+
if (!isResolvedToolCall(part)) {
|
|
488
639
|
return message.id;
|
|
489
640
|
}
|
|
490
641
|
fallbackMessageId ??= message.id;
|
|
@@ -493,11 +644,26 @@ export class AgUiThreadRuntimeCore {
|
|
|
493
644
|
return fallbackMessageId;
|
|
494
645
|
}
|
|
495
646
|
|
|
647
|
+
private cancelUnresolvedToolCalls(messageId: string): void {
|
|
648
|
+
const updated = this.updateMessage(messageId, (message) => {
|
|
649
|
+
if (message.role !== "assistant") return message;
|
|
650
|
+
const assistant = message as ThreadAssistantMessage;
|
|
651
|
+
const content = assistant.content.map((part) => {
|
|
652
|
+
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
653
|
+
return {
|
|
654
|
+
...part,
|
|
655
|
+
result: { error: "Tool call cancelled by user" },
|
|
656
|
+
isError: true,
|
|
657
|
+
};
|
|
658
|
+
});
|
|
659
|
+
return { ...assistant, content };
|
|
660
|
+
});
|
|
661
|
+
if (updated) this.notifyUpdate();
|
|
662
|
+
}
|
|
663
|
+
|
|
496
664
|
addToolResult(options: AddToolResultOptions): void {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (message.id !== options.messageId || message.role !== "assistant")
|
|
500
|
-
return message;
|
|
665
|
+
const updated = this.updateMessage(options.messageId, (message) => {
|
|
666
|
+
if (message.role !== "assistant") return message;
|
|
501
667
|
const assistant = message as ThreadAssistantMessage;
|
|
502
668
|
let matchedToolCall = false;
|
|
503
669
|
const content = assistant.content.map((part) => {
|
|
@@ -512,7 +678,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
512
678
|
};
|
|
513
679
|
});
|
|
514
680
|
if (!matchedToolCall) return message;
|
|
515
|
-
updated = true;
|
|
516
681
|
return { ...assistant, content };
|
|
517
682
|
});
|
|
518
683
|
|
|
@@ -537,7 +702,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
537
702
|
}
|
|
538
703
|
|
|
539
704
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
540
|
-
const message = this.
|
|
705
|
+
const message = this.tryGetMessage(messageId)?.message;
|
|
541
706
|
if (!message || message.role !== "assistant") return false;
|
|
542
707
|
const assistant = message as ThreadAssistantMessage;
|
|
543
708
|
if (
|
|
@@ -547,20 +712,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
547
712
|
return false;
|
|
548
713
|
}
|
|
549
714
|
const allResolved = assistant.content.every(
|
|
550
|
-
(part) =>
|
|
551
|
-
part.type !== "tool-call" ||
|
|
552
|
-
("result" in part && part.result !== undefined),
|
|
715
|
+
(part) => part.type !== "tool-call" || isResolvedToolCall(part),
|
|
553
716
|
);
|
|
554
717
|
if (!allResolved) return false;
|
|
555
718
|
|
|
556
|
-
|
|
557
|
-
|
|
719
|
+
const updated = this.updateMessage(messageId, (current) =>
|
|
720
|
+
current.role === "assistant"
|
|
558
721
|
? {
|
|
559
|
-
...(
|
|
722
|
+
...(current as ThreadAssistantMessage),
|
|
560
723
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
561
724
|
}
|
|
562
|
-
:
|
|
725
|
+
: current,
|
|
563
726
|
);
|
|
727
|
+
if (!updated) return false;
|
|
564
728
|
this.notifyUpdate();
|
|
565
729
|
this.persistAssistantHistory(messageId);
|
|
566
730
|
return true;
|
|
@@ -574,9 +738,111 @@ export class AgUiThreadRuntimeCore {
|
|
|
574
738
|
|
|
575
739
|
applyExternalMessages(messages: readonly ThreadMessage[]): void {
|
|
576
740
|
this.assistantHistoryParents.clear();
|
|
577
|
-
|
|
741
|
+
|
|
742
|
+
if (messages.length === 0) {
|
|
743
|
+
this.clearRepository();
|
|
744
|
+
} else {
|
|
745
|
+
let expectedParentId: string | null = null;
|
|
746
|
+
let lastAppliedId: string | null = null;
|
|
747
|
+
let hardReplace = false;
|
|
748
|
+
const seen = new Set<string>();
|
|
749
|
+
|
|
750
|
+
for (const message of messages) {
|
|
751
|
+
if (seen.has(message.id)) continue;
|
|
752
|
+
seen.add(message.id);
|
|
753
|
+
const existing = this.tryGetMessage(message.id);
|
|
754
|
+
if (existing && existing.parentId !== expectedParentId) {
|
|
755
|
+
hardReplace = true;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
this.addOrUpdateMessage(expectedParentId, message);
|
|
759
|
+
expectedParentId = message.id;
|
|
760
|
+
lastAppliedId = message.id;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (hardReplace) {
|
|
764
|
+
this.clearRepository();
|
|
765
|
+
expectedParentId = null;
|
|
766
|
+
lastAppliedId = null;
|
|
767
|
+
seen.clear();
|
|
768
|
+
for (const message of messages) {
|
|
769
|
+
if (seen.has(message.id)) continue;
|
|
770
|
+
seen.add(message.id);
|
|
771
|
+
this.addOrUpdateMessage(expectedParentId, message);
|
|
772
|
+
expectedParentId = message.id;
|
|
773
|
+
lastAppliedId = message.id;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
this.resetRepositoryHead(lastAppliedId);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.recordedHistoryIds.clear();
|
|
781
|
+
for (const { message } of this.getMessageRepository().messages) {
|
|
782
|
+
this.recordedHistoryIds.add(message.id);
|
|
783
|
+
}
|
|
784
|
+
this.notifyUpdate();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private applyExternalMessageRepository(
|
|
788
|
+
loaded: ExportedMessageRepository,
|
|
789
|
+
): void {
|
|
790
|
+
const headId = loaded.headId ?? loaded.messages.at(-1)?.message.id ?? null;
|
|
791
|
+
const ids = new Set<string>();
|
|
792
|
+
let degenerate = false;
|
|
793
|
+
for (const { message } of loaded.messages) {
|
|
794
|
+
if (ids.has(message.id)) {
|
|
795
|
+
degenerate = true;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
ids.add(message.id);
|
|
799
|
+
}
|
|
800
|
+
if (headId !== null && !ids.has(headId)) degenerate = true;
|
|
801
|
+
|
|
802
|
+
if (!degenerate) {
|
|
803
|
+
this.clearRepository();
|
|
804
|
+
let pending = [...loaded.messages];
|
|
805
|
+
const importedIds = new Set<string>();
|
|
806
|
+
|
|
807
|
+
while (pending.length > 0) {
|
|
808
|
+
const unresolved: typeof pending = [];
|
|
809
|
+
let progressed = false;
|
|
810
|
+
for (const item of pending) {
|
|
811
|
+
if (item.parentId !== null && !importedIds.has(item.parentId)) {
|
|
812
|
+
unresolved.push(item);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
this.addOrUpdateMessage(item.parentId, item.message);
|
|
816
|
+
importedIds.add(item.message.id);
|
|
817
|
+
progressed = true;
|
|
818
|
+
}
|
|
819
|
+
if (!progressed) {
|
|
820
|
+
degenerate = true;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
pending = unresolved;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (degenerate) {
|
|
828
|
+
this.clearRepository();
|
|
829
|
+
let previousId: string | null = null;
|
|
830
|
+
for (const { message } of loaded.messages) {
|
|
831
|
+
const existing = this.tryGetMessage(message.id);
|
|
832
|
+
this.addOrUpdateMessage(
|
|
833
|
+
existing ? existing.parentId : previousId,
|
|
834
|
+
message,
|
|
835
|
+
);
|
|
836
|
+
previousId = message.id;
|
|
837
|
+
}
|
|
838
|
+
this.resetRepositoryHead(previousId);
|
|
839
|
+
} else {
|
|
840
|
+
this.resetRepositoryHead(headId);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
this.assistantHistoryParents.clear();
|
|
578
844
|
this.recordedHistoryIds.clear();
|
|
579
|
-
for (const message of
|
|
845
|
+
for (const { message } of loaded.messages) {
|
|
580
846
|
this.recordedHistoryIds.add(message.id);
|
|
581
847
|
}
|
|
582
848
|
this.notifyUpdate();
|
|
@@ -587,6 +853,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
587
853
|
this.notifyUpdate();
|
|
588
854
|
}
|
|
589
855
|
|
|
856
|
+
setState(
|
|
857
|
+
next:
|
|
858
|
+
| ReadonlyJSONValue
|
|
859
|
+
| ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
|
|
860
|
+
): void {
|
|
861
|
+
this.stateSnapshot =
|
|
862
|
+
typeof next === "function" ? next(this.stateSnapshot) : next;
|
|
863
|
+
this.notifyUpdate();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
resetState(): void {
|
|
867
|
+
this.stateSnapshot = undefined;
|
|
868
|
+
this.notifyUpdate();
|
|
869
|
+
}
|
|
870
|
+
|
|
590
871
|
private async startRun(
|
|
591
872
|
parentId: string | null,
|
|
592
873
|
runConfig?: RunConfig,
|
|
@@ -595,20 +876,32 @@ export class AgUiThreadRuntimeCore {
|
|
|
595
876
|
): Promise<void> {
|
|
596
877
|
const normalizedRunConfig = runConfig ?? {};
|
|
597
878
|
this.lastRunConfig = normalizedRunConfig;
|
|
598
|
-
this.
|
|
599
|
-
const
|
|
879
|
+
const parent = parentId === null ? undefined : this.tryGetMessage(parentId);
|
|
880
|
+
const shouldEagerlyInsertAssistant =
|
|
881
|
+
parentId !== null &&
|
|
882
|
+
parent !== undefined &&
|
|
883
|
+
parentId !== this.repository.headId;
|
|
884
|
+
const historicalMessages = [
|
|
885
|
+
...(shouldEagerlyInsertAssistant
|
|
886
|
+
? (this.tryGetMessages(parentId) ?? this.repository.getMessages())
|
|
887
|
+
: this.repository.getMessages()),
|
|
888
|
+
];
|
|
600
889
|
|
|
601
890
|
this.pendingError = null;
|
|
602
|
-
const assistantParentId = parentId
|
|
891
|
+
const assistantParentId = parent ? parentId : this.repository.headId;
|
|
603
892
|
let assistantMessageId: string | undefined;
|
|
604
893
|
const ensureAssistant = () => {
|
|
605
894
|
if (assistantMessageId) return assistantMessageId;
|
|
606
|
-
const created = this.insertAssistantPlaceholder(
|
|
895
|
+
const created = this.insertAssistantPlaceholder(
|
|
896
|
+
assistantParentId ?? null,
|
|
897
|
+
);
|
|
607
898
|
assistantMessageId = created;
|
|
608
899
|
this.markPendingAssistantHistory(created, assistantParentId ?? null);
|
|
609
900
|
return created;
|
|
610
901
|
};
|
|
611
902
|
|
|
903
|
+
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
904
|
+
|
|
612
905
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
613
906
|
const resolved = this.updateAssistantMessage(ensureAssistant(), update);
|
|
614
907
|
if (resolved !== assistantMessageId) {
|
|
@@ -623,8 +916,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
623
916
|
onServerMessageId: (serverId) => {
|
|
624
917
|
const placeholder = ensureAssistant();
|
|
625
918
|
if (placeholder === serverId) return;
|
|
626
|
-
this.reassignAssistantId(placeholder, serverId)
|
|
627
|
-
|
|
919
|
+
if (this.reassignAssistantId(placeholder, serverId)) {
|
|
920
|
+
assistantMessageId = serverId;
|
|
921
|
+
}
|
|
628
922
|
},
|
|
629
923
|
});
|
|
630
924
|
const dispatch = (event: AgUiEvent) => this.handleEvent(aggregator, event);
|
|
@@ -744,7 +1038,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
744
1038
|
unstable_threadId: ctx.threadId,
|
|
745
1039
|
unstable_parentId: ctx.parentId,
|
|
746
1040
|
unstable_getMessage: () => {
|
|
747
|
-
const message = this.
|
|
1041
|
+
const message = this.tryGetMessage(currentId())?.message;
|
|
748
1042
|
if (!message) {
|
|
749
1043
|
throw new Error(
|
|
750
1044
|
"[agui] resume stream requested the assistant message before it existed",
|
|
@@ -771,7 +1065,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
771
1065
|
}
|
|
772
1066
|
|
|
773
1067
|
if (ctx.abortSignal.aborted) return;
|
|
774
|
-
const current = this.
|
|
1068
|
+
const current = this.tryGetMessage(currentId())?.message;
|
|
775
1069
|
if (!current || current.status?.type === "running") {
|
|
776
1070
|
ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
777
1071
|
}
|
|
@@ -784,7 +1078,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
784
1078
|
resume?: AgUiResumeEntry[],
|
|
785
1079
|
) {
|
|
786
1080
|
const threadId = this.agent.threadId || "main";
|
|
787
|
-
const messages = toAgUiMessages(
|
|
1081
|
+
const messages = toAgUiMessages(
|
|
1082
|
+
historyMessages ?? this.repository.getMessages(),
|
|
1083
|
+
);
|
|
788
1084
|
const context = this.runtime?.thread.getModelContext();
|
|
789
1085
|
return {
|
|
790
1086
|
threadId,
|
|
@@ -837,7 +1133,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
837
1133
|
this.setRunning(false);
|
|
838
1134
|
}
|
|
839
1135
|
|
|
840
|
-
private insertAssistantPlaceholder(): string {
|
|
1136
|
+
private insertAssistantPlaceholder(parentId: string | null): string {
|
|
841
1137
|
const id = generateOptimisticId();
|
|
842
1138
|
const assistant: ThreadAssistantMessage = {
|
|
843
1139
|
id,
|
|
@@ -854,56 +1150,62 @@ export class AgUiThreadRuntimeCore {
|
|
|
854
1150
|
custom: {},
|
|
855
1151
|
},
|
|
856
1152
|
};
|
|
857
|
-
this.
|
|
1153
|
+
this.addOrUpdateMessage(parentId ?? this.repository.headId, assistant);
|
|
1154
|
+
this.switchToBranch(id);
|
|
858
1155
|
this.notifyUpdate();
|
|
859
1156
|
return id;
|
|
860
1157
|
}
|
|
861
1158
|
|
|
862
|
-
private reassignAssistantId(oldId: string, newId: string):
|
|
863
|
-
if (oldId === newId) return;
|
|
1159
|
+
private reassignAssistantId(oldId: string, newId: string): boolean {
|
|
1160
|
+
if (oldId === newId) return true;
|
|
1161
|
+
const oldItem = this.tryGetMessage(oldId);
|
|
1162
|
+
if (!oldItem) return false;
|
|
864
1163
|
|
|
865
|
-
const collidesWithExisting = this.
|
|
1164
|
+
const collidesWithExisting = this.hasMessage(newId);
|
|
866
1165
|
|
|
867
1166
|
if (collidesWithExisting) {
|
|
868
1167
|
this.logger.debug?.(
|
|
869
|
-
"[agui] reassignAssistantId: server id already present in messages, dropping placeholder",
|
|
1168
|
+
"[agui] reassignAssistantId: server id already present in messages or repository, dropping placeholder",
|
|
870
1169
|
{ oldId, newId },
|
|
871
1170
|
);
|
|
872
|
-
this.
|
|
1171
|
+
this.deleteMessage(oldId, oldItem.parentId ?? null);
|
|
873
1172
|
} else {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1173
|
+
const { isOptimistic: _, ...metadata } = oldItem.message.metadata;
|
|
1174
|
+
this.addOrUpdateMessage(oldItem.parentId, {
|
|
1175
|
+
...oldItem.message,
|
|
1176
|
+
id: newId,
|
|
1177
|
+
metadata,
|
|
1178
|
+
} as ThreadMessage);
|
|
1179
|
+
this.switchToBranch(newId);
|
|
1180
|
+
this.tryDeleteMessage(oldId);
|
|
879
1181
|
}
|
|
880
1182
|
|
|
881
1183
|
const pendingParent = this.assistantHistoryParents.get(oldId);
|
|
882
1184
|
if (pendingParent !== undefined) {
|
|
883
1185
|
this.assistantHistoryParents.delete(oldId);
|
|
884
|
-
if (!this.assistantHistoryParents.has(newId)) {
|
|
1186
|
+
if (!collidesWithExisting && !this.assistantHistoryParents.has(newId)) {
|
|
885
1187
|
this.assistantHistoryParents.set(newId, pendingParent);
|
|
886
1188
|
}
|
|
887
1189
|
}
|
|
888
1190
|
|
|
889
1191
|
if (this.recordedHistoryIds.has(oldId)) {
|
|
890
1192
|
this.recordedHistoryIds.delete(oldId);
|
|
891
|
-
|
|
1193
|
+
if (!collidesWithExisting) {
|
|
1194
|
+
this.recordedHistoryIds.add(newId);
|
|
1195
|
+
}
|
|
892
1196
|
}
|
|
893
1197
|
|
|
894
1198
|
this.notifyUpdate();
|
|
1199
|
+
return !collidesWithExisting;
|
|
895
1200
|
}
|
|
896
1201
|
|
|
897
1202
|
private updateAssistantMessage(
|
|
898
1203
|
messageId: string,
|
|
899
1204
|
update: ChatModelRunResult,
|
|
900
1205
|
): string {
|
|
901
|
-
let touched = false;
|
|
902
1206
|
let latestStatus: MessageStatus | undefined;
|
|
903
|
-
|
|
904
|
-
if (message.
|
|
905
|
-
return message;
|
|
906
|
-
touched = true;
|
|
1207
|
+
const touched = this.updateMessage(messageId, (message) => {
|
|
1208
|
+
if (message.role !== "assistant") return message;
|
|
907
1209
|
const assistant = message as ThreadAssistantMessage;
|
|
908
1210
|
const metadata = update.metadata
|
|
909
1211
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
@@ -952,11 +1254,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
952
1254
|
): ThreadAssistantMessage["content"] {
|
|
953
1255
|
const resolved = new Map<string, ToolCallMessagePart>();
|
|
954
1256
|
for (const part of previous) {
|
|
955
|
-
if (
|
|
956
|
-
part.type === "tool-call" &&
|
|
957
|
-
"result" in part &&
|
|
958
|
-
part.result !== undefined
|
|
959
|
-
) {
|
|
1257
|
+
if (part.type === "tool-call" && isResolvedToolCall(part)) {
|
|
960
1258
|
resolved.set(part.toolCallId, part);
|
|
961
1259
|
}
|
|
962
1260
|
}
|
|
@@ -964,8 +1262,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
964
1262
|
|
|
965
1263
|
let changed = false;
|
|
966
1264
|
const merged = next.map((part) => {
|
|
967
|
-
if (part.type !== "tool-call") return part;
|
|
968
|
-
if ("result" in part && part.result !== undefined) return part;
|
|
1265
|
+
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
969
1266
|
const prior = resolved.get(part.toolCallId);
|
|
970
1267
|
if (!prior) return part;
|
|
971
1268
|
changed = true;
|
|
@@ -1056,10 +1353,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1056
1353
|
messageId: string,
|
|
1057
1354
|
event: Extract<AgUiEvent, { type: "TOOL_CALL_RESULT" }>,
|
|
1058
1355
|
): void {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (message.id !== messageId || message.role !== "assistant")
|
|
1062
|
-
return message;
|
|
1356
|
+
const updated = this.updateMessage(messageId, (message) => {
|
|
1357
|
+
if (message.role !== "assistant") return message;
|
|
1063
1358
|
const assistant = message as ThreadAssistantMessage;
|
|
1064
1359
|
let matchedToolCall = false;
|
|
1065
1360
|
const content = assistant.content.map((part) => {
|
|
@@ -1076,7 +1371,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1076
1371
|
};
|
|
1077
1372
|
});
|
|
1078
1373
|
if (!matchedToolCall) return message;
|
|
1079
|
-
updated = true;
|
|
1080
1374
|
return { ...assistant, content };
|
|
1081
1375
|
});
|
|
1082
1376
|
|
|
@@ -1119,18 +1413,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1119
1413
|
);
|
|
1120
1414
|
}
|
|
1121
1415
|
|
|
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
1416
|
private isTerminalStatus(status?: MessageStatus): boolean {
|
|
1135
1417
|
return status?.type === "complete" || status?.type === "incomplete";
|
|
1136
1418
|
}
|
|
@@ -1156,7 +1438,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1156
1438
|
if (!this.history) return;
|
|
1157
1439
|
const parentId = this.assistantHistoryParents.get(messageId);
|
|
1158
1440
|
if (parentId === undefined) return;
|
|
1159
|
-
const message = this.
|
|
1441
|
+
const message = this.tryGetMessage(messageId)?.message;
|
|
1160
1442
|
if (!message || message.role !== "assistant") return;
|
|
1161
1443
|
if (!this.isPersistableStatus(message.status)) return;
|
|
1162
1444
|
this.assistantHistoryParents.delete(messageId);
|