@assistant-ui/react-ag-ui 0.1.1 → 0.1.3
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 +242 -25
- 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 +2 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +42 -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 +6 -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 +346 -0
- package/src/runtime/AgUiThreadController.subagent-snapshot.test.ts +854 -0
- package/src/runtime/AgUiThreadController.test.ts +173 -1
- package/src/runtime/AgUiThreadController.ts +528 -33
- 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 +63 -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
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +384 -0
- package/src/useAgUiRuntime.state.test.tsx +87 -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 {
|
|
@@ -81,6 +82,26 @@ import {
|
|
|
81
82
|
withToolApprovalDecision,
|
|
82
83
|
} from "./adapter/tool-approval";
|
|
83
84
|
|
|
85
|
+
// The SDK does not retain initialState.
|
|
86
|
+
const agentInitialStates = new WeakMap<
|
|
87
|
+
AbstractAgent,
|
|
88
|
+
ReadonlyJSONValue | undefined
|
|
89
|
+
>();
|
|
90
|
+
|
|
91
|
+
const getAgentInitialState = (agent: AbstractAgent) => {
|
|
92
|
+
if (!agentInitialStates.has(agent)) {
|
|
93
|
+
const state = agent.state;
|
|
94
|
+
agentInitialStates.set(
|
|
95
|
+
agent,
|
|
96
|
+
state === undefined ||
|
|
97
|
+
(isPlainObject(state) && Object.keys(state).length === 0)
|
|
98
|
+
? undefined
|
|
99
|
+
: (structuredClone(state) as ReadonlyJSONValue),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return agentInitialStates.get(agent);
|
|
103
|
+
};
|
|
104
|
+
|
|
84
105
|
// AbstractAgent.runAgent declares two parameters. HttpAgent ignores a third and
|
|
85
106
|
// is cancelled through agent.abortRun(); the run options stay for subclasses
|
|
86
107
|
// that inherit the base no-op abortRun and have no other cancellation hook.
|
|
@@ -229,6 +250,73 @@ type CoreOptions = {
|
|
|
229
250
|
|
|
230
251
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
231
252
|
|
|
253
|
+
const textOf = (content: ThreadAssistantMessage["content"]) =>
|
|
254
|
+
content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Swaps `content`'s text parts, in order, for `source`'s. A text part `source`
|
|
258
|
+
* has beyond `content`'s text slots goes after the nearest earlier part of
|
|
259
|
+
* `source` that `content` also has: a tool call by id, another part by its
|
|
260
|
+
* type and position among parts of that type. With none, it goes first.
|
|
261
|
+
*/
|
|
262
|
+
const replaceTextParts = (
|
|
263
|
+
content: ThreadAssistantMessage["content"],
|
|
264
|
+
source: ThreadAssistantMessage["content"],
|
|
265
|
+
): ThreadAssistantMessage["content"] => {
|
|
266
|
+
const texts = source.filter((part) => part.type === "text");
|
|
267
|
+
const result: ThreadAssistantMessage["content"][number][] = [];
|
|
268
|
+
let used = 0;
|
|
269
|
+
for (const part of content) {
|
|
270
|
+
if (part.type !== "text") result.push(part);
|
|
271
|
+
else if (used < texts.length) result.push(texts[used++]!);
|
|
272
|
+
}
|
|
273
|
+
const positionIn = (
|
|
274
|
+
parts: readonly ThreadAssistantMessage["content"][number][],
|
|
275
|
+
part: ThreadAssistantMessage["content"][number],
|
|
276
|
+
) => parts.filter((other) => other.type === part.type).indexOf(part);
|
|
277
|
+
for (const text of texts.slice(used)) {
|
|
278
|
+
let anchor = -1;
|
|
279
|
+
for (
|
|
280
|
+
let index = source.indexOf(text) - 1;
|
|
281
|
+
index >= 0 && anchor < 0;
|
|
282
|
+
index--
|
|
283
|
+
) {
|
|
284
|
+
const before = source[index]!;
|
|
285
|
+
if (before.type === "tool-call") {
|
|
286
|
+
anchor = result.findIndex(
|
|
287
|
+
(part) =>
|
|
288
|
+
part.type === "tool-call" && part.toolCallId === before.toolCallId,
|
|
289
|
+
);
|
|
290
|
+
} else if (before.type === "text") {
|
|
291
|
+
anchor = result.indexOf(before);
|
|
292
|
+
} else {
|
|
293
|
+
const sameType = result.filter((part) => part.type === before.type);
|
|
294
|
+
const match = sameType[positionIn(source, before)];
|
|
295
|
+
anchor = match ? result.indexOf(match) : -1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
result.splice(anchor + 1, 0, text);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const settleNestedContent = (
|
|
304
|
+
content: ThreadAssistantMessage["content"],
|
|
305
|
+
status: MessageStatus,
|
|
306
|
+
) =>
|
|
307
|
+
mapToolCallPartsDeep(content, (part) =>
|
|
308
|
+
part.messages
|
|
309
|
+
? {
|
|
310
|
+
...part,
|
|
311
|
+
messages: part.messages.map((message) =>
|
|
312
|
+
message.role === "assistant" && message.status?.type === "running"
|
|
313
|
+
? { ...message, status }
|
|
314
|
+
: message,
|
|
315
|
+
),
|
|
316
|
+
}
|
|
317
|
+
: part,
|
|
318
|
+
).content;
|
|
319
|
+
|
|
232
320
|
type AgUiRuntimeCallbackName = "onError" | "onCancel";
|
|
233
321
|
|
|
234
322
|
const invokeRuntimeCallback = <TArgs extends readonly unknown[]>(
|
|
@@ -306,6 +394,10 @@ export class AgUiThreadController {
|
|
|
306
394
|
private activeRunAgent: AbstractAgent | null = null;
|
|
307
395
|
private history: ThreadHistoryAdapter | undefined;
|
|
308
396
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
397
|
+
private readonly subagentParentToolCallIds = new Map<
|
|
398
|
+
string,
|
|
399
|
+
string | undefined
|
|
400
|
+
>();
|
|
309
401
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
310
402
|
private readonly persistedHistoryIds = new Set<string>();
|
|
311
403
|
private readonly historyWrites = createHistoryWriteChain();
|
|
@@ -314,6 +406,13 @@ export class AgUiThreadController {
|
|
|
314
406
|
|
|
315
407
|
constructor(options: CoreOptions) {
|
|
316
408
|
this.agent = options.agent;
|
|
409
|
+
const initialState = getAgentInitialState(this.agent);
|
|
410
|
+
if (initialState !== undefined) {
|
|
411
|
+
this.dispatch({
|
|
412
|
+
type: "stateSnapshot",
|
|
413
|
+
snapshot: initialState,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
317
416
|
this.logger = options.logger;
|
|
318
417
|
this.showThinking = options.showThinking;
|
|
319
418
|
this.joinStrategy = options.joinStrategy;
|
|
@@ -349,7 +448,18 @@ export class AgUiThreadController {
|
|
|
349
448
|
}
|
|
350
449
|
|
|
351
450
|
updateOptions(options: CoreOptions) {
|
|
451
|
+
const agentChanged = this.agent !== options.agent;
|
|
352
452
|
this.agent = options.agent;
|
|
453
|
+
if (agentChanged) {
|
|
454
|
+
const initialState = getAgentInitialState(this.agent);
|
|
455
|
+
if (
|
|
456
|
+
this.state.stateSnapshot === undefined &&
|
|
457
|
+
initialState !== undefined
|
|
458
|
+
) {
|
|
459
|
+
this.dispatch({ type: "stateSnapshot", snapshot: initialState });
|
|
460
|
+
this.publishUpdate();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
353
463
|
this.logger = options.logger;
|
|
354
464
|
this.showThinking = options.showThinking;
|
|
355
465
|
this.joinStrategy = options.joinStrategy;
|
|
@@ -412,6 +522,7 @@ export class AgUiThreadController {
|
|
|
412
522
|
|
|
413
523
|
this.session.applyExternalMessageRepository(repo);
|
|
414
524
|
this.assistantHistoryParents.clear();
|
|
525
|
+
this.subagentParentToolCallIds.clear();
|
|
415
526
|
this.snapshotHistoryIds.clear();
|
|
416
527
|
this.persistedHistoryIds.clear();
|
|
417
528
|
for (const { message } of repo.messages) {
|
|
@@ -1067,6 +1178,7 @@ export class AgUiThreadController {
|
|
|
1067
1178
|
});
|
|
1068
1179
|
if (touched) {
|
|
1069
1180
|
this.publishUpdate();
|
|
1181
|
+
this.persistSettledAssistant(messageId);
|
|
1070
1182
|
}
|
|
1071
1183
|
}
|
|
1072
1184
|
|
|
@@ -1135,7 +1247,35 @@ export class AgUiThreadController {
|
|
|
1135
1247
|
};
|
|
1136
1248
|
});
|
|
1137
1249
|
if (!matchedToolCall) return message;
|
|
1138
|
-
|
|
1250
|
+
const settled = mapToolCallPartsDeep(content, (part) => {
|
|
1251
|
+
if (!part.messages) return part;
|
|
1252
|
+
let changed = false;
|
|
1253
|
+
const messages = part.messages.map((nested) => {
|
|
1254
|
+
if (
|
|
1255
|
+
nested.role !== "assistant" ||
|
|
1256
|
+
nested.status.type !== "requires-action" ||
|
|
1257
|
+
nested.status.reason !== "tool-calls"
|
|
1258
|
+
)
|
|
1259
|
+
return nested;
|
|
1260
|
+
const listed = listedPendingToolCallIds(nested);
|
|
1261
|
+
if (
|
|
1262
|
+
Array.from(iterateToolCallParts(nested.content)).some(
|
|
1263
|
+
(call) =>
|
|
1264
|
+
!isResolvedToolCall(call) &&
|
|
1265
|
+
(!listed || listed.has(call.toolCallId)),
|
|
1266
|
+
)
|
|
1267
|
+
)
|
|
1268
|
+
return nested;
|
|
1269
|
+
changed = true;
|
|
1270
|
+
return {
|
|
1271
|
+
...nested,
|
|
1272
|
+
status: { type: "complete" as const, reason: "unknown" as const },
|
|
1273
|
+
metadata: withoutPendingToolCallIds(nested.metadata),
|
|
1274
|
+
};
|
|
1275
|
+
});
|
|
1276
|
+
return changed ? { ...part, messages } : part;
|
|
1277
|
+
}).content;
|
|
1278
|
+
return { ...assistant, content: settled };
|
|
1139
1279
|
});
|
|
1140
1280
|
|
|
1141
1281
|
if (!updated) return;
|
|
@@ -1312,7 +1452,11 @@ export class AgUiThreadController {
|
|
|
1312
1452
|
);
|
|
1313
1453
|
if (!updated) return false;
|
|
1314
1454
|
this.publishUpdate();
|
|
1315
|
-
this.
|
|
1455
|
+
if (this.history?.update) {
|
|
1456
|
+
this.persistSettledAssistant(messageId);
|
|
1457
|
+
} else {
|
|
1458
|
+
this.persistAssistantHistory(messageId);
|
|
1459
|
+
}
|
|
1316
1460
|
return true;
|
|
1317
1461
|
}
|
|
1318
1462
|
|
|
@@ -1471,6 +1615,7 @@ export class AgUiThreadController {
|
|
|
1471
1615
|
const activeRunAgent = this.activeRunAgent;
|
|
1472
1616
|
|
|
1473
1617
|
this.dispatch({ type: "resetThread" });
|
|
1618
|
+
this.subagentParentToolCallIds.clear();
|
|
1474
1619
|
|
|
1475
1620
|
if (controller) {
|
|
1476
1621
|
this.abortController = null;
|
|
@@ -1589,6 +1734,8 @@ export class AgUiThreadController {
|
|
|
1589
1734
|
let frameOwnerIds = new Set<string>();
|
|
1590
1735
|
let restrictedPendingToolCallIds: ReadonlySet<string> | undefined;
|
|
1591
1736
|
let settlingRun = false;
|
|
1737
|
+
let latestSnapshot: ThreadAssistantMessage | undefined;
|
|
1738
|
+
let snapshotToolCallIds = new Set<string>();
|
|
1592
1739
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1593
1740
|
const cached = assistantMessageId;
|
|
1594
1741
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1993,7 +2140,102 @@ export class AgUiThreadController {
|
|
|
1993
2140
|
showThinking: this.showThinking,
|
|
1994
2141
|
joinStrategy,
|
|
1995
2142
|
logger: this.logger,
|
|
1996
|
-
emit:
|
|
2143
|
+
emit: (update) => {
|
|
2144
|
+
const currentSnapshotAssistant =
|
|
2145
|
+
assistantMessageId === undefined
|
|
2146
|
+
? undefined
|
|
2147
|
+
: this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2148
|
+
const listedSnapshotToolCallIds =
|
|
2149
|
+
currentSnapshotAssistant?.role === "assistant"
|
|
2150
|
+
? listedPendingToolCallIds(currentSnapshotAssistant)
|
|
2151
|
+
: undefined;
|
|
2152
|
+
const hasPendingSnapshotToolCall =
|
|
2153
|
+
latestSnapshot?.id === assistantMessageId &&
|
|
2154
|
+
currentSnapshotAssistant?.role === "assistant" &&
|
|
2155
|
+
Array.from(
|
|
2156
|
+
iterateToolCallParts(currentSnapshotAssistant.content),
|
|
2157
|
+
).some(
|
|
2158
|
+
(part) =>
|
|
2159
|
+
!isResolvedToolCall(part) &&
|
|
2160
|
+
(!listedSnapshotToolCallIds ||
|
|
2161
|
+
listedSnapshotToolCallIds.has(part.toolCallId)) &&
|
|
2162
|
+
(!restrictedPendingToolCallIds ||
|
|
2163
|
+
restrictedPendingToolCallIds.has(part.toolCallId)),
|
|
2164
|
+
);
|
|
2165
|
+
const reconciledUpdate =
|
|
2166
|
+
settlingRun &&
|
|
2167
|
+
update.status?.type === "complete" &&
|
|
2168
|
+
hasPendingSnapshotToolCall
|
|
2169
|
+
? {
|
|
2170
|
+
...update,
|
|
2171
|
+
status: {
|
|
2172
|
+
type: "requires-action",
|
|
2173
|
+
reason: "tool-calls",
|
|
2174
|
+
} as const,
|
|
2175
|
+
...(restrictedPendingToolCallIds
|
|
2176
|
+
? {
|
|
2177
|
+
metadata: {
|
|
2178
|
+
...update.metadata,
|
|
2179
|
+
custom: {
|
|
2180
|
+
...update.metadata?.custom,
|
|
2181
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
2182
|
+
...(update.metadata?.custom?.[
|
|
2183
|
+
AG_UI_METADATA_NAMESPACE
|
|
2184
|
+
] as AgUiCustomMetadata | undefined),
|
|
2185
|
+
pendingToolCallIds: [
|
|
2186
|
+
...restrictedPendingToolCallIds,
|
|
2187
|
+
],
|
|
2188
|
+
},
|
|
2189
|
+
},
|
|
2190
|
+
},
|
|
2191
|
+
}
|
|
2192
|
+
: {}),
|
|
2193
|
+
}
|
|
2194
|
+
: update;
|
|
2195
|
+
if (
|
|
2196
|
+
settlingRun &&
|
|
2197
|
+
latestSnapshot &&
|
|
2198
|
+
latestSnapshot.id === assistantMessageId &&
|
|
2199
|
+
reconciledUpdate.status &&
|
|
2200
|
+
reconciledUpdate.status.type !== "running" &&
|
|
2201
|
+
reconciledUpdate.status.type !== "requires-action"
|
|
2202
|
+
) {
|
|
2203
|
+
const status = reconciledUpdate.status;
|
|
2204
|
+
const current =
|
|
2205
|
+
this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2206
|
+
const content = settleNestedContent(
|
|
2207
|
+
current?.role === "assistant"
|
|
2208
|
+
? current.content
|
|
2209
|
+
: latestSnapshot.content,
|
|
2210
|
+
status,
|
|
2211
|
+
);
|
|
2212
|
+
applyUpdate({ ...reconciledUpdate, content });
|
|
2213
|
+
} else if (
|
|
2214
|
+
latestSnapshot &&
|
|
2215
|
+
latestSnapshot.id === assistantMessageId &&
|
|
2216
|
+
reconciledUpdate.content !== undefined
|
|
2217
|
+
) {
|
|
2218
|
+
const content = this.mergeSnapshotAssistantContent(
|
|
2219
|
+
latestSnapshot.content,
|
|
2220
|
+
reconciledUpdate.content as ThreadAssistantMessage["content"],
|
|
2221
|
+
reconciledUpdate.content.some((part) => part.type === "reasoning"),
|
|
2222
|
+
reconciledUpdate.content.some(isActivityPart),
|
|
2223
|
+
snapshotToolCallIds,
|
|
2224
|
+
);
|
|
2225
|
+
applyUpdate({ ...reconciledUpdate, content });
|
|
2226
|
+
} else {
|
|
2227
|
+
applyUpdate(reconciledUpdate);
|
|
2228
|
+
}
|
|
2229
|
+
const current = assistantMessageId
|
|
2230
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2231
|
+
: undefined;
|
|
2232
|
+
latestSnapshot =
|
|
2233
|
+
!settlingRun &&
|
|
2234
|
+
current?.role === "assistant" &&
|
|
2235
|
+
current.id === latestSnapshot?.id
|
|
2236
|
+
? current
|
|
2237
|
+
: undefined;
|
|
2238
|
+
},
|
|
1997
2239
|
onServerMessageId: (serverId) => {
|
|
1998
2240
|
adoptServerMessageId(serverId, false);
|
|
1999
2241
|
},
|
|
@@ -2068,6 +2310,22 @@ export class AgUiThreadController {
|
|
|
2068
2310
|
}
|
|
2069
2311
|
}
|
|
2070
2312
|
assistantMessageId = nextAssistantMessageId;
|
|
2313
|
+
const imported = assistantMessageId
|
|
2314
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2315
|
+
: undefined;
|
|
2316
|
+
const source = event.messages.find(
|
|
2317
|
+
(message) =>
|
|
2318
|
+
message?.role === "assistant" && message.id === imported?.id,
|
|
2319
|
+
);
|
|
2320
|
+
latestSnapshot =
|
|
2321
|
+
imported?.role === "assistant" && source ? imported : undefined;
|
|
2322
|
+
snapshotToolCallIds = new Set(
|
|
2323
|
+
Array.isArray(source?.toolCalls)
|
|
2324
|
+
? source.toolCalls
|
|
2325
|
+
.map((call: { id?: unknown }) => call.id)
|
|
2326
|
+
.filter((id: unknown): id is string => typeof id === "string")
|
|
2327
|
+
: [],
|
|
2328
|
+
);
|
|
2071
2329
|
if (!replayEnded && assistantMessageId === undefined) {
|
|
2072
2330
|
const tail = this.session.getMessages().at(-1);
|
|
2073
2331
|
snapshotAssistantId =
|
|
@@ -2129,7 +2387,6 @@ export class AgUiThreadController {
|
|
|
2129
2387
|
);
|
|
2130
2388
|
const subscriber = createAgUiSubscriber({
|
|
2131
2389
|
dispatch,
|
|
2132
|
-
runId,
|
|
2133
2390
|
logger: this.logger,
|
|
2134
2391
|
onRunFailed: (error) => {
|
|
2135
2392
|
if (abortSignal.aborted) return;
|
|
@@ -2140,7 +2397,7 @@ export class AgUiThreadController {
|
|
|
2140
2397
|
try {
|
|
2141
2398
|
runAgentInstance.messages = input.messages;
|
|
2142
2399
|
runAgentInstance.threadId = input.threadId;
|
|
2143
|
-
runAgentInstance.state = input.state
|
|
2400
|
+
runAgentInstance.state = input.state;
|
|
2144
2401
|
} catch {
|
|
2145
2402
|
// ignore
|
|
2146
2403
|
}
|
|
@@ -2158,6 +2415,26 @@ export class AgUiThreadController {
|
|
|
2158
2415
|
pendingError = err;
|
|
2159
2416
|
}
|
|
2160
2417
|
} finally {
|
|
2418
|
+
const current = assistantMessageId
|
|
2419
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2420
|
+
: undefined;
|
|
2421
|
+
if (
|
|
2422
|
+
this.abortController === abortController &&
|
|
2423
|
+
current?.role === "assistant" &&
|
|
2424
|
+
current.status?.type === "running"
|
|
2425
|
+
) {
|
|
2426
|
+
// Finalization is transport cleanup, not proof of a terminal event.
|
|
2427
|
+
const status: MessageStatus = {
|
|
2428
|
+
type: "incomplete",
|
|
2429
|
+
reason: "error",
|
|
2430
|
+
error: "The connection ended before the answer finished.",
|
|
2431
|
+
};
|
|
2432
|
+
settleHeldMessages(status);
|
|
2433
|
+
applyUpdate({
|
|
2434
|
+
status,
|
|
2435
|
+
content: settleNestedContent(current.content, status),
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2161
2438
|
this.finishRun(abortController);
|
|
2162
2439
|
}
|
|
2163
2440
|
|
|
@@ -2355,7 +2632,8 @@ export class AgUiThreadController {
|
|
|
2355
2632
|
const input = {
|
|
2356
2633
|
threadId,
|
|
2357
2634
|
runId,
|
|
2358
|
-
state:
|
|
2635
|
+
state:
|
|
2636
|
+
this.state.stateSnapshot === undefined ? {} : this.state.stateSnapshot,
|
|
2359
2637
|
messages,
|
|
2360
2638
|
tools: toAgUiTools(context?.tools),
|
|
2361
2639
|
context: [
|
|
@@ -2535,7 +2813,7 @@ export class AgUiThreadController {
|
|
|
2535
2813
|
update.content as ThreadAssistantMessage["content"],
|
|
2536
2814
|
)
|
|
2537
2815
|
: assistant.content;
|
|
2538
|
-
const calls =
|
|
2816
|
+
const calls = Array.from(iterateToolCallParts(content));
|
|
2539
2817
|
// A retained historical call needs existing or current stream authority.
|
|
2540
2818
|
const pending =
|
|
2541
2819
|
pendingToolCallIds ??
|
|
@@ -2549,7 +2827,11 @@ export class AgUiThreadController {
|
|
|
2549
2827
|
: undefined);
|
|
2550
2828
|
if (replay && latestStatus?.type !== "running" && pending)
|
|
2551
2829
|
replay.admittedToolCallIds = pending;
|
|
2552
|
-
if (
|
|
2830
|
+
if (
|
|
2831
|
+
replay ||
|
|
2832
|
+
(latestStatus?.type === "requires-action" &&
|
|
2833
|
+
latestStatus.reason === "interrupt")
|
|
2834
|
+
) {
|
|
2553
2835
|
// Project gates against the content the user can actually answer.
|
|
2554
2836
|
const interrupts =
|
|
2555
2837
|
latestStatus?.type === "requires-action" &&
|
|
@@ -2565,10 +2847,10 @@ export class AgUiThreadController {
|
|
|
2565
2847
|
new Set(calls.map((part) => part.toolCallId)),
|
|
2566
2848
|
);
|
|
2567
2849
|
const originalCalls = new Map(
|
|
2568
|
-
Array.from(
|
|
2569
|
-
|
|
2570
|
-
part,
|
|
2571
|
-
|
|
2850
|
+
Array.from(
|
|
2851
|
+
iterateToolCallParts(replay?.originalContent ?? assistant.content),
|
|
2852
|
+
(part) => [part.toolCallId, part],
|
|
2853
|
+
),
|
|
2572
2854
|
);
|
|
2573
2855
|
content = mapToolCallPartsDeep(content, (part) => {
|
|
2574
2856
|
const approval = approvals.get(part.toolCallId);
|
|
@@ -2713,7 +2995,7 @@ export class AgUiThreadController {
|
|
|
2713
2995
|
}
|
|
2714
2996
|
|
|
2715
2997
|
// A snapshot built from user, assistant, and tool records alone carries no
|
|
2716
|
-
// reasoning or
|
|
2998
|
+
// reasoning, activity, or custom data of its own, so locally streamed parts would be
|
|
2717
2999
|
// lost on every refresh; keep the local ones where they streamed when the
|
|
2718
3000
|
// snapshot has none of that kind, matching the merge rule of @ag-ui/client.
|
|
2719
3001
|
private mergeSnapshotAssistantContent(
|
|
@@ -2721,15 +3003,30 @@ export class AgUiThreadController {
|
|
|
2721
3003
|
next: ThreadAssistantMessage["content"],
|
|
2722
3004
|
snapshotHasReasoning: boolean,
|
|
2723
3005
|
snapshotHasActivity: boolean,
|
|
3006
|
+
keepMissingToolCalls: ReadonlySet<string> = new Set(),
|
|
3007
|
+
keepPriorTranscripts = false,
|
|
2724
3008
|
): ThreadAssistantMessage["content"] {
|
|
2725
3009
|
const shouldKeep = (part: ThreadAssistantMessage["content"][number]) =>
|
|
2726
|
-
part.type === "
|
|
2727
|
-
?
|
|
2728
|
-
|
|
3010
|
+
part.type === "tool-call"
|
|
3011
|
+
? keepMissingToolCalls.has(part.toolCallId) &&
|
|
3012
|
+
!next.some(
|
|
3013
|
+
(incoming) =>
|
|
3014
|
+
incoming.type === "tool-call" &&
|
|
3015
|
+
incoming.toolCallId === part.toolCallId,
|
|
3016
|
+
)
|
|
3017
|
+
: part.type === "reasoning"
|
|
3018
|
+
? this.showThinking && !snapshotHasReasoning
|
|
3019
|
+
: part.type === "data" && !isActivityPart(part)
|
|
3020
|
+
? !next.some(
|
|
3021
|
+
(incoming) =>
|
|
3022
|
+
incoming.type === "data" && incoming.name === part.name,
|
|
3023
|
+
)
|
|
3024
|
+
: isActivityPart(part) && !snapshotHasActivity;
|
|
2729
3025
|
const kept = previous.filter(shouldKeep);
|
|
2730
3026
|
const merged = this.reconcileSnapshotNestedContent(
|
|
2731
3027
|
previous,
|
|
2732
3028
|
this.preserveToolInteractions(previous, next),
|
|
3029
|
+
keepPriorTranscripts,
|
|
2733
3030
|
);
|
|
2734
3031
|
if (kept.length === 0) return merged;
|
|
2735
3032
|
|
|
@@ -2779,6 +3076,7 @@ export class AgUiThreadController {
|
|
|
2779
3076
|
private reconcileSnapshotNestedContent(
|
|
2780
3077
|
previous: ThreadAssistantMessage["content"],
|
|
2781
3078
|
next: ThreadAssistantMessage["content"],
|
|
3079
|
+
keepPriorTranscripts: boolean,
|
|
2782
3080
|
): ThreadAssistantMessage["content"] {
|
|
2783
3081
|
const nestedIds = new Set<string>();
|
|
2784
3082
|
for (const part of previous) {
|
|
@@ -2828,10 +3126,92 @@ export class AgUiThreadController {
|
|
|
2828
3126
|
}
|
|
2829
3127
|
if (part.type !== "tool-call") return [part];
|
|
2830
3128
|
const prior = previousCalls.get(part.toolCallId);
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3129
|
+
if (prior?.messages === undefined) return [part];
|
|
3130
|
+
if (part.messages === undefined)
|
|
3131
|
+
return [{ ...part, messages: prior.messages }];
|
|
3132
|
+
if (part.messages.length === 0) return [part];
|
|
3133
|
+
return [
|
|
3134
|
+
{
|
|
3135
|
+
...part,
|
|
3136
|
+
messages: this.mergeNestedTranscripts(
|
|
3137
|
+
prior.messages,
|
|
3138
|
+
part.messages,
|
|
3139
|
+
keepPriorTranscripts,
|
|
3140
|
+
),
|
|
3141
|
+
},
|
|
3142
|
+
];
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
/**
|
|
3147
|
+
* Merges one tool call's nested transcripts message by message. The incoming
|
|
3148
|
+
* copy is the base for tool calls and reasoning; resolved results and calls
|
|
3149
|
+
* only the prior copy has carry over, and the prior text wins when it extends
|
|
3150
|
+
* the incoming text. Importing a snapshot keeps prior messages it lacks, a
|
|
3151
|
+
* prior status that is running or incomplete, and the prior status when the
|
|
3152
|
+
* snapshot waits on calls the merged content has already resolved.
|
|
3153
|
+
*/
|
|
3154
|
+
private mergeNestedTranscripts(
|
|
3155
|
+
prior: readonly ThreadMessage[],
|
|
3156
|
+
next: readonly ThreadMessage[],
|
|
3157
|
+
importingSnapshot: boolean,
|
|
3158
|
+
): readonly ThreadMessage[] {
|
|
3159
|
+
const priorById = new Map(prior.map((message) => [message.id, message]));
|
|
3160
|
+
const merged = next.map((message) => {
|
|
3161
|
+
const previous = priorById.get(message.id);
|
|
3162
|
+
if (previous?.role !== "assistant" || message.role !== "assistant")
|
|
3163
|
+
return message;
|
|
3164
|
+
let content = this.mergeSnapshotAssistantContent(
|
|
3165
|
+
previous.content,
|
|
3166
|
+
this.preserveToolResults(previous.content, message.content),
|
|
3167
|
+
message.content.some((part) => part.type === "reasoning"),
|
|
3168
|
+
message.content.some(isActivityPart),
|
|
3169
|
+
new Set(
|
|
3170
|
+
Array.from(
|
|
3171
|
+
iterateToolCallParts(previous.content),
|
|
3172
|
+
(call) => call.toolCallId,
|
|
3173
|
+
),
|
|
3174
|
+
),
|
|
3175
|
+
importingSnapshot,
|
|
3176
|
+
);
|
|
3177
|
+
const priorText = textOf(previous.content);
|
|
3178
|
+
const incomingText = textOf(message.content);
|
|
3179
|
+
if (
|
|
3180
|
+
priorText.length > incomingText.length &&
|
|
3181
|
+
priorText.startsWith(incomingText)
|
|
3182
|
+
) {
|
|
3183
|
+
content = replaceTextParts(content, previous.content);
|
|
3184
|
+
}
|
|
3185
|
+
const status = !importingSnapshot
|
|
3186
|
+
? message.status
|
|
3187
|
+
: previous.status.type === "running" ||
|
|
3188
|
+
previous.status.type === "incomplete" ||
|
|
3189
|
+
(message.status.type === "requires-action" &&
|
|
3190
|
+
message.status.reason === "tool-calls" &&
|
|
3191
|
+
!Array.from(iterateToolCallParts(content)).some(
|
|
3192
|
+
(call) => !isResolvedToolCall(call),
|
|
3193
|
+
))
|
|
3194
|
+
? previous.status
|
|
3195
|
+
: message.status;
|
|
3196
|
+
return {
|
|
3197
|
+
...message,
|
|
3198
|
+
status,
|
|
3199
|
+
content,
|
|
3200
|
+
metadata: {
|
|
3201
|
+
...message.metadata,
|
|
3202
|
+
custom: mergeAgUiNamespace(
|
|
3203
|
+
previous.metadata.custom,
|
|
3204
|
+
message.metadata.custom,
|
|
3205
|
+
),
|
|
3206
|
+
},
|
|
3207
|
+
};
|
|
2834
3208
|
});
|
|
3209
|
+
if (!importingSnapshot) return merged;
|
|
3210
|
+
const mergedById = new Map(merged.map((message) => [message.id, message]));
|
|
3211
|
+
return [
|
|
3212
|
+
...prior.map((message) => mergedById.get(message.id) ?? message),
|
|
3213
|
+
...merged.filter((message) => !priorById.has(message.id)),
|
|
3214
|
+
];
|
|
2835
3215
|
}
|
|
2836
3216
|
|
|
2837
3217
|
private mergeAssistantMetadata(
|
|
@@ -2872,6 +3252,15 @@ export class AgUiThreadController {
|
|
|
2872
3252
|
hasRunUserBoundary: boolean,
|
|
2873
3253
|
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
2874
3254
|
): string | undefined {
|
|
3255
|
+
if (
|
|
3256
|
+
event.type === "SUBAGENT_STARTED" &&
|
|
3257
|
+
!this.subagentParentToolCallIds.has(event.subagentRunId)
|
|
3258
|
+
) {
|
|
3259
|
+
this.subagentParentToolCallIds.set(
|
|
3260
|
+
event.subagentRunId,
|
|
3261
|
+
event.parentToolCallId,
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
2875
3264
|
switch (event.type) {
|
|
2876
3265
|
case "STATE_SNAPSHOT": {
|
|
2877
3266
|
this.dispatch({
|
|
@@ -2901,6 +3290,7 @@ export class AgUiThreadController {
|
|
|
2901
3290
|
break;
|
|
2902
3291
|
}
|
|
2903
3292
|
case "MESSAGES_SNAPSHOT": {
|
|
3293
|
+
aggregator.handle(event);
|
|
2904
3294
|
const previousHeadId = this.session.headId;
|
|
2905
3295
|
const activeAssistant =
|
|
2906
3296
|
activeAssistantId === undefined
|
|
@@ -2918,13 +3308,49 @@ export class AgUiThreadController {
|
|
|
2918
3308
|
reconcileSnapshot,
|
|
2919
3309
|
);
|
|
2920
3310
|
const headId = this.session.headId;
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3311
|
+
const head =
|
|
3312
|
+
headId === null
|
|
3313
|
+
? undefined
|
|
3314
|
+
: this.session.tryGetMessage(headId)?.message;
|
|
3315
|
+
const hasNewPendingToolCallAnswer =
|
|
3316
|
+
head?.role === "assistant" &&
|
|
3317
|
+
!runStartMessageIds.has(head.id) &&
|
|
3318
|
+
head.status?.type === "requires-action" &&
|
|
3319
|
+
head.status.reason === "tool-calls";
|
|
3320
|
+
if (
|
|
3321
|
+
headId !== previousHeadId &&
|
|
3322
|
+
(hasActiveText || hasNewPendingToolCallAnswer)
|
|
3323
|
+
) {
|
|
3324
|
+
const nextActiveAssistantId =
|
|
2927
3325
|
head?.role === "assistant" ? (headId ?? undefined) : undefined;
|
|
3326
|
+
if (
|
|
3327
|
+
activeAssistantId !== nextActiveAssistantId &&
|
|
3328
|
+
activeAssistantId
|
|
3329
|
+
) {
|
|
3330
|
+
const previous =
|
|
3331
|
+
this.session.tryGetMessage(activeAssistantId)?.message;
|
|
3332
|
+
if (
|
|
3333
|
+
previous?.role === "assistant" &&
|
|
3334
|
+
previous.status?.type === "running"
|
|
3335
|
+
) {
|
|
3336
|
+
this.updateAssistantMessage(activeAssistantId, {
|
|
3337
|
+
status: FALLBACK_USER_STATUS,
|
|
3338
|
+
content: settleNestedContent(
|
|
3339
|
+
previous.content,
|
|
3340
|
+
FALLBACK_USER_STATUS,
|
|
3341
|
+
),
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
activeAssistantId = nextActiveAssistantId;
|
|
3346
|
+
if (activeAssistantId && joinStrategy !== "concat-content") {
|
|
3347
|
+
aggregator.rebaseMessage(activeAssistantId);
|
|
3348
|
+
}
|
|
3349
|
+
if (hasNewPendingToolCallAnswer && head) {
|
|
3350
|
+
this.updateAssistantMessage(head.id, {
|
|
3351
|
+
status: { type: "running" },
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
2928
3354
|
}
|
|
2929
3355
|
this.updateActiveAssistantState(activeAssistantId);
|
|
2930
3356
|
break;
|
|
@@ -3027,6 +3453,14 @@ export class AgUiThreadController {
|
|
|
3027
3453
|
...(event.messageId
|
|
3028
3454
|
? { unstable_toolMessageId: event.messageId }
|
|
3029
3455
|
: {}),
|
|
3456
|
+
...(event.metadata !== undefined
|
|
3457
|
+
? {
|
|
3458
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3459
|
+
part.providerMetadata,
|
|
3460
|
+
event.metadata,
|
|
3461
|
+
),
|
|
3462
|
+
}
|
|
3463
|
+
: {}),
|
|
3030
3464
|
};
|
|
3031
3465
|
}
|
|
3032
3466
|
const { modelContent: _modelContent, ...partWithoutModelContent } =
|
|
@@ -3053,6 +3487,14 @@ export class AgUiThreadController {
|
|
|
3053
3487
|
...(event.messageId
|
|
3054
3488
|
? { unstable_toolMessageId: event.messageId }
|
|
3055
3489
|
: {}),
|
|
3490
|
+
...(event.metadata !== undefined
|
|
3491
|
+
? {
|
|
3492
|
+
providerMetadata: withAgUiPartMessageMetadata(
|
|
3493
|
+
part.providerMetadata,
|
|
3494
|
+
event.metadata,
|
|
3495
|
+
),
|
|
3496
|
+
}
|
|
3497
|
+
: {}),
|
|
3056
3498
|
};
|
|
3057
3499
|
});
|
|
3058
3500
|
if (!matchedToolCall) return message;
|
|
@@ -3063,7 +3505,8 @@ export class AgUiThreadController {
|
|
|
3063
3505
|
this.publishUpdate();
|
|
3064
3506
|
// Not maybeResumeAfterToolResults: the delivering run is already in
|
|
3065
3507
|
// flight, and a resume from the owner would reset the head past it.
|
|
3066
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3508
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3509
|
+
this.persistSettledAssistant(messageId);
|
|
3067
3510
|
}
|
|
3068
3511
|
|
|
3069
3512
|
private applyCrossRunActivitySnapshot(
|
|
@@ -3124,7 +3567,8 @@ export class AgUiThreadController {
|
|
|
3124
3567
|
|
|
3125
3568
|
if (!updated) return;
|
|
3126
3569
|
this.publishUpdate();
|
|
3127
|
-
this.maybeCompleteAfterToolResults(messageId);
|
|
3570
|
+
if (this.maybeCompleteAfterToolResults(messageId)) return;
|
|
3571
|
+
this.persistSettledAssistant(messageId);
|
|
3128
3572
|
}
|
|
3129
3573
|
|
|
3130
3574
|
private importMessagesSnapshot(
|
|
@@ -3143,14 +3587,24 @@ export class AgUiThreadController {
|
|
|
3143
3587
|
const activeAssistantParentId = activeAssistantItem?.parentId;
|
|
3144
3588
|
const activeAssistant =
|
|
3145
3589
|
activeMessage?.role === "assistant" ? activeMessage : undefined;
|
|
3146
|
-
const normalized =
|
|
3147
|
-
|
|
3148
|
-
|
|
3590
|
+
const normalized = fromAgUiSnapshotMessages(
|
|
3591
|
+
rawMessages,
|
|
3592
|
+
{ showThinking: this.showThinking },
|
|
3593
|
+
this.subagentParentToolCallIds,
|
|
3594
|
+
);
|
|
3149
3595
|
const converted: ThreadMessage[] = [];
|
|
3150
3596
|
for (const message of normalized) {
|
|
3151
3597
|
try {
|
|
3598
|
+
const previous = message.id
|
|
3599
|
+
? this.session.tryGetMessage(message.id)?.message
|
|
3600
|
+
: undefined;
|
|
3601
|
+
const fallbackStatus =
|
|
3602
|
+
previous?.role === "assistant" &&
|
|
3603
|
+
previous.status?.type === "running"
|
|
3604
|
+
? previous.status
|
|
3605
|
+
: FALLBACK_USER_STATUS;
|
|
3152
3606
|
converted.push(
|
|
3153
|
-
fromThreadMessageLike(message, generateId(),
|
|
3607
|
+
fromThreadMessageLike(message, generateId(), fallbackStatus),
|
|
3154
3608
|
);
|
|
3155
3609
|
} catch (error) {
|
|
3156
3610
|
this.logger.error?.(
|
|
@@ -3178,14 +3632,40 @@ export class AgUiThreadController {
|
|
|
3178
3632
|
existing?.role === "assistant" ? existing : undefined;
|
|
3179
3633
|
const snapshot =
|
|
3180
3634
|
reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
|
|
3635
|
+
const preservesRunningStatus =
|
|
3636
|
+
convertedMessage.id === activeAssistantId &&
|
|
3637
|
+
previous?.status?.type === "running" &&
|
|
3638
|
+
snapshot.status?.type === "requires-action" &&
|
|
3639
|
+
snapshot.status.reason === "tool-calls";
|
|
3640
|
+
const previousToolCallIds = new Set(
|
|
3641
|
+
Array.from(
|
|
3642
|
+
iterateToolCallParts(previous?.content ?? []),
|
|
3643
|
+
(call) => call.toolCallId,
|
|
3644
|
+
),
|
|
3645
|
+
);
|
|
3646
|
+
const preservesSettledStatus =
|
|
3647
|
+
convertedMessage.id !== activeAssistantId &&
|
|
3648
|
+
this.isTerminalStatus(previous?.status) &&
|
|
3649
|
+
snapshot.status?.type === "requires-action" &&
|
|
3650
|
+
snapshot.status.reason === "tool-calls" &&
|
|
3651
|
+
Array.from(iterateToolCallParts(snapshot.content)).every(
|
|
3652
|
+
(call) =>
|
|
3653
|
+
isResolvedToolCall(call) ||
|
|
3654
|
+
previousToolCallIds.has(call.toolCallId),
|
|
3655
|
+
);
|
|
3181
3656
|
converted[index] = previous
|
|
3182
3657
|
? {
|
|
3183
3658
|
...snapshot,
|
|
3659
|
+
...(preservesRunningStatus || preservesSettledStatus
|
|
3660
|
+
? { status: previous.status }
|
|
3661
|
+
: {}),
|
|
3184
3662
|
content: this.mergeSnapshotAssistantContent(
|
|
3185
3663
|
previous.content,
|
|
3186
3664
|
snapshot.content,
|
|
3187
3665
|
snapshotHasReasoning,
|
|
3188
3666
|
snapshotHasActivity,
|
|
3667
|
+
new Set(),
|
|
3668
|
+
true,
|
|
3189
3669
|
),
|
|
3190
3670
|
}
|
|
3191
3671
|
: snapshot;
|
|
@@ -3348,6 +3828,19 @@ export class AgUiThreadController {
|
|
|
3348
3828
|
this.assistantHistoryParents.set(messageId, parentId);
|
|
3349
3829
|
}
|
|
3350
3830
|
|
|
3831
|
+
private persistSettledAssistant(messageId: string): void {
|
|
3832
|
+
const item = this.session.tryGetMessage(messageId);
|
|
3833
|
+
if (!this.history?.update || !item) return;
|
|
3834
|
+
if (!this.isTerminalStatus(item.message.status)) return;
|
|
3835
|
+
if (
|
|
3836
|
+
!this.persistedHistoryIds.has(messageId) &&
|
|
3837
|
+
this.historyWrites.pending(messageId) === undefined
|
|
3838
|
+
)
|
|
3839
|
+
return;
|
|
3840
|
+
this.markPendingAssistantHistory(messageId, item.parentId);
|
|
3841
|
+
this.persistAssistantHistory(messageId);
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3351
3844
|
private persistAssistantHistory(messageId: string) {
|
|
3352
3845
|
const history = this.history;
|
|
3353
3846
|
if (!history) return;
|
|
@@ -3360,7 +3853,9 @@ export class AgUiThreadController {
|
|
|
3360
3853
|
const update = history.update;
|
|
3361
3854
|
const shouldUpdate =
|
|
3362
3855
|
update !== undefined &&
|
|
3363
|
-
(wasPersisted ||
|
|
3856
|
+
(wasPersisted ||
|
|
3857
|
+
this.snapshotHistoryIds.has(messageId) ||
|
|
3858
|
+
this.historyWrites.pending(messageId) !== undefined);
|
|
3364
3859
|
|
|
3365
3860
|
if (shouldUpdate) {
|
|
3366
3861
|
const write = this.historyWrites.chain(messageId, () =>
|