@assistant-ui/react-ag-ui 0.0.65 → 0.0.67
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/AgUiThreadRuntimeCore.d.ts +19 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +193 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +11 -12
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +4 -16
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +80 -15
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +76 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +380 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -70
- package/src/runtime/adapter/conversions.content.test.ts +41 -0
- package/src/runtime/adapter/conversions.ts +16 -19
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +128 -33
- package/src/runtime/types.ts +25 -5
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +98 -44
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
useCallback,
|
|
5
4
|
useEffect,
|
|
6
5
|
useLayoutEffect,
|
|
7
6
|
useMemo,
|
|
@@ -61,10 +60,9 @@ export function useAgUiRuntime(
|
|
|
61
60
|
options: UseAgUiRuntimeOptions,
|
|
62
61
|
): AgUiAssistantRuntime {
|
|
63
62
|
const logger = useMemo(() => makeLogger(options.logger), [options.logger]);
|
|
64
|
-
const [_version, setVersion] = useState(0);
|
|
65
|
-
const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);
|
|
66
63
|
const coreRef = useRef<AgUiThreadRuntimeCore | null>(null);
|
|
67
64
|
const threadSwitchGenerationRef = useRef(0);
|
|
65
|
+
const switchingGenerationRef = useRef<number | null>(null);
|
|
68
66
|
const runtimeAdapters = useRuntimeAdapters();
|
|
69
67
|
|
|
70
68
|
const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;
|
|
@@ -80,11 +78,16 @@ export function useAgUiRuntime(
|
|
|
80
78
|
...(options.onError && { onError: options.onError }),
|
|
81
79
|
...(options.onCancel && { onCancel: options.onCancel }),
|
|
82
80
|
...(historyAdapter && { history: historyAdapter }),
|
|
83
|
-
|
|
81
|
+
isThreadSwitching: () => switchingGenerationRef.current !== null,
|
|
84
82
|
});
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
const core = coreRef.current;
|
|
86
|
+
const snapshot = useSyncExternalStore(
|
|
87
|
+
core.subscribe,
|
|
88
|
+
core.getSnapshot,
|
|
89
|
+
core.getSnapshot,
|
|
90
|
+
);
|
|
88
91
|
useEffect(() => {
|
|
89
92
|
core.updateOptions({
|
|
90
93
|
agent: options.agent,
|
|
@@ -105,7 +108,7 @@ export function useAgUiRuntime(
|
|
|
105
108
|
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
106
109
|
(s) => s?.type === "executing",
|
|
107
110
|
);
|
|
108
|
-
const isRunning =
|
|
111
|
+
const isRunning = snapshot.isRunning || hasExecutingTools;
|
|
109
112
|
|
|
110
113
|
// The driver sends through the agent core rather than the runtime, because
|
|
111
114
|
// the runtime's append routes every tail append back into this queue.
|
|
@@ -122,6 +125,12 @@ export function useAgUiRuntime(
|
|
|
122
125
|
queueRef.current = createMessageQueue({
|
|
123
126
|
run: (message) => {
|
|
124
127
|
const controller = queueRef.current;
|
|
128
|
+
// A send dispatched mid-switch is dropped, and the queue released, or
|
|
129
|
+
// it would wait for an idle edge that never comes.
|
|
130
|
+
if (switchingGenerationRef.current !== null) {
|
|
131
|
+
controller?.notifyIdle();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
125
134
|
const edgesAtDispatch = busyEdgesRef.current;
|
|
126
135
|
// The queue drops the item before dispatching and stays busy until an
|
|
127
136
|
// idle edge. An append observed as busy is released by that falling
|
|
@@ -205,43 +214,82 @@ export function useAgUiRuntime(
|
|
|
205
214
|
const { onSwitchToNewThread, onSwitchToThread, ...rest } =
|
|
206
215
|
threadListAdapter;
|
|
207
216
|
|
|
217
|
+
const abandonPreviousThread = () => {
|
|
218
|
+
queueRef.current?.notifyCancelled();
|
|
219
|
+
if (core.isRunning()) {
|
|
220
|
+
void core.cancel().catch((error: unknown) => {
|
|
221
|
+
logger.error?.(
|
|
222
|
+
"[agui] cancelling the run on thread switch failed",
|
|
223
|
+
error,
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
queueRef.current?.clear();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const releaseSwitch = (generation: number) => {
|
|
231
|
+
if (switchingGenerationRef.current === generation) {
|
|
232
|
+
switchingGenerationRef.current = null;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
208
236
|
return {
|
|
209
237
|
...rest,
|
|
210
238
|
onSwitchToNewThread: onSwitchToNewThread
|
|
211
239
|
? async () => {
|
|
212
240
|
const generation = ++threadSwitchGenerationRef.current;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
241
|
+
switchingGenerationRef.current = generation;
|
|
242
|
+
try {
|
|
243
|
+
abandonPreviousThread();
|
|
244
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
245
|
+
// Clear before the thread id flips, or the old messages leak
|
|
246
|
+
// into the new thread as a sibling branch.
|
|
247
|
+
core.applyExternalMessages([]);
|
|
248
|
+
core.resetThreadState();
|
|
249
|
+
await onSwitchToNewThread();
|
|
250
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
251
|
+
abandonPreviousThread();
|
|
252
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
253
|
+
core.applyExternalMessages([]);
|
|
254
|
+
core.resetThreadState();
|
|
255
|
+
} finally {
|
|
256
|
+
releaseSwitch(generation);
|
|
257
|
+
}
|
|
221
258
|
}
|
|
222
259
|
: undefined,
|
|
223
260
|
onSwitchToThread: onSwitchToThread
|
|
224
261
|
? async (threadId: string) => {
|
|
225
262
|
const generation = ++threadSwitchGenerationRef.current;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
263
|
+
switchingGenerationRef.current = generation;
|
|
264
|
+
try {
|
|
265
|
+
abandonPreviousThread();
|
|
266
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
267
|
+
// Clear before the thread id flips, or the old messages leak
|
|
268
|
+
// into the new thread as a sibling branch.
|
|
269
|
+
core.applyExternalMessages([]);
|
|
270
|
+
core.resetThreadState();
|
|
271
|
+
const result = await onSwitchToThread(threadId);
|
|
272
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
273
|
+
abandonPreviousThread();
|
|
274
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
275
|
+
core.applyExternalMessages([]);
|
|
276
|
+
core.resetThreadState();
|
|
277
|
+
core.applyExternalMessages(result.messages);
|
|
278
|
+
if (result.state !== undefined) {
|
|
279
|
+
core.loadExternalState(result.state);
|
|
280
|
+
}
|
|
281
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
282
|
+
releaseSwitch(generation);
|
|
283
|
+
if (result.unstable_resume) {
|
|
284
|
+
void core.resumeInFlightRun(result.messages);
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
releaseSwitch(generation);
|
|
240
288
|
}
|
|
241
289
|
}
|
|
242
290
|
: undefined,
|
|
243
291
|
};
|
|
244
|
-
}, [threadListAdapter, core]);
|
|
292
|
+
}, [threadListAdapter, core, logger]);
|
|
245
293
|
|
|
246
294
|
const adapters = options.adapters;
|
|
247
295
|
const adapterAdapters = useMemo(
|
|
@@ -259,14 +307,12 @@ export function useAgUiRuntime(
|
|
|
259
307
|
const shared = useExternalStoreSharedOptions(options);
|
|
260
308
|
const store = useMemo(
|
|
261
309
|
() => {
|
|
262
|
-
void _version; // rerender on version change
|
|
263
|
-
|
|
264
310
|
return {
|
|
265
311
|
...shared,
|
|
266
|
-
isLoading:
|
|
267
|
-
messageRepository:
|
|
268
|
-
state:
|
|
269
|
-
isRunning:
|
|
312
|
+
isLoading: snapshot.isLoading,
|
|
313
|
+
messageRepository: snapshot.messageRepository,
|
|
314
|
+
state: snapshot.state,
|
|
315
|
+
isRunning: snapshot.isRunning,
|
|
270
316
|
extras: agUiExtras.provide({
|
|
271
317
|
interrupts:
|
|
272
318
|
core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,
|
|
@@ -274,13 +320,16 @@ export function useAgUiRuntime(
|
|
|
274
320
|
submitInterruptResponses: (responses) =>
|
|
275
321
|
core.submitInterruptResponses(responses),
|
|
276
322
|
steerAway: (message, responses) => core.steerAway(message, responses),
|
|
277
|
-
state:
|
|
323
|
+
state: snapshot.state,
|
|
278
324
|
setState: (next) => core.setState(next),
|
|
279
325
|
}),
|
|
280
326
|
unstable_enableToolInvocations: true,
|
|
281
327
|
unstable_persistsHistory: true,
|
|
282
328
|
setToolStatuses,
|
|
283
|
-
onNew: (message: AppendMessage) =>
|
|
329
|
+
onNew: (message: AppendMessage) =>
|
|
330
|
+
switchingGenerationRef.current !== null
|
|
331
|
+
? Promise.resolve()
|
|
332
|
+
: core.append(message),
|
|
284
333
|
onVoiceTranscript: (message) => core.appendVoiceTranscript(message),
|
|
285
334
|
onEdit: (message: AppendMessage) => {
|
|
286
335
|
queueController?.clear();
|
|
@@ -313,29 +362,34 @@ export function useAgUiRuntime(
|
|
|
313
362
|
...(queueController && { queue: queueController.adapter }),
|
|
314
363
|
} satisfies ExternalStoreAdapter<ThreadMessage>;
|
|
315
364
|
},
|
|
316
|
-
// _version is intentionally included to trigger re-computation when core state changes via notifyUpdate
|
|
317
365
|
// toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures
|
|
318
366
|
[
|
|
319
367
|
adapterAdapters,
|
|
320
368
|
core,
|
|
321
|
-
_version,
|
|
322
369
|
isRunning,
|
|
323
370
|
queueController,
|
|
324
371
|
queueItems,
|
|
325
372
|
steerQueueItems,
|
|
326
373
|
shared,
|
|
374
|
+
snapshot,
|
|
327
375
|
],
|
|
328
376
|
);
|
|
329
377
|
|
|
330
378
|
const baseRuntime = useExternalStoreRuntime(store);
|
|
331
379
|
|
|
332
380
|
const createRuntime = (): AgUiAssistantRuntime => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
381
|
+
return {
|
|
382
|
+
threads: baseRuntime.threads,
|
|
383
|
+
get thread() {
|
|
384
|
+
return baseRuntime.thread;
|
|
385
|
+
},
|
|
386
|
+
registerModelContextProvider: (provider) =>
|
|
387
|
+
baseRuntime.registerModelContextProvider(provider),
|
|
388
|
+
unstable_getPendingInterrupts: () =>
|
|
389
|
+
core.getPendingInterrupts()?.interrupts ?? [],
|
|
390
|
+
unstable_submitInterruptResponses: (responses) =>
|
|
391
|
+
core.submitInterruptResponses(responses),
|
|
392
|
+
};
|
|
339
393
|
};
|
|
340
394
|
const [pinnedRuntime, setPinnedRuntime] = useState(() => ({
|
|
341
395
|
baseRuntime,
|