@assistant-ui/react-ag-ui 0.1.3 → 0.1.5
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 +5 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +106 -10
- package/dist/runtime/AgUiThreadController.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.snapshot-head-history.test.ts +226 -0
- package/src/runtime/AgUiThreadController.snapshot-nested-settle.test.ts +210 -0
- package/src/runtime/AgUiThreadController.ts +148 -15
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +85 -7
- package/src/useAgUiRuntime.ts +1 -0
|
@@ -300,22 +300,35 @@ const replaceTextParts = (
|
|
|
300
300
|
return result;
|
|
301
301
|
};
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Settles nested sub-agent messages with `status`. Without `runSubagentIds`,
|
|
305
|
+
* only running messages settle. With it, only that run's sub-agent messages
|
|
306
|
+
* settle, and an incomplete `status` also settles one still waiting on its own
|
|
307
|
+
* tool call: the run that could answer it has ended.
|
|
308
|
+
*/
|
|
303
309
|
const settleNestedContent = (
|
|
304
310
|
content: ThreadAssistantMessage["content"],
|
|
305
311
|
status: MessageStatus,
|
|
312
|
+
runSubagentIds?: ReadonlySet<string>,
|
|
306
313
|
) =>
|
|
307
|
-
mapToolCallPartsDeep(content, (part) =>
|
|
308
|
-
part.messages
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
mapToolCallPartsDeep(content, (part) => {
|
|
315
|
+
if (!part.messages) return part;
|
|
316
|
+
let changed = false;
|
|
317
|
+
const messages = part.messages.map((message) => {
|
|
318
|
+
if (message.role !== "assistant") return message;
|
|
319
|
+
const settles = !runSubagentIds
|
|
320
|
+
? message.status?.type === "running"
|
|
321
|
+
: runSubagentIds.has(message.id) &&
|
|
322
|
+
(message.status?.type === "running" ||
|
|
323
|
+
(status.type === "incomplete" &&
|
|
324
|
+
message.status?.type === "requires-action" &&
|
|
325
|
+
message.status.reason === "tool-calls"));
|
|
326
|
+
if (!settles) return message;
|
|
327
|
+
changed = true;
|
|
328
|
+
return { ...message, status };
|
|
329
|
+
});
|
|
330
|
+
return changed ? { ...part, messages } : part;
|
|
331
|
+
}).content;
|
|
319
332
|
|
|
320
333
|
type AgUiRuntimeCallbackName = "onError" | "onCancel";
|
|
321
334
|
|
|
@@ -392,6 +405,8 @@ export class AgUiThreadController {
|
|
|
392
405
|
// The agent that started the active run. updateOptions can swap this.agent
|
|
393
406
|
// mid-run, and cancelling has to reach the agent holding the live request.
|
|
394
407
|
private activeRunAgent: AbstractAgent | null = null;
|
|
408
|
+
// Counts runs started, so a finished run can tell a newer one began.
|
|
409
|
+
private runCount = 0;
|
|
395
410
|
private history: ThreadHistoryAdapter | undefined;
|
|
396
411
|
private readonly assistantHistoryParents = new Map<string, string | null>();
|
|
397
412
|
private readonly subagentParentToolCallIds = new Map<
|
|
@@ -400,6 +415,9 @@ export class AgUiThreadController {
|
|
|
400
415
|
>();
|
|
401
416
|
private readonly snapshotHistoryIds = new Set<string>();
|
|
402
417
|
private readonly persistedHistoryIds = new Set<string>();
|
|
418
|
+
// Ids whose append has not succeeded yet. A write queued behind that append
|
|
419
|
+
// runs as an append too, since an update may not move the stored head.
|
|
420
|
+
private readonly unappendedHistoryIds = new Set<string>();
|
|
403
421
|
private readonly historyWrites = createHistoryWriteChain();
|
|
404
422
|
private _historyImportPromise: Promise<void> | undefined;
|
|
405
423
|
private _loadPromise: Promise<void> | undefined;
|
|
@@ -525,6 +543,7 @@ export class AgUiThreadController {
|
|
|
525
543
|
this.subagentParentToolCallIds.clear();
|
|
526
544
|
this.snapshotHistoryIds.clear();
|
|
527
545
|
this.persistedHistoryIds.clear();
|
|
546
|
+
this.unappendedHistoryIds.clear();
|
|
528
547
|
for (const { message } of repo.messages) {
|
|
529
548
|
this.persistedHistoryIds.add(message.id);
|
|
530
549
|
}
|
|
@@ -683,6 +702,17 @@ export class AgUiThreadController {
|
|
|
683
702
|
await this.startRun(parentId, config.runConfig);
|
|
684
703
|
}
|
|
685
704
|
|
|
705
|
+
/** Deletes a message that has no replies under it, and its stored history entry. */
|
|
706
|
+
async deleteMessage(messageId: string): Promise<void> {
|
|
707
|
+
const item = this.session.tryGetMessage(messageId);
|
|
708
|
+
if (!item) return;
|
|
709
|
+
const parentId = item.parentId ?? null;
|
|
710
|
+
this.session.deleteMessage(messageId, parentId);
|
|
711
|
+
this.snapshotHistoryIds.delete(messageId);
|
|
712
|
+
this.publishUpdate();
|
|
713
|
+
await this.history?.delete?.([{ parentId, message: item.message }]);
|
|
714
|
+
}
|
|
715
|
+
|
|
686
716
|
async cancel(): Promise<void> {
|
|
687
717
|
this.dispatch({
|
|
688
718
|
type: "historyMutationAdvanced",
|
|
@@ -1721,6 +1751,8 @@ export class AgUiThreadController {
|
|
|
1721
1751
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
1722
1752
|
let assistantMessageId: string | undefined;
|
|
1723
1753
|
let snapshotAssistantId: string | undefined;
|
|
1754
|
+
const importedRunAssistants = new Set<string>();
|
|
1755
|
+
const runSubagentIds = new Set<string>();
|
|
1724
1756
|
let replayInterruptMessageId: string | undefined;
|
|
1725
1757
|
// A snapshot the preserve gate declines still evicts the in-flight
|
|
1726
1758
|
// assistant; recreating under the cached id on the next content-bearing
|
|
@@ -2263,9 +2295,38 @@ export class AgUiThreadController {
|
|
|
2263
2295
|
adoptServerMessageId(serverId, true, frameOpenIds);
|
|
2264
2296
|
},
|
|
2265
2297
|
});
|
|
2298
|
+
const settleRunSubagents = (status: MessageStatus) => {
|
|
2299
|
+
if (runSubagentIds.size === 0) return;
|
|
2300
|
+
for (const message of this.session.getMessages()) {
|
|
2301
|
+
if (message.role !== "assistant") continue;
|
|
2302
|
+
const content = settleNestedContent(
|
|
2303
|
+
message.content,
|
|
2304
|
+
status,
|
|
2305
|
+
runSubagentIds,
|
|
2306
|
+
);
|
|
2307
|
+
if (content === message.content) continue;
|
|
2308
|
+
this.updateAssistantMessage(message.id, { content });
|
|
2309
|
+
this.persistSettledAssistant(message.id);
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
// Writes this run's other assistants in branch order: before the answer
|
|
2313
|
+
// settles, the ones above it; after, the ones below. An adapter that
|
|
2314
|
+
// moves its head on each first write then ends on the branch's tail.
|
|
2315
|
+
const persistRunAssistants = (aboveAnswer: boolean) => {
|
|
2316
|
+
for (const message of this.session.getMessages()) {
|
|
2317
|
+
if (message.id === assistantMessageId) {
|
|
2318
|
+
if (aboveAnswer) return;
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (message.role === "assistant" && !runStartMessageIds.has(message.id))
|
|
2322
|
+
this.persistAssistantHistory(message.id);
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2266
2325
|
let runFinished = false;
|
|
2267
2326
|
const dispatch = (event: AgUiEvent) => {
|
|
2268
2327
|
if (this.abortController !== abortController) return;
|
|
2328
|
+
if (event.type === "SUBAGENT_STARTED")
|
|
2329
|
+
runSubagentIds.add(event.subagentRunId);
|
|
2269
2330
|
if (event.type === "RUN_FINISHED") {
|
|
2270
2331
|
runFinished = true;
|
|
2271
2332
|
if (event.outcome?.type === "interrupt")
|
|
@@ -2288,6 +2349,7 @@ export class AgUiThreadController {
|
|
|
2288
2349
|
event.type === "RUN_FINISHED" ||
|
|
2289
2350
|
event.type === "RUN_ERROR" ||
|
|
2290
2351
|
event.type === "RUN_CANCELLED";
|
|
2352
|
+
if (settlingRun) persistRunAssistants(true);
|
|
2291
2353
|
const beforeSnapshot =
|
|
2292
2354
|
event.type === "MESSAGES_SNAPSHOT" ? this.session.getMessages() : [];
|
|
2293
2355
|
try {
|
|
@@ -2301,6 +2363,31 @@ export class AgUiThreadController {
|
|
|
2301
2363
|
candidateContexts.size ? reconcileReplaySnapshot : undefined,
|
|
2302
2364
|
);
|
|
2303
2365
|
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
2366
|
+
// Assistants a snapshot adds belong to the run's history even when
|
|
2367
|
+
// the run never streams into them. Every import resets the pending
|
|
2368
|
+
// marks, so each snapshot marks them again. The import also routes
|
|
2369
|
+
// every message to update; a run assistant this client never wrote
|
|
2370
|
+
// gets an append first, which is what moves the stored head.
|
|
2371
|
+
const previousIds = new Set(beforeSnapshot.map(({ id }) => id));
|
|
2372
|
+
for (const message of this.session.getMessages()) {
|
|
2373
|
+
if (
|
|
2374
|
+
message.role !== "assistant" ||
|
|
2375
|
+
runStartMessageIds.has(message.id)
|
|
2376
|
+
)
|
|
2377
|
+
continue;
|
|
2378
|
+
if (!previousIds.has(message.id))
|
|
2379
|
+
importedRunAssistants.add(message.id);
|
|
2380
|
+
if (
|
|
2381
|
+
!this.persistedHistoryIds.has(message.id) &&
|
|
2382
|
+
this.historyWrites.pending(message.id) === undefined
|
|
2383
|
+
)
|
|
2384
|
+
this.snapshotHistoryIds.delete(message.id);
|
|
2385
|
+
}
|
|
2386
|
+
for (const messageId of importedRunAssistants) {
|
|
2387
|
+
const item = this.session.tryGetMessage(messageId);
|
|
2388
|
+
if (item)
|
|
2389
|
+
this.markPendingAssistantHistory(messageId, item.parentId);
|
|
2390
|
+
}
|
|
2304
2391
|
for (const message of beforeSnapshot) {
|
|
2305
2392
|
const context = replayContexts.get(message.id);
|
|
2306
2393
|
const current = this.session.tryGetMessage(message.id)?.message;
|
|
@@ -2332,6 +2419,16 @@ export class AgUiThreadController {
|
|
|
2332
2419
|
tail?.role === "assistant" ? tail.id : undefined;
|
|
2333
2420
|
}
|
|
2334
2421
|
}
|
|
2422
|
+
if (event.type === "RUN_ERROR") {
|
|
2423
|
+
settleRunSubagents({
|
|
2424
|
+
type: "incomplete",
|
|
2425
|
+
reason: "error",
|
|
2426
|
+
...(event.message !== undefined ? { error: event.message } : {}),
|
|
2427
|
+
});
|
|
2428
|
+
} else if (event.type === "RUN_CANCELLED") {
|
|
2429
|
+
settleRunSubagents({ type: "incomplete", reason: "cancelled" });
|
|
2430
|
+
}
|
|
2431
|
+
if (settlingRun) persistRunAssistants(false);
|
|
2335
2432
|
} finally {
|
|
2336
2433
|
settlingRun = false;
|
|
2337
2434
|
}
|
|
@@ -2340,6 +2437,7 @@ export class AgUiThreadController {
|
|
|
2340
2437
|
this.abortController = abortController;
|
|
2341
2438
|
const runAgentInstance = this.agent;
|
|
2342
2439
|
this.activeRunAgent = runAgentInstance;
|
|
2440
|
+
const runNumber = ++this.runCount;
|
|
2343
2441
|
|
|
2344
2442
|
let cancelRun = () => dispatch({ type: "RUN_CANCELLED" });
|
|
2345
2443
|
abortSignal.addEventListener(
|
|
@@ -2423,6 +2521,7 @@ export class AgUiThreadController {
|
|
|
2423
2521
|
current?.role === "assistant" &&
|
|
2424
2522
|
current.status?.type === "running"
|
|
2425
2523
|
) {
|
|
2524
|
+
persistRunAssistants(true);
|
|
2426
2525
|
// Finalization is transport cleanup, not proof of a terminal event.
|
|
2427
2526
|
const status: MessageStatus = {
|
|
2428
2527
|
type: "incomplete",
|
|
@@ -2435,6 +2534,25 @@ export class AgUiThreadController {
|
|
|
2435
2534
|
content: settleNestedContent(current.content, status),
|
|
2436
2535
|
});
|
|
2437
2536
|
}
|
|
2537
|
+
if (this.abortController === abortController && !runFinished) {
|
|
2538
|
+
// The run ended without RUN_FINISHED, so no sub-agent call it left
|
|
2539
|
+
// open can still be answered.
|
|
2540
|
+
const settled = assistantMessageId
|
|
2541
|
+
? this.session.tryGetMessage(assistantMessageId)?.message
|
|
2542
|
+
: undefined;
|
|
2543
|
+
settleRunSubagents(
|
|
2544
|
+
settled?.status?.type === "incomplete"
|
|
2545
|
+
? settled.status
|
|
2546
|
+
: {
|
|
2547
|
+
type: "incomplete",
|
|
2548
|
+
reason: "error",
|
|
2549
|
+
error: "The connection ended before the answer finished.",
|
|
2550
|
+
},
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
// Stop may have finished this run already; a newer run owns its own
|
|
2554
|
+
// writes.
|
|
2555
|
+
if (this.runCount === runNumber) persistRunAssistants(false);
|
|
2438
2556
|
this.finishRun(abortController);
|
|
2439
2557
|
}
|
|
2440
2558
|
|
|
@@ -2773,11 +2891,14 @@ export class AgUiThreadController {
|
|
|
2773
2891
|
oldId,
|
|
2774
2892
|
collidesWithExisting ? undefined : newId,
|
|
2775
2893
|
);
|
|
2894
|
+
if (this.unappendedHistoryIds.delete(oldId) && !collidesWithExisting)
|
|
2895
|
+
this.unappendedHistoryIds.add(newId);
|
|
2776
2896
|
if (pendingWrite && !collidesWithExisting) {
|
|
2777
2897
|
void pendingWrite.then(
|
|
2778
2898
|
() => {
|
|
2779
2899
|
this.persistedHistoryIds.delete(oldId);
|
|
2780
2900
|
this.persistedHistoryIds.add(newId);
|
|
2901
|
+
this.unappendedHistoryIds.delete(newId);
|
|
2781
2902
|
},
|
|
2782
2903
|
() => {},
|
|
2783
2904
|
);
|
|
@@ -3859,7 +3980,9 @@ export class AgUiThreadController {
|
|
|
3859
3980
|
|
|
3860
3981
|
if (shouldUpdate) {
|
|
3861
3982
|
const write = this.historyWrites.chain(messageId, () =>
|
|
3862
|
-
|
|
3983
|
+
this.unappendedHistoryIds.has(messageId)
|
|
3984
|
+
? this.appendOnce(history, messageId, { parentId, message })
|
|
3985
|
+
: update.call(history, { parentId, message }),
|
|
3863
3986
|
);
|
|
3864
3987
|
this.assistantHistoryParents.delete(messageId);
|
|
3865
3988
|
void write.then(
|
|
@@ -3897,6 +4020,15 @@ export class AgUiThreadController {
|
|
|
3897
4020
|
);
|
|
3898
4021
|
}
|
|
3899
4022
|
|
|
4023
|
+
private async appendOnce(
|
|
4024
|
+
history: ThreadHistoryAdapter,
|
|
4025
|
+
messageId: string,
|
|
4026
|
+
item: { parentId: string | null; message: ThreadMessage },
|
|
4027
|
+
): Promise<void> {
|
|
4028
|
+
await history.append(item);
|
|
4029
|
+
this.unappendedHistoryIds.delete(messageId);
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3900
4032
|
private appendHistoryItem(
|
|
3901
4033
|
parentId: string | null,
|
|
3902
4034
|
message: ThreadMessage,
|
|
@@ -3905,9 +4037,10 @@ export class AgUiThreadController {
|
|
|
3905
4037
|
const pending = this.historyWrites.pending(message.id);
|
|
3906
4038
|
if (pending) return pending;
|
|
3907
4039
|
|
|
3908
|
-
const
|
|
4040
|
+
const history = this.history;
|
|
4041
|
+
this.unappendedHistoryIds.add(message.id);
|
|
3909
4042
|
const write = this.historyWrites.chain(message.id, () =>
|
|
3910
|
-
|
|
4043
|
+
this.appendOnce(history, message.id, { parentId, message }),
|
|
3911
4044
|
);
|
|
3912
4045
|
void write.then(
|
|
3913
4046
|
() => {
|
|
@@ -4,11 +4,13 @@ import { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import { HttpAgent, type RunAgentInput } from "@ag-ui/client";
|
|
5
5
|
import {
|
|
6
6
|
AssistantRuntimeProvider,
|
|
7
|
+
MessageByIndexProvider,
|
|
8
|
+
useActionBarResume,
|
|
7
9
|
useCloudThreadListAdapter,
|
|
8
10
|
useRemoteThreadListRuntime,
|
|
9
11
|
} from "@assistant-ui/core/react";
|
|
10
12
|
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
11
|
-
import { useAui, type AssistantClient } from "@assistant-ui/store";
|
|
13
|
+
import { useAui, useAuiState, type AssistantClient } from "@assistant-ui/store";
|
|
12
14
|
import { AssistantCloud } from "assistant-cloud";
|
|
13
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
16
|
import { useAgUiRuntime } from "./useAgUiRuntime";
|
|
@@ -107,6 +109,13 @@ const createServer = (
|
|
|
107
109
|
if (row) row.content = body.content;
|
|
108
110
|
return json({});
|
|
109
111
|
}
|
|
112
|
+
if (method === "DELETE" && path === "/threads/remote-1/messages") {
|
|
113
|
+
for (const id of body.message_ids as string[]) {
|
|
114
|
+
const index = rows.findIndex((r) => r.id === id);
|
|
115
|
+
if (index !== -1) rows.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
return json({});
|
|
118
|
+
}
|
|
110
119
|
if (/^\/threads\/remote-1\/runs\/[^/]+\/heartbeat$/.test(path))
|
|
111
120
|
return json({ heartbeat_interval_ms: heartbeatIntervalMs });
|
|
112
121
|
if (/^\/threads\/remote-1\/runs\/[^/]+\/end$/.test(path)) return json({});
|
|
@@ -192,6 +201,20 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
192
201
|
aui = useAui();
|
|
193
202
|
return null;
|
|
194
203
|
};
|
|
204
|
+
const resume: { current: ReturnType<typeof useActionBarResume> | undefined } =
|
|
205
|
+
{ current: undefined };
|
|
206
|
+
const CaptureResume = () => {
|
|
207
|
+
resume.current = useActionBarResume();
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
const ReplyResume = () => {
|
|
211
|
+
const hasReply = useAuiState((s) => s.thread.messages.length >= 2);
|
|
212
|
+
return hasReply ? (
|
|
213
|
+
<MessageByIndexProvider index={1}>
|
|
214
|
+
<CaptureResume />
|
|
215
|
+
</MessageByIndexProvider>
|
|
216
|
+
) : null;
|
|
217
|
+
};
|
|
195
218
|
const App = () => {
|
|
196
219
|
const adapter = useCloudThreadListAdapter({
|
|
197
220
|
cloud,
|
|
@@ -206,6 +229,7 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
206
229
|
return (
|
|
207
230
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
208
231
|
<Capture />
|
|
232
|
+
<ReplyResume />
|
|
209
233
|
</AssistantRuntimeProvider>
|
|
210
234
|
);
|
|
211
235
|
};
|
|
@@ -213,19 +237,25 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
213
237
|
await act(async () => {
|
|
214
238
|
await aui.threads.getLoadThreadsPromise();
|
|
215
239
|
});
|
|
216
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
agent,
|
|
242
|
+
getAui: () => aui,
|
|
243
|
+
getRuntime: () => runtime,
|
|
244
|
+
getResume: () => resume.current,
|
|
245
|
+
};
|
|
217
246
|
};
|
|
218
247
|
|
|
219
248
|
const assistantRow = (
|
|
220
249
|
last_heartbeat_ms_ago: number,
|
|
221
250
|
status: "running" | "ended" = "running",
|
|
251
|
+
content: readonly unknown[] = [{ type: "text", text: "half an answer" }],
|
|
222
252
|
): Row => ({
|
|
223
253
|
id: "a1",
|
|
224
254
|
parent_id: "u1",
|
|
225
255
|
format: "aui/v0",
|
|
226
256
|
content: {
|
|
227
257
|
role: "assistant",
|
|
228
|
-
content: [
|
|
258
|
+
content: [...content],
|
|
229
259
|
status: { type: "incomplete", reason: "cancelled" },
|
|
230
260
|
},
|
|
231
261
|
external_id: "a1",
|
|
@@ -304,7 +334,50 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
304
334
|
expect(reply.id).toBe("agent-msg-1");
|
|
305
335
|
});
|
|
306
336
|
|
|
307
|
-
it("shows a reloaded run quiet for longer than staleAfter as
|
|
337
|
+
it("shows a reloaded empty run quiet for longer than staleAfter as stale; Resume deletes it and runs the agent again from the user message with no sibling", async () => {
|
|
338
|
+
useServer([userRow, assistantRow(45_000, "running", [])]);
|
|
339
|
+
const { agent, getAui, getRuntime, getResume } = await renderApp();
|
|
340
|
+
await act(async () => {
|
|
341
|
+
await getRuntime().threads.switchToThread("remote-1");
|
|
342
|
+
});
|
|
343
|
+
await waitFor(() =>
|
|
344
|
+
expect(getAui().thread.message({ index: 1 }).getState().runState).toBe(
|
|
345
|
+
"stale",
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
await waitFor(() => expect(getResume()?.disabled).toBe(false));
|
|
349
|
+
|
|
350
|
+
await act(async () => {
|
|
351
|
+
getResume()!.resume();
|
|
352
|
+
});
|
|
353
|
+
await waitFor(() => expect(agent.runs).toHaveLength(1));
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
expect(createsWith(server.calls, true)).toHaveLength(1),
|
|
356
|
+
);
|
|
357
|
+
expect(createsWith(server.calls, true)[0]!.body).toMatchObject({
|
|
358
|
+
parent_id: "u1",
|
|
359
|
+
open_run: true,
|
|
360
|
+
});
|
|
361
|
+
await act(async () => {
|
|
362
|
+
agent.runs[0]!.finish();
|
|
363
|
+
});
|
|
364
|
+
await waitFor(() =>
|
|
365
|
+
expect(
|
|
366
|
+
server.calls.some((c) => c.path.endsWith("/runs/lease_1/end")),
|
|
367
|
+
).toBe(true),
|
|
368
|
+
);
|
|
369
|
+
const deletes = server.calls.filter((c) => c.method === "DELETE");
|
|
370
|
+
expect(deletes.map((c) => c.body)).toEqual([{ message_ids: ["a1"] }]);
|
|
371
|
+
expect(server.rows.map((r) => r.id)).toEqual(["u1", "msg_1"]);
|
|
372
|
+
const messages = getAui().thread.getState().messages;
|
|
373
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant"]);
|
|
374
|
+
expect(messages[1]!.id).not.toBe("a1");
|
|
375
|
+
expect(getAui().thread.message({ index: 1 }).getState().branchCount).toBe(
|
|
376
|
+
1,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("shows a reloaded run quiet for longer than staleAfter as interrupted; Regenerate posts a sibling with its own lease and runs the agent again", async () => {
|
|
308
381
|
useServer([userRow, assistantRow(45_000)]);
|
|
309
382
|
const { agent, getAui, getRuntime } = await renderApp();
|
|
310
383
|
await act(async () => {
|
|
@@ -314,7 +387,12 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
314
387
|
expect(getAui().thread.getState().messages).toHaveLength(2),
|
|
315
388
|
);
|
|
316
389
|
const stale = getAui().thread.message({ index: 1 });
|
|
317
|
-
await waitFor(() =>
|
|
390
|
+
await waitFor(() =>
|
|
391
|
+
expect(stale.getState().status).toMatchObject({
|
|
392
|
+
error: { code: "run_stale" },
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
expect(stale.getState().runState).toBeUndefined();
|
|
318
396
|
|
|
319
397
|
await act(async () => {
|
|
320
398
|
stale.reload();
|
|
@@ -363,8 +441,8 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
363
441
|
});
|
|
364
442
|
await waitFor(() =>
|
|
365
443
|
expect(
|
|
366
|
-
short.getAui().thread.message({ index: 1 }).getState().
|
|
367
|
-
).
|
|
444
|
+
short.getAui().thread.message({ index: 1 }).getState().status,
|
|
445
|
+
).toMatchObject({ error: { code: "run_stale" } }),
|
|
368
446
|
);
|
|
369
447
|
});
|
|
370
448
|
|
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -345,6 +345,7 @@ export function useAgUiRuntime(
|
|
|
345
345
|
queueController?.clear();
|
|
346
346
|
return core.reload(parentId, config);
|
|
347
347
|
},
|
|
348
|
+
onDelete: (messageId: string) => core.deleteMessage(messageId),
|
|
348
349
|
onCancel: async () => {
|
|
349
350
|
queueController?.notifyCancelled();
|
|
350
351
|
core.cancel();
|