@assistant-ui/react-ag-ui 0.0.71 → 0.1.0
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 +1 -1
- package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
- package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +246 -159
- package/dist/runtime/AgUiThreadController.js.map +1 -0
- package/dist/runtime/adapter/conversions.d.ts +4 -4
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +111 -33
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -5
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +47 -61
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -24
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +1 -22
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/agUiThreadState.d.ts +60 -0
- package/dist/runtime/agUiThreadState.d.ts.map +1 -0
- package/dist/runtime/agUiThreadState.js +95 -0
- package/dist/runtime/agUiThreadState.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +45 -48
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/mcp-tool-result.d.ts +3 -1
- package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
- package/dist/runtime/mcp-tool-result.js +6 -3
- package/dist/runtime/mcp-tool-result.js.map +1 -1
- package/dist/runtime/types.d.ts +11 -35
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +2 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +12 -11
- package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
- package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
- package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +356 -171
- package/src/runtime/adapter/conversions.content.test.ts +37 -5
- package/src/runtime/adapter/conversions.tools.test.ts +2 -1
- package/src/runtime/adapter/conversions.ts +235 -58
- package/src/runtime/adapter/run-aggregator.ts +79 -114
- package/src/runtime/adapter/subscriber.ts +1 -44
- package/src/runtime/agUiThreadState.test.ts +205 -0
- package/src/runtime/agUiThreadState.ts +130 -0
- package/src/runtime/event-parser.ts +40 -58
- package/src/runtime/mcp-tool-result.ts +16 -2
- package/src/runtime/types.ts +16 -28
- package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
- package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
- package/src/useAgUiRuntime.ts +3 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
} from "@assistant-ui/core";
|
|
25
25
|
import {
|
|
26
26
|
appendToolInteraction,
|
|
27
|
+
createHistoryWriteChain,
|
|
27
28
|
createMessageRepositorySession,
|
|
28
29
|
invokeUserCallback,
|
|
29
30
|
iterateToolCallParts,
|
|
@@ -37,8 +38,14 @@ import type {
|
|
|
37
38
|
RunAgentResult,
|
|
38
39
|
} from "@ag-ui/client";
|
|
39
40
|
import jsonpatch, { type Operation } from "fast-json-patch";
|
|
41
|
+
import {
|
|
42
|
+
createAgUiThreadState,
|
|
43
|
+
reduceAgUiThreadState,
|
|
44
|
+
type AgUiThreadAction,
|
|
45
|
+
type AgUiThreadState,
|
|
46
|
+
} from "./agUiThreadState";
|
|
40
47
|
import type { Logger } from "./logger";
|
|
41
|
-
import { readMcpAppResourceUri } from "./mcp-tool-result";
|
|
48
|
+
import { readMcpAppResourceUri, toolContentText } from "./mcp-tool-result";
|
|
42
49
|
import type {
|
|
43
50
|
AgUiCustomMetadata,
|
|
44
51
|
AgUiEvent,
|
|
@@ -85,6 +92,39 @@ const isResolvedToolCall = (
|
|
|
85
92
|
): boolean =>
|
|
86
93
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
87
94
|
|
|
95
|
+
const listedPendingToolCallIds = (
|
|
96
|
+
assistant: ThreadAssistantMessage,
|
|
97
|
+
): ReadonlySet<string> | undefined => {
|
|
98
|
+
const ids = (
|
|
99
|
+
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
100
|
+
| AgUiCustomMetadata
|
|
101
|
+
| undefined
|
|
102
|
+
)?.pendingToolCallIds;
|
|
103
|
+
return ids?.length ? new Set(ids) : undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const withoutPendingToolCallIds = (
|
|
107
|
+
metadata: ThreadAssistantMessage["metadata"],
|
|
108
|
+
): ThreadAssistantMessage["metadata"] => {
|
|
109
|
+
const namespace = metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
110
|
+
| AgUiCustomMetadata
|
|
111
|
+
| undefined;
|
|
112
|
+
if (!namespace || !("pendingToolCallIds" in namespace)) return metadata;
|
|
113
|
+
const { pendingToolCallIds: _pendingToolCallIds, ...remainingNamespace } =
|
|
114
|
+
namespace;
|
|
115
|
+
const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } =
|
|
116
|
+
metadata.custom;
|
|
117
|
+
return {
|
|
118
|
+
...metadata,
|
|
119
|
+
custom: {
|
|
120
|
+
...remainingCustom,
|
|
121
|
+
...(Object.keys(remainingNamespace).length > 0
|
|
122
|
+
? { [AG_UI_METADATA_NAMESPACE]: remainingNamespace }
|
|
123
|
+
: {}),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
88
128
|
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
89
129
|
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
90
130
|
|
|
@@ -155,7 +195,7 @@ const mergeAgUiNamespace = (
|
|
|
155
195
|
return merged;
|
|
156
196
|
};
|
|
157
197
|
|
|
158
|
-
export class
|
|
198
|
+
export class AgUiThreadController {
|
|
159
199
|
private agent: AbstractAgent;
|
|
160
200
|
private logger: Logger;
|
|
161
201
|
private showThinking: boolean;
|
|
@@ -194,27 +234,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
194
234
|
};
|
|
195
235
|
},
|
|
196
236
|
});
|
|
197
|
-
private
|
|
237
|
+
private state: AgUiThreadState = createAgUiThreadState();
|
|
198
238
|
private abortController: AbortController | null = null;
|
|
199
239
|
// The agent that started the active run. updateOptions can swap this.agent
|
|
200
240
|
// mid-run, and cancelling has to reach the agent holding the live request.
|
|
201
241
|
private activeRunAgent: AbstractAgent | null = null;
|
|
202
|
-
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
203
242
|
private history: ThreadHistoryAdapter | undefined;
|
|
204
|
-
private lastRunConfig: RunConfig | undefined;
|
|
205
243
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
206
244
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
207
245
|
private readonly persistedHistoryIds = new Set<string>();
|
|
208
|
-
private readonly historyWrites =
|
|
209
|
-
private _isLoading = false;
|
|
246
|
+
private readonly historyWrites = createHistoryWriteChain();
|
|
210
247
|
private _historyImportPromise: Promise<void> | undefined;
|
|
211
|
-
private historyMutationGeneration = 0;
|
|
212
248
|
private _loadPromise: Promise<void> | undefined;
|
|
213
|
-
private _loadRequested = false;
|
|
214
|
-
private pendingResume: { owner: AbortController; messageId: string } | null =
|
|
215
|
-
null;
|
|
216
|
-
private pendingA2uiResumeOwner: AbortController | null = null;
|
|
217
|
-
private pendingA2uiAction: Record<string, unknown> | undefined;
|
|
218
249
|
|
|
219
250
|
constructor(options: CoreOptions) {
|
|
220
251
|
this.agent = options.agent;
|
|
@@ -237,12 +268,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
237
268
|
|
|
238
269
|
getSnapshot = () =>
|
|
239
270
|
(this.snapshot ??= {
|
|
240
|
-
isLoading: this.
|
|
241
|
-
isRunning: this.
|
|
271
|
+
isLoading: this.state.isLoading,
|
|
272
|
+
isRunning: this.state.isRunning,
|
|
242
273
|
messageRepository: this.session.export(),
|
|
243
|
-
state: this.stateSnapshot,
|
|
274
|
+
state: this.state.stateSnapshot,
|
|
244
275
|
});
|
|
245
276
|
|
|
277
|
+
private dispatch(action: AgUiThreadAction): void {
|
|
278
|
+
this.state = reduceAgUiThreadState(this.state, action);
|
|
279
|
+
}
|
|
280
|
+
|
|
246
281
|
private publishUpdate(): void {
|
|
247
282
|
this.snapshot = undefined;
|
|
248
283
|
for (const listener of this.listeners) listener();
|
|
@@ -261,7 +296,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
261
296
|
const previousHistory = this.history;
|
|
262
297
|
this.history = options.history;
|
|
263
298
|
if (
|
|
264
|
-
this.
|
|
299
|
+
this.state.loadRequested &&
|
|
265
300
|
!this._loadPromise &&
|
|
266
301
|
!previousHistory &&
|
|
267
302
|
options.history &&
|
|
@@ -291,19 +326,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
291
326
|
}
|
|
292
327
|
|
|
293
328
|
getState(): ReadonlyJSONValue | undefined {
|
|
294
|
-
return this.stateSnapshot;
|
|
329
|
+
return this.state.stateSnapshot;
|
|
295
330
|
}
|
|
296
331
|
|
|
297
332
|
isRunning(): boolean {
|
|
298
|
-
return this.
|
|
333
|
+
return this.state.isRunning;
|
|
299
334
|
}
|
|
300
335
|
|
|
301
336
|
get isLoading(): boolean {
|
|
302
|
-
return this.
|
|
337
|
+
return this.state.isLoading;
|
|
303
338
|
}
|
|
304
339
|
|
|
305
340
|
__internal_load(): Promise<void> {
|
|
306
|
-
this.
|
|
341
|
+
this.dispatch({ type: "loadRequested" });
|
|
307
342
|
if (this._loadPromise) return this._loadPromise;
|
|
308
343
|
if (!this.history) return Promise.resolve();
|
|
309
344
|
|
|
@@ -326,7 +361,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
326
361
|
return repo;
|
|
327
362
|
});
|
|
328
363
|
|
|
329
|
-
this.
|
|
364
|
+
this.dispatch({ type: "loadStarted" });
|
|
330
365
|
this._historyImportPromise = promise.then(
|
|
331
366
|
() => {
|
|
332
367
|
this._historyImportPromise = undefined;
|
|
@@ -343,7 +378,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
343
378
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
344
379
|
await this.startRun(
|
|
345
380
|
parentId,
|
|
346
|
-
this.lastRunConfig,
|
|
381
|
+
this.state.lastRunConfig,
|
|
347
382
|
undefined,
|
|
348
383
|
resumeStream,
|
|
349
384
|
);
|
|
@@ -354,7 +389,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
354
389
|
this.reportError(error);
|
|
355
390
|
})
|
|
356
391
|
.finally(() => {
|
|
357
|
-
this.
|
|
392
|
+
this.dispatch({
|
|
393
|
+
type: "loadFinished",
|
|
394
|
+
});
|
|
358
395
|
this.publishUpdate();
|
|
359
396
|
});
|
|
360
397
|
|
|
@@ -365,10 +402,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
365
402
|
private waitForHistoryImport(): Promise<boolean> | undefined {
|
|
366
403
|
const historyImport = this._historyImportPromise;
|
|
367
404
|
if (!historyImport) return undefined;
|
|
368
|
-
const generation = this.historyMutationGeneration;
|
|
405
|
+
const generation = this.state.historyMutationGeneration;
|
|
369
406
|
return historyImport.then(
|
|
370
407
|
() =>
|
|
371
|
-
generation === this.historyMutationGeneration &&
|
|
408
|
+
generation === this.state.historyMutationGeneration &&
|
|
372
409
|
!this.isThreadSwitching?.(),
|
|
373
410
|
);
|
|
374
411
|
}
|
|
@@ -376,10 +413,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
376
413
|
private deferUntilHistoryImported(callback: () => void): boolean {
|
|
377
414
|
const historyImport = this._historyImportPromise;
|
|
378
415
|
if (!historyImport) return false;
|
|
379
|
-
const generation = this.historyMutationGeneration;
|
|
416
|
+
const generation = this.state.historyMutationGeneration;
|
|
380
417
|
void historyImport.then(() => {
|
|
381
418
|
if (
|
|
382
|
-
generation !== this.historyMutationGeneration ||
|
|
419
|
+
generation !== this.state.historyMutationGeneration ||
|
|
383
420
|
this.isThreadSwitching?.()
|
|
384
421
|
) {
|
|
385
422
|
return;
|
|
@@ -397,10 +434,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
397
434
|
const historyImport = this._historyImportPromise;
|
|
398
435
|
if (historyImport) {
|
|
399
436
|
const wasAtTail = message.parentId === this.session.headId;
|
|
400
|
-
const generation = this.historyMutationGeneration;
|
|
437
|
+
const generation = this.state.historyMutationGeneration;
|
|
401
438
|
await historyImport;
|
|
402
439
|
if (
|
|
403
|
-
generation !== this.historyMutationGeneration ||
|
|
440
|
+
generation !== this.state.historyMutationGeneration ||
|
|
404
441
|
this.isThreadSwitching?.()
|
|
405
442
|
)
|
|
406
443
|
return;
|
|
@@ -434,10 +471,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
434
471
|
|
|
435
472
|
private maybeAutoCancelPendingToolCalls(): void {
|
|
436
473
|
if (this.autoCancelPendingToolCalls === false) return;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
474
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
475
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
476
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
477
|
+
}
|
|
441
478
|
}
|
|
442
479
|
|
|
443
480
|
private appendEntry(message: AppendMessage): string {
|
|
@@ -471,7 +508,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
471
508
|
}
|
|
472
509
|
|
|
473
510
|
async cancel(): Promise<void> {
|
|
474
|
-
this.
|
|
511
|
+
this.dispatch({
|
|
512
|
+
type: "historyMutationAdvanced",
|
|
513
|
+
});
|
|
475
514
|
this.abortActiveRun();
|
|
476
515
|
}
|
|
477
516
|
|
|
@@ -518,7 +557,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
518
557
|
this.assertNoPendingInterrupts();
|
|
519
558
|
await this.startRun(
|
|
520
559
|
config.parentId,
|
|
521
|
-
config.runConfig ?? this.lastRunConfig,
|
|
560
|
+
config.runConfig ?? this.state.lastRunConfig,
|
|
522
561
|
undefined,
|
|
523
562
|
config.stream,
|
|
524
563
|
);
|
|
@@ -541,7 +580,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
541
580
|
try {
|
|
542
581
|
await this.startRun(
|
|
543
582
|
parentId,
|
|
544
|
-
this.lastRunConfig,
|
|
583
|
+
this.state.lastRunConfig,
|
|
545
584
|
undefined,
|
|
546
585
|
resumeStream,
|
|
547
586
|
);
|
|
@@ -573,6 +612,25 @@ export class AgUiThreadRuntimeCore {
|
|
|
573
612
|
return assistant;
|
|
574
613
|
}
|
|
575
614
|
|
|
615
|
+
private getTrailingAssistantTurn(): ThreadAssistantMessage[] {
|
|
616
|
+
const messages = this.getMessages();
|
|
617
|
+
const turn: ThreadAssistantMessage[] = [];
|
|
618
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
619
|
+
const message = messages[index];
|
|
620
|
+
if (message?.role !== "assistant") break;
|
|
621
|
+
turn.unshift(message as ThreadAssistantMessage);
|
|
622
|
+
}
|
|
623
|
+
return turn;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private getPendingToolCallMessages(): ThreadAssistantMessage[] {
|
|
627
|
+
return this.getTrailingAssistantTurn().filter(
|
|
628
|
+
(message) =>
|
|
629
|
+
message.status?.type === "requires-action" &&
|
|
630
|
+
message.status.reason === "tool-calls",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
576
634
|
getPendingInterrupts(): {
|
|
577
635
|
messageId: string;
|
|
578
636
|
interrupts: readonly AgUiInterrupt[];
|
|
@@ -592,15 +650,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
592
650
|
messageId: string;
|
|
593
651
|
toolCallIds: string[];
|
|
594
652
|
} | null {
|
|
595
|
-
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
596
|
-
if (!assistant) return null;
|
|
597
653
|
const toolCallIds: string[] = [];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
654
|
+
let messageId: string | undefined;
|
|
655
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
656
|
+
const listed = listedPendingToolCallIds(assistant);
|
|
657
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
658
|
+
if (
|
|
659
|
+
isResolvedToolCall(part) ||
|
|
660
|
+
(listed && !listed.has(part.toolCallId))
|
|
661
|
+
)
|
|
662
|
+
continue;
|
|
663
|
+
messageId ??= assistant.id;
|
|
664
|
+
toolCallIds.push(part.toolCallId);
|
|
665
|
+
}
|
|
601
666
|
}
|
|
602
|
-
|
|
603
|
-
return { messageId: assistant.id, toolCallIds };
|
|
667
|
+
return messageId === undefined ? null : { messageId, toolCallIds };
|
|
604
668
|
}
|
|
605
669
|
|
|
606
670
|
async submitInterruptResponses(
|
|
@@ -632,6 +696,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
632
696
|
this.assertInterruptsAnswerable(
|
|
633
697
|
"submitInterruptResponses",
|
|
634
698
|
pending.interrupts,
|
|
699
|
+
responsesById,
|
|
635
700
|
);
|
|
636
701
|
|
|
637
702
|
await this.resumeWithResponses(
|
|
@@ -651,7 +716,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
651
716
|
resume: AgUiResumeEntry[],
|
|
652
717
|
): Promise<void> {
|
|
653
718
|
this.clearPendingInterrupts(messageId, resume);
|
|
654
|
-
await this.startRun(messageId, this.lastRunConfig, {
|
|
719
|
+
await this.startRun(messageId, this.state.lastRunConfig, {
|
|
655
720
|
entries: resume,
|
|
656
721
|
interruptedMessageId: messageId,
|
|
657
722
|
});
|
|
@@ -679,6 +744,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
679
744
|
private assertInterruptsAnswerable(
|
|
680
745
|
method: "submitInterruptResponses" | "respondToToolApproval",
|
|
681
746
|
interrupts: readonly AgUiInterrupt[],
|
|
747
|
+
responsesById?: ReadonlyMap<string, AgUiResumeEntry>,
|
|
682
748
|
): void {
|
|
683
749
|
const now = Date.now();
|
|
684
750
|
for (const interrupt of interrupts) {
|
|
@@ -689,13 +755,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
689
755
|
`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`,
|
|
690
756
|
);
|
|
691
757
|
}
|
|
692
|
-
if (
|
|
758
|
+
if (
|
|
759
|
+
expiry <= now &&
|
|
760
|
+
responsesById?.get(interrupt.id)?.status !== "cancelled"
|
|
761
|
+
) {
|
|
693
762
|
throw new Error(
|
|
694
763
|
`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`,
|
|
695
764
|
);
|
|
696
765
|
}
|
|
697
766
|
}
|
|
698
|
-
if (this.
|
|
767
|
+
if (this.state.isRunning) {
|
|
699
768
|
throw new Error(`[agui] ${method}: a run is already in progress`);
|
|
700
769
|
}
|
|
701
770
|
}
|
|
@@ -789,11 +858,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
789
858
|
"[agui] steerAway: responses are only valid for pending interrupts",
|
|
790
859
|
);
|
|
791
860
|
}
|
|
792
|
-
if (this.
|
|
861
|
+
if (this.state.isRunning) {
|
|
793
862
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
794
863
|
}
|
|
795
|
-
this.
|
|
796
|
-
|
|
864
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
865
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
866
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
867
|
+
}
|
|
797
868
|
const normalized = this.toAppendMessage(message);
|
|
798
869
|
const threadMessageId = this.appendEntry(normalized);
|
|
799
870
|
await this.startRun(threadMessageId, normalized.runConfig);
|
|
@@ -810,7 +881,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
810
881
|
|
|
811
882
|
const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
|
|
812
883
|
|
|
813
|
-
if (this.
|
|
884
|
+
if (this.state.isRunning) {
|
|
814
885
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
815
886
|
}
|
|
816
887
|
|
|
@@ -1067,7 +1138,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1067
1138
|
return;
|
|
1068
1139
|
}
|
|
1069
1140
|
|
|
1070
|
-
await this.
|
|
1141
|
+
await this.historyWrites.chain(sessionMessageId, () =>
|
|
1071
1142
|
history.update!({ parentId: item.parentId, message }),
|
|
1072
1143
|
);
|
|
1073
1144
|
}
|
|
@@ -1090,11 +1161,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1090
1161
|
if (!("timestamp" in userAction)) {
|
|
1091
1162
|
userAction.timestamp = new Date().toISOString();
|
|
1092
1163
|
}
|
|
1093
|
-
this.
|
|
1164
|
+
this.dispatch({
|
|
1165
|
+
type: "a2uiActionDeferred",
|
|
1166
|
+
action: userAction,
|
|
1167
|
+
});
|
|
1094
1168
|
|
|
1095
1169
|
const owner = this.abortController;
|
|
1096
1170
|
if (owner) {
|
|
1097
|
-
this.
|
|
1171
|
+
this.dispatch({
|
|
1172
|
+
type: "a2uiResumeDeferred",
|
|
1173
|
+
owner,
|
|
1174
|
+
});
|
|
1098
1175
|
return;
|
|
1099
1176
|
}
|
|
1100
1177
|
this.startResumeRun(parentId);
|
|
@@ -1105,15 +1182,25 @@ export class AgUiThreadRuntimeCore {
|
|
|
1105
1182
|
// run is still draining) or after it.
|
|
1106
1183
|
private maybeResumeAfterToolResults(messageId: string): void {
|
|
1107
1184
|
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
1185
|
+
if (this.getPendingInterrupts()) return;
|
|
1186
|
+
const turn = this.getTrailingAssistantTurn();
|
|
1187
|
+
if (!turn.some((message) => message.id === messageId)) return;
|
|
1188
|
+
if (this.getPendingToolCalls()) return;
|
|
1189
|
+
const headId = turn.at(-1)?.id;
|
|
1190
|
+
if (headId === undefined) return;
|
|
1108
1191
|
|
|
1109
1192
|
const owner = this.abortController;
|
|
1110
1193
|
if (owner) {
|
|
1111
1194
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
1112
1195
|
// closed). Defer until startRun's tail so we never start two runs.
|
|
1113
|
-
this.
|
|
1196
|
+
this.dispatch({
|
|
1197
|
+
type: "resumeDeferred",
|
|
1198
|
+
owner,
|
|
1199
|
+
messageId: headId,
|
|
1200
|
+
});
|
|
1114
1201
|
return;
|
|
1115
1202
|
}
|
|
1116
|
-
this.startResumeRun(
|
|
1203
|
+
this.startResumeRun(headId);
|
|
1117
1204
|
}
|
|
1118
1205
|
|
|
1119
1206
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
@@ -1127,7 +1214,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1127
1214
|
return false;
|
|
1128
1215
|
}
|
|
1129
1216
|
let allResolved = true;
|
|
1217
|
+
const listed = listedPendingToolCallIds(assistant);
|
|
1130
1218
|
for (const part of iterateToolCallParts(assistant.content)) {
|
|
1219
|
+
if (listed && !listed.has(part.toolCallId)) continue;
|
|
1131
1220
|
if (!isResolvedToolCall(part)) {
|
|
1132
1221
|
allResolved = false;
|
|
1133
1222
|
break;
|
|
@@ -1140,6 +1229,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1140
1229
|
? {
|
|
1141
1230
|
...(current as ThreadAssistantMessage),
|
|
1142
1231
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
1232
|
+
metadata: withoutPendingToolCallIds(current.metadata),
|
|
1143
1233
|
}
|
|
1144
1234
|
: current,
|
|
1145
1235
|
);
|
|
@@ -1150,7 +1240,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1150
1240
|
}
|
|
1151
1241
|
|
|
1152
1242
|
private startResumeRun(messageId: string): void {
|
|
1153
|
-
void this.startRun(messageId, this.lastRunConfig).catch((error) => {
|
|
1243
|
+
void this.startRun(messageId, this.state.lastRunConfig).catch((error) => {
|
|
1154
1244
|
this.reportError(error);
|
|
1155
1245
|
});
|
|
1156
1246
|
}
|
|
@@ -1167,8 +1257,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1167
1257
|
metadata: { ...message.metadata, modality },
|
|
1168
1258
|
} as ThreadMessage;
|
|
1169
1259
|
});
|
|
1170
|
-
this.
|
|
1171
|
-
|
|
1260
|
+
this.dispatch({
|
|
1261
|
+
type: "a2uiContinuationsCleared",
|
|
1262
|
+
});
|
|
1172
1263
|
this.assistantHistoryParents.clear();
|
|
1173
1264
|
|
|
1174
1265
|
if (messages.length === 0) {
|
|
@@ -1217,18 +1308,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
1217
1308
|
// continuation whose target survived the snapshot is still answerable.
|
|
1218
1309
|
// Membership is the head branch, not the repository: a soft merge leaves
|
|
1219
1310
|
// the messages it dropped behind as off-branch nodes.
|
|
1220
|
-
const resumeTarget = this.pendingResume?.messageId;
|
|
1311
|
+
const resumeTarget = this.state.pendingResume?.messageId;
|
|
1221
1312
|
if (
|
|
1222
1313
|
resumeTarget !== undefined &&
|
|
1223
1314
|
!this.session.getMessages().some((message) => message.id === resumeTarget)
|
|
1224
1315
|
) {
|
|
1225
|
-
this.
|
|
1316
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1226
1317
|
}
|
|
1227
1318
|
this.publishUpdate();
|
|
1228
1319
|
}
|
|
1229
1320
|
|
|
1230
1321
|
loadExternalState(state: ReadonlyJSONValue): void {
|
|
1231
|
-
this.
|
|
1322
|
+
this.dispatch({
|
|
1323
|
+
type: "stateSnapshot",
|
|
1324
|
+
snapshot: state,
|
|
1325
|
+
});
|
|
1232
1326
|
this.publishUpdate();
|
|
1233
1327
|
}
|
|
1234
1328
|
|
|
@@ -1237,25 +1331,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
1237
1331
|
| ReadonlyJSONValue
|
|
1238
1332
|
| ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
|
|
1239
1333
|
): void {
|
|
1240
|
-
|
|
1241
|
-
typeof next === "function" ? next(this.stateSnapshot) : next;
|
|
1334
|
+
const snapshot =
|
|
1335
|
+
typeof next === "function" ? next(this.state.stateSnapshot) : next;
|
|
1336
|
+
this.dispatch({
|
|
1337
|
+
type: "stateSnapshot",
|
|
1338
|
+
snapshot,
|
|
1339
|
+
});
|
|
1242
1340
|
this.publishUpdate();
|
|
1243
1341
|
}
|
|
1244
1342
|
|
|
1245
1343
|
resetState(): void {
|
|
1246
|
-
this.
|
|
1344
|
+
this.dispatch({ type: "resetState" });
|
|
1247
1345
|
this.publishUpdate();
|
|
1248
1346
|
}
|
|
1249
1347
|
|
|
1250
1348
|
resetThreadState(): void {
|
|
1251
|
-
this.
|
|
1349
|
+
this.dispatch({
|
|
1350
|
+
type: "historyMutationAdvanced",
|
|
1351
|
+
});
|
|
1252
1352
|
const controller = this.abortController;
|
|
1253
1353
|
const activeRunAgent = this.activeRunAgent;
|
|
1254
1354
|
|
|
1255
|
-
this.
|
|
1256
|
-
this.pendingResume = null;
|
|
1257
|
-
this.pendingA2uiResumeOwner = null;
|
|
1258
|
-
this.pendingA2uiAction = undefined;
|
|
1355
|
+
this.dispatch({ type: "resetThread" });
|
|
1259
1356
|
|
|
1260
1357
|
if (controller) {
|
|
1261
1358
|
this.abortController = null;
|
|
@@ -1286,7 +1383,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1286
1383
|
if (!this.supersedeActiveRun()) return;
|
|
1287
1384
|
|
|
1288
1385
|
const normalizedRunConfig = runConfig ?? {};
|
|
1289
|
-
this.
|
|
1386
|
+
this.dispatch({
|
|
1387
|
+
type: "runConfigRecorded",
|
|
1388
|
+
runConfig: normalizedRunConfig,
|
|
1389
|
+
});
|
|
1290
1390
|
const parent =
|
|
1291
1391
|
parentId === null ? undefined : this.session.tryGetMessage(parentId);
|
|
1292
1392
|
const shouldEagerlyInsertAssistant =
|
|
@@ -1315,6 +1415,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1315
1415
|
// resurrecting for custom-event data would leave a trailing data-only
|
|
1316
1416
|
// duplicate.
|
|
1317
1417
|
let assistantCollided = false;
|
|
1418
|
+
const heldMessages = new Map<string, Set<string>>();
|
|
1419
|
+
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1420
|
+
let settlingRun = false;
|
|
1318
1421
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1319
1422
|
const cached = assistantMessageId;
|
|
1320
1423
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1344,7 +1447,66 @@ export class AgUiThreadRuntimeCore {
|
|
|
1344
1447
|
|
|
1345
1448
|
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1346
1449
|
|
|
1450
|
+
const settleHeldMessages = (terminalStatus: MessageStatus): string[] => {
|
|
1451
|
+
const pendingMessages: string[] = [];
|
|
1452
|
+
for (const [messageId, openToolCallIds] of heldMessages) {
|
|
1453
|
+
let requiresToolResult = false;
|
|
1454
|
+
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1455
|
+
if (message.role !== "assistant") return message;
|
|
1456
|
+
const assistant = message as ThreadAssistantMessage;
|
|
1457
|
+
const pendingToolCallIds = Array.from(openToolCallIds).filter(
|
|
1458
|
+
(id) =>
|
|
1459
|
+
!restrictedPendingToolCallIds ||
|
|
1460
|
+
restrictedPendingToolCallIds.has(id),
|
|
1461
|
+
);
|
|
1462
|
+
const canWaitForTools =
|
|
1463
|
+
terminalStatus.type === "complete" ||
|
|
1464
|
+
terminalStatus.type === "requires-action";
|
|
1465
|
+
const status: MessageStatus = canWaitForTools
|
|
1466
|
+
? pendingToolCallIds.length > 0
|
|
1467
|
+
? { type: "requires-action", reason: "tool-calls" }
|
|
1468
|
+
: { type: "complete", reason: "unknown" }
|
|
1469
|
+
: terminalStatus;
|
|
1470
|
+
requiresToolResult = status.type === "requires-action";
|
|
1471
|
+
const metadata = withoutPendingToolCallIds(assistant.metadata);
|
|
1472
|
+
return {
|
|
1473
|
+
...assistant,
|
|
1474
|
+
status,
|
|
1475
|
+
metadata:
|
|
1476
|
+
status.type === "requires-action" && restrictedPendingToolCallIds
|
|
1477
|
+
? {
|
|
1478
|
+
...metadata,
|
|
1479
|
+
custom: {
|
|
1480
|
+
...metadata.custom,
|
|
1481
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
1482
|
+
...(metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
1483
|
+
| AgUiCustomMetadata
|
|
1484
|
+
| undefined),
|
|
1485
|
+
pendingToolCallIds,
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
}
|
|
1489
|
+
: metadata,
|
|
1490
|
+
};
|
|
1491
|
+
});
|
|
1492
|
+
if (updated) {
|
|
1493
|
+
this.publishUpdate();
|
|
1494
|
+
this.persistAssistantHistory(messageId);
|
|
1495
|
+
if (requiresToolResult) {
|
|
1496
|
+
pendingMessages.push(messageId);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
heldMessages.clear();
|
|
1501
|
+
return pendingMessages;
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1347
1504
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1505
|
+
const terminalStatus = update.status;
|
|
1506
|
+
const pendingMessages =
|
|
1507
|
+
settlingRun && terminalStatus && terminalStatus.type !== "running"
|
|
1508
|
+
? settleHeldMessages(terminalStatus)
|
|
1509
|
+
: [];
|
|
1348
1510
|
const hasStreamContent =
|
|
1349
1511
|
Array.isArray(update.content) &&
|
|
1350
1512
|
update.content.some((part) => part.type !== "data");
|
|
@@ -1355,14 +1517,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
1355
1517
|
if (resolved !== assistantMessageId) {
|
|
1356
1518
|
assistantMessageId = resolved;
|
|
1357
1519
|
}
|
|
1520
|
+
for (const messageId of pendingMessages) {
|
|
1521
|
+
this.maybeResumeAfterToolResults(messageId);
|
|
1522
|
+
}
|
|
1358
1523
|
};
|
|
1359
1524
|
|
|
1360
1525
|
const adoptServerMessageId = (
|
|
1361
1526
|
serverId: string,
|
|
1362
1527
|
startNewMessage: boolean,
|
|
1528
|
+
openToolCallIds: string[] = [],
|
|
1363
1529
|
) => {
|
|
1364
1530
|
if (startNewMessage && assistantMessageId !== undefined) {
|
|
1365
|
-
|
|
1531
|
+
if (openToolCallIds.length > 0) {
|
|
1532
|
+
heldMessages.set(assistantMessageId, new Set(openToolCallIds));
|
|
1533
|
+
} else {
|
|
1534
|
+
applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1535
|
+
}
|
|
1366
1536
|
const previousId = assistantMessageId;
|
|
1367
1537
|
assistantMessageId = undefined;
|
|
1368
1538
|
if (this.session.tryGetMessage(previousId)) {
|
|
@@ -1404,22 +1574,43 @@ export class AgUiThreadRuntimeCore {
|
|
|
1404
1574
|
onServerMessageId: (serverId) => {
|
|
1405
1575
|
adoptServerMessageId(serverId, false);
|
|
1406
1576
|
},
|
|
1407
|
-
onTextMessageStart: (serverId) =>
|
|
1577
|
+
onTextMessageStart: (serverId, openToolCallIds) =>
|
|
1578
|
+
adoptServerMessageId(serverId, true, openToolCallIds),
|
|
1408
1579
|
});
|
|
1409
1580
|
let runFinished = false;
|
|
1410
1581
|
const dispatch = (event: AgUiEvent) => {
|
|
1411
1582
|
if (this.abortController !== abortController) return;
|
|
1412
|
-
if (event.type === "RUN_FINISHED")
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
)
|
|
1421
|
-
|
|
1422
|
-
|
|
1583
|
+
if (event.type === "RUN_FINISHED") {
|
|
1584
|
+
runFinished = true;
|
|
1585
|
+
const ids =
|
|
1586
|
+
event.outcome?.type === "success"
|
|
1587
|
+
? event.outcome.pendingToolCallIds
|
|
1588
|
+
: undefined;
|
|
1589
|
+
restrictedPendingToolCallIds = ids?.length ? new Set(ids) : undefined;
|
|
1590
|
+
}
|
|
1591
|
+
if (event.type === "TOOL_CALL_RESULT") {
|
|
1592
|
+
for (const openToolCallIds of heldMessages.values()) {
|
|
1593
|
+
openToolCallIds.delete(event.toolCallId);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
settlingRun =
|
|
1597
|
+
event.type === "RUN_FINISHED" ||
|
|
1598
|
+
event.type === "RUN_ERROR" ||
|
|
1599
|
+
event.type === "RUN_CANCELLED";
|
|
1600
|
+
try {
|
|
1601
|
+
const nextAssistantMessageId = this.handleEvent(
|
|
1602
|
+
aggregator,
|
|
1603
|
+
event,
|
|
1604
|
+
assistantMessageId,
|
|
1605
|
+
joinStrategy,
|
|
1606
|
+
runStartMessageIds,
|
|
1607
|
+
hasRunUserBoundary,
|
|
1608
|
+
);
|
|
1609
|
+
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
1610
|
+
assistantMessageId = nextAssistantMessageId;
|
|
1611
|
+
}
|
|
1612
|
+
} finally {
|
|
1613
|
+
settlingRun = false;
|
|
1423
1614
|
}
|
|
1424
1615
|
};
|
|
1425
1616
|
|
|
@@ -1482,9 +1673,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1482
1673
|
},
|
|
1483
1674
|
});
|
|
1484
1675
|
try {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1676
|
+
runAgentInstance.messages = input.messages;
|
|
1677
|
+
runAgentInstance.threadId = input.threadId;
|
|
1678
|
+
runAgentInstance.state = input.state ?? null;
|
|
1488
1679
|
} catch {
|
|
1489
1680
|
// ignore
|
|
1490
1681
|
}
|
|
@@ -1514,19 +1705,23 @@ export class AgUiThreadRuntimeCore {
|
|
|
1514
1705
|
|
|
1515
1706
|
// A tool result that landed before the run settled deferred its
|
|
1516
1707
|
// continuation here so a second run never overlaps the first.
|
|
1517
|
-
if (this.pendingResume?.owner === abortController) {
|
|
1518
|
-
const { messageId } = this.pendingResume;
|
|
1519
|
-
this.
|
|
1708
|
+
if (this.state.pendingResume?.owner === abortController) {
|
|
1709
|
+
const { messageId } = this.state.pendingResume;
|
|
1710
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1520
1711
|
if (!abortSignal.aborted) {
|
|
1521
1712
|
this.startResumeRun(messageId);
|
|
1522
1713
|
}
|
|
1523
1714
|
}
|
|
1524
1715
|
|
|
1525
|
-
if (this.pendingA2uiResumeOwner === abortController) {
|
|
1526
|
-
this.
|
|
1527
|
-
|
|
1716
|
+
if (this.state.pendingA2uiResumeOwner === abortController) {
|
|
1717
|
+
this.dispatch({
|
|
1718
|
+
type: "a2uiResumeCleared",
|
|
1719
|
+
});
|
|
1720
|
+
if (!abortSignal.aborted && this.state.pendingA2uiAction !== undefined) {
|
|
1528
1721
|
if (this.getPendingInterrupts()) {
|
|
1529
|
-
this.
|
|
1722
|
+
this.dispatch({
|
|
1723
|
+
type: "a2uiActionCleared",
|
|
1724
|
+
});
|
|
1530
1725
|
this.logger.debug(
|
|
1531
1726
|
"[agui] sendA2uiAction: pending interrupts, dropping action",
|
|
1532
1727
|
);
|
|
@@ -1537,13 +1732,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1537
1732
|
if (parentId !== null) {
|
|
1538
1733
|
this.startResumeRun(parentId);
|
|
1539
1734
|
} else {
|
|
1540
|
-
this.
|
|
1735
|
+
this.dispatch({
|
|
1736
|
+
type: "a2uiActionCleared",
|
|
1737
|
+
});
|
|
1541
1738
|
this.logger.debug(
|
|
1542
1739
|
"[agui] sendA2uiAction: no messages to resume, dropping action",
|
|
1543
1740
|
);
|
|
1544
1741
|
}
|
|
1545
1742
|
} else {
|
|
1546
|
-
this.
|
|
1743
|
+
this.dispatch({
|
|
1744
|
+
type: "a2uiActionCleared",
|
|
1745
|
+
});
|
|
1547
1746
|
}
|
|
1548
1747
|
}
|
|
1549
1748
|
}
|
|
@@ -1562,8 +1761,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1562
1761
|
getAssistantMessageId: () => string | undefined;
|
|
1563
1762
|
},
|
|
1564
1763
|
): Promise<Error | undefined> {
|
|
1565
|
-
this.
|
|
1566
|
-
|
|
1764
|
+
this.dispatch({
|
|
1765
|
+
type: "a2uiContinuationsCleared",
|
|
1766
|
+
});
|
|
1567
1767
|
const assistantId = ctx.ensureAssistant();
|
|
1568
1768
|
const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
|
|
1569
1769
|
const options: ChatModelRunOptions = {
|
|
@@ -1614,8 +1814,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1614
1814
|
historyMessages: readonly ThreadMessage[] | undefined,
|
|
1615
1815
|
resume?: ResumeDispatch,
|
|
1616
1816
|
) {
|
|
1617
|
-
const pendingA2uiAction = this.pendingA2uiAction;
|
|
1618
|
-
this.
|
|
1817
|
+
const pendingA2uiAction = this.state.pendingA2uiAction;
|
|
1818
|
+
this.dispatch({
|
|
1819
|
+
type: "a2uiActionCleared",
|
|
1820
|
+
});
|
|
1619
1821
|
const threadId = this.agent.threadId || "main";
|
|
1620
1822
|
const messages = toAgUiMessages(
|
|
1621
1823
|
this.selectRunMessages(
|
|
@@ -1635,7 +1837,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1635
1837
|
const input = {
|
|
1636
1838
|
threadId,
|
|
1637
1839
|
runId,
|
|
1638
|
-
state: this.stateSnapshot ?? null,
|
|
1840
|
+
state: this.state.stateSnapshot ?? null,
|
|
1639
1841
|
messages,
|
|
1640
1842
|
tools: toAgUiTools(context?.tools),
|
|
1641
1843
|
context: [
|
|
@@ -1678,17 +1880,20 @@ export class AgUiThreadRuntimeCore {
|
|
|
1678
1880
|
}
|
|
1679
1881
|
|
|
1680
1882
|
private clearDeferredContinuations(owner: AbortController): void {
|
|
1681
|
-
if (this.pendingResume?.owner === owner) {
|
|
1682
|
-
this.
|
|
1883
|
+
if (this.state.pendingResume?.owner === owner) {
|
|
1884
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1683
1885
|
}
|
|
1684
|
-
if (this.pendingA2uiResumeOwner === owner) {
|
|
1685
|
-
this.
|
|
1686
|
-
|
|
1886
|
+
if (this.state.pendingA2uiResumeOwner === owner) {
|
|
1887
|
+
this.dispatch({
|
|
1888
|
+
type: "a2uiContinuationsCleared",
|
|
1889
|
+
});
|
|
1687
1890
|
}
|
|
1688
1891
|
}
|
|
1689
1892
|
|
|
1690
1893
|
private setRunning(running: boolean) {
|
|
1691
|
-
this.
|
|
1894
|
+
this.dispatch({
|
|
1895
|
+
type: running ? "runStarted" : "runFinished",
|
|
1896
|
+
});
|
|
1692
1897
|
this.publishUpdate();
|
|
1693
1898
|
}
|
|
1694
1899
|
|
|
@@ -1710,7 +1915,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1710
1915
|
status: { type: "running" },
|
|
1711
1916
|
content: [],
|
|
1712
1917
|
metadata: {
|
|
1713
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
1918
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
1714
1919
|
unstable_annotations: [],
|
|
1715
1920
|
unstable_data: [],
|
|
1716
1921
|
steps: [],
|
|
@@ -1768,22 +1973,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
1768
1973
|
// An in-flight write completes under the id it was started with, so the
|
|
1769
1974
|
// rename moves the chain entry and transfers the completion mark; without
|
|
1770
1975
|
// this the resolve records the dead id and the live one appends again.
|
|
1771
|
-
const pendingWrite = this.historyWrites.
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
this.historyWrites.delete(newId);
|
|
1779
|
-
}
|
|
1780
|
-
};
|
|
1781
|
-
void pendingWrite.then(() => {
|
|
1976
|
+
const pendingWrite = this.historyWrites.move(
|
|
1977
|
+
oldId,
|
|
1978
|
+
collidesWithExisting ? undefined : newId,
|
|
1979
|
+
);
|
|
1980
|
+
if (pendingWrite && !collidesWithExisting) {
|
|
1981
|
+
void pendingWrite.then(
|
|
1982
|
+
() => {
|
|
1782
1983
|
this.persistedHistoryIds.delete(oldId);
|
|
1783
1984
|
this.persistedHistoryIds.add(newId);
|
|
1784
|
-
|
|
1785
|
-
},
|
|
1786
|
-
|
|
1985
|
+
},
|
|
1986
|
+
() => {},
|
|
1987
|
+
);
|
|
1787
1988
|
}
|
|
1788
1989
|
|
|
1789
1990
|
this.publishUpdate();
|
|
@@ -1798,10 +1999,15 @@ export class AgUiThreadRuntimeCore {
|
|
|
1798
1999
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
1799
2000
|
if (message.role !== "assistant") return message;
|
|
1800
2001
|
const assistant = message as ThreadAssistantMessage;
|
|
1801
|
-
const
|
|
2002
|
+
const mergedMetadata = update.metadata
|
|
1802
2003
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
1803
2004
|
: assistant.metadata;
|
|
1804
2005
|
latestStatus = update.status ?? assistant.status;
|
|
2006
|
+
const metadata =
|
|
2007
|
+
latestStatus?.type === "requires-action" &&
|
|
2008
|
+
latestStatus.reason === "tool-calls"
|
|
2009
|
+
? mergedMetadata
|
|
2010
|
+
: withoutPendingToolCallIds(mergedMetadata);
|
|
1805
2011
|
const content =
|
|
1806
2012
|
update.content !== undefined
|
|
1807
2013
|
? this.preserveToolResults(
|
|
@@ -2057,21 +2263,26 @@ export class AgUiThreadRuntimeCore {
|
|
|
2057
2263
|
): string | undefined {
|
|
2058
2264
|
switch (event.type) {
|
|
2059
2265
|
case "STATE_SNAPSHOT": {
|
|
2060
|
-
this.
|
|
2266
|
+
this.dispatch({
|
|
2267
|
+
type: "stateSnapshot",
|
|
2268
|
+
snapshot: event.snapshot as ReadonlyJSONValue,
|
|
2269
|
+
});
|
|
2061
2270
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2062
2271
|
break;
|
|
2063
2272
|
}
|
|
2064
2273
|
case "STATE_DELTA": {
|
|
2065
2274
|
if (event.delta.length === 0) break;
|
|
2066
2275
|
try {
|
|
2067
|
-
const state = this.stateSnapshot ?? {};
|
|
2068
2276
|
const result = jsonpatch.applyPatch(
|
|
2069
|
-
state,
|
|
2277
|
+
this.state.stateSnapshot ?? {},
|
|
2070
2278
|
event.delta as Operation[],
|
|
2071
2279
|
/* validateOperation */ true,
|
|
2072
2280
|
/* mutateDocument */ false,
|
|
2073
2281
|
);
|
|
2074
|
-
this.
|
|
2282
|
+
this.dispatch({
|
|
2283
|
+
type: "stateSnapshot",
|
|
2284
|
+
snapshot: result.newDocument as ReadonlyJSONValue,
|
|
2285
|
+
});
|
|
2075
2286
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2076
2287
|
} catch (error) {
|
|
2077
2288
|
this.logger.error?.("[agui] failed to apply state delta", error);
|
|
@@ -2156,7 +2367,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2156
2367
|
...assistant,
|
|
2157
2368
|
metadata: {
|
|
2158
2369
|
...assistant.metadata,
|
|
2159
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
2370
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
2160
2371
|
},
|
|
2161
2372
|
};
|
|
2162
2373
|
});
|
|
@@ -2172,6 +2383,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2172
2383
|
if (message.role !== "assistant") return message;
|
|
2173
2384
|
const assistant = message as ThreadAssistantMessage;
|
|
2174
2385
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
2386
|
+
const modelText = toolContentText(event.content);
|
|
2175
2387
|
let matchedToolCall = false;
|
|
2176
2388
|
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
2177
2389
|
if (part.toolCallId !== event.toolCallId) return part;
|
|
@@ -2188,11 +2400,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2188
2400
|
if (snapshotResultApplied) {
|
|
2189
2401
|
return {
|
|
2190
2402
|
...part,
|
|
2191
|
-
...(part.modelContent === undefined
|
|
2403
|
+
...(part.modelContent === undefined
|
|
2192
2404
|
? {
|
|
2193
|
-
modelContent: [
|
|
2194
|
-
{ type: "text" as const, text: event.content },
|
|
2195
|
-
],
|
|
2405
|
+
modelContent: [{ type: "text" as const, text: modelText }],
|
|
2196
2406
|
}
|
|
2197
2407
|
: {}),
|
|
2198
2408
|
...(part.isError === undefined
|
|
@@ -2207,12 +2417,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
2207
2417
|
: {}),
|
|
2208
2418
|
};
|
|
2209
2419
|
}
|
|
2420
|
+
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
2421
|
+
part;
|
|
2210
2422
|
return {
|
|
2211
|
-
...
|
|
2423
|
+
...(event.mcpResult === undefined && Array.isArray(event.content)
|
|
2424
|
+
? partWithoutModelContent
|
|
2425
|
+
: part),
|
|
2212
2426
|
result: (event.mcpResult ??
|
|
2213
|
-
|
|
2427
|
+
(Array.isArray(event.content)
|
|
2428
|
+
? event.content
|
|
2429
|
+
: tryParseJSON(event.content))) as ReadonlyJSONValue,
|
|
2214
2430
|
...(event.mcpResult !== undefined
|
|
2215
|
-
? { modelContent: [{ type: "text" as const, text:
|
|
2431
|
+
? { modelContent: [{ type: "text" as const, text: modelText }] }
|
|
2216
2432
|
: {}),
|
|
2217
2433
|
...(typeof event.mcpResult?.isError === "boolean"
|
|
2218
2434
|
? { isError: event.mcpResult.isError }
|
|
@@ -2531,7 +2747,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2531
2747
|
(wasPersisted || this.snapshotHistoryIds.has(messageId));
|
|
2532
2748
|
|
|
2533
2749
|
if (shouldUpdate) {
|
|
2534
|
-
const write = this.
|
|
2750
|
+
const write = this.historyWrites.chain(messageId, () =>
|
|
2535
2751
|
update.call(history, { parentId, message }),
|
|
2536
2752
|
);
|
|
2537
2753
|
this.assistantHistoryParents.delete(messageId);
|
|
@@ -2540,7 +2756,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2540
2756
|
this.persistedHistoryIds.add(messageId);
|
|
2541
2757
|
},
|
|
2542
2758
|
(error) => {
|
|
2543
|
-
const pending = this.historyWrites.
|
|
2759
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2544
2760
|
if (pending === undefined || pending === write) {
|
|
2545
2761
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2546
2762
|
}
|
|
@@ -2561,7 +2777,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2561
2777
|
void write.then(
|
|
2562
2778
|
() => {},
|
|
2563
2779
|
(error) => {
|
|
2564
|
-
const pending = this.historyWrites.
|
|
2780
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2565
2781
|
if (pending === undefined || pending === write) {
|
|
2566
2782
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2567
2783
|
}
|
|
@@ -2575,11 +2791,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
2575
2791
|
message: ThreadMessage,
|
|
2576
2792
|
): Promise<void> | undefined {
|
|
2577
2793
|
if (!this.history || this.persistedHistoryIds.has(message.id)) return;
|
|
2578
|
-
const pending = this.historyWrites.
|
|
2794
|
+
const pending = this.historyWrites.pending(message.id);
|
|
2579
2795
|
if (pending) return pending;
|
|
2580
2796
|
|
|
2581
2797
|
const append = this.history.append.bind(this.history);
|
|
2582
|
-
const write = this.
|
|
2798
|
+
const write = this.historyWrites.chain(message.id, () =>
|
|
2583
2799
|
append({ parentId, message }),
|
|
2584
2800
|
);
|
|
2585
2801
|
void write.then(
|
|
@@ -2590,35 +2806,4 @@ export class AgUiThreadRuntimeCore {
|
|
|
2590
2806
|
);
|
|
2591
2807
|
return write;
|
|
2592
2808
|
}
|
|
2593
|
-
|
|
2594
|
-
private chainHistoryWrite(
|
|
2595
|
-
id: string,
|
|
2596
|
-
write: () => Promise<void>,
|
|
2597
|
-
): Promise<void> {
|
|
2598
|
-
const pending = this.historyWrites.get(id);
|
|
2599
|
-
let next: Promise<void>;
|
|
2600
|
-
if (pending) {
|
|
2601
|
-
next = pending.then(write, write);
|
|
2602
|
-
} else {
|
|
2603
|
-
try {
|
|
2604
|
-
next = Promise.resolve(write());
|
|
2605
|
-
} catch (error) {
|
|
2606
|
-
next = Promise.reject(error);
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
this.historyWrites.set(id, next);
|
|
2610
|
-
void next.then(
|
|
2611
|
-
() => {
|
|
2612
|
-
if (this.historyWrites.get(id) === next) {
|
|
2613
|
-
this.historyWrites.delete(id);
|
|
2614
|
-
}
|
|
2615
|
-
},
|
|
2616
|
-
() => {
|
|
2617
|
-
if (this.historyWrites.get(id) === next) {
|
|
2618
|
-
this.historyWrites.delete(id);
|
|
2619
|
-
}
|
|
2620
|
-
},
|
|
2621
|
-
);
|
|
2622
|
-
return next;
|
|
2623
|
-
}
|
|
2624
2809
|
}
|