@assistant-ui/react-ag-ui 0.0.64 → 0.0.66
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 +19 -29
- 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 +100 -23
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +16 -8
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +30 -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 +80 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -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.nested-tool-calls.test.ts +107 -0
- package/src/runtime/adapter/conversions.ts +37 -64
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +163 -40
- package/src/runtime/event-parser.ts +18 -4
- package/src/runtime/types.ts +43 -11
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.queue.test.tsx +63 -2
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +106 -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
|
|
@@ -175,6 +184,14 @@ export function useAgUiRuntime(
|
|
|
175
184
|
// and the queue drops an item before dispatching it, so draining into that
|
|
176
185
|
// refusal would lose the message instead of keeping it visible.
|
|
177
186
|
const queueBusy = isRunning || core.getPendingInterrupts() !== null;
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (options.isSendDisabled === true) {
|
|
189
|
+
queueController?.hold();
|
|
190
|
+
} else {
|
|
191
|
+
queueController?.release();
|
|
192
|
+
}
|
|
193
|
+
}, [options.isSendDisabled, queueController]);
|
|
194
|
+
|
|
178
195
|
useEffect(() => {
|
|
179
196
|
if (!queueController) return;
|
|
180
197
|
if (
|
|
@@ -197,43 +214,82 @@ export function useAgUiRuntime(
|
|
|
197
214
|
const { onSwitchToNewThread, onSwitchToThread, ...rest } =
|
|
198
215
|
threadListAdapter;
|
|
199
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
|
+
|
|
200
236
|
return {
|
|
201
237
|
...rest,
|
|
202
238
|
onSwitchToNewThread: onSwitchToNewThread
|
|
203
239
|
? async () => {
|
|
204
240
|
const generation = ++threadSwitchGenerationRef.current;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
258
|
}
|
|
214
259
|
: undefined,
|
|
215
260
|
onSwitchToThread: onSwitchToThread
|
|
216
261
|
? async (threadId: string) => {
|
|
217
262
|
const generation = ++threadSwitchGenerationRef.current;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
232
288
|
}
|
|
233
289
|
}
|
|
234
290
|
: undefined,
|
|
235
291
|
};
|
|
236
|
-
}, [threadListAdapter, core]);
|
|
292
|
+
}, [threadListAdapter, core, logger]);
|
|
237
293
|
|
|
238
294
|
const adapters = options.adapters;
|
|
239
295
|
const adapterAdapters = useMemo(
|
|
@@ -251,14 +307,12 @@ export function useAgUiRuntime(
|
|
|
251
307
|
const shared = useExternalStoreSharedOptions(options);
|
|
252
308
|
const store = useMemo(
|
|
253
309
|
() => {
|
|
254
|
-
void _version; // rerender on version change
|
|
255
|
-
|
|
256
310
|
return {
|
|
257
311
|
...shared,
|
|
258
|
-
isLoading:
|
|
259
|
-
messageRepository:
|
|
260
|
-
state:
|
|
261
|
-
isRunning:
|
|
312
|
+
isLoading: snapshot.isLoading,
|
|
313
|
+
messageRepository: snapshot.messageRepository,
|
|
314
|
+
state: snapshot.state,
|
|
315
|
+
isRunning: snapshot.isRunning,
|
|
262
316
|
extras: agUiExtras.provide({
|
|
263
317
|
interrupts:
|
|
264
318
|
core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,
|
|
@@ -266,13 +320,16 @@ export function useAgUiRuntime(
|
|
|
266
320
|
submitInterruptResponses: (responses) =>
|
|
267
321
|
core.submitInterruptResponses(responses),
|
|
268
322
|
steerAway: (message, responses) => core.steerAway(message, responses),
|
|
269
|
-
state:
|
|
323
|
+
state: snapshot.state,
|
|
270
324
|
setState: (next) => core.setState(next),
|
|
271
325
|
}),
|
|
272
326
|
unstable_enableToolInvocations: true,
|
|
273
327
|
unstable_persistsHistory: true,
|
|
274
328
|
setToolStatuses,
|
|
275
|
-
onNew: (message: AppendMessage) =>
|
|
329
|
+
onNew: (message: AppendMessage) =>
|
|
330
|
+
switchingGenerationRef.current !== null
|
|
331
|
+
? Promise.resolve()
|
|
332
|
+
: core.append(message),
|
|
276
333
|
onVoiceTranscript: (message) => core.appendVoiceTranscript(message),
|
|
277
334
|
onEdit: (message: AppendMessage) => {
|
|
278
335
|
queueController?.clear();
|
|
@@ -305,29 +362,34 @@ export function useAgUiRuntime(
|
|
|
305
362
|
...(queueController && { queue: queueController.adapter }),
|
|
306
363
|
} satisfies ExternalStoreAdapter<ThreadMessage>;
|
|
307
364
|
},
|
|
308
|
-
// _version is intentionally included to trigger re-computation when core state changes via notifyUpdate
|
|
309
365
|
// toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures
|
|
310
366
|
[
|
|
311
367
|
adapterAdapters,
|
|
312
368
|
core,
|
|
313
|
-
_version,
|
|
314
369
|
isRunning,
|
|
315
370
|
queueController,
|
|
316
371
|
queueItems,
|
|
317
372
|
steerQueueItems,
|
|
318
373
|
shared,
|
|
374
|
+
snapshot,
|
|
319
375
|
],
|
|
320
376
|
);
|
|
321
377
|
|
|
322
378
|
const baseRuntime = useExternalStoreRuntime(store);
|
|
323
379
|
|
|
324
380
|
const createRuntime = (): AgUiAssistantRuntime => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
};
|
|
331
393
|
};
|
|
332
394
|
const [pinnedRuntime, setPinnedRuntime] = useState(() => ({
|
|
333
395
|
baseRuntime,
|