@assistant-ui/react-ag-ui 0.1.4 → 0.1.6
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/runtime/AgUiThreadController.d.ts +2 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +93 -13
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +37 -31
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +363 -218
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +1 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +2 -2
- package/src/runtime/AgUiThreadController.disconnect.test.ts +198 -3
- package/src/runtime/AgUiThreadController.replay.test.ts +57 -0
- package/src/runtime/AgUiThreadController.snapshot-head-history.test.ts +34 -2
- package/src/runtime/AgUiThreadController.snapshot-head-move.test.ts +510 -0
- package/src/runtime/AgUiThreadController.ts +190 -28
- package/src/runtime/adapter/run-aggregator.routing.test.ts +359 -0
- package/src/runtime/adapter/run-aggregator.ts +539 -258
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +85 -7
- package/src/useAgUiRuntime.ts +1 -0
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
isPlainObject,
|
|
61
61
|
RunAggregator,
|
|
62
62
|
tryParseJSON,
|
|
63
|
+
type RetainedMessageState,
|
|
63
64
|
} from "./adapter/run-aggregator";
|
|
64
65
|
import {
|
|
65
66
|
fromAgUiSnapshotMessages,
|
|
@@ -250,6 +251,17 @@ type CoreOptions = {
|
|
|
250
251
|
|
|
251
252
|
const FALLBACK_USER_STATUS = { type: "complete", reason: "unknown" } as const;
|
|
252
253
|
|
|
254
|
+
const snapshotRecordToolCallIds = (record: unknown): Set<string> => {
|
|
255
|
+
const toolCalls = (record as { toolCalls?: unknown } | undefined)?.toolCalls;
|
|
256
|
+
return new Set(
|
|
257
|
+
Array.isArray(toolCalls)
|
|
258
|
+
? toolCalls
|
|
259
|
+
.map((call: { id?: unknown } | undefined) => call?.id)
|
|
260
|
+
.filter((id): id is string => typeof id === "string")
|
|
261
|
+
: [],
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
253
265
|
const textOf = (content: ThreadAssistantMessage["content"]) =>
|
|
254
266
|
content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("");
|
|
255
267
|
|
|
@@ -702,6 +714,17 @@ export class AgUiThreadController {
|
|
|
702
714
|
await this.startRun(parentId, config.runConfig);
|
|
703
715
|
}
|
|
704
716
|
|
|
717
|
+
/** Deletes a message that has no replies under it, and its stored history entry. */
|
|
718
|
+
async deleteMessage(messageId: string): Promise<void> {
|
|
719
|
+
const item = this.session.tryGetMessage(messageId);
|
|
720
|
+
if (!item) return;
|
|
721
|
+
const parentId = item.parentId ?? null;
|
|
722
|
+
this.session.deleteMessage(messageId, parentId);
|
|
723
|
+
this.snapshotHistoryIds.delete(messageId);
|
|
724
|
+
this.publishUpdate();
|
|
725
|
+
await this.history?.delete?.([{ parentId, message: item.message }]);
|
|
726
|
+
}
|
|
727
|
+
|
|
705
728
|
async cancel(): Promise<void> {
|
|
706
729
|
this.dispatch({
|
|
707
730
|
type: "historyMutationAdvanced",
|
|
@@ -1757,6 +1780,12 @@ export class AgUiThreadController {
|
|
|
1757
1780
|
let settlingRun = false;
|
|
1758
1781
|
let latestSnapshot: ThreadAssistantMessage | undefined;
|
|
1759
1782
|
let snapshotToolCallIds = new Set<string>();
|
|
1783
|
+
const snapshotResultIds = new Set<string>();
|
|
1784
|
+
let announcingMove = false;
|
|
1785
|
+
const retainedSnapshots = new Map<
|
|
1786
|
+
string,
|
|
1787
|
+
{ message: ThreadAssistantMessage; toolCallIds: ReadonlySet<string> }
|
|
1788
|
+
>();
|
|
1760
1789
|
const ensureAssistant = (allowRecreate = false): string => {
|
|
1761
1790
|
const cached = assistantMessageId;
|
|
1762
1791
|
if (cached !== undefined && this.session.tryGetMessage(cached))
|
|
@@ -1823,6 +1852,8 @@ export class AgUiThreadController {
|
|
|
1823
1852
|
}
|
|
1824
1853
|
for (const [messageId, openToolCallIds] of heldMessages) {
|
|
1825
1854
|
const existing = this.session.tryGetMessage(messageId);
|
|
1855
|
+
if (!canWaitForTools && this.isTerminalStatus(existing?.message.status))
|
|
1856
|
+
continue;
|
|
1826
1857
|
if (existing)
|
|
1827
1858
|
this.markPendingAssistantHistory(messageId, existing.parentId);
|
|
1828
1859
|
let requiresToolResult = false;
|
|
@@ -2155,6 +2186,85 @@ export class AgUiThreadController {
|
|
|
2155
2186
|
}
|
|
2156
2187
|
};
|
|
2157
2188
|
|
|
2189
|
+
// A message a snapshot moved the head past keeps streaming until nothing
|
|
2190
|
+
// on it is open; held, it settles with the run.
|
|
2191
|
+
const applyRetainedUpdate = (
|
|
2192
|
+
update: ChatModelRunResult,
|
|
2193
|
+
{ messageId, streaming, openToolCallIds }: RetainedMessageState,
|
|
2194
|
+
) => {
|
|
2195
|
+
const item = this.session.tryGetMessage(messageId);
|
|
2196
|
+
if (item?.message.role !== "assistant") return;
|
|
2197
|
+
this.markPendingAssistantHistory(messageId, item.parentId);
|
|
2198
|
+
let snapshot = retainedSnapshots.get(messageId);
|
|
2199
|
+
if (!snapshot) {
|
|
2200
|
+
snapshot = {
|
|
2201
|
+
message: item.message as ThreadAssistantMessage,
|
|
2202
|
+
toolCallIds: new Set(
|
|
2203
|
+
Array.from(
|
|
2204
|
+
iterateToolCallParts(item.message.content),
|
|
2205
|
+
(part) => part.toolCallId,
|
|
2206
|
+
),
|
|
2207
|
+
),
|
|
2208
|
+
};
|
|
2209
|
+
retainedSnapshots.set(messageId, snapshot);
|
|
2210
|
+
}
|
|
2211
|
+
const content =
|
|
2212
|
+
update.content !== undefined
|
|
2213
|
+
? this.mergeSnapshotAssistantContent(
|
|
2214
|
+
snapshot.message.content,
|
|
2215
|
+
update.content as ThreadAssistantMessage["content"],
|
|
2216
|
+
update.content.some((part) => part.type === "reasoning"),
|
|
2217
|
+
update.content.some(isActivityPart),
|
|
2218
|
+
snapshot.toolCallIds,
|
|
2219
|
+
)
|
|
2220
|
+
: update.content;
|
|
2221
|
+
// A call only the snapshot carried stays open until a result arrives.
|
|
2222
|
+
const unresolved = [
|
|
2223
|
+
...new Set([
|
|
2224
|
+
...openToolCallIds,
|
|
2225
|
+
...Array.from(iterateToolCallParts(content ?? item.message.content))
|
|
2226
|
+
.filter(
|
|
2227
|
+
(part) =>
|
|
2228
|
+
snapshot.toolCallIds.has(part.toolCallId) &&
|
|
2229
|
+
!isResolvedToolCall(part),
|
|
2230
|
+
)
|
|
2231
|
+
.map((part) => part.toolCallId),
|
|
2232
|
+
]),
|
|
2233
|
+
].filter((id) => !snapshotResultIds.has(id));
|
|
2234
|
+
// A terminal event settles a held message with the run instead.
|
|
2235
|
+
const held =
|
|
2236
|
+
streaming ||
|
|
2237
|
+
unresolved.length > 0 ||
|
|
2238
|
+
(settlingRun && heldMessages.has(messageId));
|
|
2239
|
+
if (held) heldMessages.set(messageId, new Set(unresolved));
|
|
2240
|
+
else heldMessages.delete(messageId);
|
|
2241
|
+
this.updateAssistantMessage(messageId, {
|
|
2242
|
+
...update,
|
|
2243
|
+
...(content !== undefined ? { content } : {}),
|
|
2244
|
+
status: held
|
|
2245
|
+
? { type: "running" }
|
|
2246
|
+
: { type: "complete", reason: "unknown" },
|
|
2247
|
+
});
|
|
2248
|
+
const current = this.session.tryGetMessage(messageId)?.message;
|
|
2249
|
+
if (current?.role === "assistant")
|
|
2250
|
+
retainedSnapshots.set(messageId, { ...snapshot, message: current });
|
|
2251
|
+
// The run is still streaming, which the message it streams into shows
|
|
2252
|
+
// as soon as any event arrives after the move.
|
|
2253
|
+
const head =
|
|
2254
|
+
assistantMessageId === undefined
|
|
2255
|
+
? undefined
|
|
2256
|
+
: this.session.tryGetMessage(assistantMessageId)?.message;
|
|
2257
|
+
if (
|
|
2258
|
+
!settlingRun &&
|
|
2259
|
+
!announcingMove &&
|
|
2260
|
+
head?.role === "assistant" &&
|
|
2261
|
+
head.status?.type !== "running" &&
|
|
2262
|
+
!runStartMessageIds.has(head.id) &&
|
|
2263
|
+
!aggregator.isRetained(head.id)
|
|
2264
|
+
)
|
|
2265
|
+
this.updateAssistantMessage(head.id, { status: { type: "running" } });
|
|
2266
|
+
};
|
|
2267
|
+
|
|
2158
2268
|
const abortController = new AbortController();
|
|
2159
2269
|
const abortSignal = abortController.signal;
|
|
2160
2270
|
const aggregator = new RunAggregator({
|
|
@@ -2257,10 +2367,24 @@ export class AgUiThreadController {
|
|
|
2257
2367
|
? current
|
|
2258
2368
|
: undefined;
|
|
2259
2369
|
},
|
|
2370
|
+
onRetainedMessage: applyRetainedUpdate,
|
|
2371
|
+
canRetainMessage: (messageId) =>
|
|
2372
|
+
messageId !== assistantMessageId &&
|
|
2373
|
+
messageId !== this.session.headId &&
|
|
2374
|
+
!runStartMessageIds.has(messageId) &&
|
|
2375
|
+
this.session.tryGetMessage(messageId)?.message.role === "assistant",
|
|
2260
2376
|
onServerMessageId: (serverId) => {
|
|
2261
2377
|
adoptServerMessageId(serverId, false);
|
|
2262
2378
|
},
|
|
2263
2379
|
onTextMessageStart: (serverId, openToolCallIds) => {
|
|
2380
|
+
const outgoingId =
|
|
2381
|
+
assistantMessageId !== undefined &&
|
|
2382
|
+
!assistantCollided &&
|
|
2383
|
+
!runStartMessageIds.has(assistantMessageId) &&
|
|
2384
|
+
!replayContexts.has(assistantMessageId) &&
|
|
2385
|
+
this.session.hasMessage(assistantMessageId)
|
|
2386
|
+
? assistantMessageId
|
|
2387
|
+
: undefined;
|
|
2264
2388
|
const owners = this.getTrailingAssistantTurn().filter((message) =>
|
|
2265
2389
|
replayContexts.has(message.id),
|
|
2266
2390
|
);
|
|
@@ -2282,6 +2406,7 @@ export class AgUiThreadController {
|
|
|
2282
2406
|
}
|
|
2283
2407
|
frameOwnerIds.clear();
|
|
2284
2408
|
adoptServerMessageId(serverId, true, frameOpenIds);
|
|
2409
|
+
return outgoingId;
|
|
2285
2410
|
},
|
|
2286
2411
|
});
|
|
2287
2412
|
const settleRunSubagents = (status: MessageStatus) => {
|
|
@@ -2334,6 +2459,17 @@ export class AgUiThreadController {
|
|
|
2334
2459
|
openToolCallIds.delete(event.toolCallId);
|
|
2335
2460
|
}
|
|
2336
2461
|
}
|
|
2462
|
+
// A result a snapshot delivers answers its call the way a streamed
|
|
2463
|
+
// TOOL_CALL_RESULT does.
|
|
2464
|
+
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
2465
|
+
for (const record of event.messages) {
|
|
2466
|
+
if (record?.role !== "tool" || typeof record.toolCallId !== "string")
|
|
2467
|
+
continue;
|
|
2468
|
+
snapshotResultIds.add(record.toolCallId);
|
|
2469
|
+
for (const openToolCallIds of heldMessages.values())
|
|
2470
|
+
openToolCallIds.delete(record.toolCallId);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2337
2473
|
settlingRun =
|
|
2338
2474
|
event.type === "RUN_FINISHED" ||
|
|
2339
2475
|
event.type === "RUN_ERROR" ||
|
|
@@ -2349,6 +2485,7 @@ export class AgUiThreadController {
|
|
|
2349
2485
|
joinStrategy,
|
|
2350
2486
|
runStartMessageIds,
|
|
2351
2487
|
hasRunUserBoundary,
|
|
2488
|
+
heldMessages,
|
|
2352
2489
|
candidateContexts.size ? reconcileReplaySnapshot : undefined,
|
|
2353
2490
|
);
|
|
2354
2491
|
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
@@ -2395,18 +2532,31 @@ export class AgUiThreadController {
|
|
|
2395
2532
|
);
|
|
2396
2533
|
latestSnapshot =
|
|
2397
2534
|
imported?.role === "assistant" && source ? imported : undefined;
|
|
2398
|
-
snapshotToolCallIds =
|
|
2399
|
-
Array.isArray(source?.toolCalls)
|
|
2400
|
-
? source.toolCalls
|
|
2401
|
-
.map((call: { id?: unknown }) => call.id)
|
|
2402
|
-
.filter((id: unknown): id is string => typeof id === "string")
|
|
2403
|
-
: [],
|
|
2404
|
-
);
|
|
2535
|
+
snapshotToolCallIds = snapshotRecordToolCallIds(source);
|
|
2405
2536
|
if (!replayEnded && assistantMessageId === undefined) {
|
|
2406
2537
|
const tail = this.session.getMessages().at(-1);
|
|
2407
2538
|
snapshotAssistantId =
|
|
2408
2539
|
tail?.role === "assistant" ? tail.id : undefined;
|
|
2409
2540
|
}
|
|
2541
|
+
for (const record of event.messages) {
|
|
2542
|
+
if (
|
|
2543
|
+
record?.role !== "assistant" ||
|
|
2544
|
+
!aggregator.isRetained(record.id)
|
|
2545
|
+
)
|
|
2546
|
+
continue;
|
|
2547
|
+
const message = this.session.tryGetMessage(record.id)?.message;
|
|
2548
|
+
if (message?.role !== "assistant") continue;
|
|
2549
|
+
retainedSnapshots.set(record.id, {
|
|
2550
|
+
message,
|
|
2551
|
+
toolCallIds: snapshotRecordToolCallIds(record),
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
announcingMove = true;
|
|
2555
|
+
try {
|
|
2556
|
+
aggregator.announceRetained();
|
|
2557
|
+
} finally {
|
|
2558
|
+
announcingMove = false;
|
|
2559
|
+
}
|
|
2410
2560
|
}
|
|
2411
2561
|
if (event.type === "RUN_ERROR") {
|
|
2412
2562
|
settleRunSubagents({
|
|
@@ -2505,10 +2655,11 @@ export class AgUiThreadController {
|
|
|
2505
2655
|
const current = assistantMessageId
|
|
2506
2656
|
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2507
2657
|
: undefined;
|
|
2658
|
+
const currentRunning =
|
|
2659
|
+
current?.role === "assistant" && current.status?.type === "running";
|
|
2508
2660
|
if (
|
|
2509
2661
|
this.abortController === abortController &&
|
|
2510
|
-
|
|
2511
|
-
current.status?.type === "running"
|
|
2662
|
+
(currentRunning || heldMessages.size > 0)
|
|
2512
2663
|
) {
|
|
2513
2664
|
persistRunAssistants(true);
|
|
2514
2665
|
// Finalization is transport cleanup, not proof of a terminal event.
|
|
@@ -2518,10 +2669,11 @@ export class AgUiThreadController {
|
|
|
2518
2669
|
error: "The connection ended before the answer finished.",
|
|
2519
2670
|
};
|
|
2520
2671
|
settleHeldMessages(status);
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2672
|
+
if (currentRunning)
|
|
2673
|
+
applyUpdate({
|
|
2674
|
+
status,
|
|
2675
|
+
content: settleNestedContent(current.content, status),
|
|
2676
|
+
});
|
|
2525
2677
|
}
|
|
2526
2678
|
if (this.abortController === abortController && !runFinished) {
|
|
2527
2679
|
// The run ended without RUN_FINISHED, so no sub-agent call it left
|
|
@@ -3360,6 +3512,7 @@ export class AgUiThreadController {
|
|
|
3360
3512
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
3361
3513
|
runStartMessageIds: ReadonlySet<string>,
|
|
3362
3514
|
hasRunUserBoundary: boolean,
|
|
3515
|
+
heldMessages: ReadonlyMap<string, unknown>,
|
|
3363
3516
|
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
3364
3517
|
): string | undefined {
|
|
3365
3518
|
if (
|
|
@@ -3415,6 +3568,7 @@ export class AgUiThreadController {
|
|
|
3415
3568
|
joinStrategy,
|
|
3416
3569
|
runStartMessageIds,
|
|
3417
3570
|
hasRunUserBoundary,
|
|
3571
|
+
heldMessages,
|
|
3418
3572
|
reconcileSnapshot,
|
|
3419
3573
|
);
|
|
3420
3574
|
const headId = this.session.headId;
|
|
@@ -3433,17 +3587,22 @@ export class AgUiThreadController {
|
|
|
3433
3587
|
) {
|
|
3434
3588
|
const nextActiveAssistantId =
|
|
3435
3589
|
head?.role === "assistant" ? (headId ?? undefined) : undefined;
|
|
3436
|
-
|
|
3437
|
-
activeAssistantId !== nextActiveAssistantId
|
|
3438
|
-
|
|
3439
|
-
|
|
3590
|
+
const previousActiveId =
|
|
3591
|
+
activeAssistantId !== nextActiveAssistantId
|
|
3592
|
+
? activeAssistantId
|
|
3593
|
+
: undefined;
|
|
3594
|
+
const retained =
|
|
3595
|
+
nextActiveAssistantId !== undefined &&
|
|
3596
|
+
joinStrategy !== "concat-content" &&
|
|
3597
|
+
aggregator.rebaseMessage(nextActiveAssistantId, previousActiveId);
|
|
3598
|
+
if (previousActiveId && !retained) {
|
|
3440
3599
|
const previous =
|
|
3441
|
-
this.session.tryGetMessage(
|
|
3600
|
+
this.session.tryGetMessage(previousActiveId)?.message;
|
|
3442
3601
|
if (
|
|
3443
3602
|
previous?.role === "assistant" &&
|
|
3444
3603
|
previous.status?.type === "running"
|
|
3445
3604
|
) {
|
|
3446
|
-
this.updateAssistantMessage(
|
|
3605
|
+
this.updateAssistantMessage(previousActiveId, {
|
|
3447
3606
|
status: FALLBACK_USER_STATUS,
|
|
3448
3607
|
content: settleNestedContent(
|
|
3449
3608
|
previous.content,
|
|
@@ -3453,14 +3612,15 @@ export class AgUiThreadController {
|
|
|
3453
3612
|
}
|
|
3454
3613
|
}
|
|
3455
3614
|
activeAssistantId = nextActiveAssistantId;
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3615
|
+
}
|
|
3616
|
+
if (
|
|
3617
|
+
hasNewPendingToolCallAnswer &&
|
|
3618
|
+
head &&
|
|
3619
|
+
head.id === activeAssistantId
|
|
3620
|
+
) {
|
|
3621
|
+
this.updateAssistantMessage(head.id, {
|
|
3622
|
+
status: { type: "running" },
|
|
3623
|
+
});
|
|
3464
3624
|
}
|
|
3465
3625
|
this.updateActiveAssistantState(activeAssistantId);
|
|
3466
3626
|
break;
|
|
@@ -3687,6 +3847,7 @@ export class AgUiThreadController {
|
|
|
3687
3847
|
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
3688
3848
|
runStartMessageIds: ReadonlySet<string>,
|
|
3689
3849
|
hasRunUserBoundary: boolean,
|
|
3850
|
+
heldMessages: ReadonlyMap<string, unknown>,
|
|
3690
3851
|
reconcileSnapshot?: SnapshotMessageReconciler,
|
|
3691
3852
|
) {
|
|
3692
3853
|
try {
|
|
@@ -3743,7 +3904,8 @@ export class AgUiThreadController {
|
|
|
3743
3904
|
const snapshot =
|
|
3744
3905
|
reconcileSnapshot?.(convertedMessage, previous) ?? convertedMessage;
|
|
3745
3906
|
const preservesRunningStatus =
|
|
3746
|
-
convertedMessage.id === activeAssistantId
|
|
3907
|
+
(convertedMessage.id === activeAssistantId ||
|
|
3908
|
+
heldMessages.has(convertedMessage.id)) &&
|
|
3747
3909
|
previous?.status?.type === "running" &&
|
|
3748
3910
|
snapshot.status?.type === "requires-action" &&
|
|
3749
3911
|
snapshot.status.reason === "tool-calls";
|