@assistant-ui/react-ag-ui 0.1.1 → 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/runtime/AgUiThreadController.d.ts +11 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +212 -22
- 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 +184 -9
- package/dist/runtime/adapter/conversions.js.map +1 -1
- 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 +34 -12
- 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 +1 -8
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +1 -0
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +2 -1
- package/dist/runtime/types.js.map +1 -1
- package/package.json +5 -5
- 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 +8 -1
- 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 +40 -0
- package/src/runtime/AgUiThreadController.ts +459 -27
- package/src/runtime/adapter/conversions.content.test.ts +129 -0
- package/src/runtime/adapter/conversions.subagent-messages.test.ts +550 -0
- package/src/runtime/adapter/conversions.ts +331 -20
- 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/event-parser.ts +3 -0
- package/src/runtime/types.ts +5 -0
|
@@ -62,9 +62,10 @@ 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";
|
|
70
71
|
import {
|
|
@@ -229,6 +230,73 @@ type CoreOptions = {
|
|
|
229
230
|
|
|
230
231
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
231
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
|
+
|
|
232
300
|
type AgUiRuntimeCallbackName = "onError" | "onCancel";
|
|
233
301
|
|
|
234
302
|
const invokeRuntimeCallback = <TArgs extends readonly unknown[]>(
|
|
@@ -306,6 +374,10 @@ export class AgUiThreadController {
|
|
|
306
374
|
private activeRunAgent: AbstractAgent | null = null;
|
|
307
375
|
private history: ThreadHistoryAdapter | undefined;
|
|
308
376
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
377
|
+
private readonly subagentParentToolCallIds = new Map<
|
|
378
|
+
string,
|
|
379
|
+
string | undefined
|
|
380
|
+
>();
|
|
309
381
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
310
382
|
private readonly persistedHistoryIds = new Set<string>();
|
|
311
383
|
private readonly historyWrites = createHistoryWriteChain();
|
|
@@ -412,6 +484,7 @@ export class AgUiThreadController {
|
|
|
412
484
|
|
|
413
485
|
this.session.applyExternalMessageRepository(repo);
|
|
414
486
|
this.assistantHistoryParents.clear();
|
|
487
|
+
this.subagentParentToolCallIds.clear();
|
|
415
488
|
this.snapshotHistoryIds.clear();
|
|
416
489
|
this.persistedHistoryIds.clear();
|
|
417
490
|
for (const { message } of repo.messages) {
|
|
@@ -1067,6 +1140,7 @@ export class AgUiThreadController {
|
|
|
1067
1140
|
});
|
|
1068
1141
|
if (touched) {
|
|
1069
1142
|
this.publishUpdate();
|
|
1143
|
+
this.persistSettledAssistant(messageId);
|
|
1070
1144
|
}
|
|
1071
1145
|
}
|
|
1072
1146
|
|
|
@@ -1135,7 +1209,35 @@ export class AgUiThreadController {
|
|
|
1135
1209
|
};
|
|
1136
1210
|
});
|
|
1137
1211
|
if (!matchedToolCall) return message;
|
|
1138
|
-
|
|
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 };
|
|
1139
1241
|
});
|
|
1140
1242
|
|
|
1141
1243
|
if (!updated) return;
|
|
@@ -1312,7 +1414,11 @@ export class AgUiThreadController {
|
|
|
1312
1414
|
);
|
|
1313
1415
|
if (!updated) return false;
|
|
1314
1416
|
this.publishUpdate();
|
|
1315
|
-
this.
|
|
1417
|
+
if (this.history?.update) {
|
|
1418
|
+
this.persistSettledAssistant(messageId);
|
|
1419
|
+
} else {
|
|
1420
|
+
this.persistAssistantHistory(messageId);
|
|
1421
|
+
}
|
|
1316
1422
|
return true;
|
|
1317
1423
|
}
|
|
1318
1424
|
|
|
@@ -1471,6 +1577,7 @@ export class AgUiThreadController {
|
|
|
1471
1577
|
const activeRunAgent = this.activeRunAgent;
|
|
1472
1578
|
|
|
1473
1579
|
this.dispatch({ type: "resetThread" });
|
|
1580
|
+
this.subagentParentToolCallIds.clear();
|
|
1474
1581
|
|
|
1475
1582
|
if (controller) {
|
|
1476
1583
|
this.abortController = null;
|
|
@@ -1589,6 +1696,8 @@ export class AgUiThreadController {
|
|
|
1589
1696
|
let frameOwnerIds = new Set<string>();
|
|
1590
1697
|
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1591
1698
|
let settlingRun = false;
|
|
1699
|
+
let latestSnapshot: ThreadAssistantMessage | undefined;
|
|
1700
|
+
let snapshotToolCallIds = new Set<string>();
|
|
1592
1701
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1593
1702
|
const cached = assistantMessageId;
|
|
1594
1703
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1993,7 +2102,102 @@ export class AgUiThreadController {
|
|
|
1993
2102
|
showThinking: this.showThinking,
|
|
1994
2103
|
joinStrategy,
|
|
1995
2104
|
logger: this.logger,
|
|
1996
|
-
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
|
+
},
|
|
1997
2201
|
onServerMessageId: (serverId) => {
|
|
1998
2202
|
adoptServerMessageId(serverId, false);
|
|
1999
2203
|
},
|
|
@@ -2068,6 +2272,22 @@ export class AgUiThreadController {
|
|
|
2068
2272
|
}
|
|
2069
2273
|
}
|
|
2070
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
|
+
);
|
|
2071
2291
|
if (!replayEnded && assistantMessageId === undefined) {
|
|
2072
2292
|
const tail = this.session.getMessages().at(-1);
|
|
2073
2293
|
snapshotAssistantId =
|
|
@@ -2129,7 +2349,6 @@ export class AgUiThreadController {
|
|
|
2129
2349
|
);
|
|
2130
2350
|
const subscriber = createAgUiSubscriber({
|
|
2131
2351
|
dispatch,
|
|
2132
|
-
runId,
|
|
2133
2352
|
logger: this.logger,
|
|
2134
2353
|
onRunFailed: (error) => {
|
|
2135
2354
|
if (abortSignal.aborted) return;
|
|
@@ -2140,7 +2359,7 @@ export class AgUiThreadController {
|
|
|
2140
2359
|
try {
|
|
2141
2360
|
runAgentInstance.messages = input.messages;
|
|
2142
2361
|
runAgentInstance.threadId = input.threadId;
|
|
2143
|
-
runAgentInstance.state = input.state
|
|
2362
|
+
runAgentInstance.state = input.state;
|
|
2144
2363
|
} catch {
|
|
2145
2364
|
// ignore
|
|
2146
2365
|
}
|
|
@@ -2158,6 +2377,26 @@ export class AgUiThreadController {
|
|
|
2158
2377
|
pendingError = err;
|
|
2159
2378
|
}
|
|
2160
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
|
+
}
|
|
2161
2400
|
this.finishRun(abortController);
|
|
2162
2401
|
}
|
|
2163
2402
|
|
|
@@ -2355,7 +2594,8 @@ export class AgUiThreadController {
|
|
|
2355
2594
|
const input = {
|
|
2356
2595
|
threadId,
|
|
2357
2596
|
runId,
|
|
2358
|
-
state:
|
|
2597
|
+
state:
|
|
2598
|
+
this.state.stateSnapshot === undefined ? {} : this.state.stateSnapshot,
|
|
2359
2599
|
messages,
|
|
2360
2600
|
tools: toAgUiTools(context?.tools),
|
|
2361
2601
|
context: [
|
|
@@ -2713,7 +2953,7 @@ export class AgUiThreadController {
|
|
|
2713
2953
|
}
|
|
2714
2954
|
|
|
2715
2955
|
// A snapshot built from user, assistant, and tool records alone carries no
|
|
2716
|
-
// reasoning or
|
|
2956
|
+
// reasoning, activity, or custom data of its own, so locally streamed parts would be
|
|
2717
2957
|
// lost on every refresh; keep the local ones where they streamed when the
|
|
2718
2958
|
// snapshot has none of that kind, matching the merge rule of @ag-ui/client.
|
|
2719
2959
|
private mergeSnapshotAssistantContent(
|
|
@@ -2721,15 +2961,30 @@ export class AgUiThreadController {
|
|
|
2721
2961
|
next: ThreadAssistantMessage["content"],
|
|
2722
2962
|
snapshotHasReasoning: boolean,
|
|
2723
2963
|
snapshotHasActivity: boolean,
|
|
2964
|
+
keepMissingToolCalls: ReadonlySet<string> = new Set(),
|
|
2965
|
+
keepPriorTranscripts = false,
|
|
2724
2966
|
): ThreadAssistantMessage["content"] {
|
|
2725
2967
|
const shouldKeep = (part: ThreadAssistantMessage["content"][number]) =>
|
|
2726
|
-
part.type === "
|
|
2727
|
-
?
|
|
2728
|
-
|
|
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;
|
|
2729
2983
|
const kept = previous.filter(shouldKeep);
|
|
2730
2984
|
const merged = this.reconcileSnapshotNestedContent(
|
|
2731
2985
|
previous,
|
|
2732
2986
|
this.preserveToolInteractions(previous, next),
|
|
2987
|
+
keepPriorTranscripts,
|
|
2733
2988
|
);
|
|
2734
2989
|
if (kept.length === 0) return merged;
|
|
2735
2990
|
|
|
@@ -2779,6 +3034,7 @@ export class AgUiThreadController {
|
|
|
2779
3034
|
private reconcileSnapshotNestedContent(
|
|
2780
3035
|
previous: ThreadAssistantMessage["content"],
|
|
2781
3036
|
next: ThreadAssistantMessage["content"],
|
|
3037
|
+
keepPriorTranscripts: boolean,
|
|
2782
3038
|
): ThreadAssistantMessage["content"] {
|
|
2783
3039
|
const nestedIds = new Set<string>();
|
|
2784
3040
|
for (const part of previous) {
|
|
@@ -2828,10 +3084,92 @@ export class AgUiThreadController {
|
|
|
2828
3084
|
}
|
|
2829
3085
|
if (part.type !== "tool-call") return [part];
|
|
2830
3086
|
const prior = previousCalls.get(part.toolCallId);
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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
|
+
};
|
|
2834
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
|
+
];
|
|
2835
3173
|
}
|
|
2836
3174
|
|
|
2837
3175
|
private mergeAssistantMetadata(
|
|
@@ -2872,6 +3210,15 @@ export class AgUiThreadController {
|
|
|
2872
3210
|
hasRunUserBoundary: boolean,
|
|
2873
3211
|
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2874
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
|
+
}
|
|
2875
3222
|
switch (event.type) {
|
|
2876
3223
|
case "STATE_SNAPSHOT": {
|
|
2877
3224
|
this.dispatch({
|
|
@@ -2901,6 +3248,7 @@ export class AgUiThreadController {
|
|
|
2901
3248
|
break;
|
|
2902
3249
|
}
|
|
2903
3250
|
case "MESSAGES_SNAPSHOT": {
|
|
3251
|
+
aggregator.handle(event);
|
|
2904
3252
|
const previousHeadId = this.session.headId;
|
|
2905
3253
|
const activeAssistant =
|
|
2906
3254
|
activeAssistantId === undefined
|
|
@@ -2918,13 +3266,46 @@ export class AgUiThreadController {
|
|
|
2918
3266
|
reconcileSnapshot,
|
|
2919
3267
|
);
|
|
2920
3268
|
const headId = this.session.headId;
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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 =
|
|
2927
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
|
+
}
|
|
2928
3309
|
}
|
|
2929
3310
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2930
3311
|
break;
|
|
@@ -3027,6 +3408,14 @@ export class AgUiThreadController {
|
|
|
3027
3408
|
...(event.messageId
|
|
3028
3409
|
? { unstable_toolMessageId: event.messageId }
|
|
3029
3410
|
: {}),
|
|
3411
|
+
...(event.metadata !== undefined
|
|
3412
|
+
? {
|
|
3413
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3414
|
+
part.providerMetadata,
|
|
3415
|
+
event.metadata,
|
|
3416
|
+
),
|
|
3417
|
+
}
|
|
3418
|
+
: {}),
|
|
3030
3419
|
};
|
|
3031
3420
|
}
|
|
3032
3421
|
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
@@ -3053,6 +3442,14 @@ export class AgUiThreadController {
|
|
|
3053
3442
|
...(event.messageId
|
|
3054
3443
|
? { unstable_toolMessageId: event.messageId }
|
|
3055
3444
|
: {}),
|
|
3445
|
+
...(event.metadata !== undefined
|
|
3446
|
+
? {
|
|
3447
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3448
|
+
part.providerMetadata,
|
|
3449
|
+
event.metadata,
|
|
3450
|
+
),
|
|
3451
|
+
}
|
|
3452
|
+
: {}),
|
|
3056
3453
|
};
|
|
3057
3454
|
});
|
|
3058
3455
|
if (!matchedToolCall) return message;
|
|
@@ -3063,7 +3460,8 @@ export class AgUiThreadController {
|
|
|
3063
3460
|
this.publishUpdate();
|
|
3064
3461
|
// Not maybeResumeAfterToolResults: the delivering run is already in
|
|
3065
3462
|
// flight, and a resume from the owner would reset the head past it.
|
|
3066
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3463
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3464
|
+
this.persistSettledAssistant(messageId);
|
|
3067
3465
|
}
|
|
3068
3466
|
|
|
3069
3467
|
private applyCrossRunActivitySnapshot(
|
|
@@ -3124,7 +3522,8 @@ export class AgUiThreadController {
|
|
|
3124
3522
|
|
|
3125
3523
|
if (!updated) return;
|
|
3126
3524
|
this.publishUpdate();
|
|
3127
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3525
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3526
|
+
this.persistSettledAssistant(messageId);
|
|
3128
3527
|
}
|
|
3129
3528
|
|
|
3130
3529
|
private importMessagesSnapshot(
|
|
@@ -3143,14 +3542,24 @@ export class AgUiThreadController {
|
|
|
3143
3542
|
const activeAssistantParentId = activeAssistantItem?.parentId;
|
|
3144
3543
|
const activeAssistant =
|
|
3145
3544
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
3146
|
-
const normalized =
|
|
3147
|
-
|
|
3148
|
-
|
|
3545
|
+
const normalized = fromAgUiSnapshotMessages(
|
|
3546
|
+
rawMessages,
|
|
3547
|
+
{ showThinking: this.showThinking },
|
|
3548
|
+
this.subagentParentToolCallIds,
|
|
3549
|
+
);
|
|
3149
3550
|
const converted: ThreadMessage[] = [];
|
|
3150
3551
|
for (const message of normalized) {
|
|
3151
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;
|
|
3152
3561
|
converted.push(
|
|
3153
|
-
fromThreadMessageLike(message, generateId(),
|
|
3562
|
+
fromThreadMessageLike(message, generateId(), fallbackStatus),
|
|
3154
3563
|
);
|
|
3155
3564
|
} catch (error) {
|
|
3156
3565
|
this.logger.error?.(
|
|
@@ -3178,14 +3587,22 @@ export class AgUiThreadController {
|
|
|
3178
3587
|
existing?.role === "assistant" ? existing : undefined;
|
|
3179
3588
|
const snapshot =
|
|
3180
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";
|
|
3181
3595
|
converted[index] = previous
|
|
3182
3596
|
? {
|
|
3183
3597
|
...snapshot,
|
|
3598
|
+
...(preservesRunningStatus ? { status: previous.status } : {}),
|
|
3184
3599
|
content: this.mergeSnapshotAssistantContent(
|
|
3185
3600
|
previous.content,
|
|
3186
3601
|
snapshot.content,
|
|
3187
3602
|
snapshotHasReasoning,
|
|
3188
3603
|
snapshotHasActivity,
|
|
3604
|
+
new Set(),
|
|
3605
|
+
true,
|
|
3189
3606
|
),
|
|
3190
3607
|
}
|
|
3191
3608
|
: snapshot;
|
|
@@ -3348,6 +3765,19 @@ export class AgUiThreadController {
|
|
|
3348
3765
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
3349
3766
|
}
|
|
3350
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
|
+
|
|
3351
3781
|
private persistAssistantHistory(messageId: string) {
|
|
3352
3782
|
const history = this.history;
|
|
3353
3783
|
if (!history) return;
|
|
@@ -3360,7 +3790,9 @@ export class AgUiThreadController {
|
|
|
3360
3790
|
const update = history.update;
|
|
3361
3791
|
const shouldUpdate =
|
|
3362
3792
|
update !== undefined &&
|
|
3363
|
-
(wasPersisted ||
|
|
3793
|
+
(wasPersisted ||
|
|
3794
|
+
this.snapshotHistoryIds.has(messageId) ||
|
|
3795
|
+
this.historyWrites.pending(messageId) !== undefined);
|
|
3364
3796
|
|
|
3365
3797
|
if (shouldUpdate) {
|
|
3366
3798
|
const write = this.historyWrites.chain(messageId, () =>
|