@assistant-ui/react-ag-ui 0.1.0 → 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/dist/agUiExtras.js +1 -2
- package/dist/hooks.js +1 -2
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +354 -68
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +10 -3
- 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.js +1 -2
- package/dist/runtime/adapter/subscriber.js +1 -2
- 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.js +1 -2
- 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.js +1 -2
- package/dist/useAgUiRuntime.js +1 -2
- package/package.json +5 -5
- package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
- package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
- package/src/runtime/AgUiThreadController.test.ts +79 -0
- package/src/runtime/AgUiThreadController.ts +725 -109
- package/src/runtime/adapter/conversions.content.test.ts +113 -1
- package/src/runtime/adapter/conversions.ts +18 -8
- package/src/runtime/adapter/replay-content.test.ts +235 -0
- package/src/runtime/adapter/replay-content.ts +306 -0
- package/src/runtime/adapter/tool-approval.ts +41 -3
- package/src/runtime/agUiThreadState.test.ts +21 -2
- package/src/runtime/agUiThreadState.ts +12 -3
|
@@ -67,9 +67,16 @@ import {
|
|
|
67
67
|
toAgUiTools,
|
|
68
68
|
} from "./adapter/conversions";
|
|
69
69
|
import { createAgUiSubscriber } from "./adapter/subscriber";
|
|
70
|
+
import {
|
|
71
|
+
createReplayContentMerger,
|
|
72
|
+
groupReplayToolContent,
|
|
73
|
+
indexReplayToolOwners,
|
|
74
|
+
preserveToolResult,
|
|
75
|
+
} from "./adapter/replay-content";
|
|
70
76
|
import {
|
|
71
77
|
buildToolApprovalResume,
|
|
72
78
|
projectAgUiToolApprovals,
|
|
79
|
+
preserveToolApprovalDecision,
|
|
73
80
|
withSettledToolApprovals,
|
|
74
81
|
withToolApprovalDecision,
|
|
75
82
|
} from "./adapter/tool-approval";
|
|
@@ -92,17 +99,38 @@ const isResolvedToolCall = (
|
|
|
92
99
|
): boolean =>
|
|
93
100
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
94
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
|
+
|
|
95
110
|
const listedPendingToolCallIds = (
|
|
96
|
-
assistant: ThreadAssistantMessage,
|
|
111
|
+
assistant: Pick<ThreadAssistantMessage, "metadata">,
|
|
97
112
|
): ReadonlySet<string> | undefined => {
|
|
98
113
|
const ids = (
|
|
99
114
|
assistant.metadata.custom[AG_UI_METADATA_NAMESPACE] as
|
|
100
115
|
| AgUiCustomMetadata
|
|
101
116
|
| undefined
|
|
102
117
|
)?.pendingToolCallIds;
|
|
103
|
-
return ids
|
|
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>;
|
|
104
127
|
};
|
|
105
128
|
|
|
129
|
+
type SnapshotMessageReconciler = (
|
|
130
|
+
snapshot: ThreadAssistantMessage,
|
|
131
|
+
previous: ThreadAssistantMessage | undefined,
|
|
132
|
+
) => ThreadAssistantMessage;
|
|
133
|
+
|
|
106
134
|
const withoutPendingToolCallIds = (
|
|
107
135
|
metadata: ThreadAssistantMessage["metadata"],
|
|
108
136
|
): ThreadAssistantMessage["metadata"] => {
|
|
@@ -125,6 +153,43 @@ const withoutPendingToolCallIds = (
|
|
|
125
153
|
};
|
|
126
154
|
};
|
|
127
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
|
+
|
|
128
193
|
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
129
194
|
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
130
195
|
|
|
@@ -560,6 +625,7 @@ export class AgUiThreadController {
|
|
|
560
625
|
config.runConfig ?? this.state.lastRunConfig,
|
|
561
626
|
undefined,
|
|
562
627
|
config.stream,
|
|
628
|
+
true,
|
|
563
629
|
);
|
|
564
630
|
}
|
|
565
631
|
|
|
@@ -599,17 +665,13 @@ export class AgUiThreadController {
|
|
|
599
665
|
private findRequiresActionAssistant(
|
|
600
666
|
reason: "interrupt" | "tool-calls",
|
|
601
667
|
): ThreadAssistantMessage | null {
|
|
602
|
-
|
|
603
|
-
(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
return assistant;
|
|
668
|
+
return (
|
|
669
|
+
this.getTrailingAssistantTurn().findLast(
|
|
670
|
+
(message) =>
|
|
671
|
+
message.status?.type === "requires-action" &&
|
|
672
|
+
message.status.reason === reason,
|
|
673
|
+
) ?? null
|
|
674
|
+
);
|
|
613
675
|
}
|
|
614
676
|
|
|
615
677
|
private getTrailingAssistantTurn(): ThreadAssistantMessage[] {
|
|
@@ -715,8 +777,9 @@ export class AgUiThreadController {
|
|
|
715
777
|
messageId: string,
|
|
716
778
|
resume: AgUiResumeEntry[],
|
|
717
779
|
): Promise<void> {
|
|
780
|
+
const parentId = this.session.headId ?? messageId;
|
|
718
781
|
this.clearPendingInterrupts(messageId, resume);
|
|
719
|
-
await this.startRun(
|
|
782
|
+
await this.startRun(parentId, this.state.lastRunConfig, {
|
|
720
783
|
entries: resume,
|
|
721
784
|
interruptedMessageId: messageId,
|
|
722
785
|
});
|
|
@@ -1180,15 +1243,26 @@ export class AgUiThreadController {
|
|
|
1180
1243
|
// The continuation fires whether the frontend result lands before
|
|
1181
1244
|
// RUN_FINISHED (the status flips to requires-action only later, while the
|
|
1182
1245
|
// run is still draining) or after it.
|
|
1183
|
-
private maybeResumeAfterToolResults(
|
|
1246
|
+
private maybeResumeAfterToolResults(
|
|
1247
|
+
messageId: string,
|
|
1248
|
+
accountedResultIds?: ReadonlySet<string>,
|
|
1249
|
+
): void {
|
|
1184
1250
|
if (!this.maybeCompleteAfterToolResults(messageId)) return;
|
|
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
|
+
}
|
|
1185
1263
|
if (this.getPendingInterrupts()) return;
|
|
1186
1264
|
const turn = this.getTrailingAssistantTurn();
|
|
1187
1265
|
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
1266
|
const owner = this.abortController;
|
|
1193
1267
|
if (owner) {
|
|
1194
1268
|
// A run is still draining (RUN_FINISHED arrived but the stream has not
|
|
@@ -1196,10 +1270,13 @@ export class AgUiThreadController {
|
|
|
1196
1270
|
this.dispatch({
|
|
1197
1271
|
type: "resumeDeferred",
|
|
1198
1272
|
owner,
|
|
1199
|
-
messageId
|
|
1273
|
+
messageId,
|
|
1200
1274
|
});
|
|
1201
1275
|
return;
|
|
1202
1276
|
}
|
|
1277
|
+
if (this.getPendingToolCalls()) return;
|
|
1278
|
+
const headId = turn.at(-1)?.id;
|
|
1279
|
+
if (headId === undefined) return;
|
|
1203
1280
|
this.startResumeRun(headId);
|
|
1204
1281
|
}
|
|
1205
1282
|
|
|
@@ -1269,6 +1346,19 @@ export class AgUiThreadController {
|
|
|
1269
1346
|
let lastAppliedId: string | null = null;
|
|
1270
1347
|
let hardReplace = false;
|
|
1271
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;
|
|
1272
1362
|
|
|
1273
1363
|
for (const message of messages) {
|
|
1274
1364
|
if (seen.has(message.id)) continue;
|
|
@@ -1278,12 +1368,21 @@ export class AgUiThreadController {
|
|
|
1278
1368
|
hardReplace = true;
|
|
1279
1369
|
break;
|
|
1280
1370
|
}
|
|
1281
|
-
this.session.addOrUpdateMessage(
|
|
1371
|
+
this.session.addOrUpdateMessage(
|
|
1372
|
+
expectedParentId,
|
|
1373
|
+
retainToolResults(existing?.message, message),
|
|
1374
|
+
);
|
|
1282
1375
|
expectedParentId = message.id;
|
|
1283
1376
|
lastAppliedId = message.id;
|
|
1284
1377
|
}
|
|
1285
1378
|
|
|
1286
1379
|
if (hardReplace) {
|
|
1380
|
+
const previous = new Map(
|
|
1381
|
+
this.getMessageRepository().messages.map(({ message }) => [
|
|
1382
|
+
message.id,
|
|
1383
|
+
message,
|
|
1384
|
+
]),
|
|
1385
|
+
);
|
|
1287
1386
|
this.session.clear();
|
|
1288
1387
|
expectedParentId = null;
|
|
1289
1388
|
lastAppliedId = null;
|
|
@@ -1291,29 +1390,48 @@ export class AgUiThreadController {
|
|
|
1291
1390
|
for (const message of messages) {
|
|
1292
1391
|
if (seen.has(message.id)) continue;
|
|
1293
1392
|
seen.add(message.id);
|
|
1294
|
-
this.session.addOrUpdateMessage(
|
|
1393
|
+
this.session.addOrUpdateMessage(
|
|
1394
|
+
expectedParentId,
|
|
1395
|
+
retainToolResults(previous.get(message.id), message),
|
|
1396
|
+
);
|
|
1295
1397
|
expectedParentId = message.id;
|
|
1296
1398
|
lastAppliedId = message.id;
|
|
1297
1399
|
}
|
|
1298
1400
|
}
|
|
1299
1401
|
|
|
1300
1402
|
this.session.resetHead(lastAppliedId);
|
|
1403
|
+
for (const messageId of seen) {
|
|
1404
|
+
this.maybeCompleteAfterToolResults(messageId);
|
|
1405
|
+
}
|
|
1301
1406
|
}
|
|
1302
1407
|
|
|
1303
1408
|
this.snapshotHistoryIds.clear();
|
|
1304
1409
|
for (const { message } of this.getMessageRepository().messages) {
|
|
1305
1410
|
this.snapshotHistoryIds.add(message.id);
|
|
1306
1411
|
}
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
+
}
|
|
1317
1435
|
}
|
|
1318
1436
|
this.publishUpdate();
|
|
1319
1437
|
}
|
|
@@ -1375,6 +1493,7 @@ export class AgUiThreadController {
|
|
|
1375
1493
|
runConfig?: RunConfig,
|
|
1376
1494
|
resume?: ResumeDispatch,
|
|
1377
1495
|
resumeStream?: ResumeStream,
|
|
1496
|
+
replayExistingMessages = false,
|
|
1378
1497
|
): Promise<void> {
|
|
1379
1498
|
if (this.isThreadSwitching?.()) return;
|
|
1380
1499
|
// A default AG-UI run supersedes the active run; the hook's opt-in message
|
|
@@ -1382,6 +1501,7 @@ export class AgUiThreadController {
|
|
|
1382
1501
|
// its message; this covers the entry points that start a run without one.
|
|
1383
1502
|
if (!this.supersedeActiveRun()) return;
|
|
1384
1503
|
|
|
1504
|
+
const generation = this.state.historyMutationGeneration;
|
|
1385
1505
|
const normalizedRunConfig = runConfig ?? {};
|
|
1386
1506
|
this.dispatch({
|
|
1387
1507
|
type: "runConfigRecorded",
|
|
@@ -1401,12 +1521,62 @@ export class AgUiThreadController {
|
|
|
1401
1521
|
const runStartMessageIds = new Set(
|
|
1402
1522
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1403
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);
|
|
1404
1572
|
const joinStrategy = this.joinStrategy;
|
|
1405
1573
|
const hasRunUserBoundary = parent?.message.role === "user" && !resume;
|
|
1406
1574
|
|
|
1407
1575
|
let pendingError: Error | null = null;
|
|
1408
1576
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1409
1577
|
let assistantMessageId: string | undefined;
|
|
1578
|
+
let snapshotAssistantId: string | undefined;
|
|
1579
|
+
let replayInterruptMessageId: string | undefined;
|
|
1410
1580
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
1411
1581
|
// assistant; recreating under the cached id on the next content-bearing
|
|
1412
1582
|
// emit keeps both the stream and the message identity. Status-only emits,
|
|
@@ -1416,6 +1586,7 @@ export class AgUiThreadController {
|
|
|
1416
1586
|
// duplicate.
|
|
1417
1587
|
let assistantCollided = false;
|
|
1418
1588
|
const heldMessages = new Map<string, Set<string>>();
|
|
1589
|
+
let frameOwnerIds = new Set<string>();
|
|
1419
1590
|
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1420
1591
|
let settlingRun = false;
|
|
1421
1592
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
@@ -1427,10 +1598,16 @@ export class AgUiThreadController {
|
|
|
1427
1598
|
}
|
|
1428
1599
|
const repositoryHeadId = this.session.headId;
|
|
1429
1600
|
const shouldUseSelectedParent =
|
|
1430
|
-
|
|
1431
|
-
(
|
|
1432
|
-
(
|
|
1433
|
-
|
|
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));
|
|
1434
1611
|
// Branch runs keep their selected parent unless the snapshot advanced to
|
|
1435
1612
|
// a message introduced during this run.
|
|
1436
1613
|
const parentId =
|
|
@@ -1445,23 +1622,59 @@ export class AgUiThreadController {
|
|
|
1445
1622
|
return created;
|
|
1446
1623
|
};
|
|
1447
1624
|
|
|
1448
|
-
if (shouldEagerlyInsertAssistant) ensureAssistant();
|
|
1625
|
+
if (shouldEagerlyInsertAssistant && replayEnded) ensureAssistant();
|
|
1449
1626
|
|
|
1450
1627
|
const settleHeldMessages = (terminalStatus: MessageStatus): string[] => {
|
|
1451
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
|
+
}
|
|
1452
1656
|
for (const [messageId, openToolCallIds] of heldMessages) {
|
|
1657
|
+
const existing = this.session.tryGetMessage(messageId);
|
|
1658
|
+
if (existing)
|
|
1659
|
+
this.markPendingAssistantHistory(messageId, existing.parentId);
|
|
1453
1660
|
let requiresToolResult = false;
|
|
1454
1661
|
const updated = this.session.updateMessage(messageId, (message) => {
|
|
1455
1662
|
if (message.role !== "assistant") return message;
|
|
1456
1663
|
const assistant = message as ThreadAssistantMessage;
|
|
1457
|
-
const pendingToolCallIds =
|
|
1458
|
-
(
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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);
|
|
1465
1678
|
const status: MessageStatus = canWaitForTools
|
|
1466
1679
|
? pendingToolCallIds.length > 0
|
|
1467
1680
|
? { type: "requires-action", reason: "tool-calls" }
|
|
@@ -1473,19 +1686,9 @@ export class AgUiThreadController {
|
|
|
1473
1686
|
...assistant,
|
|
1474
1687
|
status,
|
|
1475
1688
|
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
|
-
}
|
|
1689
|
+
status.type === "requires-action" &&
|
|
1690
|
+
(restrictedPendingToolCallIds || replayContexts.has(messageId))
|
|
1691
|
+
? withPendingToolCallIds(metadata, pendingToolCallIds)
|
|
1489
1692
|
: metadata,
|
|
1490
1693
|
};
|
|
1491
1694
|
});
|
|
@@ -1501,24 +1704,190 @@ export class AgUiThreadController {
|
|
|
1501
1704
|
return pendingMessages;
|
|
1502
1705
|
};
|
|
1503
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
|
+
};
|
|
1736
|
+
|
|
1504
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;
|
|
1750
|
+
const hasStreamContent =
|
|
1751
|
+
Array.isArray(update.content) &&
|
|
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;
|
|
1505
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
|
+
}
|
|
1506
1837
|
const pendingMessages =
|
|
1507
1838
|
settlingRun && terminalStatus && terminalStatus.type !== "running"
|
|
1508
1839
|
? settleHeldMessages(terminalStatus)
|
|
1509
1840
|
: [];
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
+
}
|
|
1513
1877
|
const resolved = this.updateAssistantMessage(
|
|
1514
|
-
|
|
1515
|
-
|
|
1878
|
+
messageId,
|
|
1879
|
+
primaryUpdate,
|
|
1880
|
+
replayContexts.get(messageId),
|
|
1881
|
+
restrictedPendingToolCallIds,
|
|
1516
1882
|
);
|
|
1517
|
-
if (resolved !== assistantMessageId) {
|
|
1883
|
+
if (messageId === frameMessageId && resolved !== assistantMessageId) {
|
|
1518
1884
|
assistantMessageId = resolved;
|
|
1519
1885
|
}
|
|
1520
1886
|
for (const messageId of pendingMessages) {
|
|
1521
|
-
this.maybeResumeAfterToolResults(
|
|
1887
|
+
this.maybeResumeAfterToolResults(
|
|
1888
|
+
messageId,
|
|
1889
|
+
replayMessageIds.includes(messageId) ? accountedResultIds : undefined,
|
|
1890
|
+
);
|
|
1522
1891
|
}
|
|
1523
1892
|
};
|
|
1524
1893
|
|
|
@@ -1527,20 +1896,74 @@ export class AgUiThreadController {
|
|
|
1527
1896
|
startNewMessage: boolean,
|
|
1528
1897
|
openToolCallIds: string[] = [],
|
|
1529
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
|
+
}
|
|
1530
1919
|
if (startNewMessage && assistantMessageId !== undefined) {
|
|
1531
|
-
|
|
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) {
|
|
1532
1948
|
heldMessages.set(assistantMessageId, new Set(openToolCallIds));
|
|
1533
1949
|
} else {
|
|
1534
1950
|
applyUpdate({ status: { type: "complete", reason: "unknown" } });
|
|
1535
1951
|
}
|
|
1536
1952
|
const previousId = assistantMessageId;
|
|
1537
1953
|
assistantMessageId = undefined;
|
|
1538
|
-
if (this.session.tryGetMessage(previousId)) {
|
|
1954
|
+
if (!adoptsReplay && this.session.tryGetMessage(previousId)) {
|
|
1539
1955
|
const created = this.insertAssistantPlaceholder(previousId);
|
|
1540
1956
|
assistantMessageId = created;
|
|
1541
1957
|
this.markPendingAssistantHistory(created, previousId);
|
|
1542
1958
|
}
|
|
1543
1959
|
}
|
|
1960
|
+
if (adoptsReplay) {
|
|
1961
|
+
assistantMessageId = serverId;
|
|
1962
|
+
lastReplayedIndex = replayIndex;
|
|
1963
|
+
this.markPendingAssistantHistory(serverId, existing.parentId);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
replayEnded = true;
|
|
1544
1967
|
const placeholder = ensureAssistant(true);
|
|
1545
1968
|
if (placeholder === serverId) return;
|
|
1546
1969
|
const reassigned = this.reassignAssistantId(placeholder, serverId);
|
|
@@ -1574,21 +1997,47 @@ export class AgUiThreadController {
|
|
|
1574
1997
|
onServerMessageId: (serverId) => {
|
|
1575
1998
|
adoptServerMessageId(serverId, false);
|
|
1576
1999
|
},
|
|
1577
|
-
onTextMessageStart: (serverId, openToolCallIds) =>
|
|
1578
|
-
|
|
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
|
+
},
|
|
1579
2023
|
});
|
|
1580
2024
|
let runFinished = false;
|
|
1581
2025
|
const dispatch = (event: AgUiEvent) => {
|
|
1582
2026
|
if (this.abortController !== abortController) return;
|
|
1583
2027
|
if (event.type === "RUN_FINISHED") {
|
|
1584
2028
|
runFinished = true;
|
|
2029
|
+
if (event.outcome?.type === "interrupt")
|
|
2030
|
+
replayInterruptMessageId = assistantMessageId ?? replayTail;
|
|
1585
2031
|
const ids =
|
|
1586
2032
|
event.outcome?.type === "success"
|
|
1587
2033
|
? event.outcome.pendingToolCallIds
|
|
1588
2034
|
: undefined;
|
|
1589
|
-
restrictedPendingToolCallIds =
|
|
2035
|
+
restrictedPendingToolCallIds =
|
|
2036
|
+
ids === undefined ? undefined : new Set(ids);
|
|
1590
2037
|
}
|
|
1591
2038
|
if (event.type === "TOOL_CALL_RESULT") {
|
|
2039
|
+
if (!assistantCollided && candidateContexts.size)
|
|
2040
|
+
accountedResultIds.add(event.toolCallId);
|
|
1592
2041
|
for (const openToolCallIds of heldMessages.values()) {
|
|
1593
2042
|
openToolCallIds.delete(event.toolCallId);
|
|
1594
2043
|
}
|
|
@@ -1597,6 +2046,8 @@ export class AgUiThreadController {
|
|
|
1597
2046
|
event.type === "RUN_FINISHED" ||
|
|
1598
2047
|
event.type === "RUN_ERROR" ||
|
|
1599
2048
|
event.type === "RUN_CANCELLED";
|
|
2049
|
+
const beforeSnapshot =
|
|
2050
|
+
event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
|
|
1600
2051
|
try {
|
|
1601
2052
|
const nextAssistantMessageId = this.handleEvent(
|
|
1602
2053
|
aggregator,
|
|
@@ -1605,9 +2056,23 @@ export class AgUiThreadController {
|
|
|
1605
2056
|
joinStrategy,
|
|
1606
2057
|
runStartMessageIds,
|
|
1607
2058
|
hasRunUserBoundary,
|
|
2059
|
+
candidateContexts.size ? reconcileReplaySnapshot : undefined,
|
|
1608
2060
|
);
|
|
1609
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
|
+
}
|
|
1610
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
|
+
}
|
|
1611
2076
|
}
|
|
1612
2077
|
} finally {
|
|
1613
2078
|
settlingRun = false;
|
|
@@ -1703,13 +2168,66 @@ export class AgUiThreadController {
|
|
|
1703
2168
|
throw err;
|
|
1704
2169
|
}
|
|
1705
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
|
+
|
|
1706
2204
|
// A tool result that landed before the run settled deferred its
|
|
1707
2205
|
// continuation here so a second run never overlaps the first.
|
|
1708
2206
|
if (this.state.pendingResume?.owner === abortController) {
|
|
1709
|
-
const {
|
|
2207
|
+
const { messageIds } = this.state.pendingResume;
|
|
1710
2208
|
this.dispatch({ type: "resumeCleared" });
|
|
1711
|
-
if (
|
|
1712
|
-
|
|
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
|
+
}
|
|
1713
2231
|
}
|
|
1714
2232
|
}
|
|
1715
2233
|
|
|
@@ -1994,6 +2512,8 @@ export class AgUiThreadController {
|
|
|
1994
2512
|
private updateAssistantMessage(
|
|
1995
2513
|
messageId: string,
|
|
1996
2514
|
update: ChatModelRunResult,
|
|
2515
|
+
replay?: ReplayContext,
|
|
2516
|
+
pendingToolCallIds?: ReadonlySet<string>,
|
|
1997
2517
|
): string {
|
|
1998
2518
|
let latestStatus: MessageStatus | undefined;
|
|
1999
2519
|
const touched = this.session.updateMessage(messageId, (message) => {
|
|
@@ -2003,18 +2523,102 @@ export class AgUiThreadController {
|
|
|
2003
2523
|
? this.mergeAssistantMetadata(assistant.metadata, update.metadata)
|
|
2004
2524
|
: assistant.metadata;
|
|
2005
2525
|
latestStatus = update.status ?? assistant.status;
|
|
2526
|
+
let content =
|
|
2527
|
+
update.content !== undefined
|
|
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
|
+
)
|
|
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;
|
|
2006
2615
|
const metadata =
|
|
2007
2616
|
latestStatus?.type === "requires-action" &&
|
|
2008
2617
|
latestStatus.reason === "tool-calls"
|
|
2009
|
-
?
|
|
2010
|
-
:
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
? this.preserveToolResults(
|
|
2014
|
-
assistant.content,
|
|
2015
|
-
update.content as ThreadAssistantMessage["content"],
|
|
2016
|
-
)
|
|
2017
|
-
: assistant.content;
|
|
2618
|
+
? admission
|
|
2619
|
+
: replay && latestStatus?.type === "running"
|
|
2620
|
+
? mergedMetadata
|
|
2621
|
+
: withoutPendingToolCallIds(admission);
|
|
2018
2622
|
return {
|
|
2019
2623
|
...assistant,
|
|
2020
2624
|
content,
|
|
@@ -2037,7 +2641,10 @@ export class AgUiThreadController {
|
|
|
2037
2641
|
if (this.isPersistableStatus(latestStatus)) {
|
|
2038
2642
|
this.persistAssistantHistory(resolvedMessageId);
|
|
2039
2643
|
}
|
|
2040
|
-
this.maybeResumeAfterToolResults(
|
|
2644
|
+
this.maybeResumeAfterToolResults(
|
|
2645
|
+
resolvedMessageId,
|
|
2646
|
+
replay?.accountedResultIds,
|
|
2647
|
+
);
|
|
2041
2648
|
return resolvedMessageId;
|
|
2042
2649
|
}
|
|
2043
2650
|
|
|
@@ -2048,6 +2655,7 @@ export class AgUiThreadController {
|
|
|
2048
2655
|
private preserveToolResults(
|
|
2049
2656
|
previous: ThreadAssistantMessage["content"],
|
|
2050
2657
|
next: ThreadAssistantMessage["content"],
|
|
2658
|
+
decisionSource?: ThreadAssistantMessage["content"],
|
|
2051
2659
|
): ThreadAssistantMessage["content"] {
|
|
2052
2660
|
const preserved = new Map<string, ToolCallMessagePart>();
|
|
2053
2661
|
for (const part of iterateToolCallParts(previous)) {
|
|
@@ -2055,25 +2663,27 @@ export class AgUiThreadController {
|
|
|
2055
2663
|
preserved.set(part.toolCallId, part);
|
|
2056
2664
|
}
|
|
2057
2665
|
}
|
|
2058
|
-
|
|
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) {
|
|
2059
2672
|
return this.preserveToolInteractions(previous, next);
|
|
2060
2673
|
}
|
|
2061
2674
|
|
|
2062
2675
|
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
2063
2676
|
const prior = preserved.get(part.toolCallId);
|
|
2064
|
-
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
result
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
...
|
|
2072
|
-
? { modelContent: prior.modelContent }
|
|
2073
|
-
: {}),
|
|
2074
|
-
};
|
|
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 };
|
|
2075
2685
|
}
|
|
2076
|
-
return
|
|
2686
|
+
return result;
|
|
2077
2687
|
});
|
|
2078
2688
|
return this.preserveToolInteractions(previous, changed ? merged : next);
|
|
2079
2689
|
}
|
|
@@ -2260,6 +2870,7 @@ export class AgUiThreadController {
|
|
|
2260
2870
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2261
2871
|
runStartMessageIds: ReadonlySet<string>,
|
|
2262
2872
|
hasRunUserBoundary: boolean,
|
|
2873
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2263
2874
|
): string | undefined {
|
|
2264
2875
|
switch (event.type) {
|
|
2265
2876
|
case "STATE_SNAPSHOT": {
|
|
@@ -2304,6 +2915,7 @@ export class AgUiThreadController {
|
|
|
2304
2915
|
joinStrategy,
|
|
2305
2916
|
runStartMessageIds,
|
|
2306
2917
|
hasRunUserBoundary,
|
|
2918
|
+
reconcileSnapshot,
|
|
2307
2919
|
);
|
|
2308
2920
|
const headId = this.session.headId;
|
|
2309
2921
|
if (hasActiveText && headId !== previousHeadId) {
|
|
@@ -2521,6 +3133,7 @@ export class AgUiThreadController {
|
|
|
2521
3133
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2522
3134
|
runStartMessageIds: ReadonlySet<string>,
|
|
2523
3135
|
hasRunUserBoundary: boolean,
|
|
3136
|
+
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2524
3137
|
) {
|
|
2525
3138
|
try {
|
|
2526
3139
|
const activeAssistantItem = activeAssistantId
|
|
@@ -2560,19 +3173,22 @@ export class AgUiThreadController {
|
|
|
2560
3173
|
const existing = this.session.tryGetMessage(
|
|
2561
3174
|
convertedMessage.id,
|
|
2562
3175
|
)?.message;
|
|
2563
|
-
if (
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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;
|
|
2576
3192
|
}
|
|
2577
3193
|
}
|
|
2578
3194
|
if (joinStrategy === "concat-content") {
|