@assistant-ui/react-ag-ui 0.1.0 → 0.1.2
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 +2 -0
- package/dist/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- package/dist/runtime/AgUiThreadController.d.ts +11 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +565 -89
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +9 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +194 -12
- 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 +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +35 -14
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +0 -1
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +2 -10
- 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 +1 -1
- package/dist/runtime/agUiThreadState.d.ts.map +1 -1
- package/dist/runtime/agUiThreadState.js +3 -4
- package/dist/runtime/agUiThreadState.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -2
- 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.js +1 -2
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +3 -3
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.js +1 -2
- package/package.json +6 -6
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/AgUiThreadController.disconnect.test.ts +914 -0
- package/src/runtime/AgUiThreadController.interrupt-history.test.ts +392 -0
- package/src/runtime/AgUiThreadController.replay.test.ts +3939 -0
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +167 -0
- package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
- package/src/runtime/AgUiThreadController.test.ts +119 -0
- package/src/runtime/AgUiThreadController.ts +1184 -136
- package/src/runtime/adapter/conversions.content.test.ts +242 -1
- package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
- package/src/runtime/adapter/conversions.ts +349 -28
- 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.metadata.test.ts +151 -0
- package/src/runtime/adapter/run-aggregator.ts +52 -9
- package/src/runtime/adapter/subscriber.test.ts +1 -1
- package/src/runtime/adapter/subscriber.ts +1 -6
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +21 -2
- package/src/runtime/agUiThreadState.ts +12 -3
- package/src/runtime/event-parser.ts +3 -0
- package/src/runtime/types.ts +5 -0
|
@@ -62,14 +62,22 @@ import {
|
|
|
62
62
|
tryParseJSON,
|
|
63
63
|
} from "./adapter/run-aggregator";
|
|
64
64
|
import {
|
|
65
|
-
|
|
65
|
+
fromAgUiSnapshotMessages,
|
|
66
66
|
toAgUiMessages,
|
|
67
67
|
toAgUiTools,
|
|
68
|
+
withAgUiPartMessageMetadata,
|
|
68
69
|
} from "./adapter/conversions";
|
|
69
70
|
import { createAgUiSubscriber } from "./adapter/subscriber";
|
|
71
|
+
import {
|
|
72
|
+
createReplayContentMerger,
|
|
73
|
+
groupReplayToolContent,
|
|
74
|
+
indexReplayToolOwners,
|
|
75
|
+
preserveToolResult,
|
|
76
|
+
} from "./adapter/replay-content";
|
|
70
77
|
import {
|
|
71
78
|
buildToolApprovalResume,
|
|
72
79
|
projectAgUiToolApprovals,
|
|
80
|
+
preserveToolApprovalDecision,
|
|
73
81
|
withSettledToolApprovals,
|
|
74
82
|
withToolApprovalDecision,
|
|
75
83
|
} from "./adapter/tool-approval";
|
|
@@ -92,17 +100,38 @@ const isResolvedToolCall = (
|
|
|
92
100
|
): boolean =>
|
|
93
101
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
94
102
|
|
|
103
|
+
const hasResolvedToolCalls = (message: ThreadMessage): boolean => {
|
|
104
|
+
if (message.role !== "assistant") return false;
|
|
105
|
+
for (const part of iterateToolCallParts(message.content)) {
|
|
106
|
+
if (isResolvedToolCall(part)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
};
|
|
110
|
+
|
|
95
111
|
const listedPendingToolCallIds = (
|
|
96
|
-
assistant: ThreadAssistantMessage,
|
|
112
|
+
assistant: Pick<ThreadAssistantMessage, "metadata">,
|
|
97
113
|
): ReadonlySet<string> | undefined => {
|
|
98
114
|
const ids = (
|
|
99
115
|
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
100
116
|
| AgUiCustomMetadata
|
|
101
117
|
| undefined
|
|
102
118
|
)?.pendingToolCallIds;
|
|
103
|
-
return ids
|
|
119
|
+
return ids === undefined ? undefined : new Set(ids);
|
|
104
120
|
};
|
|
105
121
|
|
|
122
|
+
type ReplayContext = {
|
|
123
|
+
// Immutable history reference: partial argument frames cannot redefine consent.
|
|
124
|
+
readonly originalContent: ThreadAssistantMessage["content"];
|
|
125
|
+
accountedResultIds: ReadonlySet<string>;
|
|
126
|
+
admittedToolCallIds: ReadonlySet<string>;
|
|
127
|
+
merge: ReturnType<typeof createReplayContentMerger>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type SnapshotMessageReconciler = (
|
|
131
|
+
snapshot: ThreadAssistantMessage,
|
|
132
|
+
previous: ThreadAssistantMessage | undefined,
|
|
133
|
+
) => ThreadAssistantMessage;
|
|
134
|
+
|
|
106
135
|
const withoutPendingToolCallIds = (
|
|
107
136
|
metadata: ThreadAssistantMessage["metadata"],
|
|
108
137
|
): ThreadAssistantMessage["metadata"] => {
|
|
@@ -125,6 +154,43 @@ const withoutPendingToolCallIds = (
|
|
|
125
154
|
};
|
|
126
155
|
};
|
|
127
156
|
|
|
157
|
+
const withPendingToolCallIds = (
|
|
158
|
+
metadata: ThreadAssistantMessage["metadata"],
|
|
159
|
+
ids: Iterable<string>,
|
|
160
|
+
): ThreadAssistantMessage["metadata"] => ({
|
|
161
|
+
...metadata,
|
|
162
|
+
custom: {
|
|
163
|
+
...metadata.custom,
|
|
164
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
165
|
+
...(metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
166
|
+
| AgUiCustomMetadata
|
|
167
|
+
| undefined),
|
|
168
|
+
pendingToolCallIds: [...ids],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const actionableToolCallIds = (
|
|
174
|
+
message: ThreadMessage | undefined,
|
|
175
|
+
): ReadonlySet<string> => {
|
|
176
|
+
if (
|
|
177
|
+
message?.role !== "assistant" ||
|
|
178
|
+
message.status?.type !== "requires-action" ||
|
|
179
|
+
message.status.reason !== "tool-calls"
|
|
180
|
+
)
|
|
181
|
+
return new Set();
|
|
182
|
+
const permitted = listedPendingToolCallIds(message);
|
|
183
|
+
return new Set(
|
|
184
|
+
Array.from(iterateToolCallParts(message.content))
|
|
185
|
+
.filter(
|
|
186
|
+
(part) =>
|
|
187
|
+
!isResolvedToolCall(part) &&
|
|
188
|
+
(!permitted || permitted.has(part.toolCallId)),
|
|
189
|
+
)
|
|
190
|
+
.map((part) => part.toolCallId),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
128
194
|
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
129
195
|
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
130
196
|
|
|
@@ -164,6 +230,73 @@ type CoreOptions = {
|
|
|
164
230
|
|
|
165
231
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
166
232
|
|
|
233
|
+
const textOf = (content: ThreadAssistantMessage["content"]) =>
|
|
234
|
+
content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Swaps `content`'s text parts, in order, for `source`'s. A text part `source`
|
|
238
|
+
* has beyond `content`'s text slots goes after the nearest earlier part of
|
|
239
|
+
* `source` that `content` also has: a tool call by id, another part by its
|
|
240
|
+
* type and position among parts of that type. With none, it goes first.
|
|
241
|
+
*/
|
|
242
|
+
const replaceTextParts = (
|
|
243
|
+
content: ThreadAssistantMessage["content"],
|
|
244
|
+
source: ThreadAssistantMessage["content"],
|
|
245
|
+
): ThreadAssistantMessage["content"] => {
|
|
246
|
+
const texts = source.filter((part) => part.type === "text");
|
|
247
|
+
const result: ThreadAssistantMessage["content"][number][] = [];
|
|
248
|
+
let used = 0;
|
|
249
|
+
for (const part of content) {
|
|
250
|
+
if (part.type !== "text") result.push(part);
|
|
251
|
+
else if (used < texts.length) result.push(texts[used++]!);
|
|
252
|
+
}
|
|
253
|
+
const positionIn = (
|
|
254
|
+
parts: readonly ThreadAssistantMessage["content"][number][],
|
|
255
|
+
part: ThreadAssistantMessage["content"][number],
|
|
256
|
+
) => parts.filter((other) => other.type === part.type).indexOf(part);
|
|
257
|
+
for (const text of texts.slice(used)) {
|
|
258
|
+
let anchor = -1;
|
|
259
|
+
for (
|
|
260
|
+
let index = source.indexOf(text) - 1;
|
|
261
|
+
index >= 0 && anchor < 0;
|
|
262
|
+
index--
|
|
263
|
+
) {
|
|
264
|
+
const before = source[index]!;
|
|
265
|
+
if (before.type === "tool-call") {
|
|
266
|
+
anchor = result.findIndex(
|
|
267
|
+
(part) =>
|
|
268
|
+
part.type === "tool-call" && part.toolCallId === before.toolCallId,
|
|
269
|
+
);
|
|
270
|
+
} else if (before.type === "text") {
|
|
271
|
+
anchor = result.indexOf(before);
|
|
272
|
+
} else {
|
|
273
|
+
const sameType = result.filter((part) => part.type === before.type);
|
|
274
|
+
const match = sameType[positionIn(source, before)];
|
|
275
|
+
anchor = match ? result.indexOf(match) : -1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
result.splice(anchor + 1, 0, text);
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const settleNestedContent = (
|
|
284
|
+
content: ThreadAssistantMessage["content"],
|
|
285
|
+
status: MessageStatus,
|
|
286
|
+
) =>
|
|
287
|
+
mapToolCallPartsDeep(content, (part) =>
|
|
288
|
+
part.messages
|
|
289
|
+
? {
|
|
290
|
+
...part,
|
|
291
|
+
messages: part.messages.map((message) =>
|
|
292
|
+
message.role === "assistant" && message.status?.type === "running"
|
|
293
|
+
? { ...message, status }
|
|
294
|
+
: message,
|
|
295
|
+
),
|
|
296
|
+
}
|
|
297
|
+
: part,
|
|
298
|
+
).content;
|
|
299
|
+
|
|
167
300
|
type AgUiRuntimeCallbackName = "onError" | "onCancel";
|
|
168
301
|
|
|
169
302
|
const invokeRuntimeCallback = <TArgs extends readonly unknown[]>(
|
|
@@ -241,6 +374,10 @@ export class AgUiThreadController {
|
|
|
241
374
|
private activeRunAgent: AbstractAgent | null = null;
|
|
242
375
|
private history: ThreadHistoryAdapter | undefined;
|
|
243
376
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
377
|
+
private readonly subagentParentToolCallIds = new Map<
|
|
378
|
+
string,
|
|
379
|
+
string | undefined
|
|
380
|
+
>();
|
|
244
381
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
245
382
|
private readonly persistedHistoryIds = new Set<string>();
|
|
246
383
|
private readonly historyWrites = createHistoryWriteChain();
|
|
@@ -347,6 +484,7 @@ export class AgUiThreadController {
|
|
|
347
484
|
|
|
348
485
|
this.session.applyExternalMessageRepository(repo);
|
|
349
486
|
this.assistantHistoryParents.clear();
|
|
487
|
+
this.subagentParentToolCallIds.clear();
|
|
350
488
|
this.snapshotHistoryIds.clear();
|
|
351
489
|
this.persistedHistoryIds.clear();
|
|
352
490
|
for (const { message } of repo.messages) {
|
|
@@ -560,6 +698,7 @@ export class AgUiThreadController {
|
|
|
560
698
|
config.runConfig ?? this.state.lastRunConfig,
|
|
561
699
|
undefined,
|
|
562
700
|
config.stream,
|
|
701
|
+
true,
|
|
563
702
|
);
|
|
564
703
|
}
|
|
565
704
|
|
|
@@ -599,17 +738,13 @@ export class AgUiThreadController {
|
|
|
599
738
|
private findRequiresActionAssistant(
|
|
600
739
|
reason: "interrupt" | "tool-calls",
|
|
601
740
|
): ThreadAssistantMessage | null {
|
|
602
|
-
|
|
603
|
-
(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
return assistant;
|
|
741
|
+
return (
|
|
742
|
+
this.getTrailingAssistantTurn().findLast(
|
|
743
|
+
(message) =>
|
|
744
|
+
message.status?.type === "requires-action" &&
|
|
745
|
+
message.status.reason === reason,
|
|
746
|
+
) ?? null
|
|
747
|
+
);
|
|
613
748
|
}
|
|
614
749
|
|
|
615
750
|
private getTrailingAssistantTurn(): ThreadAssistantMessage[] {
|
|
@@ -715,8 +850,9 @@ export class AgUiThreadController {
|
|
|
715
850
|
messageId: string,
|
|
716
851
|
resume: AgUiResumeEntry[],
|
|
717
852
|
): Promise<void> {
|
|
853
|
+
const parentId = this.session.headId ?? messageId;
|
|
718
854
|
this.clearPendingInterrupts(messageId, resume);
|
|
719
|
-
await this.startRun(
|
|
855
|
+
await this.startRun(parentId, this.state.lastRunConfig, {
|
|
720
856
|
entries: resume,
|
|
721
857
|
interruptedMessageId: messageId,
|
|
722
858
|
});
|
|
@@ -1004,6 +1140,7 @@ export class AgUiThreadController {
|
|
|
1004
1140
|
});
|
|
1005
1141
|
if (touched) {
|
|
1006
1142
|
this.publishUpdate();
|
|
1143
|
+
this.persistSettledAssistant(messageId);
|
|
1007
1144
|
}
|
|
1008
1145
|
}
|
|
1009
1146
|
|
|
@@ -1072,7 +1209,35 @@ export class AgUiThreadController {
|
|
|
1072
1209
|
};
|
|
1073
1210
|
});
|
|
1074
1211
|
if (!matchedToolCall) return message;
|
|
1075
|
-
|
|
1212
|
+
const settled = mapToolCallPartsDeep(content, (part) => {
|
|
1213
|
+
if (!part.messages) return part;
|
|
1214
|
+
let changed = false;
|
|
1215
|
+
const messages = part.messages.map((nested) => {
|
|
1216
|
+
if (
|
|
1217
|
+
nested.role !== "assistant" ||
|
|
1218
|
+
nested.status.type !== "requires-action" ||
|
|
1219
|
+
nested.status.reason !== "tool-calls"
|
|
1220
|
+
)
|
|
1221
|
+
return nested;
|
|
1222
|
+
const listed = listedPendingToolCallIds(nested);
|
|
1223
|
+
if (
|
|
1224
|
+
Array.from(iterateToolCallParts(nested.content)).some(
|
|
1225
|
+
(call) =>
|
|
1226
|
+
!isResolvedToolCall(call) &&
|
|
1227
|
+
(!listed || listed.has(call.toolCallId)),
|
|
1228
|
+
)
|
|
1229
|
+
)
|
|
1230
|
+
return nested;
|
|
1231
|
+
changed = true;
|
|
1232
|
+
return {
|
|
1233
|
+
...nested,
|
|
1234
|
+
status: { type: "complete" as const, reason: "unknown" as const },
|
|
1235
|
+
metadata: withoutPendingToolCallIds(nested.metadata),
|
|
1236
|
+
};
|
|
1237
|
+
});
|
|
1238
|
+
return changed ? { ...part, messages } : part;
|
|
1239
|
+
}).content;
|
|
1240
|
+
return { ...assistant, content: settled };
|
|
1076
1241
|
});
|
|
1077
1242
|
|
|
1078
1243
|
if (!updated) return;
|
|
@@ -1180,15 +1345,26 @@ export class AgUiThreadController {
|
|
|
1180
1345
|
// The continuation fires whether the frontend result lands before
|
|
1181
1346
|
// RUN_FINISHED (the status flips to requires-action only later, while the
|
|
1182
1347
|
// run is still draining) or after it.
|
|
1183
|
-
private maybeResumeAfterToolResults(
|
|
1348
|
+
private maybeResumeAfterToolResults(
|
|
1349
|
+
messageId: string,
|
|
1350
|
+
accountedResultIds?: ReadonlySet<string>,
|
|
1351
|
+
): void {
|
|
1184
1352
|
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
1353
|
+
if (accountedResultIds) {
|
|
1354
|
+
const message = this.session.tryGetMessage(messageId)?.message;
|
|
1355
|
+
if (
|
|
1356
|
+
message?.role !== "assistant" ||
|
|
1357
|
+
!Array.from(iterateToolCallParts(message.content)).some(
|
|
1358
|
+
(part) =>
|
|
1359
|
+
isResolvedToolCall(part) &&
|
|
1360
|
+
!accountedResultIds.has(part.toolCallId),
|
|
1361
|
+
)
|
|
1362
|
+
)
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1185
1365
|
if (this.getPendingInterrupts()) return;
|
|
1186
1366
|
const turn = this.getTrailingAssistantTurn();
|
|
1187
1367
|
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;
|
|
1191
|
-
|
|
1192
1368
|
const owner = this.abortController;
|
|
1193
1369
|
if (owner) {
|
|
1194
1370
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
@@ -1196,10 +1372,13 @@ export class AgUiThreadController {
|
|
|
1196
1372
|
this.dispatch({
|
|
1197
1373
|
type: "resumeDeferred",
|
|
1198
1374
|
owner,
|
|
1199
|
-
messageId
|
|
1375
|
+
messageId,
|
|
1200
1376
|
});
|
|
1201
1377
|
return;
|
|
1202
1378
|
}
|
|
1379
|
+
if (this.getPendingToolCalls()) return;
|
|
1380
|
+
const headId = turn.at(-1)?.id;
|
|
1381
|
+
if (headId === undefined) return;
|
|
1203
1382
|
this.startResumeRun(headId);
|
|
1204
1383
|
}
|
|
1205
1384
|
|
|
@@ -1235,7 +1414,11 @@ export class AgUiThreadController {
|
|
|
1235
1414
|
);
|
|
1236
1415
|
if (!updated) return false;
|
|
1237
1416
|
this.publishUpdate();
|
|
1238
|
-
this.
|
|
1417
|
+
if (this.history?.update) {
|
|
1418
|
+
this.persistSettledAssistant(messageId);
|
|
1419
|
+
} else {
|
|
1420
|
+
this.persistAssistantHistory(messageId);
|
|
1421
|
+
}
|
|
1239
1422
|
return true;
|
|
1240
1423
|
}
|
|
1241
1424
|
|
|
@@ -1269,6 +1452,19 @@ export class AgUiThreadController {
|
|
|
1269
1452
|
let lastAppliedId: string | null = null;
|
|
1270
1453
|
let hardReplace = false;
|
|
1271
1454
|
const seen = new Set<string>();
|
|
1455
|
+
const retainToolResults = (
|
|
1456
|
+
existing: ThreadMessage | undefined,
|
|
1457
|
+
message: ThreadMessage,
|
|
1458
|
+
): ThreadMessage =>
|
|
1459
|
+
existing?.role === "assistant" && message.role === "assistant"
|
|
1460
|
+
? {
|
|
1461
|
+
...message,
|
|
1462
|
+
content: this.preserveToolResults(
|
|
1463
|
+
existing.content,
|
|
1464
|
+
message.content,
|
|
1465
|
+
),
|
|
1466
|
+
}
|
|
1467
|
+
: message;
|
|
1272
1468
|
|
|
1273
1469
|
for (const message of messages) {
|
|
1274
1470
|
if (seen.has(message.id)) continue;
|
|
@@ -1278,12 +1474,21 @@ export class AgUiThreadController {
|
|
|
1278
1474
|
hardReplace = true;
|
|
1279
1475
|
break;
|
|
1280
1476
|
}
|
|
1281
|
-
this.session.addOrUpdateMessage(
|
|
1477
|
+
this.session.addOrUpdateMessage(
|
|
1478
|
+
expectedParentId,
|
|
1479
|
+
retainToolResults(existing?.message, message),
|
|
1480
|
+
);
|
|
1282
1481
|
expectedParentId = message.id;
|
|
1283
1482
|
lastAppliedId = message.id;
|
|
1284
1483
|
}
|
|
1285
1484
|
|
|
1286
1485
|
if (hardReplace) {
|
|
1486
|
+
const previous = new Map(
|
|
1487
|
+
this.getMessageRepository().messages.map(({ message }) => [
|
|
1488
|
+
message.id,
|
|
1489
|
+
message,
|
|
1490
|
+
]),
|
|
1491
|
+
);
|
|
1287
1492
|
this.session.clear();
|
|
1288
1493
|
expectedParentId = null;
|
|
1289
1494
|
lastAppliedId = null;
|
|
@@ -1291,29 +1496,48 @@ export class AgUiThreadController {
|
|
|
1291
1496
|
for (const message of messages) {
|
|
1292
1497
|
if (seen.has(message.id)) continue;
|
|
1293
1498
|
seen.add(message.id);
|
|
1294
|
-
this.session.addOrUpdateMessage(
|
|
1499
|
+
this.session.addOrUpdateMessage(
|
|
1500
|
+
expectedParentId,
|
|
1501
|
+
retainToolResults(previous.get(message.id), message),
|
|
1502
|
+
);
|
|
1295
1503
|
expectedParentId = message.id;
|
|
1296
1504
|
lastAppliedId = message.id;
|
|
1297
1505
|
}
|
|
1298
1506
|
}
|
|
1299
1507
|
|
|
1300
1508
|
this.session.resetHead(lastAppliedId);
|
|
1509
|
+
for (const messageId of seen) {
|
|
1510
|
+
this.maybeCompleteAfterToolResults(messageId);
|
|
1511
|
+
}
|
|
1301
1512
|
}
|
|
1302
1513
|
|
|
1303
1514
|
this.snapshotHistoryIds.clear();
|
|
1304
1515
|
for (const { message } of this.getMessageRepository().messages) {
|
|
1305
1516
|
this.snapshotHistoryIds.add(message.id);
|
|
1306
1517
|
}
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1518
|
+
// A soft merge leaves dropped messages off branch; only selected
|
|
1519
|
+
// messages with resolved tool calls can still own a continuation.
|
|
1520
|
+
const pendingResume = this.state.pendingResume;
|
|
1521
|
+
if (pendingResume) {
|
|
1522
|
+
const selectedResultOwnerIds = new Set(
|
|
1523
|
+
this.session
|
|
1524
|
+
.getMessages()
|
|
1525
|
+
.filter(hasResolvedToolCalls)
|
|
1526
|
+
.map((message) => message.id),
|
|
1527
|
+
);
|
|
1528
|
+
const retained = pendingResume.messageIds.filter((id) =>
|
|
1529
|
+
selectedResultOwnerIds.has(id),
|
|
1530
|
+
);
|
|
1531
|
+
if (retained.length !== pendingResume.messageIds.length) {
|
|
1532
|
+
this.dispatch({ type: "resumeCleared" });
|
|
1533
|
+
for (const messageId of retained) {
|
|
1534
|
+
this.dispatch({
|
|
1535
|
+
type: "resumeDeferred",
|
|
1536
|
+
owner: pendingResume.owner,
|
|
1537
|
+
messageId,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1317
1541
|
}
|
|
1318
1542
|
this.publishUpdate();
|
|
1319
1543
|
}
|
|
@@ -1353,6 +1577,7 @@ export class AgUiThreadController {
|
|
|
1353
1577
|
const activeRunAgent = this.activeRunAgent;
|
|
1354
1578
|
|
|
1355
1579
|
this.dispatch({ type: "resetThread" });
|
|
1580
|
+
this.subagentParentToolCallIds.clear();
|
|
1356
1581
|
|
|
1357
1582
|
if (controller) {
|
|
1358
1583
|
this.abortController = null;
|
|
@@ -1375,6 +1600,7 @@ export class AgUiThreadController {
|
|
|
1375
1600
|
runConfig?: RunConfig,
|
|
1376
1601
|
resume?: ResumeDispatch,
|
|
1377
1602
|
resumeStream?: ResumeStream,
|
|
1603
|
+
replayExistingMessages = false,
|
|
1378
1604
|
): Promise<void> {
|
|
1379
1605
|
if (this.isThreadSwitching?.()) return;
|
|
1380
1606
|
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
@@ -1382,6 +1608,7 @@ export class AgUiThreadController {
|
|
|
1382
1608
|
// its message; this covers the entry points that start a run without one.
|
|
1383
1609
|
if (!this.supersedeActiveRun()) return;
|
|
1384
1610
|
|
|
1611
|
+
const generation = this.state.historyMutationGeneration;
|
|
1385
1612
|
const normalizedRunConfig = runConfig ?? {};
|
|
1386
1613
|
this.dispatch({
|
|
1387
1614
|
type: "runConfigRecorded",
|
|
@@ -1401,12 +1628,62 @@ export class AgUiThreadController {
|
|
|
1401
1628
|
const runStartMessageIds = new Set(
|
|
1402
1629
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1403
1630
|
);
|
|
1631
|
+
const selectedMessages = this.session.getMessages();
|
|
1632
|
+
const parentIndex = selectedMessages.findIndex(
|
|
1633
|
+
(message) => message.id === parentId,
|
|
1634
|
+
);
|
|
1635
|
+
let replayStart = parentIndex + 1;
|
|
1636
|
+
if (selectedMessages[parentIndex]?.role === "assistant") {
|
|
1637
|
+
replayStart = parentIndex;
|
|
1638
|
+
while (
|
|
1639
|
+
replayStart > 0 &&
|
|
1640
|
+
selectedMessages[replayStart - 1]?.role === "assistant"
|
|
1641
|
+
)
|
|
1642
|
+
replayStart--;
|
|
1643
|
+
}
|
|
1644
|
+
const replayMessageIds: string[] = [];
|
|
1645
|
+
if (replayExistingMessages && !resumeStream && parentIndex >= 0) {
|
|
1646
|
+
for (const message of selectedMessages.slice(replayStart)) {
|
|
1647
|
+
if (message.role !== "assistant") break;
|
|
1648
|
+
replayMessageIds.push(message.id);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
// Only request-delivered and producer-observed results suppress continuation.
|
|
1652
|
+
const accountedResultIds = new Set(
|
|
1653
|
+
historicalMessages.flatMap((message) =>
|
|
1654
|
+
message.role === "assistant"
|
|
1655
|
+
? Array.from(iterateToolCallParts(message.content))
|
|
1656
|
+
.filter(isResolvedToolCall)
|
|
1657
|
+
.map((part) => part.toolCallId)
|
|
1658
|
+
: [],
|
|
1659
|
+
),
|
|
1660
|
+
);
|
|
1661
|
+
const candidateContexts = new Map<string, ReplayContext>();
|
|
1662
|
+
for (const message of selectedMessages) {
|
|
1663
|
+
if (
|
|
1664
|
+
message.role !== "assistant" ||
|
|
1665
|
+
!replayMessageIds.includes(message.id)
|
|
1666
|
+
)
|
|
1667
|
+
continue;
|
|
1668
|
+
candidateContexts.set(message.id, {
|
|
1669
|
+
originalContent: message.content,
|
|
1670
|
+
accountedResultIds,
|
|
1671
|
+
admittedToolCallIds: actionableToolCallIds(message),
|
|
1672
|
+
merge: createReplayContentMerger(),
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
let replayContexts = new Map<string, ReplayContext>();
|
|
1676
|
+
let lastReplayedIndex = -1;
|
|
1677
|
+
let replayEnded = replayMessageIds.length === 0;
|
|
1678
|
+
const replayTail = replayMessageIds.at(-1);
|
|
1404
1679
|
const joinStrategy = this.joinStrategy;
|
|
1405
1680
|
const hasRunUserBoundary = parent?.message.role === "user" && !resume;
|
|
1406
1681
|
|
|
1407
1682
|
let pendingError: Error | null = null;
|
|
1408
1683
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1409
1684
|
let assistantMessageId: string | undefined;
|
|
1685
|
+
let snapshotAssistantId: string | undefined;
|
|
1686
|
+
let replayInterruptMessageId: string | undefined;
|
|
1410
1687
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
1411
1688
|
// assistant; recreating under the cached id on the next content-bearing
|
|
1412
1689
|
// emit keeps both the stream and the message identity. Status-only emits,
|
|
@@ -1416,8 +1693,11 @@ export class AgUiThreadController {
|
|
|
1416
1693
|
// duplicate.
|
|
1417
1694
|
let assistantCollided = false;
|
|
1418
1695
|
const heldMessages = new Map<string, Set<string>>();
|
|
1696
|
+
let frameOwnerIds = new Set<string>();
|
|
1419
1697
|
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1420
1698
|
let settlingRun = false;
|
|
1699
|
+
let latestSnapshot: ThreadAssistantMessage | undefined;
|
|
1700
|
+
let snapshotToolCallIds = new Set<string>();
|
|
1421
1701
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1422
1702
|
const cached = assistantMessageId;
|
|
1423
1703
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1427,10 +1707,16 @@ export class AgUiThreadController {
|
|
|
1427
1707
|
}
|
|
1428
1708
|
const repositoryHeadId = this.session.headId;
|
|
1429
1709
|
const shouldUseSelectedParent =
|
|
1430
|
-
|
|
1431
|
-
(
|
|
1432
|
-
(
|
|
1433
|
-
|
|
1710
|
+
replayContexts.size === 0 &&
|
|
1711
|
+
(cached === undefined ||
|
|
1712
|
+
(shouldEagerlyInsertAssistant &&
|
|
1713
|
+
(repositoryHeadId === null ||
|
|
1714
|
+
runStartMessageIds.has(repositoryHeadId)))) &&
|
|
1715
|
+
// Replay candidates defer the placeholder; honor snapshots that advanced
|
|
1716
|
+
// the head before the producer identified its first assistant message.
|
|
1717
|
+
(replayMessageIds.length === 0 ||
|
|
1718
|
+
repositoryHeadId === null ||
|
|
1719
|
+
runStartMessageIds.has(repositoryHeadId));
|
|
1434
1720
|
// Branch runs keep their selected parent unless the snapshot advanced to
|
|
1435
1721
|
// a message introduced during this run.
|
|
1436
1722
|
const parentId =
|
|
@@ -1445,23 +1731,59 @@ export class AgUiThreadController {
|
|
|
1445
1731
|
return created;
|
|
1446
1732
|
};
|
|
1447
1733
|
|
|
1448
|
-
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1734
|
+
if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
|
|
1449
1735
|
|
|
1450
1736
|
const settleHeldMessages = (terminalStatus: MessageStatus): string[] => {
|
|
1451
1737
|
const pendingMessages: string[] = [];
|
|
1738
|
+
const producerPending = restrictedPendingToolCallIds;
|
|
1739
|
+
const canWaitForTools =
|
|
1740
|
+
terminalStatus.type === "complete" ||
|
|
1741
|
+
terminalStatus.type === "requires-action";
|
|
1742
|
+
if (producerPending && canWaitForTools) {
|
|
1743
|
+
for (const message of this.getTrailingAssistantTurn()) {
|
|
1744
|
+
const replay = replayContexts.get(message.id);
|
|
1745
|
+
if (
|
|
1746
|
+
message.id === assistantMessageId ||
|
|
1747
|
+
heldMessages.has(message.id) ||
|
|
1748
|
+
!replay ||
|
|
1749
|
+
(message.status?.type === "requires-action" &&
|
|
1750
|
+
message.status.reason === "interrupt")
|
|
1751
|
+
)
|
|
1752
|
+
continue;
|
|
1753
|
+
if (
|
|
1754
|
+
replay.admittedToolCallIds.size > 0 ||
|
|
1755
|
+
actionableToolCallIds(message).size > 0 ||
|
|
1756
|
+
Array.from(iterateToolCallParts(message.content)).some(
|
|
1757
|
+
(part) =>
|
|
1758
|
+
producerPending.has(part.toolCallId) &&
|
|
1759
|
+
!accountedResultIds.has(part.toolCallId),
|
|
1760
|
+
)
|
|
1761
|
+
)
|
|
1762
|
+
heldMessages.set(message.id, new Set());
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1452
1765
|
for (const [messageId, openToolCallIds] of heldMessages) {
|
|
1766
|
+
const existing = this.session.tryGetMessage(messageId);
|
|
1767
|
+
if (existing)
|
|
1768
|
+
this.markPendingAssistantHistory(messageId, existing.parentId);
|
|
1453
1769
|
let requiresToolResult = false;
|
|
1454
1770
|
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1455
1771
|
if (message.role !== "assistant") return message;
|
|
1456
1772
|
const assistant = message as ThreadAssistantMessage;
|
|
1457
|
-
const pendingToolCallIds =
|
|
1458
|
-
(
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1773
|
+
const pendingToolCallIds = producerPending
|
|
1774
|
+
? Array.from(iterateToolCallParts(assistant.content))
|
|
1775
|
+
.filter(
|
|
1776
|
+
(part) =>
|
|
1777
|
+
producerPending.has(part.toolCallId) &&
|
|
1778
|
+
(!isResolvedToolCall(part) ||
|
|
1779
|
+
openToolCallIds.has(part.toolCallId) ||
|
|
1780
|
+
(replayContexts.has(messageId) &&
|
|
1781
|
+
!accountedResultIds.has(part.toolCallId))),
|
|
1782
|
+
)
|
|
1783
|
+
.map((part) => part.toolCallId)
|
|
1784
|
+
: Array.from(openToolCallIds);
|
|
1785
|
+
const replay = replayContexts.get(messageId);
|
|
1786
|
+
if (replay) replay.admittedToolCallIds = new Set(pendingToolCallIds);
|
|
1465
1787
|
const status: MessageStatus = canWaitForTools
|
|
1466
1788
|
? pendingToolCallIds.length > 0
|
|
1467
1789
|
? { type: "requires-action", reason: "tool-calls" }
|
|
@@ -1473,19 +1795,9 @@ export class AgUiThreadController {
|
|
|
1473
1795
|
...assistant,
|
|
1474
1796
|
status,
|
|
1475
1797
|
metadata:
|
|
1476
|
-
status.type === "requires-action" &&
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
}
|
|
1798
|
+
status.type === "requires-action" &&
|
|
1799
|
+
(restrictedPendingToolCallIds || replayContexts.has(messageId))
|
|
1800
|
+
? withPendingToolCallIds(metadata, pendingToolCallIds)
|
|
1489
1801
|
: metadata,
|
|
1490
1802
|
};
|
|
1491
1803
|
});
|
|
@@ -1501,24 +1813,190 @@ export class AgUiThreadController {
|
|
|
1501
1813
|
return pendingMessages;
|
|
1502
1814
|
};
|
|
1503
1815
|
|
|
1816
|
+
const accountProducerResults = (
|
|
1817
|
+
content: ThreadAssistantMessage["content"],
|
|
1818
|
+
) => {
|
|
1819
|
+
for (const part of iterateToolCallParts(content))
|
|
1820
|
+
if (isResolvedToolCall(part)) accountedResultIds.add(part.toolCallId);
|
|
1821
|
+
};
|
|
1822
|
+
const reconcileReplaySnapshot: SnapshotMessageReconciler = (
|
|
1823
|
+
snapshot,
|
|
1824
|
+
previous,
|
|
1825
|
+
) => {
|
|
1826
|
+
// Observe the producer before preserving any local frontend results.
|
|
1827
|
+
accountProducerResults(snapshot.content);
|
|
1828
|
+
const replay = candidateContexts.get(snapshot.id);
|
|
1829
|
+
if (!replay) return snapshot;
|
|
1830
|
+
replay.admittedToolCallIds = actionableToolCallIds(snapshot);
|
|
1831
|
+
if (heldMessages.has(snapshot.id))
|
|
1832
|
+
heldMessages.set(snapshot.id, new Set(replay.admittedToolCallIds));
|
|
1833
|
+
if (!replayContexts.has(snapshot.id)) return snapshot;
|
|
1834
|
+
return previous
|
|
1835
|
+
? {
|
|
1836
|
+
...snapshot,
|
|
1837
|
+
content: this.preserveToolResults(
|
|
1838
|
+
previous.content,
|
|
1839
|
+
snapshot.content,
|
|
1840
|
+
replay.originalContent,
|
|
1841
|
+
),
|
|
1842
|
+
}
|
|
1843
|
+
: snapshot;
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1504
1846
|
const applyUpdate = (update: ChatModelRunResult) => {
|
|
1847
|
+
if (
|
|
1848
|
+
!assistantCollided &&
|
|
1849
|
+
replayContexts.size &&
|
|
1850
|
+
update.content !== undefined
|
|
1851
|
+
)
|
|
1852
|
+
accountProducerResults(
|
|
1853
|
+
update.content as ThreadAssistantMessage["content"],
|
|
1854
|
+
);
|
|
1855
|
+
let ownerParts:
|
|
1856
|
+
| Map<string, ThreadAssistantMessage["content"][number][]>
|
|
1857
|
+
| undefined;
|
|
1858
|
+
let contentOwnerIds: Set<string> | undefined;
|
|
1859
|
+
const hasStreamContent =
|
|
1860
|
+
Array.isArray(update.content) &&
|
|
1861
|
+
update.content.some((part) => part.type !== "data");
|
|
1862
|
+
if (!replayEnded && assistantMessageId === undefined) {
|
|
1863
|
+
if (!update.status || update.status.type === "running") return;
|
|
1864
|
+
// No matching producer identity arrived: retain native regeneration.
|
|
1865
|
+
replayEnded = true;
|
|
1866
|
+
if (
|
|
1867
|
+
replayContexts.size === 0 &&
|
|
1868
|
+
snapshotAssistantId !== undefined &&
|
|
1869
|
+
this.session.headId === snapshotAssistantId &&
|
|
1870
|
+
!hasStreamContent
|
|
1871
|
+
) {
|
|
1872
|
+
if (update.status.type !== "complete") {
|
|
1873
|
+
this.updateAssistantMessage(snapshotAssistantId, {
|
|
1874
|
+
status: update.status,
|
|
1875
|
+
...(update.metadata ? { metadata: update.metadata } : {}),
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const streamMessageId = assistantMessageId;
|
|
1882
|
+
let primaryMessageId = streamMessageId;
|
|
1505
1883
|
const terminalStatus = update.status;
|
|
1884
|
+
const replayInterrupt =
|
|
1885
|
+
replayContexts.size > 0 &&
|
|
1886
|
+
terminalStatus?.type === "requires-action" &&
|
|
1887
|
+
terminalStatus.reason === "interrupt";
|
|
1888
|
+
if (
|
|
1889
|
+
streamMessageId !== undefined &&
|
|
1890
|
+
!assistantCollided &&
|
|
1891
|
+
replayContexts.size > 0 &&
|
|
1892
|
+
(update.content !== undefined || replayInterrupt)
|
|
1893
|
+
) {
|
|
1894
|
+
const owners = this.getTrailingAssistantTurn().filter((message) =>
|
|
1895
|
+
replayContexts.has(message.id),
|
|
1896
|
+
);
|
|
1897
|
+
// Buffered calls keep their owners when a later text frame arrives.
|
|
1898
|
+
const grouped =
|
|
1899
|
+
update.content !== undefined
|
|
1900
|
+
? groupReplayToolContent(owners, update.content, streamMessageId)
|
|
1901
|
+
: new Map([[streamMessageId, [...(update.content ?? [])]]]);
|
|
1902
|
+
if (update.content !== undefined) {
|
|
1903
|
+
const currentOwnerIds = new Set(grouped.keys());
|
|
1904
|
+
for (const ownerId of frameOwnerIds)
|
|
1905
|
+
grouped.set(ownerId, grouped.get(ownerId) ?? []);
|
|
1906
|
+
contentOwnerIds = new Set(grouped.keys());
|
|
1907
|
+
frameOwnerIds = currentOwnerIds;
|
|
1908
|
+
}
|
|
1909
|
+
ownerParts = grouped;
|
|
1910
|
+
if (replayInterrupt) {
|
|
1911
|
+
const interrupts = (
|
|
1912
|
+
update.metadata?.custom?.[AG_UI_METADATA_NAMESPACE] as
|
|
1913
|
+
| AgUiCustomMetadata
|
|
1914
|
+
| undefined
|
|
1915
|
+
)?.interrupts;
|
|
1916
|
+
const approvals = projectAgUiToolApprovals(interrupts);
|
|
1917
|
+
for (const owner of owners) {
|
|
1918
|
+
if (
|
|
1919
|
+
Array.from(iterateToolCallParts(owner.content)).some((part) =>
|
|
1920
|
+
approvals.has(part.toolCallId),
|
|
1921
|
+
)
|
|
1922
|
+
)
|
|
1923
|
+
grouped.set(owner.id, grouped.get(owner.id) ?? []);
|
|
1924
|
+
}
|
|
1925
|
+
const anchor = owners.find(
|
|
1926
|
+
(message) =>
|
|
1927
|
+
projectAgUiToolApprovals(
|
|
1928
|
+
interrupts,
|
|
1929
|
+
new Set(
|
|
1930
|
+
Array.from(
|
|
1931
|
+
iterateToolCallParts([
|
|
1932
|
+
...message.content,
|
|
1933
|
+
...(grouped.get(message.id) ?? []),
|
|
1934
|
+
]),
|
|
1935
|
+
).map((part) => part.toolCallId),
|
|
1936
|
+
),
|
|
1937
|
+
).size > 0,
|
|
1938
|
+
);
|
|
1939
|
+
if (anchor) {
|
|
1940
|
+
primaryMessageId = anchor.id;
|
|
1941
|
+
replayInterruptMessageId = anchor.id;
|
|
1942
|
+
grouped.set(anchor.id, grouped.get(anchor.id) ?? []);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1506
1946
|
const pendingMessages =
|
|
1507
1947
|
settlingRun && terminalStatus && terminalStatus.type !== "running"
|
|
1508
1948
|
? settleHeldMessages(terminalStatus)
|
|
1509
1949
|
: [];
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1950
|
+
const frameMessageId = ensureAssistant(hasStreamContent);
|
|
1951
|
+
const messageId = primaryMessageId ?? frameMessageId;
|
|
1952
|
+
let primaryUpdate = update;
|
|
1953
|
+
if (ownerParts) {
|
|
1954
|
+
const interrupt =
|
|
1955
|
+
terminalStatus?.type === "requires-action" &&
|
|
1956
|
+
terminalStatus.reason === "interrupt";
|
|
1957
|
+
const ownerStatus: MessageStatus | undefined =
|
|
1958
|
+
terminalStatus?.type === "requires-action"
|
|
1959
|
+
? { type: "complete", reason: "unknown" }
|
|
1960
|
+
: terminalStatus;
|
|
1961
|
+
for (const [ownerId, content] of ownerParts) {
|
|
1962
|
+
const existing = this.session.tryGetMessage(ownerId);
|
|
1963
|
+
if (existing)
|
|
1964
|
+
this.markPendingAssistantHistory(ownerId, existing.parentId);
|
|
1965
|
+
if (ownerId === messageId) continue;
|
|
1966
|
+
this.updateAssistantMessage(
|
|
1967
|
+
ownerId,
|
|
1968
|
+
{
|
|
1969
|
+
...(contentOwnerIds?.has(ownerId) ? { content } : {}),
|
|
1970
|
+
...(ownerStatus ? { status: ownerStatus } : {}),
|
|
1971
|
+
},
|
|
1972
|
+
replayContexts.get(ownerId),
|
|
1973
|
+
interrupt ? new Set<string>() : restrictedPendingToolCallIds,
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
const { content: _content, ...controlUpdate } = update;
|
|
1977
|
+
const content = ownerParts.get(messageId) ?? [];
|
|
1978
|
+
primaryUpdate = {
|
|
1979
|
+
...controlUpdate,
|
|
1980
|
+
...(contentOwnerIds?.has(messageId) ? { content } : {}),
|
|
1981
|
+
...(ownerStatus && !interrupt && replayContexts.has(messageId)
|
|
1982
|
+
? { status: ownerStatus }
|
|
1983
|
+
: {}),
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1513
1986
|
const resolved = this.updateAssistantMessage(
|
|
1514
|
-
|
|
1515
|
-
|
|
1987
|
+
messageId,
|
|
1988
|
+
primaryUpdate,
|
|
1989
|
+
replayContexts.get(messageId),
|
|
1990
|
+
restrictedPendingToolCallIds,
|
|
1516
1991
|
);
|
|
1517
|
-
if (resolved !== assistantMessageId) {
|
|
1992
|
+
if (messageId === frameMessageId && resolved !== assistantMessageId) {
|
|
1518
1993
|
assistantMessageId = resolved;
|
|
1519
1994
|
}
|
|
1520
1995
|
for (const messageId of pendingMessages) {
|
|
1521
|
-
this.maybeResumeAfterToolResults(
|
|
1996
|
+
this.maybeResumeAfterToolResults(
|
|
1997
|
+
messageId,
|
|
1998
|
+
replayMessageIds.includes(messageId) ? accountedResultIds : undefined,
|
|
1999
|
+
);
|
|
1522
2000
|
}
|
|
1523
2001
|
};
|
|
1524
2002
|
|
|
@@ -1527,20 +2005,74 @@ export class AgUiThreadController {
|
|
|
1527
2005
|
startNewMessage: boolean,
|
|
1528
2006
|
openToolCallIds: string[] = [],
|
|
1529
2007
|
) => {
|
|
2008
|
+
const existing = this.session.tryGetMessage(serverId);
|
|
2009
|
+
const replayIndex = replayMessageIds.indexOf(serverId);
|
|
2010
|
+
const adoptsReplay =
|
|
2011
|
+
!replayEnded &&
|
|
2012
|
+
!assistantCollided &&
|
|
2013
|
+
replayIndex > lastReplayedIndex &&
|
|
2014
|
+
existing?.message.role === "assistant" &&
|
|
2015
|
+
this.session.getMessages().some((message) => message.id === serverId);
|
|
2016
|
+
if (adoptsReplay && replayContexts.size === 0) {
|
|
2017
|
+
replayContexts = candidateContexts;
|
|
2018
|
+
const selected = this.session.getMessages();
|
|
2019
|
+
let tailId = serverId;
|
|
2020
|
+
for (const message of selected.slice(
|
|
2021
|
+
selected.findIndex((message) => message.id === serverId) + 1,
|
|
2022
|
+
)) {
|
|
2023
|
+
if (message.role !== "assistant") break;
|
|
2024
|
+
tailId = message.id;
|
|
2025
|
+
}
|
|
2026
|
+
this.session.selectHead(tailId);
|
|
2027
|
+
}
|
|
1530
2028
|
if (startNewMessage && assistantMessageId !== undefined) {
|
|
1531
|
-
|
|
2029
|
+
let hasReplayCallsToHold = false;
|
|
2030
|
+
const replay = replayContexts.get(assistantMessageId);
|
|
2031
|
+
if (replay) {
|
|
2032
|
+
const current =
|
|
2033
|
+
this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2034
|
+
if (current?.role === "assistant") {
|
|
2035
|
+
const retainedPending = Array.from(
|
|
2036
|
+
iterateToolCallParts(current.content),
|
|
2037
|
+
).filter(
|
|
2038
|
+
(part) =>
|
|
2039
|
+
!isResolvedToolCall(part) ||
|
|
2040
|
+
(replay.admittedToolCallIds.has(part.toolCallId) &&
|
|
2041
|
+
!accountedResultIds.has(part.toolCallId)),
|
|
2042
|
+
);
|
|
2043
|
+
hasReplayCallsToHold = retainedPending.length > 0;
|
|
2044
|
+
openToolCallIds = [
|
|
2045
|
+
...new Set([
|
|
2046
|
+
...openToolCallIds,
|
|
2047
|
+
...retainedPending
|
|
2048
|
+
.filter((part) =>
|
|
2049
|
+
replay.admittedToolCallIds.has(part.toolCallId),
|
|
2050
|
+
)
|
|
2051
|
+
.map((part) => part.toolCallId),
|
|
2052
|
+
]),
|
|
2053
|
+
];
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (openToolCallIds.length > 0 || hasReplayCallsToHold) {
|
|
1532
2057
|
heldMessages.set(assistantMessageId, new Set(openToolCallIds));
|
|
1533
2058
|
} else {
|
|
1534
2059
|
applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1535
2060
|
}
|
|
1536
2061
|
const previousId = assistantMessageId;
|
|
1537
2062
|
assistantMessageId = undefined;
|
|
1538
|
-
if (this.session.tryGetMessage(previousId)) {
|
|
2063
|
+
if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
|
|
1539
2064
|
const created = this.insertAssistantPlaceholder(previousId);
|
|
1540
2065
|
assistantMessageId = created;
|
|
1541
2066
|
this.markPendingAssistantHistory(created, previousId);
|
|
1542
2067
|
}
|
|
1543
2068
|
}
|
|
2069
|
+
if (adoptsReplay) {
|
|
2070
|
+
assistantMessageId = serverId;
|
|
2071
|
+
lastReplayedIndex = replayIndex;
|
|
2072
|
+
this.markPendingAssistantHistory(serverId, existing.parentId);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
replayEnded = true;
|
|
1544
2076
|
const placeholder = ensureAssistant(true);
|
|
1545
2077
|
if (placeholder === serverId) return;
|
|
1546
2078
|
const reassigned = this.reassignAssistantId(placeholder, serverId);
|
|
@@ -1570,25 +2102,146 @@ export class AgUiThreadController {
|
|
|
1570
2102
|
showThinking: this.showThinking,
|
|
1571
2103
|
joinStrategy,
|
|
1572
2104
|
logger: this.logger,
|
|
1573
|
-
emit:
|
|
2105
|
+
emit: (update) => {
|
|
2106
|
+
const currentSnapshotAssistant =
|
|
2107
|
+
assistantMessageId === undefined
|
|
2108
|
+
? undefined
|
|
2109
|
+
: this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2110
|
+
const listedSnapshotToolCallIds =
|
|
2111
|
+
currentSnapshotAssistant?.role === "assistant"
|
|
2112
|
+
? listedPendingToolCallIds(currentSnapshotAssistant)
|
|
2113
|
+
: undefined;
|
|
2114
|
+
const hasPendingSnapshotToolCall =
|
|
2115
|
+
latestSnapshot?.id === assistantMessageId &&
|
|
2116
|
+
currentSnapshotAssistant?.role === "assistant" &&
|
|
2117
|
+
Array.from(
|
|
2118
|
+
iterateToolCallParts(currentSnapshotAssistant.content),
|
|
2119
|
+
).some(
|
|
2120
|
+
(part) =>
|
|
2121
|
+
!isResolvedToolCall(part) &&
|
|
2122
|
+
(!listedSnapshotToolCallIds ||
|
|
2123
|
+
listedSnapshotToolCallIds.has(part.toolCallId)) &&
|
|
2124
|
+
(!restrictedPendingToolCallIds ||
|
|
2125
|
+
restrictedPendingToolCallIds.has(part.toolCallId)),
|
|
2126
|
+
);
|
|
2127
|
+
const reconciledUpdate =
|
|
2128
|
+
settlingRun &&
|
|
2129
|
+
update.status?.type === "complete" &&
|
|
2130
|
+
hasPendingSnapshotToolCall
|
|
2131
|
+
? {
|
|
2132
|
+
...update,
|
|
2133
|
+
status: {
|
|
2134
|
+
type: "requires-action",
|
|
2135
|
+
reason: "tool-calls",
|
|
2136
|
+
} as const,
|
|
2137
|
+
...(restrictedPendingToolCallIds
|
|
2138
|
+
? {
|
|
2139
|
+
metadata: {
|
|
2140
|
+
...update.metadata,
|
|
2141
|
+
custom: {
|
|
2142
|
+
...update.metadata?.custom,
|
|
2143
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
2144
|
+
...(update.metadata?.custom?.[
|
|
2145
|
+
AG_UI_METADATA_NAMESPACE
|
|
2146
|
+
] as AgUiCustomMetadata | undefined),
|
|
2147
|
+
pendingToolCallIds: [
|
|
2148
|
+
...restrictedPendingToolCallIds,
|
|
2149
|
+
],
|
|
2150
|
+
},
|
|
2151
|
+
},
|
|
2152
|
+
},
|
|
2153
|
+
}
|
|
2154
|
+
: {}),
|
|
2155
|
+
}
|
|
2156
|
+
: update;
|
|
2157
|
+
if (
|
|
2158
|
+
settlingRun &&
|
|
2159
|
+
latestSnapshot &&
|
|
2160
|
+
latestSnapshot.id === assistantMessageId &&
|
|
2161
|
+
reconciledUpdate.status &&
|
|
2162
|
+
reconciledUpdate.status.type !== "running" &&
|
|
2163
|
+
reconciledUpdate.status.type !== "requires-action"
|
|
2164
|
+
) {
|
|
2165
|
+
const status = reconciledUpdate.status;
|
|
2166
|
+
const current =
|
|
2167
|
+
this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2168
|
+
const content = settleNestedContent(
|
|
2169
|
+
current?.role === "assistant"
|
|
2170
|
+
? current.content
|
|
2171
|
+
: latestSnapshot.content,
|
|
2172
|
+
status,
|
|
2173
|
+
);
|
|
2174
|
+
applyUpdate({ ...reconciledUpdate, content });
|
|
2175
|
+
} else if (
|
|
2176
|
+
latestSnapshot &&
|
|
2177
|
+
latestSnapshot.id === assistantMessageId &&
|
|
2178
|
+
reconciledUpdate.content !== undefined
|
|
2179
|
+
) {
|
|
2180
|
+
const content = this.mergeSnapshotAssistantContent(
|
|
2181
|
+
latestSnapshot.content,
|
|
2182
|
+
reconciledUpdate.content as ThreadAssistantMessage["content"],
|
|
2183
|
+
reconciledUpdate.content.some((part) => part.type === "reasoning"),
|
|
2184
|
+
reconciledUpdate.content.some(isActivityPart),
|
|
2185
|
+
snapshotToolCallIds,
|
|
2186
|
+
);
|
|
2187
|
+
applyUpdate({ ...reconciledUpdate, content });
|
|
2188
|
+
} else {
|
|
2189
|
+
applyUpdate(reconciledUpdate);
|
|
2190
|
+
}
|
|
2191
|
+
const current = assistantMessageId
|
|
2192
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2193
|
+
: undefined;
|
|
2194
|
+
latestSnapshot =
|
|
2195
|
+
!settlingRun &&
|
|
2196
|
+
current?.role === "assistant" &&
|
|
2197
|
+
current.id === latestSnapshot?.id
|
|
2198
|
+
? current
|
|
2199
|
+
: undefined;
|
|
2200
|
+
},
|
|
1574
2201
|
onServerMessageId: (serverId) => {
|
|
1575
2202
|
adoptServerMessageId(serverId, false);
|
|
1576
2203
|
},
|
|
1577
|
-
onTextMessageStart: (serverId, openToolCallIds) =>
|
|
1578
|
-
|
|
2204
|
+
onTextMessageStart: (serverId, openToolCallIds) => {
|
|
2205
|
+
const owners = this.getTrailingAssistantTurn().filter((message) =>
|
|
2206
|
+
replayContexts.has(message.id),
|
|
2207
|
+
);
|
|
2208
|
+
const toolOwners = indexReplayToolOwners(owners);
|
|
2209
|
+
const frameOpenIds = openToolCallIds.filter((id) => {
|
|
2210
|
+
const ownerId = toolOwners.get(id);
|
|
2211
|
+
if (!ownerId || ownerId === assistantMessageId || assistantCollided)
|
|
2212
|
+
return true;
|
|
2213
|
+
const held = heldMessages.get(ownerId) ?? new Set<string>();
|
|
2214
|
+
held.add(id);
|
|
2215
|
+
heldMessages.set(ownerId, held);
|
|
2216
|
+
return false;
|
|
2217
|
+
});
|
|
2218
|
+
// The native accumulator just reset; prior frame contributions are
|
|
2219
|
+
// retained history, while removals within this frame remain authoritative.
|
|
2220
|
+
for (const ownerId of frameOwnerIds) {
|
|
2221
|
+
const replay = replayContexts.get(ownerId);
|
|
2222
|
+
if (replay) replay.merge = createReplayContentMerger();
|
|
2223
|
+
}
|
|
2224
|
+
frameOwnerIds.clear();
|
|
2225
|
+
adoptServerMessageId(serverId, true, frameOpenIds);
|
|
2226
|
+
},
|
|
1579
2227
|
});
|
|
1580
2228
|
let runFinished = false;
|
|
1581
2229
|
const dispatch = (event: AgUiEvent) => {
|
|
1582
2230
|
if (this.abortController !== abortController) return;
|
|
1583
2231
|
if (event.type === "RUN_FINISHED") {
|
|
1584
2232
|
runFinished = true;
|
|
2233
|
+
if (event.outcome?.type === "interrupt")
|
|
2234
|
+
replayInterruptMessageId = assistantMessageId ?? replayTail;
|
|
1585
2235
|
const ids =
|
|
1586
2236
|
event.outcome?.type === "success"
|
|
1587
2237
|
? event.outcome.pendingToolCallIds
|
|
1588
2238
|
: undefined;
|
|
1589
|
-
restrictedPendingToolCallIds =
|
|
2239
|
+
restrictedPendingToolCallIds =
|
|
2240
|
+
ids === undefined ? undefined : new Set(ids);
|
|
1590
2241
|
}
|
|
1591
2242
|
if (event.type === "TOOL_CALL_RESULT") {
|
|
2243
|
+
if (!assistantCollided && candidateContexts.size)
|
|
2244
|
+
accountedResultIds.add(event.toolCallId);
|
|
1592
2245
|
for (const openToolCallIds of heldMessages.values()) {
|
|
1593
2246
|
openToolCallIds.delete(event.toolCallId);
|
|
1594
2247
|
}
|
|
@@ -1597,6 +2250,8 @@ export class AgUiThreadController {
|
|
|
1597
2250
|
event.type === "RUN_FINISHED" ||
|
|
1598
2251
|
event.type === "RUN_ERROR" ||
|
|
1599
2252
|
event.type === "RUN_CANCELLED";
|
|
2253
|
+
const beforeSnapshot =
|
|
2254
|
+
event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
|
|
1600
2255
|
try {
|
|
1601
2256
|
const nextAssistantMessageId = this.handleEvent(
|
|
1602
2257
|
aggregator,
|
|
@@ -1605,9 +2260,39 @@ export class AgUiThreadController {
|
|
|
1605
2260
|
joinStrategy,
|
|
1606
2261
|
runStartMessageIds,
|
|
1607
2262
|
hasRunUserBoundary,
|
|
2263
|
+
candidateContexts.size ? reconcileReplaySnapshot : undefined,
|
|
1608
2264
|
);
|
|
1609
2265
|
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
2266
|
+
for (const message of beforeSnapshot) {
|
|
2267
|
+
const context = replayContexts.get(message.id);
|
|
2268
|
+
const current = this.session.tryGetMessage(message.id)?.message;
|
|
2269
|
+
if (context && current?.content !== message.content) {
|
|
2270
|
+
context.merge = createReplayContentMerger();
|
|
2271
|
+
frameOwnerIds.delete(message.id);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1610
2274
|
assistantMessageId = nextAssistantMessageId;
|
|
2275
|
+
const imported = assistantMessageId
|
|
2276
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2277
|
+
: undefined;
|
|
2278
|
+
const source = event.messages.find(
|
|
2279
|
+
(message) =>
|
|
2280
|
+
message?.role === "assistant" && message.id === imported?.id,
|
|
2281
|
+
);
|
|
2282
|
+
latestSnapshot =
|
|
2283
|
+
imported?.role === "assistant" && source ? imported : undefined;
|
|
2284
|
+
snapshotToolCallIds = new Set(
|
|
2285
|
+
Array.isArray(source?.toolCalls)
|
|
2286
|
+
? source.toolCalls
|
|
2287
|
+
.map((call: { id?: unknown }) => call.id)
|
|
2288
|
+
.filter((id: unknown): id is string => typeof id === "string")
|
|
2289
|
+
: [],
|
|
2290
|
+
);
|
|
2291
|
+
if (!replayEnded && assistantMessageId === undefined) {
|
|
2292
|
+
const tail = this.session.getMessages().at(-1);
|
|
2293
|
+
snapshotAssistantId =
|
|
2294
|
+
tail?.role === "assistant" ? tail.id : undefined;
|
|
2295
|
+
}
|
|
1611
2296
|
}
|
|
1612
2297
|
} finally {
|
|
1613
2298
|
settlingRun = false;
|
|
@@ -1664,7 +2349,6 @@ export class AgUiThreadController {
|
|
|
1664
2349
|
);
|
|
1665
2350
|
const subscriber = createAgUiSubscriber({
|
|
1666
2351
|
dispatch,
|
|
1667
|
-
runId,
|
|
1668
2352
|
logger: this.logger,
|
|
1669
2353
|
onRunFailed: (error) => {
|
|
1670
2354
|
if (abortSignal.aborted) return;
|
|
@@ -1675,7 +2359,7 @@ export class AgUiThreadController {
|
|
|
1675
2359
|
try {
|
|
1676
2360
|
runAgentInstance.messages = input.messages;
|
|
1677
2361
|
runAgentInstance.threadId = input.threadId;
|
|
1678
|
-
runAgentInstance.state = input.state
|
|
2362
|
+
runAgentInstance.state = input.state;
|
|
1679
2363
|
} catch {
|
|
1680
2364
|
// ignore
|
|
1681
2365
|
}
|
|
@@ -1693,6 +2377,26 @@ export class AgUiThreadController {
|
|
|
1693
2377
|
pendingError = err;
|
|
1694
2378
|
}
|
|
1695
2379
|
} finally {
|
|
2380
|
+
const current = assistantMessageId
|
|
2381
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2382
|
+
: undefined;
|
|
2383
|
+
if (
|
|
2384
|
+
this.abortController === abortController &&
|
|
2385
|
+
current?.role === "assistant" &&
|
|
2386
|
+
current.status?.type === "running"
|
|
2387
|
+
) {
|
|
2388
|
+
// Finalization is transport cleanup, not proof of a terminal event.
|
|
2389
|
+
const status: MessageStatus = {
|
|
2390
|
+
type: "incomplete",
|
|
2391
|
+
reason: "error",
|
|
2392
|
+
error: "The connection ended before the answer finished.",
|
|
2393
|
+
};
|
|
2394
|
+
settleHeldMessages(status);
|
|
2395
|
+
applyUpdate({
|
|
2396
|
+
status,
|
|
2397
|
+
content: settleNestedContent(current.content, status),
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
1696
2400
|
this.finishRun(abortController);
|
|
1697
2401
|
}
|
|
1698
2402
|
|
|
@@ -1703,13 +2407,66 @@ export class AgUiThreadController {
|
|
|
1703
2407
|
throw err;
|
|
1704
2408
|
}
|
|
1705
2409
|
|
|
2410
|
+
const pending = replayInterruptMessageId
|
|
2411
|
+
? this.getPendingInterrupts()
|
|
2412
|
+
: null;
|
|
2413
|
+
if (
|
|
2414
|
+
pending &&
|
|
2415
|
+
pending.messageId === replayInterruptMessageId &&
|
|
2416
|
+
replayContexts.has(pending.messageId) &&
|
|
2417
|
+
!abortSignal.aborted &&
|
|
2418
|
+
this.abortController === null &&
|
|
2419
|
+
generation === this.state.historyMutationGeneration &&
|
|
2420
|
+
!this.isThreadSwitching?.()
|
|
2421
|
+
) {
|
|
2422
|
+
const message = this.session.tryGetMessage(pending.messageId)?.message;
|
|
2423
|
+
const responses =
|
|
2424
|
+
message?.role === "assistant"
|
|
2425
|
+
? buildToolApprovalResume(message.content, pending.interrupts)
|
|
2426
|
+
: null;
|
|
2427
|
+
if (responses) {
|
|
2428
|
+
this.clearDeferredContinuations(abortController);
|
|
2429
|
+
try {
|
|
2430
|
+
await this.submitInterruptResponses(responses);
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2433
|
+
if (!this.reportedErrors.has(err)) {
|
|
2434
|
+
this.reportError(err);
|
|
2435
|
+
this.reportedErrors.add(err);
|
|
2436
|
+
}
|
|
2437
|
+
throw err;
|
|
2438
|
+
}
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
1706
2443
|
// A tool result that landed before the run settled deferred its
|
|
1707
2444
|
// continuation here so a second run never overlaps the first.
|
|
1708
2445
|
if (this.state.pendingResume?.owner === abortController) {
|
|
1709
|
-
const {
|
|
2446
|
+
const { messageIds } = this.state.pendingResume;
|
|
1710
2447
|
this.dispatch({ type: "resumeCleared" });
|
|
1711
|
-
if (
|
|
1712
|
-
|
|
2448
|
+
if (
|
|
2449
|
+
!abortSignal.aborted &&
|
|
2450
|
+
!this.getPendingInterrupts() &&
|
|
2451
|
+
!this.getPendingToolCalls()
|
|
2452
|
+
) {
|
|
2453
|
+
const turn = this.getTrailingAssistantTurn();
|
|
2454
|
+
if (
|
|
2455
|
+
turn.some((message) => {
|
|
2456
|
+
if (!messageIds.includes(message.id)) return false;
|
|
2457
|
+
const replay = replayContexts.get(message.id);
|
|
2458
|
+
if (!replay) return hasResolvedToolCalls(message);
|
|
2459
|
+
return Array.from(iterateToolCallParts(message.content)).some(
|
|
2460
|
+
(part) =>
|
|
2461
|
+
isResolvedToolCall(part) &&
|
|
2462
|
+
replay.admittedToolCallIds.has(part.toolCallId) &&
|
|
2463
|
+
!accountedResultIds.has(part.toolCallId),
|
|
2464
|
+
);
|
|
2465
|
+
})
|
|
2466
|
+
) {
|
|
2467
|
+
const headId = turn.at(-1)?.id;
|
|
2468
|
+
if (headId !== undefined) this.startResumeRun(headId);
|
|
2469
|
+
}
|
|
1713
2470
|
}
|
|
1714
2471
|
}
|
|
1715
2472
|
|
|
@@ -1837,7 +2594,8 @@ export class AgUiThreadController {
|
|
|
1837
2594
|
const input = {
|
|
1838
2595
|
threadId,
|
|
1839
2596
|
runId,
|
|
1840
|
-
state:
|
|
2597
|
+
state:
|
|
2598
|
+
this.state.stateSnapshot === undefined ? {} : this.state.stateSnapshot,
|
|
1841
2599
|
messages,
|
|
1842
2600
|
tools: toAgUiTools(context?.tools),
|
|
1843
2601
|
context: [
|
|
@@ -1994,6 +2752,8 @@ export class AgUiThreadController {
|
|
|
1994
2752
|
private updateAssistantMessage(
|
|
1995
2753
|
messageId: string,
|
|
1996
2754
|
update: ChatModelRunResult,
|
|
2755
|
+
replay?: ReplayContext,
|
|
2756
|
+
pendingToolCallIds?: ReadonlySet<string>,
|
|
1997
2757
|
): string {
|
|
1998
2758
|
let latestStatus: MessageStatus | undefined;
|
|
1999
2759
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
@@ -2003,18 +2763,102 @@ export class AgUiThreadController {
|
|
|
2003
2763
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
2004
2764
|
: assistant.metadata;
|
|
2005
2765
|
latestStatus = update.status ?? assistant.status;
|
|
2766
|
+
let content =
|
|
2767
|
+
update.content !== undefined
|
|
2768
|
+
? replay
|
|
2769
|
+
? replay.merge(
|
|
2770
|
+
assistant.content,
|
|
2771
|
+
update.content as ThreadAssistantMessage["content"],
|
|
2772
|
+
)
|
|
2773
|
+
: this.preserveToolResults(
|
|
2774
|
+
assistant.content,
|
|
2775
|
+
update.content as ThreadAssistantMessage["content"],
|
|
2776
|
+
)
|
|
2777
|
+
: assistant.content;
|
|
2778
|
+
const calls = replay ? Array.from(iterateToolCallParts(content)) : [];
|
|
2779
|
+
// A retained historical call needs existing or current stream authority.
|
|
2780
|
+
const pending =
|
|
2781
|
+
pendingToolCallIds ??
|
|
2782
|
+
(replay
|
|
2783
|
+
? new Set([
|
|
2784
|
+
...replay.admittedToolCallIds,
|
|
2785
|
+
...Array.from(iterateToolCallParts(update.content ?? []))
|
|
2786
|
+
.filter((part) => !isResolvedToolCall(part))
|
|
2787
|
+
.map((part) => part.toolCallId),
|
|
2788
|
+
])
|
|
2789
|
+
: undefined);
|
|
2790
|
+
if (replay && latestStatus?.type !== "running" && pending)
|
|
2791
|
+
replay.admittedToolCallIds = pending;
|
|
2792
|
+
if (replay) {
|
|
2793
|
+
// Project gates against the content the user can actually answer.
|
|
2794
|
+
const interrupts =
|
|
2795
|
+
latestStatus?.type === "requires-action" &&
|
|
2796
|
+
latestStatus.reason === "interrupt"
|
|
2797
|
+
? (
|
|
2798
|
+
mergedMetadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
2799
|
+
| AgUiCustomMetadata
|
|
2800
|
+
| undefined
|
|
2801
|
+
)?.interrupts
|
|
2802
|
+
: undefined;
|
|
2803
|
+
const approvals = projectAgUiToolApprovals(
|
|
2804
|
+
interrupts,
|
|
2805
|
+
new Set(calls.map((part) => part.toolCallId)),
|
|
2806
|
+
);
|
|
2807
|
+
const originalCalls = new Map(
|
|
2808
|
+
Array.from(iterateToolCallParts(replay.originalContent), (part) => [
|
|
2809
|
+
part.toolCallId,
|
|
2810
|
+
part,
|
|
2811
|
+
]),
|
|
2812
|
+
);
|
|
2813
|
+
content = mapToolCallPartsDeep(content, (part) => {
|
|
2814
|
+
const approval = approvals.get(part.toolCallId);
|
|
2815
|
+
if (approval)
|
|
2816
|
+
return {
|
|
2817
|
+
...part,
|
|
2818
|
+
approval:
|
|
2819
|
+
preserveToolApprovalDecision(
|
|
2820
|
+
originalCalls.get(part.toolCallId),
|
|
2821
|
+
{ ...part, approval },
|
|
2822
|
+
) ?? approval,
|
|
2823
|
+
};
|
|
2824
|
+
if (
|
|
2825
|
+
!part.approval ||
|
|
2826
|
+
part.approval.approved !== undefined ||
|
|
2827
|
+
part.approval.resolution
|
|
2828
|
+
)
|
|
2829
|
+
return part;
|
|
2830
|
+
const { approval: _approval, ...withoutApproval } = part;
|
|
2831
|
+
return withoutApproval;
|
|
2832
|
+
}).content;
|
|
2833
|
+
}
|
|
2834
|
+
if (replay && latestStatus?.type === "complete") {
|
|
2835
|
+
const listed =
|
|
2836
|
+
pending ??
|
|
2837
|
+
listedPendingToolCallIds({
|
|
2838
|
+
...assistant,
|
|
2839
|
+
metadata: mergedMetadata,
|
|
2840
|
+
});
|
|
2841
|
+
if (
|
|
2842
|
+
calls.some(
|
|
2843
|
+
(part) =>
|
|
2844
|
+
(!isResolvedToolCall(part) ||
|
|
2845
|
+
!replay.accountedResultIds.has(part.toolCallId)) &&
|
|
2846
|
+
(!listed || listed.has(part.toolCallId)),
|
|
2847
|
+
)
|
|
2848
|
+
)
|
|
2849
|
+
latestStatus = { type: "requires-action", reason: "tool-calls" };
|
|
2850
|
+
}
|
|
2851
|
+
const admission =
|
|
2852
|
+
pending && replay
|
|
2853
|
+
? withPendingToolCallIds(mergedMetadata, pending)
|
|
2854
|
+
: mergedMetadata;
|
|
2006
2855
|
const metadata =
|
|
2007
2856
|
latestStatus?.type === "requires-action" &&
|
|
2008
2857
|
latestStatus.reason === "tool-calls"
|
|
2009
|
-
?
|
|
2010
|
-
:
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
? this.preserveToolResults(
|
|
2014
|
-
assistant.content,
|
|
2015
|
-
update.content as ThreadAssistantMessage["content"],
|
|
2016
|
-
)
|
|
2017
|
-
: assistant.content;
|
|
2858
|
+
? admission
|
|
2859
|
+
: replay && latestStatus?.type === "running"
|
|
2860
|
+
? mergedMetadata
|
|
2861
|
+
: withoutPendingToolCallIds(admission);
|
|
2018
2862
|
return {
|
|
2019
2863
|
...assistant,
|
|
2020
2864
|
content,
|
|
@@ -2037,7 +2881,10 @@ export class AgUiThreadController {
|
|
|
2037
2881
|
if (this.isPersistableStatus(latestStatus)) {
|
|
2038
2882
|
this.persistAssistantHistory(resolvedMessageId);
|
|
2039
2883
|
}
|
|
2040
|
-
this.maybeResumeAfterToolResults(
|
|
2884
|
+
this.maybeResumeAfterToolResults(
|
|
2885
|
+
resolvedMessageId,
|
|
2886
|
+
replay?.accountedResultIds,
|
|
2887
|
+
);
|
|
2041
2888
|
return resolvedMessageId;
|
|
2042
2889
|
}
|
|
2043
2890
|
|
|
@@ -2048,6 +2895,7 @@ export class AgUiThreadController {
|
|
|
2048
2895
|
private preserveToolResults(
|
|
2049
2896
|
previous: ThreadAssistantMessage["content"],
|
|
2050
2897
|
next: ThreadAssistantMessage["content"],
|
|
2898
|
+
decisionSource?: ThreadAssistantMessage["content"],
|
|
2051
2899
|
): ThreadAssistantMessage["content"] {
|
|
2052
2900
|
const preserved = new Map<string, ToolCallMessagePart>();
|
|
2053
2901
|
for (const part of iterateToolCallParts(previous)) {
|
|
@@ -2055,25 +2903,27 @@ export class AgUiThreadController {
|
|
|
2055
2903
|
preserved.set(part.toolCallId, part);
|
|
2056
2904
|
}
|
|
2057
2905
|
}
|
|
2058
|
-
|
|
2906
|
+
const decisions = new Map(
|
|
2907
|
+
Array.from(iterateToolCallParts(decisionSource ?? []))
|
|
2908
|
+
.filter((part) => part.approval?.approved !== undefined)
|
|
2909
|
+
.map((part) => [part.toolCallId, part]),
|
|
2910
|
+
);
|
|
2911
|
+
if (preserved.size === 0 && decisions.size === 0) {
|
|
2059
2912
|
return this.preserveToolInteractions(previous, next);
|
|
2060
2913
|
}
|
|
2061
2914
|
|
|
2062
2915
|
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
2063
2916
|
const prior = preserved.get(part.toolCallId);
|
|
2064
|
-
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
result
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
...
|
|
2072
|
-
? { modelContent: prior.modelContent }
|
|
2073
|
-
: {}),
|
|
2074
|
-
};
|
|
2917
|
+
const result = prior ? preserveToolResult(prior, part) : part;
|
|
2918
|
+
if (result.approval && decisions.has(part.toolCallId)) {
|
|
2919
|
+
const approval = preserveToolApprovalDecision(
|
|
2920
|
+
decisions.get(part.toolCallId),
|
|
2921
|
+
result,
|
|
2922
|
+
);
|
|
2923
|
+
if (approval && approval !== result.approval)
|
|
2924
|
+
return { ...result, approval };
|
|
2075
2925
|
}
|
|
2076
|
-
return
|
|
2926
|
+
return result;
|
|
2077
2927
|
});
|
|
2078
2928
|
return this.preserveToolInteractions(previous, changed ? merged : next);
|
|
2079
2929
|
}
|
|
@@ -2103,7 +2953,7 @@ export class AgUiThreadController {
|
|
|
2103
2953
|
}
|
|
2104
2954
|
|
|
2105
2955
|
// A snapshot built from user, assistant, and tool records alone carries no
|
|
2106
|
-
// reasoning or
|
|
2956
|
+
// reasoning, activity, or custom data of its own, so locally streamed parts would be
|
|
2107
2957
|
// lost on every refresh; keep the local ones where they streamed when the
|
|
2108
2958
|
// snapshot has none of that kind, matching the merge rule of @ag-ui/client.
|
|
2109
2959
|
private mergeSnapshotAssistantContent(
|
|
@@ -2111,15 +2961,30 @@ export class AgUiThreadController {
|
|
|
2111
2961
|
next: ThreadAssistantMessage["content"],
|
|
2112
2962
|
snapshotHasReasoning: boolean,
|
|
2113
2963
|
snapshotHasActivity: boolean,
|
|
2964
|
+
keepMissingToolCalls: ReadonlySet<string> = new Set(),
|
|
2965
|
+
keepPriorTranscripts = false,
|
|
2114
2966
|
): ThreadAssistantMessage["content"] {
|
|
2115
2967
|
const shouldKeep = (part: ThreadAssistantMessage["content"][number]) =>
|
|
2116
|
-
part.type === "
|
|
2117
|
-
?
|
|
2118
|
-
|
|
2968
|
+
part.type === "tool-call"
|
|
2969
|
+
? keepMissingToolCalls.has(part.toolCallId) &&
|
|
2970
|
+
!next.some(
|
|
2971
|
+
(incoming) =>
|
|
2972
|
+
incoming.type === "tool-call" &&
|
|
2973
|
+
incoming.toolCallId === part.toolCallId,
|
|
2974
|
+
)
|
|
2975
|
+
: part.type === "reasoning"
|
|
2976
|
+
? this.showThinking && !snapshotHasReasoning
|
|
2977
|
+
: part.type === "data" && !isActivityPart(part)
|
|
2978
|
+
? !next.some(
|
|
2979
|
+
(incoming) =>
|
|
2980
|
+
incoming.type === "data" && incoming.name === part.name,
|
|
2981
|
+
)
|
|
2982
|
+
: isActivityPart(part) && !snapshotHasActivity;
|
|
2119
2983
|
const kept = previous.filter(shouldKeep);
|
|
2120
2984
|
const merged = this.reconcileSnapshotNestedContent(
|
|
2121
2985
|
previous,
|
|
2122
2986
|
this.preserveToolInteractions(previous, next),
|
|
2987
|
+
keepPriorTranscripts,
|
|
2123
2988
|
);
|
|
2124
2989
|
if (kept.length === 0) return merged;
|
|
2125
2990
|
|
|
@@ -2169,6 +3034,7 @@ export class AgUiThreadController {
|
|
|
2169
3034
|
private reconcileSnapshotNestedContent(
|
|
2170
3035
|
previous: ThreadAssistantMessage["content"],
|
|
2171
3036
|
next: ThreadAssistantMessage["content"],
|
|
3037
|
+
keepPriorTranscripts: boolean,
|
|
2172
3038
|
): ThreadAssistantMessage["content"] {
|
|
2173
3039
|
const nestedIds = new Set<string>();
|
|
2174
3040
|
for (const part of previous) {
|
|
@@ -2218,10 +3084,92 @@ export class AgUiThreadController {
|
|
|
2218
3084
|
}
|
|
2219
3085
|
if (part.type !== "tool-call") return [part];
|
|
2220
3086
|
const prior = previousCalls.get(part.toolCallId);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
3087
|
+
if (prior?.messages === undefined) return [part];
|
|
3088
|
+
if (part.messages === undefined)
|
|
3089
|
+
return [{ ...part, messages: prior.messages }];
|
|
3090
|
+
if (part.messages.length === 0) return [part];
|
|
3091
|
+
return [
|
|
3092
|
+
{
|
|
3093
|
+
...part,
|
|
3094
|
+
messages: this.mergeNestedTranscripts(
|
|
3095
|
+
prior.messages,
|
|
3096
|
+
part.messages,
|
|
3097
|
+
keepPriorTranscripts,
|
|
3098
|
+
),
|
|
3099
|
+
},
|
|
3100
|
+
];
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
/**
|
|
3105
|
+
* Merges one tool call's nested transcripts message by message. The incoming
|
|
3106
|
+
* copy is the base for tool calls and reasoning; resolved results and calls
|
|
3107
|
+
* only the prior copy has carry over, and the prior text wins when it extends
|
|
3108
|
+
* the incoming text. Importing a snapshot keeps prior messages it lacks, a
|
|
3109
|
+
* prior status that is running or incomplete, and the prior status when the
|
|
3110
|
+
* snapshot waits on calls the merged content has already resolved.
|
|
3111
|
+
*/
|
|
3112
|
+
private mergeNestedTranscripts(
|
|
3113
|
+
prior: readonly ThreadMessage[],
|
|
3114
|
+
next: readonly ThreadMessage[],
|
|
3115
|
+
importingSnapshot: boolean,
|
|
3116
|
+
): readonly ThreadMessage[] {
|
|
3117
|
+
const priorById = new Map(prior.map((message) => [message.id, message]));
|
|
3118
|
+
const merged = next.map((message) => {
|
|
3119
|
+
const previous = priorById.get(message.id);
|
|
3120
|
+
if (previous?.role !== "assistant" || message.role !== "assistant")
|
|
3121
|
+
return message;
|
|
3122
|
+
let content = this.mergeSnapshotAssistantContent(
|
|
3123
|
+
previous.content,
|
|
3124
|
+
this.preserveToolResults(previous.content, message.content),
|
|
3125
|
+
message.content.some((part) => part.type === "reasoning"),
|
|
3126
|
+
message.content.some(isActivityPart),
|
|
3127
|
+
new Set(
|
|
3128
|
+
Array.from(
|
|
3129
|
+
iterateToolCallParts(previous.content),
|
|
3130
|
+
(call) => call.toolCallId,
|
|
3131
|
+
),
|
|
3132
|
+
),
|
|
3133
|
+
importingSnapshot,
|
|
3134
|
+
);
|
|
3135
|
+
const priorText = textOf(previous.content);
|
|
3136
|
+
const incomingText = textOf(message.content);
|
|
3137
|
+
if (
|
|
3138
|
+
priorText.length > incomingText.length &&
|
|
3139
|
+
priorText.startsWith(incomingText)
|
|
3140
|
+
) {
|
|
3141
|
+
content = replaceTextParts(content, previous.content);
|
|
3142
|
+
}
|
|
3143
|
+
const status = !importingSnapshot
|
|
3144
|
+
? message.status
|
|
3145
|
+
: previous.status.type === "running" ||
|
|
3146
|
+
previous.status.type === "incomplete" ||
|
|
3147
|
+
(message.status.type === "requires-action" &&
|
|
3148
|
+
message.status.reason === "tool-calls" &&
|
|
3149
|
+
!Array.from(iterateToolCallParts(content)).some(
|
|
3150
|
+
(call) => !isResolvedToolCall(call),
|
|
3151
|
+
))
|
|
3152
|
+
? previous.status
|
|
3153
|
+
: message.status;
|
|
3154
|
+
return {
|
|
3155
|
+
...message,
|
|
3156
|
+
status,
|
|
3157
|
+
content,
|
|
3158
|
+
metadata: {
|
|
3159
|
+
...message.metadata,
|
|
3160
|
+
custom: mergeAgUiNamespace(
|
|
3161
|
+
previous.metadata.custom,
|
|
3162
|
+
message.metadata.custom,
|
|
3163
|
+
),
|
|
3164
|
+
},
|
|
3165
|
+
};
|
|
2224
3166
|
});
|
|
3167
|
+
if (!importingSnapshot) return merged;
|
|
3168
|
+
const mergedById = new Map(merged.map((message) => [message.id, message]));
|
|
3169
|
+
return [
|
|
3170
|
+
...prior.map((message) => mergedById.get(message.id) ?? message),
|
|
3171
|
+
...merged.filter((message) => !priorById.has(message.id)),
|
|
3172
|
+
];
|
|
2225
3173
|
}
|
|
2226
3174
|
|
|
2227
3175
|
private mergeAssistantMetadata(
|
|
@@ -2260,7 +3208,17 @@ export class AgUiThreadController {
|
|
|
2260
3208
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2261
3209
|
runStartMessageIds: ReadonlySet<string>,
|
|
2262
3210
|
hasRunUserBoundary: boolean,
|
|
3211
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2263
3212
|
): string | undefined {
|
|
3213
|
+
if (
|
|
3214
|
+
event.type === "SUBAGENT_STARTED" &&
|
|
3215
|
+
!this.subagentParentToolCallIds.has(event.subagentRunId)
|
|
3216
|
+
) {
|
|
3217
|
+
this.subagentParentToolCallIds.set(
|
|
3218
|
+
event.subagentRunId,
|
|
3219
|
+
event.parentToolCallId,
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
2264
3222
|
switch (event.type) {
|
|
2265
3223
|
case "STATE_SNAPSHOT": {
|
|
2266
3224
|
this.dispatch({
|
|
@@ -2290,6 +3248,7 @@ export class AgUiThreadController {
|
|
|
2290
3248
|
break;
|
|
2291
3249
|
}
|
|
2292
3250
|
case "MESSAGES_SNAPSHOT": {
|
|
3251
|
+
aggregator.handle(event);
|
|
2293
3252
|
const previousHeadId = this.session.headId;
|
|
2294
3253
|
const activeAssistant =
|
|
2295
3254
|
activeAssistantId === undefined
|
|
@@ -2304,15 +3263,49 @@ export class AgUiThreadController {
|
|
|
2304
3263
|
joinStrategy,
|
|
2305
3264
|
runStartMessageIds,
|
|
2306
3265
|
hasRunUserBoundary,
|
|
3266
|
+
reconcileSnapshot,
|
|
2307
3267
|
);
|
|
2308
3268
|
const headId = this.session.headId;
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
3269
|
+
const head =
|
|
3270
|
+
headId === null
|
|
3271
|
+
? undefined
|
|
3272
|
+
: this.session.tryGetMessage(headId)?.message;
|
|
3273
|
+
const hasNewPendingToolCallAnswer =
|
|
3274
|
+
head?.role === "assistant" &&
|
|
3275
|
+
!runStartMessageIds.has(head.id) &&
|
|
3276
|
+
head.status?.type === "requires-action" &&
|
|
3277
|
+
head.status.reason === "tool-calls";
|
|
3278
|
+
if (
|
|
3279
|
+
headId !== previousHeadId &&
|
|
3280
|
+
(hasActiveText || hasNewPendingToolCallAnswer)
|
|
3281
|
+
) {
|
|
3282
|
+
const nextActiveAssistantId =
|
|
2315
3283
|
head?.role === "assistant" ? (headId ?? undefined) : undefined;
|
|
3284
|
+
if (
|
|
3285
|
+
activeAssistantId !== nextActiveAssistantId &&
|
|
3286
|
+
activeAssistantId
|
|
3287
|
+
) {
|
|
3288
|
+
const previous =
|
|
3289
|
+
this.session.tryGetMessage(activeAssistantId)?.message;
|
|
3290
|
+
if (
|
|
3291
|
+
previous?.role === "assistant" &&
|
|
3292
|
+
previous.status?.type === "running"
|
|
3293
|
+
) {
|
|
3294
|
+
this.updateAssistantMessage(activeAssistantId, {
|
|
3295
|
+
status: FALLBACK_USER_STATUS,
|
|
3296
|
+
content: settleNestedContent(
|
|
3297
|
+
previous.content,
|
|
3298
|
+
FALLBACK_USER_STATUS,
|
|
3299
|
+
),
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
activeAssistantId = nextActiveAssistantId;
|
|
3304
|
+
if (hasNewPendingToolCallAnswer && head) {
|
|
3305
|
+
this.updateAssistantMessage(head.id, {
|
|
3306
|
+
status: { type: "running" },
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
2316
3309
|
}
|
|
2317
3310
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2318
3311
|
break;
|
|
@@ -2415,6 +3408,14 @@ export class AgUiThreadController {
|
|
|
2415
3408
|
...(event.messageId
|
|
2416
3409
|
? { unstable_toolMessageId: event.messageId }
|
|
2417
3410
|
: {}),
|
|
3411
|
+
...(event.metadata !== undefined
|
|
3412
|
+
? {
|
|
3413
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3414
|
+
part.providerMetadata,
|
|
3415
|
+
event.metadata,
|
|
3416
|
+
),
|
|
3417
|
+
}
|
|
3418
|
+
: {}),
|
|
2418
3419
|
};
|
|
2419
3420
|
}
|
|
2420
3421
|
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
@@ -2441,6 +3442,14 @@ export class AgUiThreadController {
|
|
|
2441
3442
|
...(event.messageId
|
|
2442
3443
|
? { unstable_toolMessageId: event.messageId }
|
|
2443
3444
|
: {}),
|
|
3445
|
+
...(event.metadata !== undefined
|
|
3446
|
+
? {
|
|
3447
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3448
|
+
part.providerMetadata,
|
|
3449
|
+
event.metadata,
|
|
3450
|
+
),
|
|
3451
|
+
}
|
|
3452
|
+
: {}),
|
|
2444
3453
|
};
|
|
2445
3454
|
});
|
|
2446
3455
|
if (!matchedToolCall) return message;
|
|
@@ -2451,7 +3460,8 @@ export class AgUiThreadController {
|
|
|
2451
3460
|
this.publishUpdate();
|
|
2452
3461
|
// Not maybeResumeAfterToolResults: the delivering run is already in
|
|
2453
3462
|
// flight, and a resume from the owner would reset the head past it.
|
|
2454
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3463
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3464
|
+
this.persistSettledAssistant(messageId);
|
|
2455
3465
|
}
|
|
2456
3466
|
|
|
2457
3467
|
private applyCrossRunActivitySnapshot(
|
|
@@ -2512,7 +3522,8 @@ export class AgUiThreadController {
|
|
|
2512
3522
|
|
|
2513
3523
|
if (!updated) return;
|
|
2514
3524
|
this.publishUpdate();
|
|
2515
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3525
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3526
|
+
this.persistSettledAssistant(messageId);
|
|
2516
3527
|
}
|
|
2517
3528
|
|
|
2518
3529
|
private importMessagesSnapshot(
|
|
@@ -2521,6 +3532,7 @@ export class AgUiThreadController {
|
|
|
2521
3532
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2522
3533
|
runStartMessageIds: ReadonlySet<string>,
|
|
2523
3534
|
hasRunUserBoundary: boolean,
|
|
3535
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2524
3536
|
) {
|
|
2525
3537
|
try {
|
|
2526
3538
|
const activeAssistantItem = activeAssistantId
|
|
@@ -2530,14 +3542,24 @@ export class AgUiThreadController {
|
|
|
2530
3542
|
const activeAssistantParentId = activeAssistantItem?.parentId;
|
|
2531
3543
|
const activeAssistant =
|
|
2532
3544
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
2533
|
-
const normalized =
|
|
2534
|
-
|
|
2535
|
-
|
|
3545
|
+
const normalized = fromAgUiSnapshotMessages(
|
|
3546
|
+
rawMessages,
|
|
3547
|
+
{ showThinking: this.showThinking },
|
|
3548
|
+
this.subagentParentToolCallIds,
|
|
3549
|
+
);
|
|
2536
3550
|
const converted: ThreadMessage[] = [];
|
|
2537
3551
|
for (const message of normalized) {
|
|
2538
3552
|
try {
|
|
3553
|
+
const previous = message.id
|
|
3554
|
+
? this.session.tryGetMessage(message.id)?.message
|
|
3555
|
+
: undefined;
|
|
3556
|
+
const fallbackStatus =
|
|
3557
|
+
previous?.role === "assistant" &&
|
|
3558
|
+
previous.status?.type === "running"
|
|
3559
|
+
? previous.status
|
|
3560
|
+
: FALLBACK_USER_STATUS;
|
|
2539
3561
|
converted.push(
|
|
2540
|
-
fromThreadMessageLike(message, generateId(),
|
|
3562
|
+
fromThreadMessageLike(message, generateId(), fallbackStatus),
|
|
2541
3563
|
);
|
|
2542
3564
|
} catch (error) {
|
|
2543
3565
|
this.logger.error?.(
|
|
@@ -2560,19 +3582,30 @@ export class AgUiThreadController {
|
|
|
2560
3582
|
const existing = this.session.tryGetMessage(
|
|
2561
3583
|
convertedMessage.id,
|
|
2562
3584
|
)?.message;
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3585
|
+
if (convertedMessage.role === "assistant") {
|
|
3586
|
+
const previous =
|
|
3587
|
+
existing?.role === "assistant" ? existing : undefined;
|
|
3588
|
+
const snapshot =
|
|
3589
|
+
reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
|
|
3590
|
+
const preservesRunningStatus =
|
|
3591
|
+
convertedMessage.id === activeAssistantId &&
|
|
3592
|
+
previous?.status?.type === "running" &&
|
|
3593
|
+
snapshot.status?.type === "requires-action" &&
|
|
3594
|
+
snapshot.status.reason === "tool-calls";
|
|
3595
|
+
converted[index] = previous
|
|
3596
|
+
? {
|
|
3597
|
+
...snapshot,
|
|
3598
|
+
...(preservesRunningStatus ? { status: previous.status } : {}),
|
|
3599
|
+
content: this.mergeSnapshotAssistantContent(
|
|
3600
|
+
previous.content,
|
|
3601
|
+
snapshot.content,
|
|
3602
|
+
snapshotHasReasoning,
|
|
3603
|
+
snapshotHasActivity,
|
|
3604
|
+
new Set(),
|
|
3605
|
+
true,
|
|
3606
|
+
),
|
|
3607
|
+
}
|
|
3608
|
+
: snapshot;
|
|
2576
3609
|
}
|
|
2577
3610
|
}
|
|
2578
3611
|
if (joinStrategy === "concat-content") {
|
|
@@ -2732,6 +3765,19 @@ export class AgUiThreadController {
|
|
|
2732
3765
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
2733
3766
|
}
|
|
2734
3767
|
|
|
3768
|
+
private persistSettledAssistant(messageId: string): void {
|
|
3769
|
+
const item = this.session.tryGetMessage(messageId);
|
|
3770
|
+
if (!this.history?.update || !item) return;
|
|
3771
|
+
if (!this.isTerminalStatus(item.message.status)) return;
|
|
3772
|
+
if (
|
|
3773
|
+
!this.persistedHistoryIds.has(messageId) &&
|
|
3774
|
+
this.historyWrites.pending(messageId) === undefined
|
|
3775
|
+
)
|
|
3776
|
+
return;
|
|
3777
|
+
this.markPendingAssistantHistory(messageId, item.parentId);
|
|
3778
|
+
this.persistAssistantHistory(messageId);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
2735
3781
|
private persistAssistantHistory(messageId: string) {
|
|
2736
3782
|
const history = this.history;
|
|
2737
3783
|
if (!history) return;
|
|
@@ -2744,7 +3790,9 @@ export class AgUiThreadController {
|
|
|
2744
3790
|
const update = history.update;
|
|
2745
3791
|
const shouldUpdate =
|
|
2746
3792
|
update !== undefined &&
|
|
2747
|
-
(wasPersisted ||
|
|
3793
|
+
(wasPersisted ||
|
|
3794
|
+
this.snapshotHistoryIds.has(messageId) ||
|
|
3795
|
+
this.historyWrites.pending(messageId) !== undefined);
|
|
2748
3796
|
|
|
2749
3797
|
if (shouldUpdate) {
|
|
2750
3798
|
const write = this.historyWrites.chain(messageId, () =>
|