@assistant-ui/react-ag-ui 0.0.57 → 0.0.59
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 +16 -0
- package/dist/index.d.ts +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +19 -8
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +118 -86
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +34 -11
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +385 -119
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +4 -1
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -1
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +11 -12
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +176 -45
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +67 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +15 -5
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +1 -0
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +18 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +204 -120
- package/src/runtime/adapter/conversions.ts +87 -17
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +595 -139
- package/src/runtime/adapter/subscriber.ts +10 -1
- package/src/runtime/adapter/tool-approval.test.ts +75 -0
- package/src/runtime/adapter/tool-approval.ts +17 -12
- package/src/runtime/event-parser.ts +216 -36
- package/src/runtime/types.ts +96 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.queue.test.tsx +54 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +199 -0
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +15 -3
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
import {
|
|
24
24
|
createMessageRepositorySession,
|
|
25
25
|
invokeUserCallback,
|
|
26
|
+
iterateToolCallParts,
|
|
27
|
+
mapToolCallPartsDeep,
|
|
26
28
|
} from "@assistant-ui/core/internal";
|
|
27
29
|
import type {
|
|
28
30
|
AbstractAgent,
|
|
@@ -33,7 +35,12 @@ import type {
|
|
|
33
35
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
34
36
|
import type { Logger } from "./logger";
|
|
35
37
|
import { readMcpAppResourceUri } from "./mcp-tool-result";
|
|
36
|
-
import type {
|
|
38
|
+
import type {
|
|
39
|
+
AgUiEvent,
|
|
40
|
+
AgUiInterrupt,
|
|
41
|
+
AgUiResumeEntry,
|
|
42
|
+
AgUiResumeTranscript,
|
|
43
|
+
} from "./types";
|
|
37
44
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
38
45
|
import {
|
|
39
46
|
AG_UI_METADATA_NAMESPACE,
|
|
@@ -74,8 +81,6 @@ const isResolvedToolCall = (
|
|
|
74
81
|
): boolean =>
|
|
75
82
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
76
83
|
|
|
77
|
-
const symbolResumeShim = Symbol("agui-resume-shim");
|
|
78
|
-
|
|
79
84
|
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
80
85
|
type ResumeStream = (
|
|
81
86
|
options: ChatModelRunOptions,
|
|
@@ -87,10 +92,21 @@ type ResumeRunConfig = {
|
|
|
87
92
|
stream?: ResumeStream;
|
|
88
93
|
};
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A resume submission, pairing the entries that answer the open interrupts with
|
|
97
|
+
* the assistant message they were opened on. That message anchors the resume
|
|
98
|
+
* transcript: under `resumeTranscript: "appended"` only what follows it is sent.
|
|
99
|
+
*/
|
|
100
|
+
type ResumeDispatch = {
|
|
101
|
+
entries: AgUiResumeEntry[];
|
|
102
|
+
interruptedMessageId: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
90
105
|
type CoreOptions = {
|
|
91
106
|
agent: AbstractAgent;
|
|
92
107
|
logger: Logger;
|
|
93
108
|
showThinking: boolean;
|
|
109
|
+
resumeTranscript?: AgUiResumeTranscript | undefined;
|
|
94
110
|
autoCancelPendingToolCalls?: boolean | undefined;
|
|
95
111
|
onError?: (error: Error) => void;
|
|
96
112
|
onCancel?: () => void;
|
|
@@ -135,6 +151,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
135
151
|
private agent: AbstractAgent;
|
|
136
152
|
private logger: Logger;
|
|
137
153
|
private showThinking: boolean;
|
|
154
|
+
private resumeTranscript: AgUiResumeTranscript;
|
|
138
155
|
private autoCancelPendingToolCalls: boolean | undefined;
|
|
139
156
|
private onError: ((error: Error) => void) | undefined;
|
|
140
157
|
private onCancel: (() => void) | undefined;
|
|
@@ -164,7 +181,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
164
181
|
// mid-run, and cancelling has to reach the agent holding the live request.
|
|
165
182
|
private activeRunAgent: AbstractAgent | null = null;
|
|
166
183
|
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
167
|
-
private pendingError: Error | null = null;
|
|
168
184
|
private history: ThreadHistoryAdapter | undefined;
|
|
169
185
|
private lastRunConfig: RunConfig | undefined;
|
|
170
186
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
@@ -174,33 +190,33 @@ export class AgUiThreadRuntimeCore {
|
|
|
174
190
|
private _isLoading = false;
|
|
175
191
|
private _loadPromise: Promise<void> | undefined;
|
|
176
192
|
private _loadRequested = false;
|
|
177
|
-
private
|
|
178
|
-
|
|
193
|
+
private pendingResume: { owner: AbortController; messageId: string } | null =
|
|
194
|
+
null;
|
|
195
|
+
private pendingA2uiResumeOwner: AbortController | null = null;
|
|
179
196
|
private pendingA2uiAction: Record<string, unknown> | undefined;
|
|
180
197
|
|
|
181
198
|
constructor(options: CoreOptions) {
|
|
182
199
|
this.agent = options.agent;
|
|
183
200
|
this.logger = options.logger;
|
|
184
201
|
this.showThinking = options.showThinking;
|
|
202
|
+
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
185
203
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
186
204
|
this.onError = options.onError;
|
|
187
205
|
this.onCancel = options.onCancel;
|
|
188
206
|
this.history = options.history;
|
|
189
207
|
this.notifyUpdate = options.notifyUpdate;
|
|
190
|
-
this.installResumeShim();
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
updateOptions(options: Omit<CoreOptions, "notifyUpdate">) {
|
|
194
211
|
this.agent = options.agent;
|
|
195
212
|
this.logger = options.logger;
|
|
196
213
|
this.showThinking = options.showThinking;
|
|
214
|
+
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
197
215
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
198
216
|
this.onError = options.onError;
|
|
199
217
|
this.onCancel = options.onCancel;
|
|
200
218
|
const previousHistory = this.history;
|
|
201
219
|
this.history = options.history;
|
|
202
|
-
this.installResumeShim();
|
|
203
|
-
|
|
204
220
|
if (
|
|
205
221
|
this._loadRequested &&
|
|
206
222
|
!this._loadPromise &&
|
|
@@ -299,12 +315,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
299
315
|
|
|
300
316
|
async append(message: AppendMessage): Promise<void> {
|
|
301
317
|
const startRun = message.startRun ?? message.role === "user";
|
|
318
|
+
let ownsThread = true;
|
|
302
319
|
if (startRun) {
|
|
303
320
|
this.assertNoPendingInterrupts();
|
|
304
321
|
this.maybeAutoCancelPendingToolCalls();
|
|
322
|
+
// Before the message is linked: callers parent it on the in-flight
|
|
323
|
+
// assistant, and settling that assistant reassigns its optimistic id,
|
|
324
|
+
// which would evict it and reparent this message onto a branch.
|
|
325
|
+
ownsThread = this.supersedeActiveRun();
|
|
305
326
|
}
|
|
306
327
|
const threadMessageId = this.appendEntry(message);
|
|
307
|
-
if (!startRun) return;
|
|
328
|
+
if (!startRun || !ownsThread) return;
|
|
308
329
|
await this.startRun(threadMessageId, message.runConfig);
|
|
309
330
|
}
|
|
310
331
|
|
|
@@ -347,19 +368,46 @@ export class AgUiThreadRuntimeCore {
|
|
|
347
368
|
}
|
|
348
369
|
|
|
349
370
|
async cancel(): Promise<void> {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
371
|
+
this.abortActiveRun();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// abortRun is what stops an upstream agent: AbstractAgent.runAgent declares
|
|
375
|
+
// two parameters and HttpAgent binds its request to a controller of its own,
|
|
376
|
+
// so the run options object reaches only subclasses that read it. The local
|
|
377
|
+
// abort comes last and is unconditional: its listener runs onCancel
|
|
378
|
+
// synchronously, so a callback that starts another run must keep the
|
|
379
|
+
// controller it installed, and a throw from a subclass's abortRun would
|
|
380
|
+
// otherwise strand the thread as running.
|
|
381
|
+
private abortActiveRun(): void {
|
|
382
|
+
const controller = this.abortController;
|
|
383
|
+
if (!controller) return;
|
|
356
384
|
try {
|
|
357
385
|
(this.activeRunAgent ?? this.agent).abortRun();
|
|
358
386
|
} finally {
|
|
359
|
-
|
|
387
|
+
controller.abort();
|
|
360
388
|
}
|
|
361
389
|
}
|
|
362
390
|
|
|
391
|
+
// Starting a run supersedes the active one, and reports whether the caller
|
|
392
|
+
// still owns the thread afterwards: the local abort runs onCancel
|
|
393
|
+
// synchronously, so a callback that starts its own run keeps it. abortRun is a
|
|
394
|
+
// subclass's code, and a throw there must not abandon the run being started,
|
|
395
|
+
// unlike cancel(), where the failed operation is the caller's own. Calling
|
|
396
|
+
// this twice for one run is harmless, because the second call finds no active
|
|
397
|
+
// run to supersede.
|
|
398
|
+
private supersedeActiveRun(): boolean {
|
|
399
|
+
try {
|
|
400
|
+
this.abortActiveRun();
|
|
401
|
+
} catch (error) {
|
|
402
|
+
this.logger.error?.("[agui] agent abortRun failed", error);
|
|
403
|
+
}
|
|
404
|
+
if (this.abortController === null) return true;
|
|
405
|
+
this.logger.debug?.(
|
|
406
|
+
"[agui] onCancel started a replacement run; dropping the superseding send",
|
|
407
|
+
);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
363
411
|
async resume(config: ResumeRunConfig): Promise<void> {
|
|
364
412
|
this.assertNoPendingInterrupts();
|
|
365
413
|
await this.startRun(
|
|
@@ -439,8 +487,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
439
487
|
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
440
488
|
if (!assistant) return null;
|
|
441
489
|
const toolCallIds: string[] = [];
|
|
442
|
-
for (const part of assistant.content) {
|
|
443
|
-
if (part.type !== "tool-call") continue;
|
|
490
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
444
491
|
if (isResolvedToolCall(part)) continue;
|
|
445
492
|
toolCallIds.push(part.toolCallId);
|
|
446
493
|
}
|
|
@@ -494,13 +541,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
494
541
|
resume: AgUiResumeEntry[],
|
|
495
542
|
): Promise<void> {
|
|
496
543
|
this.clearPendingInterrupts(messageId, resume);
|
|
497
|
-
await this.startRun(messageId, this.lastRunConfig,
|
|
544
|
+
await this.startRun(messageId, this.lastRunConfig, {
|
|
545
|
+
entries: resume,
|
|
546
|
+
interruptedMessageId: messageId,
|
|
547
|
+
});
|
|
498
548
|
}
|
|
499
549
|
|
|
500
550
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
551
|
+
* A rejected decision reaches the caller through the approval seam, and
|
|
552
|
+
* `onError` in addition, because a consumer watching only the runtime would
|
|
553
|
+
* otherwise miss it. A failure raised by the resumed run itself was already
|
|
504
554
|
* reported by `startRun` before it rethrew, so reporting it here again would
|
|
505
555
|
* give the consumer two notifications for one failure.
|
|
506
556
|
*/
|
|
@@ -557,9 +607,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
557
607
|
const gated = projectAgUiToolApprovals(
|
|
558
608
|
pending.interrupts,
|
|
559
609
|
new Set(
|
|
560
|
-
(gatedMessage?.content ?? [])
|
|
561
|
-
|
|
562
|
-
|
|
610
|
+
Array.from(iterateToolCallParts(gatedMessage?.content ?? [])).map(
|
|
611
|
+
(part) => part.toolCallId,
|
|
612
|
+
),
|
|
563
613
|
),
|
|
564
614
|
);
|
|
565
615
|
const isGated = [...gated.values()].some(
|
|
@@ -653,7 +703,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
653
703
|
const normalized = this.toAppendMessage(message);
|
|
654
704
|
this.clearPendingInterrupts(pending.messageId, resume);
|
|
655
705
|
const threadMessageId = this.appendEntry(normalized);
|
|
656
|
-
await this.startRun(threadMessageId, normalized.runConfig,
|
|
706
|
+
await this.startRun(threadMessageId, normalized.runConfig, {
|
|
707
|
+
entries: resume,
|
|
708
|
+
interruptedMessageId: pending.messageId,
|
|
709
|
+
});
|
|
657
710
|
}
|
|
658
711
|
|
|
659
712
|
private resolveSteerAwayResume(
|
|
@@ -722,7 +775,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
722
775
|
}
|
|
723
776
|
return {
|
|
724
777
|
createdAt: message.createdAt ?? new Date(),
|
|
725
|
-
parentId:
|
|
778
|
+
parentId:
|
|
779
|
+
message.parentId === undefined ? this.session.headId : message.parentId,
|
|
726
780
|
sourceId: message.sourceId ?? null,
|
|
727
781
|
role: message.role ?? "user",
|
|
728
782
|
content: message.content,
|
|
@@ -774,9 +828,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
774
828
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
775
829
|
const message = messages[index];
|
|
776
830
|
if (!message || message.role !== "assistant") continue;
|
|
777
|
-
for (const part of message.content) {
|
|
778
|
-
if (part.
|
|
779
|
-
continue;
|
|
831
|
+
for (const part of iterateToolCallParts(message.content)) {
|
|
832
|
+
if (part.toolCallId !== toolCallId) continue;
|
|
780
833
|
if (!isResolvedToolCall(part)) {
|
|
781
834
|
return message.id;
|
|
782
835
|
}
|
|
@@ -790,8 +843,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
790
843
|
const updated = this.session.updateMessage(messageId, (message) => {
|
|
791
844
|
if (message.role !== "assistant") return message;
|
|
792
845
|
const assistant = message as ThreadAssistantMessage;
|
|
793
|
-
const content = assistant.content
|
|
794
|
-
if (
|
|
846
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
847
|
+
if (isResolvedToolCall(part)) return part;
|
|
795
848
|
return {
|
|
796
849
|
...part,
|
|
797
850
|
result: { error: "Tool call cancelled by user" },
|
|
@@ -804,13 +857,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
804
857
|
}
|
|
805
858
|
|
|
806
859
|
addToolResult(options: AddToolResultOptions): void {
|
|
807
|
-
|
|
860
|
+
// Core's ToolInvocationTracker resolves a nested call to the nested
|
|
861
|
+
// subagent message's id, which is not a session message; re-anchor on the
|
|
862
|
+
// top-level message that owns the tree so the update can land.
|
|
863
|
+
const sessionMessageId = this.session.tryGetMessage(options.messageId)
|
|
864
|
+
? options.messageId
|
|
865
|
+
: this.findMessageIdForToolCall(options.toolCallId);
|
|
866
|
+
if (sessionMessageId === undefined) return;
|
|
867
|
+
const updated = this.session.updateMessage(sessionMessageId, (message) => {
|
|
808
868
|
if (message.role !== "assistant") return message;
|
|
809
869
|
const assistant = message as ThreadAssistantMessage;
|
|
810
870
|
let matchedToolCall = false;
|
|
811
|
-
const content = assistant.content
|
|
812
|
-
if (part.
|
|
813
|
-
return part;
|
|
871
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
872
|
+
if (part.toolCallId !== options.toolCallId) return part;
|
|
814
873
|
matchedToolCall = true;
|
|
815
874
|
return {
|
|
816
875
|
...part,
|
|
@@ -825,7 +884,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
825
884
|
|
|
826
885
|
if (!updated) return;
|
|
827
886
|
this.notifyUpdate();
|
|
828
|
-
this.maybeResumeAfterToolResults(
|
|
887
|
+
this.maybeResumeAfterToolResults(sessionMessageId);
|
|
829
888
|
}
|
|
830
889
|
|
|
831
890
|
sendA2uiAction(action: Record<string, unknown>): void {
|
|
@@ -846,8 +905,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
846
905
|
}
|
|
847
906
|
this.pendingA2uiAction = userAction;
|
|
848
907
|
|
|
849
|
-
|
|
850
|
-
|
|
908
|
+
const owner = this.abortController;
|
|
909
|
+
if (owner) {
|
|
910
|
+
this.pendingA2uiResumeOwner = owner;
|
|
851
911
|
return;
|
|
852
912
|
}
|
|
853
913
|
this.startResumeRun(parentId);
|
|
@@ -859,10 +919,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
859
919
|
private maybeResumeAfterToolResults(messageId: string): void {
|
|
860
920
|
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
861
921
|
|
|
862
|
-
|
|
922
|
+
const owner = this.abortController;
|
|
923
|
+
if (owner) {
|
|
863
924
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
864
925
|
// closed). Defer until startRun's tail so we never start two runs.
|
|
865
|
-
this.
|
|
926
|
+
this.pendingResume = { owner, messageId };
|
|
866
927
|
return;
|
|
867
928
|
}
|
|
868
929
|
this.startResumeRun(messageId);
|
|
@@ -878,9 +939,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
878
939
|
) {
|
|
879
940
|
return false;
|
|
880
941
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
942
|
+
let allResolved = true;
|
|
943
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
944
|
+
if (!isResolvedToolCall(part)) {
|
|
945
|
+
allResolved = false;
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
884
949
|
if (!allResolved) return false;
|
|
885
950
|
|
|
886
951
|
const updated = this.session.updateMessage(messageId, (current) =>
|
|
@@ -908,7 +973,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
908
973
|
}
|
|
909
974
|
|
|
910
975
|
applyExternalMessages(messages: readonly ThreadMessage[]): void {
|
|
911
|
-
this.
|
|
976
|
+
this.pendingA2uiResumeOwner = null;
|
|
912
977
|
this.pendingA2uiAction = undefined;
|
|
913
978
|
this.assistantHistoryParents.clear();
|
|
914
979
|
|
|
@@ -954,6 +1019,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
954
1019
|
for (const { message } of this.getMessageRepository().messages) {
|
|
955
1020
|
this.snapshotHistoryIds.add(message.id);
|
|
956
1021
|
}
|
|
1022
|
+
// MESSAGES_SNAPSHOT re-appends the active assistant, so a parked
|
|
1023
|
+
// continuation whose target survived the snapshot is still answerable.
|
|
1024
|
+
// Membership is the head branch, not the repository: a soft merge leaves
|
|
1025
|
+
// the messages it dropped behind as off-branch nodes.
|
|
1026
|
+
const resumeTarget = this.pendingResume?.messageId;
|
|
1027
|
+
if (
|
|
1028
|
+
resumeTarget !== undefined &&
|
|
1029
|
+
!this.session.getMessages().some((message) => message.id === resumeTarget)
|
|
1030
|
+
) {
|
|
1031
|
+
this.pendingResume = null;
|
|
1032
|
+
}
|
|
957
1033
|
this.notifyUpdate();
|
|
958
1034
|
}
|
|
959
1035
|
|
|
@@ -980,9 +1056,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
980
1056
|
private async startRun(
|
|
981
1057
|
parentId: string | null,
|
|
982
1058
|
runConfig?: RunConfig,
|
|
983
|
-
resume?:
|
|
1059
|
+
resume?: ResumeDispatch,
|
|
984
1060
|
resumeStream?: ResumeStream,
|
|
985
1061
|
): Promise<void> {
|
|
1062
|
+
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
1063
|
+
// queue serializes sends instead. append supersedes earlier, before it links
|
|
1064
|
+
// its message; this covers the entry points that start a run without one.
|
|
1065
|
+
if (!this.supersedeActiveRun()) return;
|
|
1066
|
+
|
|
986
1067
|
const normalizedRunConfig = runConfig ?? {};
|
|
987
1068
|
this.lastRunConfig = normalizedRunConfig;
|
|
988
1069
|
const parent =
|
|
@@ -1000,7 +1081,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1000
1081
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1001
1082
|
);
|
|
1002
1083
|
|
|
1003
|
-
|
|
1084
|
+
let pendingError: Error | null = null;
|
|
1004
1085
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1005
1086
|
let assistantMessageId: string | undefined;
|
|
1006
1087
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
@@ -1126,16 +1207,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1126
1207
|
// Cancel flips only the status; an aggregator RUN_CANCELLED would emit an empty snapshot and wipe the replayed content.
|
|
1127
1208
|
cancelRun = () =>
|
|
1128
1209
|
applyUpdate({ status: { type: "incomplete", reason: "cancelled" } });
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1210
|
+
pendingError =
|
|
1211
|
+
(await this.consumeResumeStream(resumeStream, {
|
|
1212
|
+
runConfig: normalizedRunConfig,
|
|
1213
|
+
threadId: this.agent.threadId || "main",
|
|
1214
|
+
parentId: assistantParentId,
|
|
1215
|
+
historicalMessages,
|
|
1216
|
+
abortSignal,
|
|
1217
|
+
ensureAssistant,
|
|
1218
|
+
applyUpdate,
|
|
1219
|
+
getAssistantMessageId: () => assistantMessageId,
|
|
1220
|
+
})) ?? null;
|
|
1139
1221
|
} else {
|
|
1140
1222
|
const runId = generateId();
|
|
1141
1223
|
aggregator.handle({ type: "RUN_STARTED", runId });
|
|
@@ -1151,7 +1233,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1151
1233
|
logger: this.logger,
|
|
1152
1234
|
onRunFailed: (error) => {
|
|
1153
1235
|
if (abortSignal.aborted) return;
|
|
1154
|
-
|
|
1236
|
+
pendingError = error;
|
|
1155
1237
|
invokeRuntimeCallback("onError", this.onError, error);
|
|
1156
1238
|
},
|
|
1157
1239
|
});
|
|
@@ -1171,36 +1253,31 @@ export class AgUiThreadRuntimeCore {
|
|
|
1171
1253
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1172
1254
|
dispatch({ type: "RUN_ERROR", message: err.message });
|
|
1173
1255
|
invokeRuntimeCallback("onError", this.onError, err);
|
|
1174
|
-
|
|
1256
|
+
pendingError ??= err;
|
|
1175
1257
|
}
|
|
1176
1258
|
} finally {
|
|
1177
1259
|
this.finishRun(abortController);
|
|
1178
1260
|
}
|
|
1179
1261
|
|
|
1180
|
-
if (
|
|
1181
|
-
const err =
|
|
1262
|
+
if (pendingError) {
|
|
1263
|
+
const err = pendingError;
|
|
1182
1264
|
this.reportedErrors.add(err);
|
|
1183
|
-
this.
|
|
1184
|
-
this.pendingResumeMessageId = null;
|
|
1185
|
-
this.pendingA2uiResume = false;
|
|
1186
|
-
this.pendingA2uiAction = undefined;
|
|
1265
|
+
this.clearDeferredContinuations(abortController);
|
|
1187
1266
|
throw err;
|
|
1188
1267
|
}
|
|
1189
1268
|
|
|
1190
1269
|
// A tool result that landed before the run settled deferred its
|
|
1191
1270
|
// continuation here so a second run never overlaps the first.
|
|
1192
|
-
if (this.
|
|
1193
|
-
const
|
|
1194
|
-
this.
|
|
1271
|
+
if (this.pendingResume?.owner === abortController) {
|
|
1272
|
+
const { messageId } = this.pendingResume;
|
|
1273
|
+
this.pendingResume = null;
|
|
1195
1274
|
if (!abortSignal.aborted) {
|
|
1196
|
-
this.startResumeRun(
|
|
1197
|
-
} else {
|
|
1198
|
-
this.pendingA2uiAction = undefined;
|
|
1275
|
+
this.startResumeRun(messageId);
|
|
1199
1276
|
}
|
|
1200
1277
|
}
|
|
1201
1278
|
|
|
1202
|
-
if (this.
|
|
1203
|
-
this.
|
|
1279
|
+
if (this.pendingA2uiResumeOwner === abortController) {
|
|
1280
|
+
this.pendingA2uiResumeOwner = null;
|
|
1204
1281
|
if (!abortSignal.aborted && this.pendingA2uiAction !== undefined) {
|
|
1205
1282
|
if (this.getPendingInterrupts()) {
|
|
1206
1283
|
this.pendingA2uiAction = undefined;
|
|
@@ -1238,9 +1315,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1238
1315
|
applyUpdate: (update: ChatModelRunResult) => void;
|
|
1239
1316
|
getAssistantMessageId: () => string | undefined;
|
|
1240
1317
|
},
|
|
1241
|
-
): Promise<
|
|
1318
|
+
): Promise<Error | undefined> {
|
|
1242
1319
|
this.pendingA2uiAction = undefined;
|
|
1243
|
-
this.
|
|
1320
|
+
this.pendingA2uiResumeOwner = null;
|
|
1244
1321
|
const assistantId = ctx.ensureAssistant();
|
|
1245
1322
|
const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
|
|
1246
1323
|
const options: ChatModelRunOptions = {
|
|
@@ -1264,36 +1341,39 @@ export class AgUiThreadRuntimeCore {
|
|
|
1264
1341
|
|
|
1265
1342
|
try {
|
|
1266
1343
|
for await (const result of stream(options)) {
|
|
1267
|
-
if (ctx.abortSignal.aborted) return;
|
|
1344
|
+
if (ctx.abortSignal.aborted) return undefined;
|
|
1268
1345
|
ctx.applyUpdate(result);
|
|
1269
1346
|
}
|
|
1270
1347
|
} catch (error) {
|
|
1271
|
-
if (ctx.abortSignal.aborted) return;
|
|
1348
|
+
if (ctx.abortSignal.aborted) return undefined;
|
|
1272
1349
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1273
1350
|
ctx.applyUpdate({
|
|
1274
1351
|
status: { type: "incomplete", reason: "error", error: err.message },
|
|
1275
1352
|
});
|
|
1276
1353
|
invokeRuntimeCallback("onError", this.onError, err);
|
|
1277
|
-
|
|
1278
|
-
return;
|
|
1354
|
+
return err;
|
|
1279
1355
|
}
|
|
1280
1356
|
|
|
1281
|
-
if (ctx.abortSignal.aborted) return;
|
|
1357
|
+
if (ctx.abortSignal.aborted) return undefined;
|
|
1282
1358
|
const current = this.session.tryGetMessage(currentId())?.message;
|
|
1283
1359
|
if (!current || current.status?.type === "running") {
|
|
1284
1360
|
ctx.applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1285
1361
|
}
|
|
1362
|
+
return undefined;
|
|
1286
1363
|
}
|
|
1287
1364
|
|
|
1288
1365
|
private buildRunInput(
|
|
1289
1366
|
runId: string,
|
|
1290
1367
|
runConfig: RunConfig | undefined,
|
|
1291
1368
|
historyMessages: readonly ThreadMessage[] | undefined,
|
|
1292
|
-
resume?:
|
|
1369
|
+
resume?: ResumeDispatch,
|
|
1293
1370
|
) {
|
|
1294
1371
|
const threadId = this.agent.threadId || "main";
|
|
1295
1372
|
const messages = toAgUiMessages(
|
|
1296
|
-
|
|
1373
|
+
this.selectRunMessages(
|
|
1374
|
+
historyMessages ?? this.session.getMessages(),
|
|
1375
|
+
resume,
|
|
1376
|
+
),
|
|
1297
1377
|
);
|
|
1298
1378
|
const context = this.runtime?.thread.getModelContext();
|
|
1299
1379
|
const input = {
|
|
@@ -1313,31 +1393,40 @@ export class AgUiThreadRuntimeCore {
|
|
|
1313
1393
|
? { a2uiAction: { userAction: this.pendingA2uiAction } }
|
|
1314
1394
|
: {}),
|
|
1315
1395
|
},
|
|
1316
|
-
...(resume !== undefined ? { resume } : {}),
|
|
1396
|
+
...(resume !== undefined ? { resume: resume.entries } : {}),
|
|
1317
1397
|
};
|
|
1318
1398
|
this.pendingA2uiAction = undefined;
|
|
1319
1399
|
return input;
|
|
1320
1400
|
}
|
|
1321
1401
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1402
|
+
/**
|
|
1403
|
+
* An anchor the thread no longer carries falls back to the whole transcript.
|
|
1404
|
+
* The anchor is missing exactly when a snapshot import replaced the messages
|
|
1405
|
+
* this resume was raised against, and a host that expects a delta tolerates
|
|
1406
|
+
* being told more than it knows, while one rebuilding the run from the
|
|
1407
|
+
* transcript cannot recover from being told less.
|
|
1408
|
+
*/
|
|
1409
|
+
private selectRunMessages(
|
|
1410
|
+
messages: readonly ThreadMessage[],
|
|
1411
|
+
resume: ResumeDispatch | undefined,
|
|
1412
|
+
): readonly ThreadMessage[] {
|
|
1413
|
+
if (resume === undefined || this.resumeTranscript !== "appended")
|
|
1414
|
+
return messages;
|
|
1415
|
+
const anchor = messages.findIndex(
|
|
1416
|
+
(message) => message.id === resume.interruptedMessageId,
|
|
1417
|
+
);
|
|
1418
|
+
if (anchor === -1) return messages;
|
|
1419
|
+
return messages.slice(anchor + 1);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private clearDeferredContinuations(owner: AbortController): void {
|
|
1423
|
+
if (this.pendingResume?.owner === owner) {
|
|
1424
|
+
this.pendingResume = null;
|
|
1425
|
+
}
|
|
1426
|
+
if (this.pendingA2uiResumeOwner === owner) {
|
|
1427
|
+
this.pendingA2uiResumeOwner = null;
|
|
1428
|
+
this.pendingA2uiAction = undefined;
|
|
1429
|
+
}
|
|
1341
1430
|
}
|
|
1342
1431
|
|
|
1343
1432
|
private setRunning(running: boolean) {
|
|
@@ -1346,10 +1435,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1346
1435
|
}
|
|
1347
1436
|
|
|
1348
1437
|
private finishRun(controller: AbortController | null) {
|
|
1349
|
-
if (this.abortController
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
}
|
|
1438
|
+
if (this.abortController !== controller) return;
|
|
1439
|
+
this.abortController = null;
|
|
1440
|
+
this.activeRunAgent = null;
|
|
1353
1441
|
this.setRunning(false);
|
|
1354
1442
|
}
|
|
1355
1443
|
|
|
@@ -1498,19 +1586,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1498
1586
|
next: ThreadAssistantMessage["content"],
|
|
1499
1587
|
): ThreadAssistantMessage["content"] {
|
|
1500
1588
|
const resolved = new Map<string, ToolCallMessagePart>();
|
|
1501
|
-
for (const part of previous) {
|
|
1502
|
-
if (
|
|
1589
|
+
for (const part of iterateToolCallParts(previous)) {
|
|
1590
|
+
if (isResolvedToolCall(part)) {
|
|
1503
1591
|
resolved.set(part.toolCallId, part);
|
|
1504
1592
|
}
|
|
1505
1593
|
}
|
|
1506
1594
|
if (resolved.size === 0) return next;
|
|
1507
1595
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
1596
|
+
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
1597
|
+
if (isResolvedToolCall(part)) return part;
|
|
1511
1598
|
const prior = resolved.get(part.toolCallId);
|
|
1512
1599
|
if (!prior) return part;
|
|
1513
|
-
changed = true;
|
|
1514
1600
|
return {
|
|
1515
1601
|
...part,
|
|
1516
1602
|
result: prior.result,
|
|
@@ -1518,7 +1604,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1518
1604
|
...(prior.isError !== undefined ? { isError: prior.isError } : {}),
|
|
1519
1605
|
};
|
|
1520
1606
|
});
|
|
1521
|
-
return changed ?
|
|
1607
|
+
return changed ? merged : next;
|
|
1522
1608
|
}
|
|
1523
1609
|
|
|
1524
1610
|
private mergeAssistantMetadata(
|
|
@@ -1623,9 +1709,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1623
1709
|
const assistant = message as ThreadAssistantMessage;
|
|
1624
1710
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
1625
1711
|
let matchedToolCall = false;
|
|
1626
|
-
const content = assistant.content
|
|
1627
|
-
if (part.
|
|
1628
|
-
return part;
|
|
1712
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
1713
|
+
if (part.toolCallId !== event.toolCallId) return part;
|
|
1629
1714
|
matchedToolCall = true;
|
|
1630
1715
|
// An applied activity snapshot owns part.result; a later result only
|
|
1631
1716
|
// fills what is missing, mirroring the aggregator's finishToolCall.
|
|
@@ -1709,9 +1794,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1709
1794
|
if (message.role !== "assistant") return message;
|
|
1710
1795
|
const assistant = message as ThreadAssistantMessage;
|
|
1711
1796
|
let matchedToolCall = false;
|
|
1712
|
-
const content = assistant.content
|
|
1713
|
-
if (part.
|
|
1714
|
-
return part;
|
|
1797
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
1798
|
+
if (part.toolCallId !== toolCallId) return part;
|
|
1715
1799
|
matchedToolCall = true;
|
|
1716
1800
|
return {
|
|
1717
1801
|
...part,
|