@assistant-ui/react-ag-ui 0.0.71 → 0.1.1
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/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- 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} +567 -194
- 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 +120 -35
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/replay-content.d.ts +9 -0
- package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
- package/dist/runtime/adapter/replay-content.js +185 -0
- package/dist/runtime/adapter/replay-content.js.map +1 -0
- 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 +48 -63
- 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 +2 -24
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +2 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +23 -6
- package/dist/runtime/adapter/tool-approval.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 +94 -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 +46 -50
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.js +1 -2
- package/dist/runtime/logger.js +1 -2
- 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 +7 -5
- 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 +1 -2
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +3 -4
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +12 -11
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
- package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
- package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +114 -6
- package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +1037 -236
- package/src/runtime/adapter/conversions.content.test.ts +149 -5
- package/src/runtime/adapter/conversions.tools.test.ts +2 -1
- package/src/runtime/adapter/conversions.ts +249 -62
- package/src/runtime/adapter/replay-content.test.ts +235 -0
- package/src/runtime/adapter/replay-content.ts +306 -0
- package/src/runtime/adapter/run-aggregator.ts +79 -114
- package/src/runtime/adapter/subscriber.ts +1 -44
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +224 -0
- package/src/runtime/agUiThreadState.ts +139 -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,
|
|
@@ -60,9 +67,16 @@ import {
|
|
|
60
67
|
toAgUiTools,
|
|
61
68
|
} from "./adapter/conversions";
|
|
62
69
|
import { createAgUiSubscriber } from "./adapter/subscriber";
|
|
70
|
+
import {
|
|
71
|
+
createReplayContentMerger,
|
|
72
|
+
groupReplayToolContent,
|
|
73
|
+
indexReplayToolOwners,
|
|
74
|
+
preserveToolResult,
|
|
75
|
+
} from "./adapter/replay-content";
|
|
63
76
|
import {
|
|
64
77
|
buildToolApprovalResume,
|
|
65
78
|
projectAgUiToolApprovals,
|
|
79
|
+
preserveToolApprovalDecision,
|
|
66
80
|
withSettledToolApprovals,
|
|
67
81
|
withToolApprovalDecision,
|
|
68
82
|
} from "./adapter/tool-approval";
|
|
@@ -85,6 +99,97 @@ const isResolvedToolCall = (
|
|
|
85
99
|
): boolean =>
|
|
86
100
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
87
101
|
|
|
102
|
+
const hasResolvedToolCalls = (message: ThreadMessage): boolean => {
|
|
103
|
+
if (message.role !== "assistant") return false;
|
|
104
|
+
for (const part of iterateToolCallParts(message.content)) {
|
|
105
|
+
if (isResolvedToolCall(part)) return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const listedPendingToolCallIds = (
|
|
111
|
+
assistant: Pick<ThreadAssistantMessage, "metadata">,
|
|
112
|
+
): ReadonlySet<string> | undefined => {
|
|
113
|
+
const ids = (
|
|
114
|
+
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
115
|
+
| AgUiCustomMetadata
|
|
116
|
+
| undefined
|
|
117
|
+
)?.pendingToolCallIds;
|
|
118
|
+
return ids === undefined ? undefined : new Set(ids);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type ReplayContext = {
|
|
122
|
+
// Immutable history reference: partial argument frames cannot redefine consent.
|
|
123
|
+
readonly originalContent: ThreadAssistantMessage["content"];
|
|
124
|
+
accountedResultIds: ReadonlySet<string>;
|
|
125
|
+
admittedToolCallIds: ReadonlySet<string>;
|
|
126
|
+
merge: ReturnType<typeof createReplayContentMerger>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type SnapshotMessageReconciler = (
|
|
130
|
+
snapshot: ThreadAssistantMessage,
|
|
131
|
+
previous: ThreadAssistantMessage | undefined,
|
|
132
|
+
) => ThreadAssistantMessage;
|
|
133
|
+
|
|
134
|
+
const withoutPendingToolCallIds = (
|
|
135
|
+
metadata: ThreadAssistantMessage["metadata"],
|
|
136
|
+
): ThreadAssistantMessage["metadata"] => {
|
|
137
|
+
const namespace = metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
138
|
+
| AgUiCustomMetadata
|
|
139
|
+
| undefined;
|
|
140
|
+
if (!namespace || !("pendingToolCallIds" in namespace)) return metadata;
|
|
141
|
+
const { pendingToolCallIds: _pendingToolCallIds, ...remainingNamespace } =
|
|
142
|
+
namespace;
|
|
143
|
+
const { [AG_UI_METADATA_NAMESPACE]: _namespace, ...remainingCustom } =
|
|
144
|
+
metadata.custom;
|
|
145
|
+
return {
|
|
146
|
+
...metadata,
|
|
147
|
+
custom: {
|
|
148
|
+
...remainingCustom,
|
|
149
|
+
...(Object.keys(remainingNamespace).length > 0
|
|
150
|
+
? { [AG_UI_METADATA_NAMESPACE]: remainingNamespace }
|
|
151
|
+
: {}),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const withPendingToolCallIds = (
|
|
157
|
+
metadata: ThreadAssistantMessage["metadata"],
|
|
158
|
+
ids: Iterable<string>,
|
|
159
|
+
): ThreadAssistantMessage["metadata"] => ({
|
|
160
|
+
...metadata,
|
|
161
|
+
custom: {
|
|
162
|
+
...metadata.custom,
|
|
163
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
164
|
+
...(metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
165
|
+
| AgUiCustomMetadata
|
|
166
|
+
| undefined),
|
|
167
|
+
pendingToolCallIds: [...ids],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const actionableToolCallIds = (
|
|
173
|
+
message: ThreadMessage | undefined,
|
|
174
|
+
): ReadonlySet<string> => {
|
|
175
|
+
if (
|
|
176
|
+
message?.role !== "assistant" ||
|
|
177
|
+
message.status?.type !== "requires-action" ||
|
|
178
|
+
message.status.reason !== "tool-calls"
|
|
179
|
+
)
|
|
180
|
+
return new Set();
|
|
181
|
+
const permitted = listedPendingToolCallIds(message);
|
|
182
|
+
return new Set(
|
|
183
|
+
Array.from(iterateToolCallParts(message.content))
|
|
184
|
+
.filter(
|
|
185
|
+
(part) =>
|
|
186
|
+
!isResolvedToolCall(part) &&
|
|
187
|
+
(!permitted || permitted.has(part.toolCallId)),
|
|
188
|
+
)
|
|
189
|
+
.map((part) => part.toolCallId),
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
88
193
|
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
89
194
|
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
90
195
|
|
|
@@ -155,7 +260,7 @@ const mergeAgUiNamespace = (
|
|
|
155
260
|
return merged;
|
|
156
261
|
};
|
|
157
262
|
|
|
158
|
-
export class
|
|
263
|
+
export class AgUiThreadController {
|
|
159
264
|
private agent: AbstractAgent;
|
|
160
265
|
private logger: Logger;
|
|
161
266
|
private showThinking: boolean;
|
|
@@ -194,27 +299,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
194
299
|
};
|
|
195
300
|
},
|
|
196
301
|
});
|
|
197
|
-
private
|
|
302
|
+
private state: AgUiThreadState = createAgUiThreadState();
|
|
198
303
|
private abortController: AbortController | null = null;
|
|
199
304
|
// The agent that started the active run. updateOptions can swap this.agent
|
|
200
305
|
// mid-run, and cancelling has to reach the agent holding the live request.
|
|
201
306
|
private activeRunAgent: AbstractAgent | null = null;
|
|
202
|
-
private stateSnapshot: ReadonlyJSONValue | undefined;
|
|
203
307
|
private history: ThreadHistoryAdapter | undefined;
|
|
204
|
-
private lastRunConfig: RunConfig | undefined;
|
|
205
308
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
206
309
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
207
310
|
private readonly persistedHistoryIds = new Set<string>();
|
|
208
|
-
private readonly historyWrites =
|
|
209
|
-
private _isLoading = false;
|
|
311
|
+
private readonly historyWrites = createHistoryWriteChain();
|
|
210
312
|
private _historyImportPromise: Promise<void> | undefined;
|
|
211
|
-
private historyMutationGeneration = 0;
|
|
212
313
|
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
314
|
|
|
219
315
|
constructor(options: CoreOptions) {
|
|
220
316
|
this.agent = options.agent;
|
|
@@ -237,12 +333,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
237
333
|
|
|
238
334
|
getSnapshot = () =>
|
|
239
335
|
(this.snapshot ??= {
|
|
240
|
-
isLoading: this.
|
|
241
|
-
isRunning: this.
|
|
336
|
+
isLoading: this.state.isLoading,
|
|
337
|
+
isRunning: this.state.isRunning,
|
|
242
338
|
messageRepository: this.session.export(),
|
|
243
|
-
state: this.stateSnapshot,
|
|
339
|
+
state: this.state.stateSnapshot,
|
|
244
340
|
});
|
|
245
341
|
|
|
342
|
+
private dispatch(action: AgUiThreadAction): void {
|
|
343
|
+
this.state = reduceAgUiThreadState(this.state, action);
|
|
344
|
+
}
|
|
345
|
+
|
|
246
346
|
private publishUpdate(): void {
|
|
247
347
|
this.snapshot = undefined;
|
|
248
348
|
for (const listener of this.listeners) listener();
|
|
@@ -261,7 +361,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
261
361
|
const previousHistory = this.history;
|
|
262
362
|
this.history = options.history;
|
|
263
363
|
if (
|
|
264
|
-
this.
|
|
364
|
+
this.state.loadRequested &&
|
|
265
365
|
!this._loadPromise &&
|
|
266
366
|
!previousHistory &&
|
|
267
367
|
options.history &&
|
|
@@ -291,19 +391,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
291
391
|
}
|
|
292
392
|
|
|
293
393
|
getState(): ReadonlyJSONValue | undefined {
|
|
294
|
-
return this.stateSnapshot;
|
|
394
|
+
return this.state.stateSnapshot;
|
|
295
395
|
}
|
|
296
396
|
|
|
297
397
|
isRunning(): boolean {
|
|
298
|
-
return this.
|
|
398
|
+
return this.state.isRunning;
|
|
299
399
|
}
|
|
300
400
|
|
|
301
401
|
get isLoading(): boolean {
|
|
302
|
-
return this.
|
|
402
|
+
return this.state.isLoading;
|
|
303
403
|
}
|
|
304
404
|
|
|
305
405
|
__internal_load(): Promise<void> {
|
|
306
|
-
this.
|
|
406
|
+
this.dispatch({ type: "loadRequested" });
|
|
307
407
|
if (this._loadPromise) return this._loadPromise;
|
|
308
408
|
if (!this.history) return Promise.resolve();
|
|
309
409
|
|
|
@@ -326,7 +426,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
326
426
|
return repo;
|
|
327
427
|
});
|
|
328
428
|
|
|
329
|
-
this.
|
|
429
|
+
this.dispatch({ type: "loadStarted" });
|
|
330
430
|
this._historyImportPromise = promise.then(
|
|
331
431
|
() => {
|
|
332
432
|
this._historyImportPromise = undefined;
|
|
@@ -343,7 +443,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
343
443
|
const resumeStream = this.history?.resume?.bind(this.history);
|
|
344
444
|
await this.startRun(
|
|
345
445
|
parentId,
|
|
346
|
-
this.lastRunConfig,
|
|
446
|
+
this.state.lastRunConfig,
|
|
347
447
|
undefined,
|
|
348
448
|
resumeStream,
|
|
349
449
|
);
|
|
@@ -354,7 +454,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
354
454
|
this.reportError(error);
|
|
355
455
|
})
|
|
356
456
|
.finally(() => {
|
|
357
|
-
this.
|
|
457
|
+
this.dispatch({
|
|
458
|
+
type: "loadFinished",
|
|
459
|
+
});
|
|
358
460
|
this.publishUpdate();
|
|
359
461
|
});
|
|
360
462
|
|
|
@@ -365,10 +467,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
365
467
|
private waitForHistoryImport(): Promise<boolean> | undefined {
|
|
366
468
|
const historyImport = this._historyImportPromise;
|
|
367
469
|
if (!historyImport) return undefined;
|
|
368
|
-
const generation = this.historyMutationGeneration;
|
|
470
|
+
const generation = this.state.historyMutationGeneration;
|
|
369
471
|
return historyImport.then(
|
|
370
472
|
() =>
|
|
371
|
-
generation === this.historyMutationGeneration &&
|
|
473
|
+
generation === this.state.historyMutationGeneration &&
|
|
372
474
|
!this.isThreadSwitching?.(),
|
|
373
475
|
);
|
|
374
476
|
}
|
|
@@ -376,10 +478,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
376
478
|
private deferUntilHistoryImported(callback: () => void): boolean {
|
|
377
479
|
const historyImport = this._historyImportPromise;
|
|
378
480
|
if (!historyImport) return false;
|
|
379
|
-
const generation = this.historyMutationGeneration;
|
|
481
|
+
const generation = this.state.historyMutationGeneration;
|
|
380
482
|
void historyImport.then(() => {
|
|
381
483
|
if (
|
|
382
|
-
generation !== this.historyMutationGeneration ||
|
|
484
|
+
generation !== this.state.historyMutationGeneration ||
|
|
383
485
|
this.isThreadSwitching?.()
|
|
384
486
|
) {
|
|
385
487
|
return;
|
|
@@ -397,10 +499,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
397
499
|
const historyImport = this._historyImportPromise;
|
|
398
500
|
if (historyImport) {
|
|
399
501
|
const wasAtTail = message.parentId === this.session.headId;
|
|
400
|
-
const generation = this.historyMutationGeneration;
|
|
502
|
+
const generation = this.state.historyMutationGeneration;
|
|
401
503
|
await historyImport;
|
|
402
504
|
if (
|
|
403
|
-
generation !== this.historyMutationGeneration ||
|
|
505
|
+
generation !== this.state.historyMutationGeneration ||
|
|
404
506
|
this.isThreadSwitching?.()
|
|
405
507
|
)
|
|
406
508
|
return;
|
|
@@ -434,10 +536,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
434
536
|
|
|
435
537
|
private maybeAutoCancelPendingToolCalls(): void {
|
|
436
538
|
if (this.autoCancelPendingToolCalls === false) return;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
539
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
540
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
541
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
542
|
+
}
|
|
441
543
|
}
|
|
442
544
|
|
|
443
545
|
private appendEntry(message: AppendMessage): string {
|
|
@@ -471,7 +573,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
471
573
|
}
|
|
472
574
|
|
|
473
575
|
async cancel(): Promise<void> {
|
|
474
|
-
this.
|
|
576
|
+
this.dispatch({
|
|
577
|
+
type: "historyMutationAdvanced",
|
|
578
|
+
});
|
|
475
579
|
this.abortActiveRun();
|
|
476
580
|
}
|
|
477
581
|
|
|
@@ -518,9 +622,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
518
622
|
this.assertNoPendingInterrupts();
|
|
519
623
|
await this.startRun(
|
|
520
624
|
config.parentId,
|
|
521
|
-
config.runConfig ?? this.lastRunConfig,
|
|
625
|
+
config.runConfig ?? this.state.lastRunConfig,
|
|
522
626
|
undefined,
|
|
523
627
|
config.stream,
|
|
628
|
+
true,
|
|
524
629
|
);
|
|
525
630
|
}
|
|
526
631
|
|
|
@@ -541,7 +646,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
541
646
|
try {
|
|
542
647
|
await this.startRun(
|
|
543
648
|
parentId,
|
|
544
|
-
this.lastRunConfig,
|
|
649
|
+
this.state.lastRunConfig,
|
|
545
650
|
undefined,
|
|
546
651
|
resumeStream,
|
|
547
652
|
);
|
|
@@ -560,17 +665,32 @@ export class AgUiThreadRuntimeCore {
|
|
|
560
665
|
private findRequiresActionAssistant(
|
|
561
666
|
reason: "interrupt" | "tool-calls",
|
|
562
667
|
): ThreadAssistantMessage | null {
|
|
563
|
-
|
|
564
|
-
(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
668
|
+
return (
|
|
669
|
+
this.getTrailingAssistantTurn().findLast(
|
|
670
|
+
(message) =>
|
|
671
|
+
message.status?.type === "requires-action" &&
|
|
672
|
+
message.status.reason === reason,
|
|
673
|
+
) ?? null
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private getTrailingAssistantTurn(): ThreadAssistantMessage[] {
|
|
678
|
+
const messages = this.getMessages();
|
|
679
|
+
const turn: ThreadAssistantMessage[] = [];
|
|
680
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
681
|
+
const message = messages[index];
|
|
682
|
+
if (message?.role !== "assistant") break;
|
|
683
|
+
turn.unshift(message as ThreadAssistantMessage);
|
|
572
684
|
}
|
|
573
|
-
return
|
|
685
|
+
return turn;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private getPendingToolCallMessages(): ThreadAssistantMessage[] {
|
|
689
|
+
return this.getTrailingAssistantTurn().filter(
|
|
690
|
+
(message) =>
|
|
691
|
+
message.status?.type === "requires-action" &&
|
|
692
|
+
message.status.reason === "tool-calls",
|
|
693
|
+
);
|
|
574
694
|
}
|
|
575
695
|
|
|
576
696
|
getPendingInterrupts(): {
|
|
@@ -592,15 +712,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
592
712
|
messageId: string;
|
|
593
713
|
toolCallIds: string[];
|
|
594
714
|
} | null {
|
|
595
|
-
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
596
|
-
if (!assistant) return null;
|
|
597
715
|
const toolCallIds: string[] = [];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
716
|
+
let messageId: string | undefined;
|
|
717
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
718
|
+
const listed = listedPendingToolCallIds(assistant);
|
|
719
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
720
|
+
if (
|
|
721
|
+
isResolvedToolCall(part) ||
|
|
722
|
+
(listed && !listed.has(part.toolCallId))
|
|
723
|
+
)
|
|
724
|
+
continue;
|
|
725
|
+
messageId ??= assistant.id;
|
|
726
|
+
toolCallIds.push(part.toolCallId);
|
|
727
|
+
}
|
|
601
728
|
}
|
|
602
|
-
|
|
603
|
-
return { messageId: assistant.id, toolCallIds };
|
|
729
|
+
return messageId === undefined ? null : { messageId, toolCallIds };
|
|
604
730
|
}
|
|
605
731
|
|
|
606
732
|
async submitInterruptResponses(
|
|
@@ -632,6 +758,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
632
758
|
this.assertInterruptsAnswerable(
|
|
633
759
|
"submitInterruptResponses",
|
|
634
760
|
pending.interrupts,
|
|
761
|
+
responsesById,
|
|
635
762
|
);
|
|
636
763
|
|
|
637
764
|
await this.resumeWithResponses(
|
|
@@ -650,8 +777,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
650
777
|
messageId: string,
|
|
651
778
|
resume: AgUiResumeEntry[],
|
|
652
779
|
): Promise<void> {
|
|
780
|
+
const parentId = this.session.headId ?? messageId;
|
|
653
781
|
this.clearPendingInterrupts(messageId, resume);
|
|
654
|
-
await this.startRun(
|
|
782
|
+
await this.startRun(parentId, this.state.lastRunConfig, {
|
|
655
783
|
entries: resume,
|
|
656
784
|
interruptedMessageId: messageId,
|
|
657
785
|
});
|
|
@@ -679,6 +807,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
679
807
|
private assertInterruptsAnswerable(
|
|
680
808
|
method: "submitInterruptResponses" | "respondToToolApproval",
|
|
681
809
|
interrupts: readonly AgUiInterrupt[],
|
|
810
|
+
responsesById?: ReadonlyMap<string, AgUiResumeEntry>,
|
|
682
811
|
): void {
|
|
683
812
|
const now = Date.now();
|
|
684
813
|
for (const interrupt of interrupts) {
|
|
@@ -689,13 +818,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
689
818
|
`[agui] ${method}: interrupt ${interrupt.id} has malformed expiresAt "${interrupt.expiresAt}"`,
|
|
690
819
|
);
|
|
691
820
|
}
|
|
692
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
expiry <= now &&
|
|
823
|
+
responsesById?.get(interrupt.id)?.status !== "cancelled"
|
|
824
|
+
) {
|
|
693
825
|
throw new Error(
|
|
694
826
|
`[agui] ${method}: interrupt ${interrupt.id} expired at ${interrupt.expiresAt}`,
|
|
695
827
|
);
|
|
696
828
|
}
|
|
697
829
|
}
|
|
698
|
-
if (this.
|
|
830
|
+
if (this.state.isRunning) {
|
|
699
831
|
throw new Error(`[agui] ${method}: a run is already in progress`);
|
|
700
832
|
}
|
|
701
833
|
}
|
|
@@ -789,11 +921,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
789
921
|
"[agui] steerAway: responses are only valid for pending interrupts",
|
|
790
922
|
);
|
|
791
923
|
}
|
|
792
|
-
if (this.
|
|
924
|
+
if (this.state.isRunning) {
|
|
793
925
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
794
926
|
}
|
|
795
|
-
this.
|
|
796
|
-
|
|
927
|
+
for (const assistant of this.getPendingToolCallMessages()) {
|
|
928
|
+
this.cancelUnresolvedToolCalls(assistant.id);
|
|
929
|
+
this.maybeCompleteAfterToolResults(assistant.id);
|
|
930
|
+
}
|
|
797
931
|
const normalized = this.toAppendMessage(message);
|
|
798
932
|
const threadMessageId = this.appendEntry(normalized);
|
|
799
933
|
await this.startRun(threadMessageId, normalized.runConfig);
|
|
@@ -810,7 +944,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
810
944
|
|
|
811
945
|
const resume = this.resolveSteerAwayResume(pending.interrupts, responses);
|
|
812
946
|
|
|
813
|
-
if (this.
|
|
947
|
+
if (this.state.isRunning) {
|
|
814
948
|
throw new Error("[agui] steerAway: a run is already in progress");
|
|
815
949
|
}
|
|
816
950
|
|
|
@@ -1067,7 +1201,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1067
1201
|
return;
|
|
1068
1202
|
}
|
|
1069
1203
|
|
|
1070
|
-
await this.
|
|
1204
|
+
await this.historyWrites.chain(sessionMessageId, () =>
|
|
1071
1205
|
history.update!({ parentId: item.parentId, message }),
|
|
1072
1206
|
);
|
|
1073
1207
|
}
|
|
@@ -1090,11 +1224,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1090
1224
|
if (!("timestamp" in userAction)) {
|
|
1091
1225
|
userAction.timestamp = new Date().toISOString();
|
|
1092
1226
|
}
|
|
1093
|
-
this.
|
|
1227
|
+
this.dispatch({
|
|
1228
|
+
type: "a2uiActionDeferred",
|
|
1229
|
+
action: userAction,
|
|
1230
|
+
});
|
|
1094
1231
|
|
|
1095
1232
|
const owner = this.abortController;
|
|
1096
1233
|
if (owner) {
|
|
1097
|
-
this.
|
|
1234
|
+
this.dispatch({
|
|
1235
|
+
type: "a2uiResumeDeferred",
|
|
1236
|
+
owner,
|
|
1237
|
+
});
|
|
1098
1238
|
return;
|
|
1099
1239
|
}
|
|
1100
1240
|
this.startResumeRun(parentId);
|
|
@@ -1103,17 +1243,41 @@ export class AgUiThreadRuntimeCore {
|
|
|
1103
1243
|
// The continuation fires whether the frontend result lands before
|
|
1104
1244
|
// RUN_FINISHED (the status flips to requires-action only later, while the
|
|
1105
1245
|
// run is still draining) or after it.
|
|
1106
|
-
private maybeResumeAfterToolResults(
|
|
1246
|
+
private maybeResumeAfterToolResults(
|
|
1247
|
+
messageId: string,
|
|
1248
|
+
accountedResultIds?: ReadonlySet<string>,
|
|
1249
|
+
): void {
|
|
1107
1250
|
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
1108
|
-
|
|
1251
|
+
if (accountedResultIds) {
|
|
1252
|
+
const message = this.session.tryGetMessage(messageId)?.message;
|
|
1253
|
+
if (
|
|
1254
|
+
message?.role !== "assistant" ||
|
|
1255
|
+
!Array.from(iterateToolCallParts(message.content)).some(
|
|
1256
|
+
(part) =>
|
|
1257
|
+
isResolvedToolCall(part) &&
|
|
1258
|
+
!accountedResultIds.has(part.toolCallId),
|
|
1259
|
+
)
|
|
1260
|
+
)
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (this.getPendingInterrupts()) return;
|
|
1264
|
+
const turn = this.getTrailingAssistantTurn();
|
|
1265
|
+
if (!turn.some((message) => message.id === messageId)) return;
|
|
1109
1266
|
const owner = this.abortController;
|
|
1110
1267
|
if (owner) {
|
|
1111
1268
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
1112
1269
|
// closed). Defer until startRun's tail so we never start two runs.
|
|
1113
|
-
this.
|
|
1270
|
+
this.dispatch({
|
|
1271
|
+
type: "resumeDeferred",
|
|
1272
|
+
owner,
|
|
1273
|
+
messageId,
|
|
1274
|
+
});
|
|
1114
1275
|
return;
|
|
1115
1276
|
}
|
|
1116
|
-
this.
|
|
1277
|
+
if (this.getPendingToolCalls()) return;
|
|
1278
|
+
const headId = turn.at(-1)?.id;
|
|
1279
|
+
if (headId === undefined) return;
|
|
1280
|
+
this.startResumeRun(headId);
|
|
1117
1281
|
}
|
|
1118
1282
|
|
|
1119
1283
|
private maybeCompleteAfterToolResults(messageId: string): boolean {
|
|
@@ -1127,7 +1291,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1127
1291
|
return false;
|
|
1128
1292
|
}
|
|
1129
1293
|
let allResolved = true;
|
|
1294
|
+
const listed = listedPendingToolCallIds(assistant);
|
|
1130
1295
|
for (const part of iterateToolCallParts(assistant.content)) {
|
|
1296
|
+
if (listed && !listed.has(part.toolCallId)) continue;
|
|
1131
1297
|
if (!isResolvedToolCall(part)) {
|
|
1132
1298
|
allResolved = false;
|
|
1133
1299
|
break;
|
|
@@ -1140,6 +1306,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1140
1306
|
? {
|
|
1141
1307
|
...(current as ThreadAssistantMessage),
|
|
1142
1308
|
status: { type: "complete" as const, reason: "unknown" as const },
|
|
1309
|
+
metadata: withoutPendingToolCallIds(current.metadata),
|
|
1143
1310
|
}
|
|
1144
1311
|
: current,
|
|
1145
1312
|
);
|
|
@@ -1150,7 +1317,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1150
1317
|
}
|
|
1151
1318
|
|
|
1152
1319
|
private startResumeRun(messageId: string): void {
|
|
1153
|
-
void this.startRun(messageId, this.lastRunConfig).catch((error) => {
|
|
1320
|
+
void this.startRun(messageId, this.state.lastRunConfig).catch((error) => {
|
|
1154
1321
|
this.reportError(error);
|
|
1155
1322
|
});
|
|
1156
1323
|
}
|
|
@@ -1167,8 +1334,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1167
1334
|
metadata: { ...message.metadata, modality },
|
|
1168
1335
|
} as ThreadMessage;
|
|
1169
1336
|
});
|
|
1170
|
-
this.
|
|
1171
|
-
|
|
1337
|
+
this.dispatch({
|
|
1338
|
+
type: "a2uiContinuationsCleared",
|
|
1339
|
+
});
|
|
1172
1340
|
this.assistantHistoryParents.clear();
|
|
1173
1341
|
|
|
1174
1342
|
if (messages.length === 0) {
|
|
@@ -1178,6 +1346,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
1178
1346
|
let lastAppliedId: string | null = null;
|
|
1179
1347
|
let hardReplace = false;
|
|
1180
1348
|
const seen = new Set<string>();
|
|
1349
|
+
const retainToolResults = (
|
|
1350
|
+
existing: ThreadMessage | undefined,
|
|
1351
|
+
message: ThreadMessage,
|
|
1352
|
+
): ThreadMessage =>
|
|
1353
|
+
existing?.role === "assistant" && message.role === "assistant"
|
|
1354
|
+
? {
|
|
1355
|
+
...message,
|
|
1356
|
+
content: this.preserveToolResults(
|
|
1357
|
+
existing.content,
|
|
1358
|
+
message.content,
|
|
1359
|
+
),
|
|
1360
|
+
}
|
|
1361
|
+
: message;
|
|
1181
1362
|
|
|
1182
1363
|
for (const message of messages) {
|
|
1183
1364
|
if (seen.has(message.id)) continue;
|
|
@@ -1187,12 +1368,21 @@ export class AgUiThreadRuntimeCore {
|
|
|
1187
1368
|
hardReplace = true;
|
|
1188
1369
|
break;
|
|
1189
1370
|
}
|
|
1190
|
-
this.session.addOrUpdateMessage(
|
|
1371
|
+
this.session.addOrUpdateMessage(
|
|
1372
|
+
expectedParentId,
|
|
1373
|
+
retainToolResults(existing?.message, message),
|
|
1374
|
+
);
|
|
1191
1375
|
expectedParentId = message.id;
|
|
1192
1376
|
lastAppliedId = message.id;
|
|
1193
1377
|
}
|
|
1194
1378
|
|
|
1195
1379
|
if (hardReplace) {
|
|
1380
|
+
const previous = new Map(
|
|
1381
|
+
this.getMessageRepository().messages.map(({ message }) => [
|
|
1382
|
+
message.id,
|
|
1383
|
+
message,
|
|
1384
|
+
]),
|
|
1385
|
+
);
|
|
1196
1386
|
this.session.clear();
|
|
1197
1387
|
expectedParentId = null;
|
|
1198
1388
|
lastAppliedId = null;
|
|
@@ -1200,35 +1390,57 @@ export class AgUiThreadRuntimeCore {
|
|
|
1200
1390
|
for (const message of messages) {
|
|
1201
1391
|
if (seen.has(message.id)) continue;
|
|
1202
1392
|
seen.add(message.id);
|
|
1203
|
-
this.session.addOrUpdateMessage(
|
|
1393
|
+
this.session.addOrUpdateMessage(
|
|
1394
|
+
expectedParentId,
|
|
1395
|
+
retainToolResults(previous.get(message.id), message),
|
|
1396
|
+
);
|
|
1204
1397
|
expectedParentId = message.id;
|
|
1205
1398
|
lastAppliedId = message.id;
|
|
1206
1399
|
}
|
|
1207
1400
|
}
|
|
1208
1401
|
|
|
1209
1402
|
this.session.resetHead(lastAppliedId);
|
|
1403
|
+
for (const messageId of seen) {
|
|
1404
|
+
this.maybeCompleteAfterToolResults(messageId);
|
|
1405
|
+
}
|
|
1210
1406
|
}
|
|
1211
1407
|
|
|
1212
1408
|
this.snapshotHistoryIds.clear();
|
|
1213
1409
|
for (const { message } of this.getMessageRepository().messages) {
|
|
1214
1410
|
this.snapshotHistoryIds.add(message.id);
|
|
1215
1411
|
}
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1412
|
+
// A soft merge leaves dropped messages off branch; only selected
|
|
1413
|
+
// messages with resolved tool calls can still own a continuation.
|
|
1414
|
+
const pendingResume = this.state.pendingResume;
|
|
1415
|
+
if (pendingResume) {
|
|
1416
|
+
const selectedResultOwnerIds = new Set(
|
|
1417
|
+
this.session
|
|
1418
|
+
.getMessages()
|
|
1419
|
+
.filter(hasResolvedToolCalls)
|
|
1420
|
+
.map((message) => message.id),
|
|
1421
|
+
);
|
|
1422
|
+
const retained = pendingResume.messageIds.filter((id) =>
|
|
1423
|
+
selectedResultOwnerIds.has(id),
|
|
1424
|
+
);
|
|
1425
|
+
if (retained.length !== pendingResume.messageIds.length) {
|
|
1426
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1427
|
+
for (const messageId of retained) {
|
|
1428
|
+
this.dispatch({
|
|
1429
|
+
type: "resumeDeferred",
|
|
1430
|
+
owner: pendingResume.owner,
|
|
1431
|
+
messageId,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1226
1435
|
}
|
|
1227
1436
|
this.publishUpdate();
|
|
1228
1437
|
}
|
|
1229
1438
|
|
|
1230
1439
|
loadExternalState(state: ReadonlyJSONValue): void {
|
|
1231
|
-
this.
|
|
1440
|
+
this.dispatch({
|
|
1441
|
+
type: "stateSnapshot",
|
|
1442
|
+
snapshot: state,
|
|
1443
|
+
});
|
|
1232
1444
|
this.publishUpdate();
|
|
1233
1445
|
}
|
|
1234
1446
|
|
|
@@ -1237,25 +1449,28 @@ export class AgUiThreadRuntimeCore {
|
|
|
1237
1449
|
| ReadonlyJSONValue
|
|
1238
1450
|
| ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
|
|
1239
1451
|
): void {
|
|
1240
|
-
|
|
1241
|
-
typeof next === "function" ? next(this.stateSnapshot) : next;
|
|
1452
|
+
const snapshot =
|
|
1453
|
+
typeof next === "function" ? next(this.state.stateSnapshot) : next;
|
|
1454
|
+
this.dispatch({
|
|
1455
|
+
type: "stateSnapshot",
|
|
1456
|
+
snapshot,
|
|
1457
|
+
});
|
|
1242
1458
|
this.publishUpdate();
|
|
1243
1459
|
}
|
|
1244
1460
|
|
|
1245
1461
|
resetState(): void {
|
|
1246
|
-
this.
|
|
1462
|
+
this.dispatch({ type: "resetState" });
|
|
1247
1463
|
this.publishUpdate();
|
|
1248
1464
|
}
|
|
1249
1465
|
|
|
1250
1466
|
resetThreadState(): void {
|
|
1251
|
-
this.
|
|
1467
|
+
this.dispatch({
|
|
1468
|
+
type: "historyMutationAdvanced",
|
|
1469
|
+
});
|
|
1252
1470
|
const controller = this.abortController;
|
|
1253
1471
|
const activeRunAgent = this.activeRunAgent;
|
|
1254
1472
|
|
|
1255
|
-
this.
|
|
1256
|
-
this.pendingResume = null;
|
|
1257
|
-
this.pendingA2uiResumeOwner = null;
|
|
1258
|
-
this.pendingA2uiAction = undefined;
|
|
1473
|
+
this.dispatch({ type: "resetThread" });
|
|
1259
1474
|
|
|
1260
1475
|
if (controller) {
|
|
1261
1476
|
this.abortController = null;
|
|
@@ -1278,6 +1493,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1278
1493
|
runConfig?: RunConfig,
|
|
1279
1494
|
resume?: ResumeDispatch,
|
|
1280
1495
|
resumeStream?: ResumeStream,
|
|
1496
|
+
replayExistingMessages = false,
|
|
1281
1497
|
): Promise<void> {
|
|
1282
1498
|
if (this.isThreadSwitching?.()) return;
|
|
1283
1499
|
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
@@ -1285,8 +1501,12 @@ export class AgUiThreadRuntimeCore {
|
|
|
1285
1501
|
// its message; this covers the entry points that start a run without one.
|
|
1286
1502
|
if (!this.supersedeActiveRun()) return;
|
|
1287
1503
|
|
|
1504
|
+
const generation = this.state.historyMutationGeneration;
|
|
1288
1505
|
const normalizedRunConfig = runConfig ?? {};
|
|
1289
|
-
this.
|
|
1506
|
+
this.dispatch({
|
|
1507
|
+
type: "runConfigRecorded",
|
|
1508
|
+
runConfig: normalizedRunConfig,
|
|
1509
|
+
});
|
|
1290
1510
|
const parent =
|
|
1291
1511
|
parentId === null ? undefined : this.session.tryGetMessage(parentId);
|
|
1292
1512
|
const shouldEagerlyInsertAssistant =
|
|
@@ -1301,12 +1521,62 @@ export class AgUiThreadRuntimeCore {
|
|
|
1301
1521
|
const runStartMessageIds = new Set(
|
|
1302
1522
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1303
1523
|
);
|
|
1524
|
+
const selectedMessages = this.session.getMessages();
|
|
1525
|
+
const parentIndex = selectedMessages.findIndex(
|
|
1526
|
+
(message) => message.id === parentId,
|
|
1527
|
+
);
|
|
1528
|
+
let replayStart = parentIndex + 1;
|
|
1529
|
+
if (selectedMessages[parentIndex]?.role === "assistant") {
|
|
1530
|
+
replayStart = parentIndex;
|
|
1531
|
+
while (
|
|
1532
|
+
replayStart > 0 &&
|
|
1533
|
+
selectedMessages[replayStart - 1]?.role === "assistant"
|
|
1534
|
+
)
|
|
1535
|
+
replayStart--;
|
|
1536
|
+
}
|
|
1537
|
+
const replayMessageIds: string[] = [];
|
|
1538
|
+
if (replayExistingMessages && !resumeStream && parentIndex >= 0) {
|
|
1539
|
+
for (const message of selectedMessages.slice(replayStart)) {
|
|
1540
|
+
if (message.role !== "assistant") break;
|
|
1541
|
+
replayMessageIds.push(message.id);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// Only request-delivered and producer-observed results suppress continuation.
|
|
1545
|
+
const accountedResultIds = new Set(
|
|
1546
|
+
historicalMessages.flatMap((message) =>
|
|
1547
|
+
message.role === "assistant"
|
|
1548
|
+
? Array.from(iterateToolCallParts(message.content))
|
|
1549
|
+
.filter(isResolvedToolCall)
|
|
1550
|
+
.map((part) => part.toolCallId)
|
|
1551
|
+
: [],
|
|
1552
|
+
),
|
|
1553
|
+
);
|
|
1554
|
+
const candidateContexts = new Map<string, ReplayContext>();
|
|
1555
|
+
for (const message of selectedMessages) {
|
|
1556
|
+
if (
|
|
1557
|
+
message.role !== "assistant" ||
|
|
1558
|
+
!replayMessageIds.includes(message.id)
|
|
1559
|
+
)
|
|
1560
|
+
continue;
|
|
1561
|
+
candidateContexts.set(message.id, {
|
|
1562
|
+
originalContent: message.content,
|
|
1563
|
+
accountedResultIds,
|
|
1564
|
+
admittedToolCallIds: actionableToolCallIds(message),
|
|
1565
|
+
merge: createReplayContentMerger(),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
let replayContexts = new Map<string, ReplayContext>();
|
|
1569
|
+
let lastReplayedIndex = -1;
|
|
1570
|
+
let replayEnded = replayMessageIds.length === 0;
|
|
1571
|
+
const replayTail = replayMessageIds.at(-1);
|
|
1304
1572
|
const joinStrategy = this.joinStrategy;
|
|
1305
1573
|
const hasRunUserBoundary = parent?.message.role === "user" && !resume;
|
|
1306
1574
|
|
|
1307
1575
|
let pendingError: Error | null = null;
|
|
1308
1576
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1309
1577
|
let assistantMessageId: string | undefined;
|
|
1578
|
+
let snapshotAssistantId: string | undefined;
|
|
1579
|
+
let replayInterruptMessageId: string | undefined;
|
|
1310
1580
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
1311
1581
|
// assistant; recreating under the cached id on the next content-bearing
|
|
1312
1582
|
// emit keeps both the stream and the message identity. Status-only emits,
|
|
@@ -1315,6 +1585,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1315
1585
|
// resurrecting for custom-event data would leave a trailing data-only
|
|
1316
1586
|
// duplicate.
|
|
1317
1587
|
let assistantCollided = false;
|
|
1588
|
+
const heldMessages = new Map<string, Set<string>>();
|
|
1589
|
+
let frameOwnerIds = new Set<string>();
|
|
1590
|
+
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1591
|
+
let settlingRun = false;
|
|
1318
1592
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1319
1593
|
const cached = assistantMessageId;
|
|
1320
1594
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1324,10 +1598,16 @@ export class AgUiThreadRuntimeCore {
|
|
|
1324
1598
|
}
|
|
1325
1599
|
const repositoryHeadId = this.session.headId;
|
|
1326
1600
|
const shouldUseSelectedParent =
|
|
1327
|
-
|
|
1328
|
-
(
|
|
1329
|
-
(
|
|
1330
|
-
|
|
1601
|
+
replayContexts.size === 0 &&
|
|
1602
|
+
(cached === undefined ||
|
|
1603
|
+
(shouldEagerlyInsertAssistant &&
|
|
1604
|
+
(repositoryHeadId === null ||
|
|
1605
|
+
runStartMessageIds.has(repositoryHeadId)))) &&
|
|
1606
|
+
// Replay candidates defer the placeholder; honor snapshots that advanced
|
|
1607
|
+
// the head before the producer identified its first assistant message.
|
|
1608
|
+
(replayMessageIds.length === 0 ||
|
|
1609
|
+
repositoryHeadId === null ||
|
|
1610
|
+
runStartMessageIds.has(repositoryHeadId));
|
|
1331
1611
|
// Branch runs keep their selected parent unless the snapshot advanced to
|
|
1332
1612
|
// a message introduced during this run.
|
|
1333
1613
|
const parentId =
|
|
@@ -1342,35 +1622,348 @@ export class AgUiThreadRuntimeCore {
|
|
|
1342
1622
|
return created;
|
|
1343
1623
|
};
|
|
1344
1624
|
|
|
1345
|
-
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1625
|
+
if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
|
|
1626
|
+
|
|
1627
|
+
const settleHeldMessages = (terminalStatus: MessageStatus): string[] => {
|
|
1628
|
+
const pendingMessages: string[] = [];
|
|
1629
|
+
const producerPending = restrictedPendingToolCallIds;
|
|
1630
|
+
const canWaitForTools =
|
|
1631
|
+
terminalStatus.type === "complete" ||
|
|
1632
|
+
terminalStatus.type === "requires-action";
|
|
1633
|
+
if (producerPending && canWaitForTools) {
|
|
1634
|
+
for (const message of this.getTrailingAssistantTurn()) {
|
|
1635
|
+
const replay = replayContexts.get(message.id);
|
|
1636
|
+
if (
|
|
1637
|
+
message.id === assistantMessageId ||
|
|
1638
|
+
heldMessages.has(message.id) ||
|
|
1639
|
+
!replay ||
|
|
1640
|
+
(message.status?.type === "requires-action" &&
|
|
1641
|
+
message.status.reason === "interrupt")
|
|
1642
|
+
)
|
|
1643
|
+
continue;
|
|
1644
|
+
if (
|
|
1645
|
+
replay.admittedToolCallIds.size > 0 ||
|
|
1646
|
+
actionableToolCallIds(message).size > 0 ||
|
|
1647
|
+
Array.from(iterateToolCallParts(message.content)).some(
|
|
1648
|
+
(part) =>
|
|
1649
|
+
producerPending.has(part.toolCallId) &&
|
|
1650
|
+
!accountedResultIds.has(part.toolCallId),
|
|
1651
|
+
)
|
|
1652
|
+
)
|
|
1653
|
+
heldMessages.set(message.id, new Set());
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
for (const [messageId, openToolCallIds] of heldMessages) {
|
|
1657
|
+
const existing = this.session.tryGetMessage(messageId);
|
|
1658
|
+
if (existing)
|
|
1659
|
+
this.markPendingAssistantHistory(messageId, existing.parentId);
|
|
1660
|
+
let requiresToolResult = false;
|
|
1661
|
+
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1662
|
+
if (message.role !== "assistant") return message;
|
|
1663
|
+
const assistant = message as ThreadAssistantMessage;
|
|
1664
|
+
const pendingToolCallIds = producerPending
|
|
1665
|
+
? Array.from(iterateToolCallParts(assistant.content))
|
|
1666
|
+
.filter(
|
|
1667
|
+
(part) =>
|
|
1668
|
+
producerPending.has(part.toolCallId) &&
|
|
1669
|
+
(!isResolvedToolCall(part) ||
|
|
1670
|
+
openToolCallIds.has(part.toolCallId) ||
|
|
1671
|
+
(replayContexts.has(messageId) &&
|
|
1672
|
+
!accountedResultIds.has(part.toolCallId))),
|
|
1673
|
+
)
|
|
1674
|
+
.map((part) => part.toolCallId)
|
|
1675
|
+
: Array.from(openToolCallIds);
|
|
1676
|
+
const replay = replayContexts.get(messageId);
|
|
1677
|
+
if (replay) replay.admittedToolCallIds = new Set(pendingToolCallIds);
|
|
1678
|
+
const status: MessageStatus = canWaitForTools
|
|
1679
|
+
? pendingToolCallIds.length > 0
|
|
1680
|
+
? { type: "requires-action", reason: "tool-calls" }
|
|
1681
|
+
: { type: "complete", reason: "unknown" }
|
|
1682
|
+
: terminalStatus;
|
|
1683
|
+
requiresToolResult = status.type === "requires-action";
|
|
1684
|
+
const metadata = withoutPendingToolCallIds(assistant.metadata);
|
|
1685
|
+
return {
|
|
1686
|
+
...assistant,
|
|
1687
|
+
status,
|
|
1688
|
+
metadata:
|
|
1689
|
+
status.type === "requires-action" &&
|
|
1690
|
+
(restrictedPendingToolCallIds || replayContexts.has(messageId))
|
|
1691
|
+
? withPendingToolCallIds(metadata, pendingToolCallIds)
|
|
1692
|
+
: metadata,
|
|
1693
|
+
};
|
|
1694
|
+
});
|
|
1695
|
+
if (updated) {
|
|
1696
|
+
this.publishUpdate();
|
|
1697
|
+
this.persistAssistantHistory(messageId);
|
|
1698
|
+
if (requiresToolResult) {
|
|
1699
|
+
pendingMessages.push(messageId);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
heldMessages.clear();
|
|
1704
|
+
return pendingMessages;
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
const accountProducerResults = (
|
|
1708
|
+
content: ThreadAssistantMessage["content"],
|
|
1709
|
+
) => {
|
|
1710
|
+
for (const part of iterateToolCallParts(content))
|
|
1711
|
+
if (isResolvedToolCall(part)) accountedResultIds.add(part.toolCallId);
|
|
1712
|
+
};
|
|
1713
|
+
const reconcileReplaySnapshot: SnapshotMessageReconciler = (
|
|
1714
|
+
snapshot,
|
|
1715
|
+
previous,
|
|
1716
|
+
) => {
|
|
1717
|
+
// Observe the producer before preserving any local frontend results.
|
|
1718
|
+
accountProducerResults(snapshot.content);
|
|
1719
|
+
const replay = candidateContexts.get(snapshot.id);
|
|
1720
|
+
if (!replay) return snapshot;
|
|
1721
|
+
replay.admittedToolCallIds = actionableToolCallIds(snapshot);
|
|
1722
|
+
if (heldMessages.has(snapshot.id))
|
|
1723
|
+
heldMessages.set(snapshot.id, new Set(replay.admittedToolCallIds));
|
|
1724
|
+
if (!replayContexts.has(snapshot.id)) return snapshot;
|
|
1725
|
+
return previous
|
|
1726
|
+
? {
|
|
1727
|
+
...snapshot,
|
|
1728
|
+
content: this.preserveToolResults(
|
|
1729
|
+
previous.content,
|
|
1730
|
+
snapshot.content,
|
|
1731
|
+
replay.originalContent,
|
|
1732
|
+
),
|
|
1733
|
+
}
|
|
1734
|
+
: snapshot;
|
|
1735
|
+
};
|
|
1346
1736
|
|
|
1347
1737
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1738
|
+
if (
|
|
1739
|
+
!assistantCollided &&
|
|
1740
|
+
replayContexts.size &&
|
|
1741
|
+
update.content !== undefined
|
|
1742
|
+
)
|
|
1743
|
+
accountProducerResults(
|
|
1744
|
+
update.content as ThreadAssistantMessage["content"],
|
|
1745
|
+
);
|
|
1746
|
+
let ownerParts:
|
|
1747
|
+
| Map<string, ThreadAssistantMessage["content"][number][]>
|
|
1748
|
+
| undefined;
|
|
1749
|
+
let contentOwnerIds: Set<string> | undefined;
|
|
1348
1750
|
const hasStreamContent =
|
|
1349
1751
|
Array.isArray(update.content) &&
|
|
1350
1752
|
update.content.some((part) => part.type !== "data");
|
|
1753
|
+
if (!replayEnded && assistantMessageId === undefined) {
|
|
1754
|
+
if (!update.status || update.status.type === "running") return;
|
|
1755
|
+
// No matching producer identity arrived: retain native regeneration.
|
|
1756
|
+
replayEnded = true;
|
|
1757
|
+
if (
|
|
1758
|
+
replayContexts.size === 0 &&
|
|
1759
|
+
snapshotAssistantId !== undefined &&
|
|
1760
|
+
this.session.headId === snapshotAssistantId &&
|
|
1761
|
+
!hasStreamContent
|
|
1762
|
+
) {
|
|
1763
|
+
if (update.status.type !== "complete") {
|
|
1764
|
+
this.updateAssistantMessage(snapshotAssistantId, {
|
|
1765
|
+
status: update.status,
|
|
1766
|
+
...(update.metadata ? { metadata: update.metadata } : {}),
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const streamMessageId = assistantMessageId;
|
|
1773
|
+
let primaryMessageId = streamMessageId;
|
|
1774
|
+
const terminalStatus = update.status;
|
|
1775
|
+
const replayInterrupt =
|
|
1776
|
+
replayContexts.size > 0 &&
|
|
1777
|
+
terminalStatus?.type === "requires-action" &&
|
|
1778
|
+
terminalStatus.reason === "interrupt";
|
|
1779
|
+
if (
|
|
1780
|
+
streamMessageId !== undefined &&
|
|
1781
|
+
!assistantCollided &&
|
|
1782
|
+
replayContexts.size > 0 &&
|
|
1783
|
+
(update.content !== undefined || replayInterrupt)
|
|
1784
|
+
) {
|
|
1785
|
+
const owners = this.getTrailingAssistantTurn().filter((message) =>
|
|
1786
|
+
replayContexts.has(message.id),
|
|
1787
|
+
);
|
|
1788
|
+
// Buffered calls keep their owners when a later text frame arrives.
|
|
1789
|
+
const grouped =
|
|
1790
|
+
update.content !== undefined
|
|
1791
|
+
? groupReplayToolContent(owners, update.content, streamMessageId)
|
|
1792
|
+
: new Map([[streamMessageId, [...(update.content ?? [])]]]);
|
|
1793
|
+
if (update.content !== undefined) {
|
|
1794
|
+
const currentOwnerIds = new Set(grouped.keys());
|
|
1795
|
+
for (const ownerId of frameOwnerIds)
|
|
1796
|
+
grouped.set(ownerId, grouped.get(ownerId) ?? []);
|
|
1797
|
+
contentOwnerIds = new Set(grouped.keys());
|
|
1798
|
+
frameOwnerIds = currentOwnerIds;
|
|
1799
|
+
}
|
|
1800
|
+
ownerParts = grouped;
|
|
1801
|
+
if (replayInterrupt) {
|
|
1802
|
+
const interrupts = (
|
|
1803
|
+
update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE] as
|
|
1804
|
+
| AgUiCustomMetadata
|
|
1805
|
+
| undefined
|
|
1806
|
+
)?.interrupts;
|
|
1807
|
+
const approvals = projectAgUiToolApprovals(interrupts);
|
|
1808
|
+
for (const owner of owners) {
|
|
1809
|
+
if (
|
|
1810
|
+
Array.from(iterateToolCallParts(owner.content)).some((part) =>
|
|
1811
|
+
approvals.has(part.toolCallId),
|
|
1812
|
+
)
|
|
1813
|
+
)
|
|
1814
|
+
grouped.set(owner.id, grouped.get(owner.id) ?? []);
|
|
1815
|
+
}
|
|
1816
|
+
const anchor = owners.find(
|
|
1817
|
+
(message) =>
|
|
1818
|
+
projectAgUiToolApprovals(
|
|
1819
|
+
interrupts,
|
|
1820
|
+
new Set(
|
|
1821
|
+
Array.from(
|
|
1822
|
+
iterateToolCallParts([
|
|
1823
|
+
...message.content,
|
|
1824
|
+
...(grouped.get(message.id) ?? []),
|
|
1825
|
+
]),
|
|
1826
|
+
).map((part) => part.toolCallId),
|
|
1827
|
+
),
|
|
1828
|
+
).size > 0,
|
|
1829
|
+
);
|
|
1830
|
+
if (anchor) {
|
|
1831
|
+
primaryMessageId = anchor.id;
|
|
1832
|
+
replayInterruptMessageId = anchor.id;
|
|
1833
|
+
grouped.set(anchor.id, grouped.get(anchor.id) ?? []);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const pendingMessages =
|
|
1838
|
+
settlingRun && terminalStatus && terminalStatus.type !== "running"
|
|
1839
|
+
? settleHeldMessages(terminalStatus)
|
|
1840
|
+
: [];
|
|
1841
|
+
const frameMessageId = ensureAssistant(hasStreamContent);
|
|
1842
|
+
const messageId = primaryMessageId ?? frameMessageId;
|
|
1843
|
+
let primaryUpdate = update;
|
|
1844
|
+
if (ownerParts) {
|
|
1845
|
+
const interrupt =
|
|
1846
|
+
terminalStatus?.type === "requires-action" &&
|
|
1847
|
+
terminalStatus.reason === "interrupt";
|
|
1848
|
+
const ownerStatus: MessageStatus | undefined =
|
|
1849
|
+
terminalStatus?.type === "requires-action"
|
|
1850
|
+
? { type: "complete", reason: "unknown" }
|
|
1851
|
+
: terminalStatus;
|
|
1852
|
+
for (const [ownerId, content] of ownerParts) {
|
|
1853
|
+
const existing = this.session.tryGetMessage(ownerId);
|
|
1854
|
+
if (existing)
|
|
1855
|
+
this.markPendingAssistantHistory(ownerId, existing.parentId);
|
|
1856
|
+
if (ownerId === messageId) continue;
|
|
1857
|
+
this.updateAssistantMessage(
|
|
1858
|
+
ownerId,
|
|
1859
|
+
{
|
|
1860
|
+
...(contentOwnerIds?.has(ownerId) ? { content } : {}),
|
|
1861
|
+
...(ownerStatus ? { status: ownerStatus } : {}),
|
|
1862
|
+
},
|
|
1863
|
+
replayContexts.get(ownerId),
|
|
1864
|
+
interrupt ? new Set<string>() : restrictedPendingToolCallIds,
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
const { content: _content, ...controlUpdate } = update;
|
|
1868
|
+
const content = ownerParts.get(messageId) ?? [];
|
|
1869
|
+
primaryUpdate = {
|
|
1870
|
+
...controlUpdate,
|
|
1871
|
+
...(contentOwnerIds?.has(messageId) ? { content } : {}),
|
|
1872
|
+
...(ownerStatus && !interrupt && replayContexts.has(messageId)
|
|
1873
|
+
? { status: ownerStatus }
|
|
1874
|
+
: {}),
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1351
1877
|
const resolved = this.updateAssistantMessage(
|
|
1352
|
-
|
|
1353
|
-
|
|
1878
|
+
messageId,
|
|
1879
|
+
primaryUpdate,
|
|
1880
|
+
replayContexts.get(messageId),
|
|
1881
|
+
restrictedPendingToolCallIds,
|
|
1354
1882
|
);
|
|
1355
|
-
if (resolved !== assistantMessageId) {
|
|
1883
|
+
if (messageId === frameMessageId && resolved !== assistantMessageId) {
|
|
1356
1884
|
assistantMessageId = resolved;
|
|
1357
1885
|
}
|
|
1886
|
+
for (const messageId of pendingMessages) {
|
|
1887
|
+
this.maybeResumeAfterToolResults(
|
|
1888
|
+
messageId,
|
|
1889
|
+
replayMessageIds.includes(messageId) ? accountedResultIds : undefined,
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1358
1892
|
};
|
|
1359
1893
|
|
|
1360
1894
|
const adoptServerMessageId = (
|
|
1361
1895
|
serverId: string,
|
|
1362
1896
|
startNewMessage: boolean,
|
|
1897
|
+
openToolCallIds: string[] = [],
|
|
1363
1898
|
) => {
|
|
1899
|
+
const existing = this.session.tryGetMessage(serverId);
|
|
1900
|
+
const replayIndex = replayMessageIds.indexOf(serverId);
|
|
1901
|
+
const adoptsReplay =
|
|
1902
|
+
!replayEnded &&
|
|
1903
|
+
!assistantCollided &&
|
|
1904
|
+
replayIndex > lastReplayedIndex &&
|
|
1905
|
+
existing?.message.role === "assistant" &&
|
|
1906
|
+
this.session.getMessages().some((message) => message.id === serverId);
|
|
1907
|
+
if (adoptsReplay && replayContexts.size === 0) {
|
|
1908
|
+
replayContexts = candidateContexts;
|
|
1909
|
+
const selected = this.session.getMessages();
|
|
1910
|
+
let tailId = serverId;
|
|
1911
|
+
for (const message of selected.slice(
|
|
1912
|
+
selected.findIndex((message) => message.id === serverId) + 1,
|
|
1913
|
+
)) {
|
|
1914
|
+
if (message.role !== "assistant") break;
|
|
1915
|
+
tailId = message.id;
|
|
1916
|
+
}
|
|
1917
|
+
this.session.selectHead(tailId);
|
|
1918
|
+
}
|
|
1364
1919
|
if (startNewMessage && assistantMessageId !== undefined) {
|
|
1365
|
-
|
|
1920
|
+
let hasReplayCallsToHold = false;
|
|
1921
|
+
const replay = replayContexts.get(assistantMessageId);
|
|
1922
|
+
if (replay) {
|
|
1923
|
+
const current =
|
|
1924
|
+
this.session.tryGetMessage(assistantMessageId)?.message;
|
|
1925
|
+
if (current?.role === "assistant") {
|
|
1926
|
+
const retainedPending = Array.from(
|
|
1927
|
+
iterateToolCallParts(current.content),
|
|
1928
|
+
).filter(
|
|
1929
|
+
(part) =>
|
|
1930
|
+
!isResolvedToolCall(part) ||
|
|
1931
|
+
(replay.admittedToolCallIds.has(part.toolCallId) &&
|
|
1932
|
+
!accountedResultIds.has(part.toolCallId)),
|
|
1933
|
+
);
|
|
1934
|
+
hasReplayCallsToHold = retainedPending.length > 0;
|
|
1935
|
+
openToolCallIds = [
|
|
1936
|
+
...new Set([
|
|
1937
|
+
...openToolCallIds,
|
|
1938
|
+
...retainedPending
|
|
1939
|
+
.filter((part) =>
|
|
1940
|
+
replay.admittedToolCallIds.has(part.toolCallId),
|
|
1941
|
+
)
|
|
1942
|
+
.map((part) => part.toolCallId),
|
|
1943
|
+
]),
|
|
1944
|
+
];
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (openToolCallIds.length > 0 || hasReplayCallsToHold) {
|
|
1948
|
+
heldMessages.set(assistantMessageId, new Set(openToolCallIds));
|
|
1949
|
+
} else {
|
|
1950
|
+
applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1951
|
+
}
|
|
1366
1952
|
const previousId = assistantMessageId;
|
|
1367
1953
|
assistantMessageId = undefined;
|
|
1368
|
-
if (this.session.tryGetMessage(previousId)) {
|
|
1954
|
+
if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
|
|
1369
1955
|
const created = this.insertAssistantPlaceholder(previousId);
|
|
1370
1956
|
assistantMessageId = created;
|
|
1371
1957
|
this.markPendingAssistantHistory(created, previousId);
|
|
1372
1958
|
}
|
|
1373
1959
|
}
|
|
1960
|
+
if (adoptsReplay) {
|
|
1961
|
+
assistantMessageId = serverId;
|
|
1962
|
+
lastReplayedIndex = replayIndex;
|
|
1963
|
+
this.markPendingAssistantHistory(serverId, existing.parentId);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
replayEnded = true;
|
|
1374
1967
|
const placeholder = ensureAssistant(true);
|
|
1375
1968
|
if (placeholder === serverId) return;
|
|
1376
1969
|
const reassigned = this.reassignAssistantId(placeholder, serverId);
|
|
@@ -1404,22 +1997,85 @@ export class AgUiThreadRuntimeCore {
|
|
|
1404
1997
|
onServerMessageId: (serverId) => {
|
|
1405
1998
|
adoptServerMessageId(serverId, false);
|
|
1406
1999
|
},
|
|
1407
|
-
onTextMessageStart: (serverId) =>
|
|
2000
|
+
onTextMessageStart: (serverId, openToolCallIds) => {
|
|
2001
|
+
const owners = this.getTrailingAssistantTurn().filter((message) =>
|
|
2002
|
+
replayContexts.has(message.id),
|
|
2003
|
+
);
|
|
2004
|
+
const toolOwners = indexReplayToolOwners(owners);
|
|
2005
|
+
const frameOpenIds = openToolCallIds.filter((id) => {
|
|
2006
|
+
const ownerId = toolOwners.get(id);
|
|
2007
|
+
if (!ownerId || ownerId === assistantMessageId || assistantCollided)
|
|
2008
|
+
return true;
|
|
2009
|
+
const held = heldMessages.get(ownerId) ?? new Set<string>();
|
|
2010
|
+
held.add(id);
|
|
2011
|
+
heldMessages.set(ownerId, held);
|
|
2012
|
+
return false;
|
|
2013
|
+
});
|
|
2014
|
+
// The native accumulator just reset; prior frame contributions are
|
|
2015
|
+
// retained history, while removals within this frame remain authoritative.
|
|
2016
|
+
for (const ownerId of frameOwnerIds) {
|
|
2017
|
+
const replay = replayContexts.get(ownerId);
|
|
2018
|
+
if (replay) replay.merge = createReplayContentMerger();
|
|
2019
|
+
}
|
|
2020
|
+
frameOwnerIds.clear();
|
|
2021
|
+
adoptServerMessageId(serverId, true, frameOpenIds);
|
|
2022
|
+
},
|
|
1408
2023
|
});
|
|
1409
2024
|
let runFinished = false;
|
|
1410
2025
|
const dispatch = (event: AgUiEvent) => {
|
|
1411
2026
|
if (this.abortController !== abortController) return;
|
|
1412
|
-
if (event.type === "RUN_FINISHED")
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
2027
|
+
if (event.type === "RUN_FINISHED") {
|
|
2028
|
+
runFinished = true;
|
|
2029
|
+
if (event.outcome?.type === "interrupt")
|
|
2030
|
+
replayInterruptMessageId = assistantMessageId ?? replayTail;
|
|
2031
|
+
const ids =
|
|
2032
|
+
event.outcome?.type === "success"
|
|
2033
|
+
? event.outcome.pendingToolCallIds
|
|
2034
|
+
: undefined;
|
|
2035
|
+
restrictedPendingToolCallIds =
|
|
2036
|
+
ids === undefined ? undefined : new Set(ids);
|
|
2037
|
+
}
|
|
2038
|
+
if (event.type === "TOOL_CALL_RESULT") {
|
|
2039
|
+
if (!assistantCollided && candidateContexts.size)
|
|
2040
|
+
accountedResultIds.add(event.toolCallId);
|
|
2041
|
+
for (const openToolCallIds of heldMessages.values()) {
|
|
2042
|
+
openToolCallIds.delete(event.toolCallId);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
settlingRun =
|
|
2046
|
+
event.type === "RUN_FINISHED" ||
|
|
2047
|
+
event.type === "RUN_ERROR" ||
|
|
2048
|
+
event.type === "RUN_CANCELLED";
|
|
2049
|
+
const beforeSnapshot =
|
|
2050
|
+
event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
|
|
2051
|
+
try {
|
|
2052
|
+
const nextAssistantMessageId = this.handleEvent(
|
|
2053
|
+
aggregator,
|
|
2054
|
+
event,
|
|
2055
|
+
assistantMessageId,
|
|
2056
|
+
joinStrategy,
|
|
2057
|
+
runStartMessageIds,
|
|
2058
|
+
hasRunUserBoundary,
|
|
2059
|
+
candidateContexts.size ? reconcileReplaySnapshot : undefined,
|
|
2060
|
+
);
|
|
2061
|
+
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
2062
|
+
for (const message of beforeSnapshot) {
|
|
2063
|
+
const context = replayContexts.get(message.id);
|
|
2064
|
+
const current = this.session.tryGetMessage(message.id)?.message;
|
|
2065
|
+
if (context && current?.content !== message.content) {
|
|
2066
|
+
context.merge = createReplayContentMerger();
|
|
2067
|
+
frameOwnerIds.delete(message.id);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
assistantMessageId = nextAssistantMessageId;
|
|
2071
|
+
if (!replayEnded && assistantMessageId === undefined) {
|
|
2072
|
+
const tail = this.session.getMessages().at(-1);
|
|
2073
|
+
snapshotAssistantId =
|
|
2074
|
+
tail?.role === "assistant" ? tail.id : undefined;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
} finally {
|
|
2078
|
+
settlingRun = false;
|
|
1423
2079
|
}
|
|
1424
2080
|
};
|
|
1425
2081
|
|
|
@@ -1482,9 +2138,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1482
2138
|
},
|
|
1483
2139
|
});
|
|
1484
2140
|
try {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2141
|
+
runAgentInstance.messages = input.messages;
|
|
2142
|
+
runAgentInstance.threadId = input.threadId;
|
|
2143
|
+
runAgentInstance.state = input.state ?? null;
|
|
1488
2144
|
} catch {
|
|
1489
2145
|
// ignore
|
|
1490
2146
|
}
|
|
@@ -1512,21 +2168,78 @@ export class AgUiThreadRuntimeCore {
|
|
|
1512
2168
|
throw err;
|
|
1513
2169
|
}
|
|
1514
2170
|
|
|
2171
|
+
const pending = replayInterruptMessageId
|
|
2172
|
+
? this.getPendingInterrupts()
|
|
2173
|
+
: null;
|
|
2174
|
+
if (
|
|
2175
|
+
pending &&
|
|
2176
|
+
pending.messageId === replayInterruptMessageId &&
|
|
2177
|
+
replayContexts.has(pending.messageId) &&
|
|
2178
|
+
!abortSignal.aborted &&
|
|
2179
|
+
this.abortController === null &&
|
|
2180
|
+
generation === this.state.historyMutationGeneration &&
|
|
2181
|
+
!this.isThreadSwitching?.()
|
|
2182
|
+
) {
|
|
2183
|
+
const message = this.session.tryGetMessage(pending.messageId)?.message;
|
|
2184
|
+
const responses =
|
|
2185
|
+
message?.role === "assistant"
|
|
2186
|
+
? buildToolApprovalResume(message.content, pending.interrupts)
|
|
2187
|
+
: null;
|
|
2188
|
+
if (responses) {
|
|
2189
|
+
this.clearDeferredContinuations(abortController);
|
|
2190
|
+
try {
|
|
2191
|
+
await this.submitInterruptResponses(responses);
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2194
|
+
if (!this.reportedErrors.has(err)) {
|
|
2195
|
+
this.reportError(err);
|
|
2196
|
+
this.reportedErrors.add(err);
|
|
2197
|
+
}
|
|
2198
|
+
throw err;
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
1515
2204
|
// A tool result that landed before the run settled deferred its
|
|
1516
2205
|
// continuation here so a second run never overlaps the first.
|
|
1517
|
-
if (this.pendingResume?.owner === abortController) {
|
|
1518
|
-
const {
|
|
1519
|
-
this.
|
|
1520
|
-
if (
|
|
1521
|
-
|
|
2206
|
+
if (this.state.pendingResume?.owner === abortController) {
|
|
2207
|
+
const { messageIds } = this.state.pendingResume;
|
|
2208
|
+
this.dispatch({ type: "resumeCleared" });
|
|
2209
|
+
if (
|
|
2210
|
+
!abortSignal.aborted &&
|
|
2211
|
+
!this.getPendingInterrupts() &&
|
|
2212
|
+
!this.getPendingToolCalls()
|
|
2213
|
+
) {
|
|
2214
|
+
const turn = this.getTrailingAssistantTurn();
|
|
2215
|
+
if (
|
|
2216
|
+
turn.some((message) => {
|
|
2217
|
+
if (!messageIds.includes(message.id)) return false;
|
|
2218
|
+
const replay = replayContexts.get(message.id);
|
|
2219
|
+
if (!replay) return hasResolvedToolCalls(message);
|
|
2220
|
+
return Array.from(iterateToolCallParts(message.content)).some(
|
|
2221
|
+
(part) =>
|
|
2222
|
+
isResolvedToolCall(part) &&
|
|
2223
|
+
replay.admittedToolCallIds.has(part.toolCallId) &&
|
|
2224
|
+
!accountedResultIds.has(part.toolCallId),
|
|
2225
|
+
);
|
|
2226
|
+
})
|
|
2227
|
+
) {
|
|
2228
|
+
const headId = turn.at(-1)?.id;
|
|
2229
|
+
if (headId !== undefined) this.startResumeRun(headId);
|
|
2230
|
+
}
|
|
1522
2231
|
}
|
|
1523
2232
|
}
|
|
1524
2233
|
|
|
1525
|
-
if (this.pendingA2uiResumeOwner === abortController) {
|
|
1526
|
-
this.
|
|
1527
|
-
|
|
2234
|
+
if (this.state.pendingA2uiResumeOwner === abortController) {
|
|
2235
|
+
this.dispatch({
|
|
2236
|
+
type: "a2uiResumeCleared",
|
|
2237
|
+
});
|
|
2238
|
+
if (!abortSignal.aborted && this.state.pendingA2uiAction !== undefined) {
|
|
1528
2239
|
if (this.getPendingInterrupts()) {
|
|
1529
|
-
this.
|
|
2240
|
+
this.dispatch({
|
|
2241
|
+
type: "a2uiActionCleared",
|
|
2242
|
+
});
|
|
1530
2243
|
this.logger.debug(
|
|
1531
2244
|
"[agui] sendA2uiAction: pending interrupts, dropping action",
|
|
1532
2245
|
);
|
|
@@ -1537,13 +2250,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1537
2250
|
if (parentId !== null) {
|
|
1538
2251
|
this.startResumeRun(parentId);
|
|
1539
2252
|
} else {
|
|
1540
|
-
this.
|
|
2253
|
+
this.dispatch({
|
|
2254
|
+
type: "a2uiActionCleared",
|
|
2255
|
+
});
|
|
1541
2256
|
this.logger.debug(
|
|
1542
2257
|
"[agui] sendA2uiAction: no messages to resume, dropping action",
|
|
1543
2258
|
);
|
|
1544
2259
|
}
|
|
1545
2260
|
} else {
|
|
1546
|
-
this.
|
|
2261
|
+
this.dispatch({
|
|
2262
|
+
type: "a2uiActionCleared",
|
|
2263
|
+
});
|
|
1547
2264
|
}
|
|
1548
2265
|
}
|
|
1549
2266
|
}
|
|
@@ -1562,8 +2279,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1562
2279
|
getAssistantMessageId: () => string | undefined;
|
|
1563
2280
|
},
|
|
1564
2281
|
): Promise<Error | undefined> {
|
|
1565
|
-
this.
|
|
1566
|
-
|
|
2282
|
+
this.dispatch({
|
|
2283
|
+
type: "a2uiContinuationsCleared",
|
|
2284
|
+
});
|
|
1567
2285
|
const assistantId = ctx.ensureAssistant();
|
|
1568
2286
|
const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
|
|
1569
2287
|
const options: ChatModelRunOptions = {
|
|
@@ -1614,8 +2332,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1614
2332
|
historyMessages: readonly ThreadMessage[] | undefined,
|
|
1615
2333
|
resume?: ResumeDispatch,
|
|
1616
2334
|
) {
|
|
1617
|
-
const pendingA2uiAction = this.pendingA2uiAction;
|
|
1618
|
-
this.
|
|
2335
|
+
const pendingA2uiAction = this.state.pendingA2uiAction;
|
|
2336
|
+
this.dispatch({
|
|
2337
|
+
type: "a2uiActionCleared",
|
|
2338
|
+
});
|
|
1619
2339
|
const threadId = this.agent.threadId || "main";
|
|
1620
2340
|
const messages = toAgUiMessages(
|
|
1621
2341
|
this.selectRunMessages(
|
|
@@ -1635,7 +2355,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1635
2355
|
const input = {
|
|
1636
2356
|
threadId,
|
|
1637
2357
|
runId,
|
|
1638
|
-
state: this.stateSnapshot ?? null,
|
|
2358
|
+
state: this.state.stateSnapshot ?? null,
|
|
1639
2359
|
messages,
|
|
1640
2360
|
tools: toAgUiTools(context?.tools),
|
|
1641
2361
|
context: [
|
|
@@ -1678,17 +2398,20 @@ export class AgUiThreadRuntimeCore {
|
|
|
1678
2398
|
}
|
|
1679
2399
|
|
|
1680
2400
|
private clearDeferredContinuations(owner: AbortController): void {
|
|
1681
|
-
if (this.pendingResume?.owner === owner) {
|
|
1682
|
-
this.
|
|
2401
|
+
if (this.state.pendingResume?.owner === owner) {
|
|
2402
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1683
2403
|
}
|
|
1684
|
-
if (this.pendingA2uiResumeOwner === owner) {
|
|
1685
|
-
this.
|
|
1686
|
-
|
|
2404
|
+
if (this.state.pendingA2uiResumeOwner === owner) {
|
|
2405
|
+
this.dispatch({
|
|
2406
|
+
type: "a2uiContinuationsCleared",
|
|
2407
|
+
});
|
|
1687
2408
|
}
|
|
1688
2409
|
}
|
|
1689
2410
|
|
|
1690
2411
|
private setRunning(running: boolean) {
|
|
1691
|
-
this.
|
|
2412
|
+
this.dispatch({
|
|
2413
|
+
type: running ? "runStarted" : "runFinished",
|
|
2414
|
+
});
|
|
1692
2415
|
this.publishUpdate();
|
|
1693
2416
|
}
|
|
1694
2417
|
|
|
@@ -1710,7 +2433,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1710
2433
|
status: { type: "running" },
|
|
1711
2434
|
content: [],
|
|
1712
2435
|
metadata: {
|
|
1713
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
2436
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
1714
2437
|
unstable_annotations: [],
|
|
1715
2438
|
unstable_data: [],
|
|
1716
2439
|
steps: [],
|
|
@@ -1768,22 +2491,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
1768
2491
|
// An in-flight write completes under the id it was started with, so the
|
|
1769
2492
|
// rename moves the chain entry and transfers the completion mark; without
|
|
1770
2493
|
// 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(() => {
|
|
2494
|
+
const pendingWrite = this.historyWrites.move(
|
|
2495
|
+
oldId,
|
|
2496
|
+
collidesWithExisting ? undefined : newId,
|
|
2497
|
+
);
|
|
2498
|
+
if (pendingWrite && !collidesWithExisting) {
|
|
2499
|
+
void pendingWrite.then(
|
|
2500
|
+
() => {
|
|
1782
2501
|
this.persistedHistoryIds.delete(oldId);
|
|
1783
2502
|
this.persistedHistoryIds.add(newId);
|
|
1784
|
-
|
|
1785
|
-
},
|
|
1786
|
-
|
|
2503
|
+
},
|
|
2504
|
+
() => {},
|
|
2505
|
+
);
|
|
1787
2506
|
}
|
|
1788
2507
|
|
|
1789
2508
|
this.publishUpdate();
|
|
@@ -1793,22 +2512,113 @@ export class AgUiThreadRuntimeCore {
|
|
|
1793
2512
|
private updateAssistantMessage(
|
|
1794
2513
|
messageId: string,
|
|
1795
2514
|
update: ChatModelRunResult,
|
|
2515
|
+
replay?: ReplayContext,
|
|
2516
|
+
pendingToolCallIds?: ReadonlySet<string>,
|
|
1796
2517
|
): string {
|
|
1797
2518
|
let latestStatus: MessageStatus | undefined;
|
|
1798
2519
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
1799
2520
|
if (message.role !== "assistant") return message;
|
|
1800
2521
|
const assistant = message as ThreadAssistantMessage;
|
|
1801
|
-
const
|
|
2522
|
+
const mergedMetadata = update.metadata
|
|
1802
2523
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
1803
2524
|
: assistant.metadata;
|
|
1804
2525
|
latestStatus = update.status ?? assistant.status;
|
|
1805
|
-
|
|
2526
|
+
let content =
|
|
1806
2527
|
update.content !== undefined
|
|
1807
|
-
?
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2528
|
+
? replay
|
|
2529
|
+
? replay.merge(
|
|
2530
|
+
assistant.content,
|
|
2531
|
+
update.content as ThreadAssistantMessage["content"],
|
|
2532
|
+
)
|
|
2533
|
+
: this.preserveToolResults(
|
|
2534
|
+
assistant.content,
|
|
2535
|
+
update.content as ThreadAssistantMessage["content"],
|
|
2536
|
+
)
|
|
1811
2537
|
: assistant.content;
|
|
2538
|
+
const calls = replay ? Array.from(iterateToolCallParts(content)) : [];
|
|
2539
|
+
// A retained historical call needs existing or current stream authority.
|
|
2540
|
+
const pending =
|
|
2541
|
+
pendingToolCallIds ??
|
|
2542
|
+
(replay
|
|
2543
|
+
? new Set([
|
|
2544
|
+
...replay.admittedToolCallIds,
|
|
2545
|
+
...Array.from(iterateToolCallParts(update.content ?? []))
|
|
2546
|
+
.filter((part) => !isResolvedToolCall(part))
|
|
2547
|
+
.map((part) => part.toolCallId),
|
|
2548
|
+
])
|
|
2549
|
+
: undefined);
|
|
2550
|
+
if (replay && latestStatus?.type !== "running" && pending)
|
|
2551
|
+
replay.admittedToolCallIds = pending;
|
|
2552
|
+
if (replay) {
|
|
2553
|
+
// Project gates against the content the user can actually answer.
|
|
2554
|
+
const interrupts =
|
|
2555
|
+
latestStatus?.type === "requires-action" &&
|
|
2556
|
+
latestStatus.reason === "interrupt"
|
|
2557
|
+
? (
|
|
2558
|
+
mergedMetadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
2559
|
+
| AgUiCustomMetadata
|
|
2560
|
+
| undefined
|
|
2561
|
+
)?.interrupts
|
|
2562
|
+
: undefined;
|
|
2563
|
+
const approvals = projectAgUiToolApprovals(
|
|
2564
|
+
interrupts,
|
|
2565
|
+
new Set(calls.map((part) => part.toolCallId)),
|
|
2566
|
+
);
|
|
2567
|
+
const originalCalls = new Map(
|
|
2568
|
+
Array.from(iterateToolCallParts(replay.originalContent), (part) => [
|
|
2569
|
+
part.toolCallId,
|
|
2570
|
+
part,
|
|
2571
|
+
]),
|
|
2572
|
+
);
|
|
2573
|
+
content = mapToolCallPartsDeep(content, (part) => {
|
|
2574
|
+
const approval = approvals.get(part.toolCallId);
|
|
2575
|
+
if (approval)
|
|
2576
|
+
return {
|
|
2577
|
+
...part,
|
|
2578
|
+
approval:
|
|
2579
|
+
preserveToolApprovalDecision(
|
|
2580
|
+
originalCalls.get(part.toolCallId),
|
|
2581
|
+
{ ...part, approval },
|
|
2582
|
+
) ?? approval,
|
|
2583
|
+
};
|
|
2584
|
+
if (
|
|
2585
|
+
!part.approval ||
|
|
2586
|
+
part.approval.approved !== undefined ||
|
|
2587
|
+
part.approval.resolution
|
|
2588
|
+
)
|
|
2589
|
+
return part;
|
|
2590
|
+
const { approval: _approval, ...withoutApproval } = part;
|
|
2591
|
+
return withoutApproval;
|
|
2592
|
+
}).content;
|
|
2593
|
+
}
|
|
2594
|
+
if (replay && latestStatus?.type === "complete") {
|
|
2595
|
+
const listed =
|
|
2596
|
+
pending ??
|
|
2597
|
+
listedPendingToolCallIds({
|
|
2598
|
+
...assistant,
|
|
2599
|
+
metadata: mergedMetadata,
|
|
2600
|
+
});
|
|
2601
|
+
if (
|
|
2602
|
+
calls.some(
|
|
2603
|
+
(part) =>
|
|
2604
|
+
(!isResolvedToolCall(part) ||
|
|
2605
|
+
!replay.accountedResultIds.has(part.toolCallId)) &&
|
|
2606
|
+
(!listed || listed.has(part.toolCallId)),
|
|
2607
|
+
)
|
|
2608
|
+
)
|
|
2609
|
+
latestStatus = { type: "requires-action", reason: "tool-calls" };
|
|
2610
|
+
}
|
|
2611
|
+
const admission =
|
|
2612
|
+
pending && replay
|
|
2613
|
+
? withPendingToolCallIds(mergedMetadata, pending)
|
|
2614
|
+
: mergedMetadata;
|
|
2615
|
+
const metadata =
|
|
2616
|
+
latestStatus?.type === "requires-action" &&
|
|
2617
|
+
latestStatus.reason === "tool-calls"
|
|
2618
|
+
? admission
|
|
2619
|
+
: replay && latestStatus?.type === "running"
|
|
2620
|
+
? mergedMetadata
|
|
2621
|
+
: withoutPendingToolCallIds(admission);
|
|
1812
2622
|
return {
|
|
1813
2623
|
...assistant,
|
|
1814
2624
|
content,
|
|
@@ -1831,7 +2641,10 @@ export class AgUiThreadRuntimeCore {
|
|
|
1831
2641
|
if (this.isPersistableStatus(latestStatus)) {
|
|
1832
2642
|
this.persistAssistantHistory(resolvedMessageId);
|
|
1833
2643
|
}
|
|
1834
|
-
this.maybeResumeAfterToolResults(
|
|
2644
|
+
this.maybeResumeAfterToolResults(
|
|
2645
|
+
resolvedMessageId,
|
|
2646
|
+
replay?.accountedResultIds,
|
|
2647
|
+
);
|
|
1835
2648
|
return resolvedMessageId;
|
|
1836
2649
|
}
|
|
1837
2650
|
|
|
@@ -1842,6 +2655,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1842
2655
|
private preserveToolResults(
|
|
1843
2656
|
previous: ThreadAssistantMessage["content"],
|
|
1844
2657
|
next: ThreadAssistantMessage["content"],
|
|
2658
|
+
decisionSource?: ThreadAssistantMessage["content"],
|
|
1845
2659
|
): ThreadAssistantMessage["content"] {
|
|
1846
2660
|
const preserved = new Map<string, ToolCallMessagePart>();
|
|
1847
2661
|
for (const part of iterateToolCallParts(previous)) {
|
|
@@ -1849,25 +2663,27 @@ export class AgUiThreadRuntimeCore {
|
|
|
1849
2663
|
preserved.set(part.toolCallId, part);
|
|
1850
2664
|
}
|
|
1851
2665
|
}
|
|
1852
|
-
|
|
2666
|
+
const decisions = new Map(
|
|
2667
|
+
Array.from(iterateToolCallParts(decisionSource ?? []))
|
|
2668
|
+
.filter((part) => part.approval?.approved !== undefined)
|
|
2669
|
+
.map((part) => [part.toolCallId, part]),
|
|
2670
|
+
);
|
|
2671
|
+
if (preserved.size === 0 && decisions.size === 0) {
|
|
1853
2672
|
return this.preserveToolInteractions(previous, next);
|
|
1854
2673
|
}
|
|
1855
2674
|
|
|
1856
2675
|
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
1857
2676
|
const prior = preserved.get(part.toolCallId);
|
|
1858
|
-
|
|
1859
|
-
if (
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
result
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
...
|
|
1866
|
-
? { modelContent: prior.modelContent }
|
|
1867
|
-
: {}),
|
|
1868
|
-
};
|
|
2677
|
+
const result = prior ? preserveToolResult(prior, part) : part;
|
|
2678
|
+
if (result.approval && decisions.has(part.toolCallId)) {
|
|
2679
|
+
const approval = preserveToolApprovalDecision(
|
|
2680
|
+
decisions.get(part.toolCallId),
|
|
2681
|
+
result,
|
|
2682
|
+
);
|
|
2683
|
+
if (approval && approval !== result.approval)
|
|
2684
|
+
return { ...result, approval };
|
|
1869
2685
|
}
|
|
1870
|
-
return
|
|
2686
|
+
return result;
|
|
1871
2687
|
});
|
|
1872
2688
|
return this.preserveToolInteractions(previous, changed ? merged : next);
|
|
1873
2689
|
}
|
|
@@ -2054,24 +2870,30 @@ export class AgUiThreadRuntimeCore {
|
|
|
2054
2870
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2055
2871
|
runStartMessageIds: ReadonlySet<string>,
|
|
2056
2872
|
hasRunUserBoundary: boolean,
|
|
2873
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2057
2874
|
): string | undefined {
|
|
2058
2875
|
switch (event.type) {
|
|
2059
2876
|
case "STATE_SNAPSHOT": {
|
|
2060
|
-
this.
|
|
2877
|
+
this.dispatch({
|
|
2878
|
+
type: "stateSnapshot",
|
|
2879
|
+
snapshot: event.snapshot as ReadonlyJSONValue,
|
|
2880
|
+
});
|
|
2061
2881
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2062
2882
|
break;
|
|
2063
2883
|
}
|
|
2064
2884
|
case "STATE_DELTA": {
|
|
2065
2885
|
if (event.delta.length === 0) break;
|
|
2066
2886
|
try {
|
|
2067
|
-
const state = this.stateSnapshot ?? {};
|
|
2068
2887
|
const result = jsonpatch.applyPatch(
|
|
2069
|
-
state,
|
|
2888
|
+
this.state.stateSnapshot ?? {},
|
|
2070
2889
|
event.delta as Operation[],
|
|
2071
2890
|
/* validateOperation */ true,
|
|
2072
2891
|
/* mutateDocument */ false,
|
|
2073
2892
|
);
|
|
2074
|
-
this.
|
|
2893
|
+
this.dispatch({
|
|
2894
|
+
type: "stateSnapshot",
|
|
2895
|
+
snapshot: result.newDocument as ReadonlyJSONValue,
|
|
2896
|
+
});
|
|
2075
2897
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2076
2898
|
} catch (error) {
|
|
2077
2899
|
this.logger.error?.("[agui] failed to apply state delta", error);
|
|
@@ -2093,6 +2915,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2093
2915
|
joinStrategy,
|
|
2094
2916
|
runStartMessageIds,
|
|
2095
2917
|
hasRunUserBoundary,
|
|
2918
|
+
reconcileSnapshot,
|
|
2096
2919
|
);
|
|
2097
2920
|
const headId = this.session.headId;
|
|
2098
2921
|
if (hasActiveText && headId !== previousHeadId) {
|
|
@@ -2156,7 +2979,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2156
2979
|
...assistant,
|
|
2157
2980
|
metadata: {
|
|
2158
2981
|
...assistant.metadata,
|
|
2159
|
-
unstable_state: this.stateSnapshot ?? null,
|
|
2982
|
+
unstable_state: this.state.stateSnapshot ?? null,
|
|
2160
2983
|
},
|
|
2161
2984
|
};
|
|
2162
2985
|
});
|
|
@@ -2172,6 +2995,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2172
2995
|
if (message.role !== "assistant") return message;
|
|
2173
2996
|
const assistant = message as ThreadAssistantMessage;
|
|
2174
2997
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
2998
|
+
const modelText = toolContentText(event.content);
|
|
2175
2999
|
let matchedToolCall = false;
|
|
2176
3000
|
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
2177
3001
|
if (part.toolCallId !== event.toolCallId) return part;
|
|
@@ -2188,11 +3012,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2188
3012
|
if (snapshotResultApplied) {
|
|
2189
3013
|
return {
|
|
2190
3014
|
...part,
|
|
2191
|
-
...(part.modelContent === undefined
|
|
3015
|
+
...(part.modelContent === undefined
|
|
2192
3016
|
? {
|
|
2193
|
-
modelContent: [
|
|
2194
|
-
{ type: "text" as const, text: event.content },
|
|
2195
|
-
],
|
|
3017
|
+
modelContent: [{ type: "text" as const, text: modelText }],
|
|
2196
3018
|
}
|
|
2197
3019
|
: {}),
|
|
2198
3020
|
...(part.isError === undefined
|
|
@@ -2207,12 +3029,18 @@ export class AgUiThreadRuntimeCore {
|
|
|
2207
3029
|
: {}),
|
|
2208
3030
|
};
|
|
2209
3031
|
}
|
|
3032
|
+
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
3033
|
+
part;
|
|
2210
3034
|
return {
|
|
2211
|
-
...
|
|
3035
|
+
...(event.mcpResult === undefined && Array.isArray(event.content)
|
|
3036
|
+
? partWithoutModelContent
|
|
3037
|
+
: part),
|
|
2212
3038
|
result: (event.mcpResult ??
|
|
2213
|
-
|
|
3039
|
+
(Array.isArray(event.content)
|
|
3040
|
+
? event.content
|
|
3041
|
+
: tryParseJSON(event.content))) as ReadonlyJSONValue,
|
|
2214
3042
|
...(event.mcpResult !== undefined
|
|
2215
|
-
? { modelContent: [{ type: "text" as const, text:
|
|
3043
|
+
? { modelContent: [{ type: "text" as const, text: modelText }] }
|
|
2216
3044
|
: {}),
|
|
2217
3045
|
...(typeof event.mcpResult?.isError === "boolean"
|
|
2218
3046
|
? { isError: event.mcpResult.isError }
|
|
@@ -2305,6 +3133,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2305
3133
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2306
3134
|
runStartMessageIds: ReadonlySet<string>,
|
|
2307
3135
|
hasRunUserBoundary: boolean,
|
|
3136
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2308
3137
|
) {
|
|
2309
3138
|
try {
|
|
2310
3139
|
const activeAssistantItem = activeAssistantId
|
|
@@ -2344,19 +3173,22 @@ export class AgUiThreadRuntimeCore {
|
|
|
2344
3173
|
const existing = this.session.tryGetMessage(
|
|
2345
3174
|
convertedMessage.id,
|
|
2346
3175
|
)?.message;
|
|
2347
|
-
if (
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
3176
|
+
if (convertedMessage.role === "assistant") {
|
|
3177
|
+
const previous =
|
|
3178
|
+
existing?.role === "assistant" ? existing : undefined;
|
|
3179
|
+
const snapshot =
|
|
3180
|
+
reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
|
|
3181
|
+
converted[index] = previous
|
|
3182
|
+
? {
|
|
3183
|
+
...snapshot,
|
|
3184
|
+
content: this.mergeSnapshotAssistantContent(
|
|
3185
|
+
previous.content,
|
|
3186
|
+
snapshot.content,
|
|
3187
|
+
snapshotHasReasoning,
|
|
3188
|
+
snapshotHasActivity,
|
|
3189
|
+
),
|
|
3190
|
+
}
|
|
3191
|
+
: snapshot;
|
|
2360
3192
|
}
|
|
2361
3193
|
}
|
|
2362
3194
|
if (joinStrategy === "concat-content") {
|
|
@@ -2531,7 +3363,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2531
3363
|
(wasPersisted || this.snapshotHistoryIds.has(messageId));
|
|
2532
3364
|
|
|
2533
3365
|
if (shouldUpdate) {
|
|
2534
|
-
const write = this.
|
|
3366
|
+
const write = this.historyWrites.chain(messageId, () =>
|
|
2535
3367
|
update.call(history, { parentId, message }),
|
|
2536
3368
|
);
|
|
2537
3369
|
this.assistantHistoryParents.delete(messageId);
|
|
@@ -2540,7 +3372,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2540
3372
|
this.persistedHistoryIds.add(messageId);
|
|
2541
3373
|
},
|
|
2542
3374
|
(error) => {
|
|
2543
|
-
const pending = this.historyWrites.
|
|
3375
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2544
3376
|
if (pending === undefined || pending === write) {
|
|
2545
3377
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2546
3378
|
}
|
|
@@ -2561,7 +3393,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
2561
3393
|
void write.then(
|
|
2562
3394
|
() => {},
|
|
2563
3395
|
(error) => {
|
|
2564
|
-
const pending = this.historyWrites.
|
|
3396
|
+
const pending = this.historyWrites.pending(messageId);
|
|
2565
3397
|
if (pending === undefined || pending === write) {
|
|
2566
3398
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2567
3399
|
}
|
|
@@ -2575,11 +3407,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
2575
3407
|
message: ThreadMessage,
|
|
2576
3408
|
): Promise<void> | undefined {
|
|
2577
3409
|
if (!this.history || this.persistedHistoryIds.has(message.id)) return;
|
|
2578
|
-
const pending = this.historyWrites.
|
|
3410
|
+
const pending = this.historyWrites.pending(message.id);
|
|
2579
3411
|
if (pending) return pending;
|
|
2580
3412
|
|
|
2581
3413
|
const append = this.history.append.bind(this.history);
|
|
2582
|
-
const write = this.
|
|
3414
|
+
const write = this.historyWrites.chain(message.id, () =>
|
|
2583
3415
|
append({ parentId, message }),
|
|
2584
3416
|
);
|
|
2585
3417
|
void write.then(
|
|
@@ -2590,35 +3422,4 @@ export class AgUiThreadRuntimeCore {
|
|
|
2590
3422
|
);
|
|
2591
3423
|
return write;
|
|
2592
3424
|
}
|
|
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
3425
|
}
|