@assistant-ui/react-ag-ui 0.0.70 → 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} +8 -12
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
- package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +302 -162
- 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 +6 -5
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +50 -62
- 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 +13 -35
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +4 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +13 -12
- 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} +471 -169
- 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 +84 -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 +18 -28
- package/src/useAgUiRuntime.external-store.test.tsx +39 -0
- package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
- package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
- package/src/useAgUiRuntime.ts +5 -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
|
|
|
@@ -112,6 +152,7 @@ type CoreOptions = {
|
|
|
112
152
|
agent: AbstractAgent;
|
|
113
153
|
logger: Logger;
|
|
114
154
|
showThinking: boolean;
|
|
155
|
+
joinStrategy?: UseAgUiRuntimeOptions["joinStrategy"];
|
|
115
156
|
getRunContext?: UseAgUiRuntimeOptions["getRunContext"];
|
|
116
157
|
resumeTranscript?: AgUiResumeTranscript | undefined;
|
|
117
158
|
autoCancelPendingToolCalls?: boolean | undefined;
|
|
@@ -154,10 +195,11 @@ const mergeAgUiNamespace = (
|
|
|
154
195
|
return merged;
|
|
155
196
|
};
|
|
156
197
|
|
|
157
|
-
export class
|
|
198
|
+
export class AgUiThreadController {
|
|
158
199
|
private agent: AbstractAgent;
|
|
159
200
|
private logger: Logger;
|
|
160
201
|
private showThinking: boolean;
|
|
202
|
+
private joinStrategy: UseAgUiRuntimeOptions["joinStrategy"];
|
|
161
203
|
private getRunContext: UseAgUiRuntimeOptions["getRunContext"];
|
|
162
204
|
private resumeTranscript: AgUiResumeTranscript;
|
|
163
205
|
private autoCancelPendingToolCalls: boolean | undefined;
|
|
@@ -192,32 +234,24 @@ export class AgUiThreadRuntimeCore {
|
|
|
192
234
|
};
|
|
193
235
|
},
|
|
194
236
|
});
|
|
195
|
-
private
|
|
237
|
+
private state: AgUiThreadState = createAgUiThreadState();
|
|
196
238
|
private abortController: AbortController | null = null;
|
|
197
239
|
// The agent that started the active run. updateOptions can swap this.agent
|
|
198
240
|
// mid-run, and cancelling has to reach the agent holding the live request.
|
|
199
241
|
private activeRunAgent: AbstractAgent | null = null;
|
|
200
|
-
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
201
242
|
private history: ThreadHistoryAdapter | undefined;
|
|
202
|
-
private lastRunConfig: RunConfig | undefined;
|
|
203
243
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
204
244
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
205
245
|
private readonly persistedHistoryIds = new Set<string>();
|
|
206
|
-
private readonly historyWrites =
|
|
207
|
-
private _isLoading = false;
|
|
246
|
+
private readonly historyWrites = createHistoryWriteChain();
|
|
208
247
|
private _historyImportPromise: Promise<void> | undefined;
|
|
209
|
-
private historyMutationGeneration = 0;
|
|
210
248
|
private _loadPromise: Promise<void> | undefined;
|
|
211
|
-
private _loadRequested = false;
|
|
212
|
-
private pendingResume: { owner: AbortController; messageId: string } | null =
|
|
213
|
-
null;
|
|
214
|
-
private pendingA2uiResumeOwner: AbortController | null = null;
|
|
215
|
-
private pendingA2uiAction: Record<string, unknown> | undefined;
|
|
216
249
|
|
|
217
250
|
constructor(options: CoreOptions) {
|
|
218
251
|
this.agent = options.agent;
|
|
219
252
|
this.logger = options.logger;
|
|
220
253
|
this.showThinking = options.showThinking;
|
|
254
|
+
this.joinStrategy = options.joinStrategy;
|
|
221
255
|
this.getRunContext = options.getRunContext;
|
|
222
256
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
223
257
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
@@ -234,12 +268,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
234
268
|
|
|
235
269
|
getSnapshot = () =>
|
|
236
270
|
(this.snapshot ??= {
|
|
237
|
-
isLoading: this.
|
|
238
|
-
isRunning: this.
|
|
271
|
+
isLoading: this.state.isLoading,
|
|
272
|
+
isRunning: this.state.isRunning,
|
|
239
273
|
messageRepository: this.session.export(),
|
|
240
|
-
state: this.stateSnapshot,
|
|
274
|
+
state: this.state.stateSnapshot,
|
|
241
275
|
});
|
|
242
276
|
|
|
277
|
+
private dispatch(action: AgUiThreadAction): void {
|
|
278
|
+
this.state = reduceAgUiThreadState(this.state, action);
|
|
279
|
+
}
|
|
280
|
+
|
|
243
281
|
private publishUpdate(): void {
|
|
244
282
|
this.snapshot = undefined;
|
|
245
283
|
for (const listener of this.listeners) listener();
|
|
@@ -249,6 +287,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
249
287
|
this.agent = options.agent;
|
|
250
288
|
this.logger = options.logger;
|
|
251
289
|
this.showThinking = options.showThinking;
|
|
290
|
+
this.joinStrategy = options.joinStrategy;
|
|
252
291
|
this.getRunContext = options.getRunContext;
|
|
253
292
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
254
293
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
@@ -257,7 +296,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
257
296
|
const previousHistory = this.history;
|
|
258
297
|
this.history = options.history;
|
|
259
298
|
if (
|
|
260
|
-
this.
|
|
299
|
+
this.state.loadRequested &&
|
|
261
300
|
!this._loadPromise &&
|
|
262
301
|
!previousHistory &&
|
|
263
302
|
options.history &&
|
|
@@ -287,19 +326,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
287
326
|
}
|
|
288
327
|
|
|
289
328
|
getState(): ReadonlyJSONValue | undefined {
|
|
290
|
-
return this.stateSnapshot;
|
|
329
|
+
return this.state.stateSnapshot;
|
|
291
330
|
}
|
|
292
331
|
|
|
293
332
|
isRunning(): boolean {
|
|
294
|
-
return this.
|
|
333
|
+
return this.state.isRunning;
|
|
295
334
|
}
|
|
296
335
|
|
|
297
336
|
get isLoading(): boolean {
|
|
298
|
-
return this.
|
|
337
|
+
return this.state.isLoading;
|
|
299
338
|
}
|
|
300
339
|
|
|
301
340
|
__internal_load(): Promise<void> {
|
|
302
|
-
this.
|
|
341
|
+
this.dispatch({ type: "loadRequested" });
|
|
303
342
|
if (this._loadPromise) return this._loadPromise;
|
|
304
343
|
if (!this.history) return Promise.resolve();
|
|
305
344
|
|
|
@@ -322,7 +361,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
322
361
|
return repo;
|
|
323
362
|
});
|
|
324
363
|
|
|
325
|
-
this.
|
|
364
|
+
this.dispatch({ type: "loadStarted" });
|
|
326
365
|
this._historyImportPromise = promise.then(
|
|
327
366
|
() => {
|
|
328
367
|
this._historyImportPromise = undefined;
|
|
@@ -339,7 +378,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
339
378
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
340
379
|
await this.startRun(
|
|
341
380
|
parentId,
|
|
342
|
-
this.lastRunConfig,
|
|
381
|
+
this.state.lastRunConfig,
|
|
343
382
|
undefined,
|
|
344
383
|
resumeStream,
|
|
345
384
|
);
|
|
@@ -350,7 +389,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
350
389
|
this.reportError(error);
|
|
351
390
|
})
|
|
352
391
|
.finally(() => {
|
|
353
|
-
this.
|
|
392
|
+
this.dispatch({
|
|
393
|
+
type: "loadFinished",
|
|
394
|
+
});
|
|
354
395
|
this.publishUpdate();
|
|
355
396
|
});
|
|
356
397
|
|
|
@@ -361,10 +402,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
361
402
|
private waitForHistoryImport(): Promise<boolean> | undefined {
|
|
362
403
|
const historyImport = this._historyImportPromise;
|
|
363
404
|
if (!historyImport) return undefined;
|
|
364
|
-
const generation = this.historyMutationGeneration;
|
|
405
|
+
const generation = this.state.historyMutationGeneration;
|
|
365
406
|
return historyImport.then(
|
|
366
407
|
() =>
|
|
367
|
-
generation === this.historyMutationGeneration &&
|
|
408
|
+
generation === this.state.historyMutationGeneration &&
|
|
368
409
|
!this.isThreadSwitching?.(),
|
|
369
410
|
);
|
|
370
411
|
}
|
|
@@ -372,10 +413,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
372
413
|
private deferUntilHistoryImported(callback: () => void): boolean {
|
|
373
414
|
const historyImport = this._historyImportPromise;
|
|
374
415
|
if (!historyImport) return false;
|
|
375
|
-
const generation = this.historyMutationGeneration;
|
|
416
|
+
const generation = this.state.historyMutationGeneration;
|
|
376
417
|
void historyImport.then(() => {
|
|
377
418
|
if (
|
|
378
|
-
generation !== this.historyMutationGeneration ||
|
|
419
|
+
generation !== this.state.historyMutationGeneration ||
|
|
379
420
|
this.isThreadSwitching?.()
|
|
380
421
|
) {
|
|
381
422
|
return;
|
|
@@ -393,10 +434,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
393
434
|
const historyImport = this._historyImportPromise;
|
|
394
435
|
if (historyImport) {
|
|
395
436
|
const wasAtTail = message.parentId === this.session.headId;
|
|
396
|
-
const generation = this.historyMutationGeneration;
|
|
437
|
+
const generation = this.state.historyMutationGeneration;
|
|
397
438
|
await historyImport;
|
|
398
439
|
if (
|
|
399
|
-
generation !== this.historyMutationGeneration ||
|
|
440
|
+
generation !== this.state.historyMutationGeneration ||
|
|
400
441
|
this.isThreadSwitching?.()
|
|
401
442
|
)
|
|
402
443
|
return;
|
|
@@ -430,10 +471,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
430
471
|
|
|
431
472
|
private maybeAutoCancelPendingToolCalls(): void {
|
|
432
473
|
if (this.autoCancelPendingToolCalls === false) return;
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
474
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
475
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
476
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
477
|
+
}
|
|
437
478
|
}
|
|
438
479
|
|
|
439
480
|
private appendEntry(message: AppendMessage): string {
|
|
@@ -467,7 +508,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
467
508
|
}
|
|
468
509
|
|
|
469
510
|
async cancel(): Promise<void> {
|
|
470
|
-
this.
|
|
511
|
+
this.dispatch({
|
|
512
|
+
type: "historyMutationAdvanced",
|
|
513
|
+
});
|
|
471
514
|
this.abortActiveRun();
|
|
472
515
|
}
|
|
473
516
|
|
|
@@ -514,7 +557,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
514
557
|
this.assertNoPendingInterrupts();
|
|
515
558
|
await this.startRun(
|
|
516
559
|
config.parentId,
|
|
517
|
-
config.runConfig ?? this.lastRunConfig,
|
|
560
|
+
config.runConfig ?? this.state.lastRunConfig,
|
|
518
561
|
undefined,
|
|
519
562
|
config.stream,
|
|
520
563
|
);
|
|
@@ -537,7 +580,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
537
580
|
try {
|
|
538
581
|
await this.startRun(
|
|
539
582
|
parentId,
|
|
540
|
-
this.lastRunConfig,
|
|
583
|
+
this.state.lastRunConfig,
|
|
541
584
|
undefined,
|
|
542
585
|
resumeStream,
|
|
543
586
|
);
|
|
@@ -569,6 +612,25 @@ export class AgUiThreadRuntimeCore {
|
|
|
569
612
|
return assistant;
|
|
570
613
|
}
|
|
571
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
|
+
|
|
572
634
|
getPendingInterrupts(): {
|
|
573
635
|
messageId: string;
|
|
574
636
|
interrupts: readonly AgUiInterrupt[];
|
|
@@ -588,15 +650,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
588
650
|
messageId: string;
|
|
589
651
|
toolCallIds: string[];
|
|
590
652
|
} | null {
|
|
591
|
-
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
592
|
-
if (!assistant) return null;
|
|
593
653
|
const toolCallIds: string[] = [];
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
597
666
|
}
|
|
598
|
-
|
|
599
|
-
return { messageId: assistant.id, toolCallIds };
|
|
667
|
+
return messageId === undefined ? null : { messageId, toolCallIds };
|
|
600
668
|
}
|
|
601
669
|
|
|
602
670
|
async submitInterruptResponses(
|
|
@@ -628,6 +696,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
628
696
|
this.assertInterruptsAnswerable(
|
|
629
697
|
"submitInterruptResponses",
|
|
630
698
|
pending.interrupts,
|
|
699
|
+
responsesById,
|
|
631
700
|
);
|
|
632
701
|
|
|
633
702
|
await this.resumeWithResponses(
|
|
@@ -647,7 +716,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
647
716
|
resume: AgUiResumeEntry[],
|
|
648
717
|
): Promise<void> {
|
|
649
718
|
this.clearPendingInterrupts(messageId, resume);
|
|
650
|
-
await this.startRun(messageId, this.lastRunConfig, {
|
|
719
|
+
await this.startRun(messageId, this.state.lastRunConfig, {
|
|
651
720
|
entries: resume,
|
|
652
721
|
interruptedMessageId: messageId,
|
|
653
722
|
});
|
|
@@ -675,6 +744,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
675
744
|
private assertInterruptsAnswerable(
|
|
676
745
|
method: "submitInterruptResponses" | "respondToToolApproval",
|
|
677
746
|
interrupts: readonly AgUiInterrupt[],
|
|
747
|
+
responsesById?: ReadonlyMap<string, AgUiResumeEntry>,
|
|
678
748
|
): void {
|
|
679
749
|
const now = Date.now();
|
|
680
750
|
for (const interrupt of interrupts) {
|
|
@@ -685,13 +755,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
685
755
|
`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`,
|
|
686
756
|
);
|
|
687
757
|
}
|
|
688
|
-
if (
|
|
758
|
+
if (
|
|
759
|
+
expiry <= now &&
|
|
760
|
+
responsesById?.get(interrupt.id)?.status !== "cancelled"
|
|
761
|
+
) {
|
|
689
762
|
throw new Error(
|
|
690
763
|
`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`,
|
|
691
764
|
);
|
|
692
765
|
}
|
|
693
766
|
}
|
|
694
|
-
if (this.
|
|
767
|
+
if (this.state.isRunning) {
|
|
695
768
|
throw new Error(`[agui] ${method}: a run is already in progress`);
|
|
696
769
|
}
|
|
697
770
|
}
|
|
@@ -785,11 +858,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
785
858
|
"[agui] steerAway: responses are only valid for pending interrupts",
|
|
786
859
|
);
|
|
787
860
|
}
|
|
788
|
-
if (this.
|
|
861
|
+
if (this.state.isRunning) {
|
|
789
862
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
790
863
|
}
|
|
791
|
-
this.
|
|
792
|
-
|
|
864
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
865
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
866
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
867
|
+
}
|
|
793
868
|
const normalized = this.toAppendMessage(message);
|
|
794
869
|
const threadMessageId = this.appendEntry(normalized);
|
|
795
870
|
await this.startRun(threadMessageId, normalized.runConfig);
|
|
@@ -806,7 +881,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
806
881
|
|
|
807
882
|
const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
|
|
808
883
|
|
|
809
|
-
if (this.
|
|
884
|
+
if (this.state.isRunning) {
|
|
810
885
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
811
886
|
}
|
|
812
887
|
|
|
@@ -1063,7 +1138,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1063
1138
|
return;
|
|
1064
1139
|
}
|
|
1065
1140
|
|
|
1066
|
-
await this.
|
|
1141
|
+
await this.historyWrites.chain(sessionMessageId, () =>
|
|
1067
1142
|
history.update!({ parentId: item.parentId, message }),
|
|
1068
1143
|
);
|
|
1069
1144
|
}
|
|
@@ -1086,11 +1161,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1086
1161
|
if (!("timestamp" in userAction)) {
|
|
1087
1162
|
userAction.timestamp = new Date().toISOString();
|
|
1088
1163
|
}
|
|
1089
|
-
this.
|
|
1164
|
+
this.dispatch({
|
|
1165
|
+
type: "a2uiActionDeferred",
|
|
1166
|
+
action: userAction,
|
|
1167
|
+
});
|
|
1090
1168
|
|
|
1091
1169
|
const owner = this.abortController;
|
|
1092
1170
|
if (owner) {
|
|
1093
|
-
this.
|
|
1171
|
+
this.dispatch({
|
|
1172
|
+
type: "a2uiResumeDeferred",
|
|
1173
|
+
owner,
|
|
1174
|
+
});
|
|
1094
1175
|
return;
|
|
1095
1176
|
}
|
|
1096
1177
|
this.startResumeRun(parentId);
|
|
@@ -1101,15 +1182,25 @@ export class AgUiThreadRuntimeCore {
|
|
|
1101
1182
|
// run is still draining) or after it.
|
|
1102
1183
|
private maybeResumeAfterToolResults(messageId: string): void {
|
|
1103
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;
|
|
1104
1191
|
|
|
1105
1192
|
const owner = this.abortController;
|
|
1106
1193
|
if (owner) {
|
|
1107
1194
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
1108
1195
|
// closed). Defer until startRun's tail so we never start two runs.
|
|
1109
|
-
this.
|
|
1196
|
+
this.dispatch({
|
|
1197
|
+
type: "resumeDeferred",
|
|
1198
|
+
owner,
|
|
1199
|
+
messageId: headId,
|
|
1200
|
+
});
|
|
1110
1201
|
return;
|
|
1111
1202
|
}
|
|
1112
|
-
this.startResumeRun(
|
|
1203
|
+
this.startResumeRun(headId);
|
|
1113
1204
|
}
|
|
1114
1205
|
|
|
1115
1206
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
@@ -1123,7 +1214,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1123
1214
|
return false;
|
|
1124
1215
|
}
|
|
1125
1216
|
let allResolved = true;
|
|
1217
|
+
const listed = listedPendingToolCallIds(assistant);
|
|
1126
1218
|
for (const part of iterateToolCallParts(assistant.content)) {
|
|
1219
|
+
if (listed && !listed.has(part.toolCallId)) continue;
|
|
1127
1220
|
if (!isResolvedToolCall(part)) {
|
|
1128
1221
|
allResolved = false;
|
|
1129
1222
|
break;
|
|
@@ -1136,6 +1229,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1136
1229
|
? {
|
|
1137
1230
|
...(current as ThreadAssistantMessage),
|
|
1138
1231
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
1232
|
+
metadata: withoutPendingToolCallIds(current.metadata),
|
|
1139
1233
|
}
|
|
1140
1234
|
: current,
|
|
1141
1235
|
);
|
|
@@ -1146,7 +1240,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1146
1240
|
}
|
|
1147
1241
|
|
|
1148
1242
|
private startResumeRun(messageId: string): void {
|
|
1149
|
-
void this.startRun(messageId, this.lastRunConfig).catch((error) => {
|
|
1243
|
+
void this.startRun(messageId, this.state.lastRunConfig).catch((error) => {
|
|
1150
1244
|
this.reportError(error);
|
|
1151
1245
|
});
|
|
1152
1246
|
}
|
|
@@ -1163,8 +1257,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1163
1257
|
metadata: { ...message.metadata, modality },
|
|
1164
1258
|
} as ThreadMessage;
|
|
1165
1259
|
});
|
|
1166
|
-
this.
|
|
1167
|
-
|
|
1260
|
+
this.dispatch({
|
|
1261
|
+
type: "a2uiContinuationsCleared",
|
|
1262
|
+
});
|
|
1168
1263
|
this.assistantHistoryParents.clear();
|
|
1169
1264
|
|
|
1170
1265
|
if (messages.length === 0) {
|
|
@@ -1213,18 +1308,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
1213
1308
|
// continuation whose target survived the snapshot is still answerable.
|
|
1214
1309
|
// Membership is the head branch, not the repository: a soft merge leaves
|
|
1215
1310
|
// the messages it dropped behind as off-branch nodes.
|
|
1216
|
-
const resumeTarget = this.pendingResume?.messageId;
|
|
1311
|
+
const resumeTarget = this.state.pendingResume?.messageId;
|
|
1217
1312
|
if (
|
|
1218
1313
|
resumeTarget !== undefined &&
|
|
1219
1314
|
!this.session.getMessages().some((message) => message.id === resumeTarget)
|
|
1220
1315
|
) {
|
|
1221
|
-
this.
|
|
1316
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1222
1317
|
}
|
|
1223
1318
|
this.publishUpdate();
|
|
1224
1319
|
}
|
|
1225
1320
|
|
|
1226
1321
|
loadExternalState(state: ReadonlyJSONValue): void {
|
|
1227
|
-
this.
|
|
1322
|
+
this.dispatch({
|
|
1323
|
+
type: "stateSnapshot",
|
|
1324
|
+
snapshot: state,
|
|
1325
|
+
});
|
|
1228
1326
|
this.publishUpdate();
|
|
1229
1327
|
}
|
|
1230
1328
|
|
|
@@ -1233,25 +1331,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
1233
1331
|
| ReadonlyJSONValue
|
|
1234
1332
|
| ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
|
|
1235
1333
|
): void {
|
|
1236
|
-
|
|
1237
|
-
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
|
+
});
|
|
1238
1340
|
this.publishUpdate();
|
|
1239
1341
|
}
|
|
1240
1342
|
|
|
1241
1343
|
resetState(): void {
|
|
1242
|
-
this.
|
|
1344
|
+
this.dispatch({ type: "resetState" });
|
|
1243
1345
|
this.publishUpdate();
|
|
1244
1346
|
}
|
|
1245
1347
|
|
|
1246
1348
|
resetThreadState(): void {
|
|
1247
|
-
this.
|
|
1349
|
+
this.dispatch({
|
|
1350
|
+
type: "historyMutationAdvanced",
|
|
1351
|
+
});
|
|
1248
1352
|
const controller = this.abortController;
|
|
1249
1353
|
const activeRunAgent = this.activeRunAgent;
|
|
1250
1354
|
|
|
1251
|
-
this.
|
|
1252
|
-
this.pendingResume = null;
|
|
1253
|
-
this.pendingA2uiResumeOwner = null;
|
|
1254
|
-
this.pendingA2uiAction = undefined;
|
|
1355
|
+
this.dispatch({ type: "resetThread" });
|
|
1255
1356
|
|
|
1256
1357
|
if (controller) {
|
|
1257
1358
|
this.abortController = null;
|
|
@@ -1282,7 +1383,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1282
1383
|
if (!this.supersedeActiveRun()) return;
|
|
1283
1384
|
|
|
1284
1385
|
const normalizedRunConfig = runConfig ?? {};
|
|
1285
|
-
this.
|
|
1386
|
+
this.dispatch({
|
|
1387
|
+
type: "runConfigRecorded",
|
|
1388
|
+
runConfig: normalizedRunConfig,
|
|
1389
|
+
});
|
|
1286
1390
|
const parent =
|
|
1287
1391
|
parentId === null ? undefined : this.session.tryGetMessage(parentId);
|
|
1288
1392
|
const shouldEagerlyInsertAssistant =
|
|
@@ -1297,6 +1401,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1297
1401
|
const runStartMessageIds = new Set(
|
|
1298
1402
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1299
1403
|
);
|
|
1404
|
+
const joinStrategy = this.joinStrategy;
|
|
1405
|
+
const hasRunUserBoundary = parent?.message.role === "user" && !resume;
|
|
1300
1406
|
|
|
1301
1407
|
let pendingError: Error | null = null;
|
|
1302
1408
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
@@ -1309,6 +1415,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1309
1415
|
// resurrecting for custom-event data would leave a trailing data-only
|
|
1310
1416
|
// duplicate.
|
|
1311
1417
|
let assistantCollided = false;
|
|
1418
|
+
const heldMessages = new Map<string, Set<string>>();
|
|
1419
|
+
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1420
|
+
let settlingRun = false;
|
|
1312
1421
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1313
1422
|
const cached = assistantMessageId;
|
|
1314
1423
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1338,7 +1447,66 @@ export class AgUiThreadRuntimeCore {
|
|
|
1338
1447
|
|
|
1339
1448
|
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1340
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
|
+
|
|
1341
1504
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1505
|
+
const terminalStatus = update.status;
|
|
1506
|
+
const pendingMessages =
|
|
1507
|
+
settlingRun && terminalStatus && terminalStatus.type !== "running"
|
|
1508
|
+
? settleHeldMessages(terminalStatus)
|
|
1509
|
+
: [];
|
|
1342
1510
|
const hasStreamContent =
|
|
1343
1511
|
Array.isArray(update.content) &&
|
|
1344
1512
|
update.content.some((part) => part.type !== "data");
|
|
@@ -1349,14 +1517,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
1349
1517
|
if (resolved !== assistantMessageId) {
|
|
1350
1518
|
assistantMessageId = resolved;
|
|
1351
1519
|
}
|
|
1520
|
+
for (const messageId of pendingMessages) {
|
|
1521
|
+
this.maybeResumeAfterToolResults(messageId);
|
|
1522
|
+
}
|
|
1352
1523
|
};
|
|
1353
1524
|
|
|
1354
1525
|
const adoptServerMessageId = (
|
|
1355
1526
|
serverId: string,
|
|
1356
1527
|
startNewMessage: boolean,
|
|
1528
|
+
openToolCallIds: string[] = [],
|
|
1357
1529
|
) => {
|
|
1358
1530
|
if (startNewMessage && assistantMessageId !== undefined) {
|
|
1359
|
-
|
|
1531
|
+
if (openToolCallIds.length > 0) {
|
|
1532
|
+
heldMessages.set(assistantMessageId, new Set(openToolCallIds));
|
|
1533
|
+
} else {
|
|
1534
|
+
applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1535
|
+
}
|
|
1360
1536
|
const previousId = assistantMessageId;
|
|
1361
1537
|
assistantMessageId = undefined;
|
|
1362
1538
|
if (this.session.tryGetMessage(previousId)) {
|
|
@@ -1392,24 +1568,49 @@ export class AgUiThreadRuntimeCore {
|
|
|
1392
1568
|
const abortSignal = abortController.signal;
|
|
1393
1569
|
const aggregator = new RunAggregator({
|
|
1394
1570
|
showThinking: this.showThinking,
|
|
1571
|
+
joinStrategy,
|
|
1395
1572
|
logger: this.logger,
|
|
1396
1573
|
emit: applyUpdate,
|
|
1397
1574
|
onServerMessageId: (serverId) => {
|
|
1398
1575
|
adoptServerMessageId(serverId, false);
|
|
1399
1576
|
},
|
|
1400
|
-
onTextMessageStart: (serverId) =>
|
|
1577
|
+
onTextMessageStart: (serverId, openToolCallIds) =>
|
|
1578
|
+
adoptServerMessageId(serverId, true, openToolCallIds),
|
|
1401
1579
|
});
|
|
1402
1580
|
let runFinished = false;
|
|
1403
1581
|
const dispatch = (event: AgUiEvent) => {
|
|
1404
1582
|
if (this.abortController !== abortController) return;
|
|
1405
|
-
if (event.type === "RUN_FINISHED")
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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;
|
|
1413
1614
|
}
|
|
1414
1615
|
};
|
|
1415
1616
|
|
|
@@ -1472,9 +1673,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1472
1673
|
},
|
|
1473
1674
|
});
|
|
1474
1675
|
try {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1676
|
+
runAgentInstance.messages = input.messages;
|
|
1677
|
+
runAgentInstance.threadId = input.threadId;
|
|
1678
|
+
runAgentInstance.state = input.state ?? null;
|
|
1478
1679
|
} catch {
|
|
1479
1680
|
// ignore
|
|
1480
1681
|
}
|
|
@@ -1504,19 +1705,23 @@ export class AgUiThreadRuntimeCore {
|
|
|
1504
1705
|
|
|
1505
1706
|
// A tool result that landed before the run settled deferred its
|
|
1506
1707
|
// continuation here so a second run never overlaps the first.
|
|
1507
|
-
if (this.pendingResume?.owner === abortController) {
|
|
1508
|
-
const { messageId } = this.pendingResume;
|
|
1509
|
-
this.
|
|
1708
|
+
if (this.state.pendingResume?.owner === abortController) {
|
|
1709
|
+
const { messageId } = this.state.pendingResume;
|
|
1710
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1510
1711
|
if (!abortSignal.aborted) {
|
|
1511
1712
|
this.startResumeRun(messageId);
|
|
1512
1713
|
}
|
|
1513
1714
|
}
|
|
1514
1715
|
|
|
1515
|
-
if (this.pendingA2uiResumeOwner === abortController) {
|
|
1516
|
-
this.
|
|
1517
|
-
|
|
1716
|
+
if (this.state.pendingA2uiResumeOwner === abortController) {
|
|
1717
|
+
this.dispatch({
|
|
1718
|
+
type: "a2uiResumeCleared",
|
|
1719
|
+
});
|
|
1720
|
+
if (!abortSignal.aborted && this.state.pendingA2uiAction !== undefined) {
|
|
1518
1721
|
if (this.getPendingInterrupts()) {
|
|
1519
|
-
this.
|
|
1722
|
+
this.dispatch({
|
|
1723
|
+
type: "a2uiActionCleared",
|
|
1724
|
+
});
|
|
1520
1725
|
this.logger.debug(
|
|
1521
1726
|
"[agui] sendA2uiAction: pending interrupts, dropping action",
|
|
1522
1727
|
);
|
|
@@ -1527,13 +1732,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1527
1732
|
if (parentId !== null) {
|
|
1528
1733
|
this.startResumeRun(parentId);
|
|
1529
1734
|
} else {
|
|
1530
|
-
this.
|
|
1735
|
+
this.dispatch({
|
|
1736
|
+
type: "a2uiActionCleared",
|
|
1737
|
+
});
|
|
1531
1738
|
this.logger.debug(
|
|
1532
1739
|
"[agui] sendA2uiAction: no messages to resume, dropping action",
|
|
1533
1740
|
);
|
|
1534
1741
|
}
|
|
1535
1742
|
} else {
|
|
1536
|
-
this.
|
|
1743
|
+
this.dispatch({
|
|
1744
|
+
type: "a2uiActionCleared",
|
|
1745
|
+
});
|
|
1537
1746
|
}
|
|
1538
1747
|
}
|
|
1539
1748
|
}
|
|
@@ -1552,8 +1761,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1552
1761
|
getAssistantMessageId: () => string | undefined;
|
|
1553
1762
|
},
|
|
1554
1763
|
): Promise<Error | undefined> {
|
|
1555
|
-
this.
|
|
1556
|
-
|
|
1764
|
+
this.dispatch({
|
|
1765
|
+
type: "a2uiContinuationsCleared",
|
|
1766
|
+
});
|
|
1557
1767
|
const assistantId = ctx.ensureAssistant();
|
|
1558
1768
|
const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
|
|
1559
1769
|
const options: ChatModelRunOptions = {
|
|
@@ -1604,8 +1814,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1604
1814
|
historyMessages: readonly ThreadMessage[] | undefined,
|
|
1605
1815
|
resume?: ResumeDispatch,
|
|
1606
1816
|
) {
|
|
1607
|
-
const pendingA2uiAction = this.pendingA2uiAction;
|
|
1608
|
-
this.
|
|
1817
|
+
const pendingA2uiAction = this.state.pendingA2uiAction;
|
|
1818
|
+
this.dispatch({
|
|
1819
|
+
type: "a2uiActionCleared",
|
|
1820
|
+
});
|
|
1609
1821
|
const threadId = this.agent.threadId || "main";
|
|
1610
1822
|
const messages = toAgUiMessages(
|
|
1611
1823
|
this.selectRunMessages(
|
|
@@ -1625,7 +1837,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1625
1837
|
const input = {
|
|
1626
1838
|
threadId,
|
|
1627
1839
|
runId,
|
|
1628
|
-
state: this.stateSnapshot ?? null,
|
|
1840
|
+
state: this.state.stateSnapshot ?? null,
|
|
1629
1841
|
messages,
|
|
1630
1842
|
tools: toAgUiTools(context?.tools),
|
|
1631
1843
|
context: [
|
|
@@ -1668,17 +1880,20 @@ export class AgUiThreadRuntimeCore {
|
|
|
1668
1880
|
}
|
|
1669
1881
|
|
|
1670
1882
|
private clearDeferredContinuations(owner: AbortController): void {
|
|
1671
|
-
if (this.pendingResume?.owner === owner) {
|
|
1672
|
-
this.
|
|
1883
|
+
if (this.state.pendingResume?.owner === owner) {
|
|
1884
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1673
1885
|
}
|
|
1674
|
-
if (this.pendingA2uiResumeOwner === owner) {
|
|
1675
|
-
this.
|
|
1676
|
-
|
|
1886
|
+
if (this.state.pendingA2uiResumeOwner === owner) {
|
|
1887
|
+
this.dispatch({
|
|
1888
|
+
type: "a2uiContinuationsCleared",
|
|
1889
|
+
});
|
|
1677
1890
|
}
|
|
1678
1891
|
}
|
|
1679
1892
|
|
|
1680
1893
|
private setRunning(running: boolean) {
|
|
1681
|
-
this.
|
|
1894
|
+
this.dispatch({
|
|
1895
|
+
type: running ? "runStarted" : "runFinished",
|
|
1896
|
+
});
|
|
1682
1897
|
this.publishUpdate();
|
|
1683
1898
|
}
|
|
1684
1899
|
|
|
@@ -1700,7 +1915,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1700
1915
|
status: { type: "running" },
|
|
1701
1916
|
content: [],
|
|
1702
1917
|
metadata: {
|
|
1703
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
1918
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
1704
1919
|
unstable_annotations: [],
|
|
1705
1920
|
unstable_data: [],
|
|
1706
1921
|
steps: [],
|
|
@@ -1758,22 +1973,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
1758
1973
|
// An in-flight write completes under the id it was started with, so the
|
|
1759
1974
|
// rename moves the chain entry and transfers the completion mark; without
|
|
1760
1975
|
// this the resolve records the dead id and the live one appends again.
|
|
1761
|
-
const pendingWrite = this.historyWrites.
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
this.historyWrites.delete(newId);
|
|
1769
|
-
}
|
|
1770
|
-
};
|
|
1771
|
-
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
|
+
() => {
|
|
1772
1983
|
this.persistedHistoryIds.delete(oldId);
|
|
1773
1984
|
this.persistedHistoryIds.add(newId);
|
|
1774
|
-
|
|
1775
|
-
},
|
|
1776
|
-
|
|
1985
|
+
},
|
|
1986
|
+
() => {},
|
|
1987
|
+
);
|
|
1777
1988
|
}
|
|
1778
1989
|
|
|
1779
1990
|
this.publishUpdate();
|
|
@@ -1788,10 +1999,15 @@ export class AgUiThreadRuntimeCore {
|
|
|
1788
1999
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
1789
2000
|
if (message.role !== "assistant") return message;
|
|
1790
2001
|
const assistant = message as ThreadAssistantMessage;
|
|
1791
|
-
const
|
|
2002
|
+
const mergedMetadata = update.metadata
|
|
1792
2003
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
1793
2004
|
: assistant.metadata;
|
|
1794
2005
|
latestStatus = update.status ?? assistant.status;
|
|
2006
|
+
const metadata =
|
|
2007
|
+
latestStatus?.type === "requires-action" &&
|
|
2008
|
+
latestStatus.reason === "tool-calls"
|
|
2009
|
+
? mergedMetadata
|
|
2010
|
+
: withoutPendingToolCallIds(mergedMetadata);
|
|
1795
2011
|
const content =
|
|
1796
2012
|
update.content !== undefined
|
|
1797
2013
|
? this.preserveToolResults(
|
|
@@ -2041,24 +2257,32 @@ export class AgUiThreadRuntimeCore {
|
|
|
2041
2257
|
aggregator: RunAggregator,
|
|
2042
2258
|
event: AgUiEvent,
|
|
2043
2259
|
activeAssistantId: string | undefined,
|
|
2260
|
+
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2261
|
+
runStartMessageIds: ReadonlySet<string>,
|
|
2262
|
+
hasRunUserBoundary: boolean,
|
|
2044
2263
|
): string | undefined {
|
|
2045
2264
|
switch (event.type) {
|
|
2046
2265
|
case "STATE_SNAPSHOT": {
|
|
2047
|
-
this.
|
|
2266
|
+
this.dispatch({
|
|
2267
|
+
type: "stateSnapshot",
|
|
2268
|
+
snapshot: event.snapshot as ReadonlyJSONValue,
|
|
2269
|
+
});
|
|
2048
2270
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2049
2271
|
break;
|
|
2050
2272
|
}
|
|
2051
2273
|
case "STATE_DELTA": {
|
|
2052
2274
|
if (event.delta.length === 0) break;
|
|
2053
2275
|
try {
|
|
2054
|
-
const state = this.stateSnapshot ?? {};
|
|
2055
2276
|
const result = jsonpatch.applyPatch(
|
|
2056
|
-
state,
|
|
2277
|
+
this.state.stateSnapshot ?? {},
|
|
2057
2278
|
event.delta as Operation[],
|
|
2058
2279
|
/* validateOperation */ true,
|
|
2059
2280
|
/* mutateDocument */ false,
|
|
2060
2281
|
);
|
|
2061
|
-
this.
|
|
2282
|
+
this.dispatch({
|
|
2283
|
+
type: "stateSnapshot",
|
|
2284
|
+
snapshot: result.newDocument as ReadonlyJSONValue,
|
|
2285
|
+
});
|
|
2062
2286
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2063
2287
|
} catch (error) {
|
|
2064
2288
|
this.logger.error?.("[agui] failed to apply state delta", error);
|
|
@@ -2074,7 +2298,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
2074
2298
|
const hasActiveText =
|
|
2075
2299
|
activeAssistant?.role === "assistant" &&
|
|
2076
2300
|
activeAssistant.content.some((part) => part.type === "text");
|
|
2077
|
-
this.importMessagesSnapshot(
|
|
2301
|
+
this.importMessagesSnapshot(
|
|
2302
|
+
event.messages,
|
|
2303
|
+
activeAssistantId,
|
|
2304
|
+
joinStrategy,
|
|
2305
|
+
runStartMessageIds,
|
|
2306
|
+
hasRunUserBoundary,
|
|
2307
|
+
);
|
|
2078
2308
|
const headId = this.session.headId;
|
|
2079
2309
|
if (hasActiveText && headId !== previousHeadId) {
|
|
2080
2310
|
const head =
|
|
@@ -2137,7 +2367,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2137
2367
|
...assistant,
|
|
2138
2368
|
metadata: {
|
|
2139
2369
|
...assistant.metadata,
|
|
2140
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
2370
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
2141
2371
|
},
|
|
2142
2372
|
};
|
|
2143
2373
|
});
|
|
@@ -2153,6 +2383,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2153
2383
|
if (message.role !== "assistant") return message;
|
|
2154
2384
|
const assistant = message as ThreadAssistantMessage;
|
|
2155
2385
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
2386
|
+
const modelText = toolContentText(event.content);
|
|
2156
2387
|
let matchedToolCall = false;
|
|
2157
2388
|
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
2158
2389
|
if (part.toolCallId !== event.toolCallId) return part;
|
|
@@ -2169,11 +2400,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2169
2400
|
if (snapshotResultApplied) {
|
|
2170
2401
|
return {
|
|
2171
2402
|
...part,
|
|
2172
|
-
...(part.modelContent === undefined
|
|
2403
|
+
...(part.modelContent === undefined
|
|
2173
2404
|
? {
|
|
2174
|
-
modelContent: [
|
|
2175
|
-
{ type: "text" as const, text: event.content },
|
|
2176
|
-
],
|
|
2405
|
+
modelContent: [{ type: "text" as const, text: modelText }],
|
|
2177
2406
|
}
|
|
2178
2407
|
: {}),
|
|
2179
2408
|
...(part.isError === undefined
|
|
@@ -2188,12 +2417,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
2188
2417
|
: {}),
|
|
2189
2418
|
};
|
|
2190
2419
|
}
|
|
2420
|
+
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
2421
|
+
part;
|
|
2191
2422
|
return {
|
|
2192
|
-
...
|
|
2423
|
+
...(event.mcpResult === undefined && Array.isArray(event.content)
|
|
2424
|
+
? partWithoutModelContent
|
|
2425
|
+
: part),
|
|
2193
2426
|
result: (event.mcpResult ??
|
|
2194
|
-
|
|
2427
|
+
(Array.isArray(event.content)
|
|
2428
|
+
? event.content
|
|
2429
|
+
: tryParseJSON(event.content))) as ReadonlyJSONValue,
|
|
2195
2430
|
...(event.mcpResult !== undefined
|
|
2196
|
-
? { modelContent: [{ type: "text" as const, text:
|
|
2431
|
+
? { modelContent: [{ type: "text" as const, text: modelText }] }
|
|
2197
2432
|
: {}),
|
|
2198
2433
|
...(typeof event.mcpResult?.isError === "boolean"
|
|
2199
2434
|
? { isError: event.mcpResult.isError }
|
|
@@ -2283,6 +2518,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2283
2518
|
private importMessagesSnapshot(
|
|
2284
2519
|
rawMessages: readonly unknown[],
|
|
2285
2520
|
activeAssistantId: string | undefined,
|
|
2521
|
+
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2522
|
+
runStartMessageIds: ReadonlySet<string>,
|
|
2523
|
+
hasRunUserBoundary: boolean,
|
|
2286
2524
|
) {
|
|
2287
2525
|
try {
|
|
2288
2526
|
const activeAssistantItem = activeAssistantId
|
|
@@ -2337,6 +2575,101 @@ export class AgUiThreadRuntimeCore {
|
|
|
2337
2575
|
};
|
|
2338
2576
|
}
|
|
2339
2577
|
}
|
|
2578
|
+
if (joinStrategy === "concat-content") {
|
|
2579
|
+
const activeIndex = converted.findIndex(
|
|
2580
|
+
(message) => message.id === activeAssistantId,
|
|
2581
|
+
);
|
|
2582
|
+
const lastKnownIndex = converted.findLastIndex((message) =>
|
|
2583
|
+
runStartMessageIds.has(message.id),
|
|
2584
|
+
);
|
|
2585
|
+
const lastUserIndex = hasRunUserBoundary
|
|
2586
|
+
? converted.findLastIndex((message) => message.role === "user")
|
|
2587
|
+
: -1;
|
|
2588
|
+
const startIndex =
|
|
2589
|
+
activeIndex >= 0
|
|
2590
|
+
? activeIndex
|
|
2591
|
+
: lastKnownIndex >= 0 || lastUserIndex >= 0
|
|
2592
|
+
? Math.max(lastKnownIndex, lastUserIndex) + 1
|
|
2593
|
+
: -1;
|
|
2594
|
+
if (startIndex >= 0 && converted[startIndex]?.role === "assistant") {
|
|
2595
|
+
let endIndex = startIndex + 1;
|
|
2596
|
+
while (converted[endIndex]?.role === "assistant") endIndex++;
|
|
2597
|
+
const runMessages = converted.slice(
|
|
2598
|
+
startIndex,
|
|
2599
|
+
endIndex,
|
|
2600
|
+
) as ThreadAssistantMessage[];
|
|
2601
|
+
const includesStreamedPrefix =
|
|
2602
|
+
activeIndex < 0 &&
|
|
2603
|
+
activeAssistant?.content.some((part) => part.type !== "data");
|
|
2604
|
+
if (includesStreamedPrefix && activeAssistant) {
|
|
2605
|
+
runMessages.unshift(activeAssistant);
|
|
2606
|
+
}
|
|
2607
|
+
if (runMessages.length > 1) {
|
|
2608
|
+
const [first, ...rest] = runMessages;
|
|
2609
|
+
let content = runMessages.flatMap((message) => message.content);
|
|
2610
|
+
if (includesStreamedPrefix) {
|
|
2611
|
+
content = [...first!.content];
|
|
2612
|
+
let cursor = 0;
|
|
2613
|
+
const keyOf = (part: (typeof content)[number]) =>
|
|
2614
|
+
part.type === "tool-call"
|
|
2615
|
+
? `tool:${part.toolCallId}`
|
|
2616
|
+
: part.type === "text" || part.type === "reasoning"
|
|
2617
|
+
? `${part.type}:${part.text}`
|
|
2618
|
+
: undefined;
|
|
2619
|
+
for (const message of rest) {
|
|
2620
|
+
for (const part of message.content) {
|
|
2621
|
+
const key = keyOf(part);
|
|
2622
|
+
const match =
|
|
2623
|
+
key === undefined
|
|
2624
|
+
? -1
|
|
2625
|
+
: content.findIndex(
|
|
2626
|
+
(previous, index) =>
|
|
2627
|
+
index >= cursor && keyOf(previous) === key,
|
|
2628
|
+
);
|
|
2629
|
+
if (match < 0) {
|
|
2630
|
+
content.push(part);
|
|
2631
|
+
cursor = content.length;
|
|
2632
|
+
} else {
|
|
2633
|
+
if (part.type === "tool-call") {
|
|
2634
|
+
content[match] = this.preserveToolResults(
|
|
2635
|
+
[content[match]!],
|
|
2636
|
+
[part],
|
|
2637
|
+
)[0]!;
|
|
2638
|
+
}
|
|
2639
|
+
cursor = match + 1;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
converted.splice(startIndex, endIndex - startIndex, {
|
|
2645
|
+
...first!,
|
|
2646
|
+
content,
|
|
2647
|
+
status: rest.at(-1)!.status,
|
|
2648
|
+
metadata: rest.reduce(
|
|
2649
|
+
(metadata, message) => ({
|
|
2650
|
+
...metadata,
|
|
2651
|
+
...message.metadata,
|
|
2652
|
+
unstable_state: message.metadata.unstable_state,
|
|
2653
|
+
unstable_annotations: [
|
|
2654
|
+
...metadata.unstable_annotations,
|
|
2655
|
+
...message.metadata.unstable_annotations,
|
|
2656
|
+
],
|
|
2657
|
+
unstable_data: [
|
|
2658
|
+
...metadata.unstable_data,
|
|
2659
|
+
...message.metadata.unstable_data,
|
|
2660
|
+
],
|
|
2661
|
+
steps: [...metadata.steps, ...message.metadata.steps],
|
|
2662
|
+
custom: mergeAgUiNamespace(
|
|
2663
|
+
metadata.custom,
|
|
2664
|
+
message.metadata.custom,
|
|
2665
|
+
),
|
|
2666
|
+
}),
|
|
2667
|
+
first!.metadata,
|
|
2668
|
+
),
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2340
2673
|
const snapshotContainsActiveAssistant = converted.some(
|
|
2341
2674
|
(message) => message.id === activeAssistant?.id,
|
|
2342
2675
|
);
|
|
@@ -2414,7 +2747,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2414
2747
|
(wasPersisted || this.snapshotHistoryIds.has(messageId));
|
|
2415
2748
|
|
|
2416
2749
|
if (shouldUpdate) {
|
|
2417
|
-
const write = this.
|
|
2750
|
+
const write = this.historyWrites.chain(messageId, () =>
|
|
2418
2751
|
update.call(history, { parentId, message }),
|
|
2419
2752
|
);
|
|
2420
2753
|
this.assistantHistoryParents.delete(messageId);
|
|
@@ -2423,7 +2756,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2423
2756
|
this.persistedHistoryIds.add(messageId);
|
|
2424
2757
|
},
|
|
2425
2758
|
(error) => {
|
|
2426
|
-
const pending = this.historyWrites.
|
|
2759
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2427
2760
|
if (pending === undefined || pending === write) {
|
|
2428
2761
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2429
2762
|
}
|
|
@@ -2444,7 +2777,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2444
2777
|
void write.then(
|
|
2445
2778
|
() => {},
|
|
2446
2779
|
(error) => {
|
|
2447
|
-
const pending = this.historyWrites.
|
|
2780
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2448
2781
|
if (pending === undefined || pending === write) {
|
|
2449
2782
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2450
2783
|
}
|
|
@@ -2458,11 +2791,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
2458
2791
|
message: ThreadMessage,
|
|
2459
2792
|
): Promise<void> | undefined {
|
|
2460
2793
|
if (!this.history || this.persistedHistoryIds.has(message.id)) return;
|
|
2461
|
-
const pending = this.historyWrites.
|
|
2794
|
+
const pending = this.historyWrites.pending(message.id);
|
|
2462
2795
|
if (pending) return pending;
|
|
2463
2796
|
|
|
2464
2797
|
const append = this.history.append.bind(this.history);
|
|
2465
|
-
const write = this.
|
|
2798
|
+
const write = this.historyWrites.chain(message.id, () =>
|
|
2466
2799
|
append({ parentId, message }),
|
|
2467
2800
|
);
|
|
2468
2801
|
void write.then(
|
|
@@ -2473,35 +2806,4 @@ export class AgUiThreadRuntimeCore {
|
|
|
2473
2806
|
);
|
|
2474
2807
|
return write;
|
|
2475
2808
|
}
|
|
2476
|
-
|
|
2477
|
-
private chainHistoryWrite(
|
|
2478
|
-
id: string,
|
|
2479
|
-
write: () => Promise<void>,
|
|
2480
|
-
): Promise<void> {
|
|
2481
|
-
const pending = this.historyWrites.get(id);
|
|
2482
|
-
let next: Promise<void>;
|
|
2483
|
-
if (pending) {
|
|
2484
|
-
next = pending.then(write, write);
|
|
2485
|
-
} else {
|
|
2486
|
-
try {
|
|
2487
|
-
next = Promise.resolve(write());
|
|
2488
|
-
} catch (error) {
|
|
2489
|
-
next = Promise.reject(error);
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
this.historyWrites.set(id, next);
|
|
2493
|
-
void next.then(
|
|
2494
|
-
() => {
|
|
2495
|
-
if (this.historyWrites.get(id) === next) {
|
|
2496
|
-
this.historyWrites.delete(id);
|
|
2497
|
-
}
|
|
2498
|
-
},
|
|
2499
|
-
() => {
|
|
2500
|
-
if (this.historyWrites.get(id) === next) {
|
|
2501
|
-
this.historyWrites.delete(id);
|
|
2502
|
-
}
|
|
2503
|
-
},
|
|
2504
|
-
);
|
|
2505
|
-
return next;
|
|
2506
|
-
}
|
|
2507
2809
|
}
|