@tanstack/ai-react 0.18.1 → 0.19.1
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 +15 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -23
- package/dist/esm/mcp-app-resource.js +45 -45
- package/dist/esm/mcp-app-resource.js.map +1 -1
- package/dist/esm/mcp-apps.js +1 -4
- package/dist/esm/types.d.ts +29 -3
- package/dist/esm/use-audio-recorder.js +41 -46
- package/dist/esm/use-audio-recorder.js.map +1 -1
- package/dist/esm/use-chat.js +330 -275
- package/dist/esm/use-chat.js.map +1 -1
- package/dist/esm/use-generate-audio.d.ts +50 -4
- package/dist/esm/use-generate-audio.js +47 -23
- package/dist/esm/use-generate-audio.js.map +1 -1
- package/dist/esm/use-generate-image.d.ts +50 -4
- package/dist/esm/use-generate-image.js +49 -23
- package/dist/esm/use-generate-image.js.map +1 -1
- package/dist/esm/use-generate-speech.d.ts +50 -4
- package/dist/esm/use-generate-speech.js +43 -23
- package/dist/esm/use-generate-speech.js.map +1 -1
- package/dist/esm/use-generate-video.d.ts +50 -4
- package/dist/esm/use-generate-video.js +141 -103
- package/dist/esm/use-generate-video.js.map +1 -1
- package/dist/esm/use-generation.d.ts +59 -6
- package/dist/esm/use-generation.js +113 -90
- package/dist/esm/use-generation.js.map +1 -1
- package/dist/esm/use-mcp-app-bridge.js +46 -23
- package/dist/esm/use-mcp-app-bridge.js.map +1 -1
- package/dist/esm/use-realtime-chat.js +185 -185
- package/dist/esm/use-realtime-chat.js.map +1 -1
- package/dist/esm/use-summarize.d.ts +50 -4
- package/dist/esm/use-summarize.js +46 -23
- package/dist/esm/use-summarize.js.map +1 -1
- package/dist/esm/use-transcription.d.ts +50 -4
- package/dist/esm/use-transcription.js +51 -20
- package/dist/esm/use-transcription.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +10 -0
- package/src/types.ts +44 -0
- package/src/use-chat.ts +221 -35
- package/src/use-generate-audio.ts +64 -17
- package/src/use-generate-image.ts +64 -17
- package/src/use-generate-speech.ts +64 -17
- package/src/use-generate-video.ts +99 -18
- package/src/use-generation.ts +112 -18
- package/src/use-summarize.ts +64 -17
- package/src/use-transcription.ts +60 -18
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/mcp-apps.js.map +0 -1
package/src/use-chat.ts
CHANGED
|
@@ -5,11 +5,15 @@ import type {
|
|
|
5
5
|
AnyClientTool,
|
|
6
6
|
InferSchemaType,
|
|
7
7
|
ModelMessage,
|
|
8
|
+
RunAgentResumeItem,
|
|
8
9
|
SchemaInput,
|
|
9
10
|
StreamChunk,
|
|
10
11
|
} from '@tanstack/ai/client'
|
|
11
12
|
import type {
|
|
12
13
|
ChatClientState,
|
|
14
|
+
ChatInterrupt,
|
|
15
|
+
ChatInterruptState,
|
|
16
|
+
ChatResumeState,
|
|
13
17
|
ConnectionStatus,
|
|
14
18
|
InferredClientContext,
|
|
15
19
|
QueuedMessage,
|
|
@@ -25,6 +29,9 @@ import type {
|
|
|
25
29
|
UseChatReturn,
|
|
26
30
|
} from './types'
|
|
27
31
|
|
|
32
|
+
const EMPTY_INTERRUPTS = Object.freeze([])
|
|
33
|
+
const EMPTY_INTERRUPT_ERRORS = Object.freeze([])
|
|
34
|
+
|
|
28
35
|
export function useChat<
|
|
29
36
|
const TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
30
37
|
TSchema extends SchemaInput | undefined = undefined,
|
|
@@ -32,8 +39,12 @@ export function useChat<
|
|
|
32
39
|
>(
|
|
33
40
|
options: UseChatOptions<TTools, TSchema, TContext>,
|
|
34
41
|
): UseChatReturn<TTools, TSchema> {
|
|
42
|
+
// The hook's identity is its `threadId` — also the persistence key, so a
|
|
43
|
+
// reload with the same `threadId` restores the same conversation. `hookId` is
|
|
44
|
+
// only a stable fallback for React's client-recreation keying when no
|
|
45
|
+
// `threadId` is given (an ephemeral chat), never a persistence key.
|
|
35
46
|
const hookId = useId()
|
|
36
|
-
const clientId = options.
|
|
47
|
+
const clientId = options.threadId ?? hookId
|
|
37
48
|
|
|
38
49
|
const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(
|
|
39
50
|
options.initialMessages || [],
|
|
@@ -46,6 +57,15 @@ export function useChat<
|
|
|
46
57
|
useState<ConnectionStatus>('disconnected')
|
|
47
58
|
const [sessionGenerating, setSessionGenerating] = useState(false)
|
|
48
59
|
const [queue, setQueue] = useState<Array<QueuedMessage>>([])
|
|
60
|
+
const [runId, setRunId] = useState<string | null>(null)
|
|
61
|
+
const [interruptState, setInterruptState] = useState<
|
|
62
|
+
ChatInterruptState<TTools>
|
|
63
|
+
>(() => ({
|
|
64
|
+
interrupts: EMPTY_INTERRUPTS,
|
|
65
|
+
pendingInterrupts: EMPTY_INTERRUPTS,
|
|
66
|
+
interruptErrors: EMPTY_INTERRUPT_ERRORS,
|
|
67
|
+
resuming: false,
|
|
68
|
+
}))
|
|
49
69
|
|
|
50
70
|
type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
|
|
51
71
|
type Final = InferSchemaType<NonNullable<TSchema>>
|
|
@@ -55,10 +75,15 @@ export function useChat<
|
|
|
55
75
|
options.initialMessages || [],
|
|
56
76
|
)
|
|
57
77
|
const isFirstMountRef = useRef(true)
|
|
78
|
+
const subscribedRef = useRef(false)
|
|
58
79
|
const activeClientRef = useRef<ChatClient | null>(null)
|
|
59
80
|
const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
60
81
|
null,
|
|
61
82
|
)
|
|
83
|
+
const cleanupDisposalRef = useRef<{
|
|
84
|
+
client: ChatClient
|
|
85
|
+
timeout: ReturnType<typeof setTimeout>
|
|
86
|
+
} | null>(null)
|
|
62
87
|
|
|
63
88
|
// Update ref synchronously during render so it's always current when useMemo runs.
|
|
64
89
|
messagesRef.current = messages
|
|
@@ -67,6 +92,12 @@ export function useChat<
|
|
|
67
92
|
const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)
|
|
68
93
|
optionsRef.current = options
|
|
69
94
|
|
|
95
|
+
const syncResumeState = useCallback((target: ChatClient | null) => {
|
|
96
|
+
if (!target) return
|
|
97
|
+
setRunId(target.getCurrentRunId())
|
|
98
|
+
setInterruptState(target.getInterruptState())
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
70
101
|
// Create ChatClient instance with callbacks to sync state
|
|
71
102
|
const client = useMemo(() => {
|
|
72
103
|
const messagesToUse = options.initialMessages || []
|
|
@@ -81,10 +112,20 @@ export function useChat<
|
|
|
81
112
|
? { connection: initialOptions.connection }
|
|
82
113
|
: { fetcher: initialOptions.fetcher }
|
|
83
114
|
|
|
115
|
+
const instanceHolder: {
|
|
116
|
+
current: ChatClient<TTools, TContext> | undefined
|
|
117
|
+
} = { current: undefined }
|
|
118
|
+
const getActiveInstance = () => {
|
|
119
|
+
const currentInstance = instanceHolder.current
|
|
120
|
+
if (!currentInstance || activeClientRef.current !== currentInstance) {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
return currentInstance
|
|
124
|
+
}
|
|
125
|
+
const pendingInitializationErrors: Array<Error> = []
|
|
84
126
|
const instance = new ChatClient<TTools, TContext>({
|
|
85
127
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
86
128
|
...transport,
|
|
87
|
-
id: clientId,
|
|
88
129
|
initialMessages: messagesToUse,
|
|
89
130
|
...(initialOptions.body !== undefined && { body: initialOptions.body }),
|
|
90
131
|
...(initialOptions.threadId !== undefined && {
|
|
@@ -96,6 +137,9 @@ export function useChat<
|
|
|
96
137
|
...(initialOptions.persistence !== undefined && {
|
|
97
138
|
persistence: initialOptions.persistence,
|
|
98
139
|
}),
|
|
140
|
+
...(initialOptions.initialResumeSnapshot !== undefined && {
|
|
141
|
+
initialResumeSnapshot: initialOptions.initialResumeSnapshot,
|
|
142
|
+
}),
|
|
99
143
|
...(initialOptions.context !== undefined && {
|
|
100
144
|
context: initialOptions.context,
|
|
101
145
|
}),
|
|
@@ -106,57 +150,64 @@ export function useChat<
|
|
|
106
150
|
outputKind: initialOptions.outputSchema ? 'structured' : 'chat',
|
|
107
151
|
},
|
|
108
152
|
onResponse: (response) => {
|
|
109
|
-
if (
|
|
153
|
+
if (!getActiveInstance()) return
|
|
110
154
|
void optionsRef.current.onResponse?.(response)
|
|
111
155
|
},
|
|
112
156
|
onChunk: (chunk: StreamChunk) => {
|
|
113
|
-
if (
|
|
157
|
+
if (!getActiveInstance()) return
|
|
114
158
|
optionsRef.current.onChunk?.(chunk)
|
|
115
159
|
},
|
|
116
160
|
onFinish: (message: UIMessage<TTools>) => {
|
|
117
|
-
if (
|
|
161
|
+
if (!getActiveInstance()) return
|
|
118
162
|
optionsRef.current.onFinish?.(message)
|
|
119
163
|
},
|
|
120
164
|
onError: (error: Error) => {
|
|
121
|
-
|
|
165
|
+
const currentInstance = instanceHolder.current
|
|
166
|
+
if (!currentInstance) {
|
|
167
|
+
pendingInitializationErrors.push(error)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (activeClientRef.current !== currentInstance) return
|
|
122
171
|
optionsRef.current.onError?.(error)
|
|
123
172
|
},
|
|
124
173
|
...(initialOptions.tools !== undefined && {
|
|
125
174
|
tools: initialOptions.tools,
|
|
126
175
|
}),
|
|
127
176
|
onCustomEvent: (eventType, data, context) => {
|
|
128
|
-
if (
|
|
177
|
+
if (!getActiveInstance()) return
|
|
129
178
|
optionsRef.current.onCustomEvent?.(eventType, data, context)
|
|
130
179
|
},
|
|
131
180
|
...(options.streamProcessor !== undefined && {
|
|
132
181
|
streamProcessor: options.streamProcessor,
|
|
133
182
|
}),
|
|
134
183
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
135
|
-
if (
|
|
184
|
+
if (!getActiveInstance()) return
|
|
136
185
|
setMessages(newMessages)
|
|
137
186
|
},
|
|
138
187
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
139
|
-
|
|
188
|
+
const currentInstance = getActiveInstance()
|
|
189
|
+
if (!currentInstance) return
|
|
140
190
|
setIsLoading(newIsLoading)
|
|
191
|
+
syncResumeState(currentInstance)
|
|
141
192
|
},
|
|
142
193
|
onErrorChange: (newError: Error | undefined) => {
|
|
143
|
-
if (
|
|
194
|
+
if (!getActiveInstance()) return
|
|
144
195
|
setError(newError)
|
|
145
196
|
},
|
|
146
197
|
onStatusChange: (status: ChatClientState) => {
|
|
147
|
-
if (
|
|
198
|
+
if (!getActiveInstance()) return
|
|
148
199
|
setStatus(status)
|
|
149
200
|
},
|
|
150
201
|
onSubscriptionChange: (nextIsSubscribed: boolean) => {
|
|
151
|
-
if (
|
|
202
|
+
if (!getActiveInstance()) return
|
|
152
203
|
setIsSubscribed(nextIsSubscribed)
|
|
153
204
|
},
|
|
154
205
|
onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
|
|
155
|
-
if (
|
|
206
|
+
if (!getActiveInstance()) return
|
|
156
207
|
setConnectionStatus(nextStatus)
|
|
157
208
|
},
|
|
158
209
|
onSessionGeneratingChange: (isGenerating: boolean) => {
|
|
159
|
-
if (
|
|
210
|
+
if (!getActiveInstance()) return
|
|
160
211
|
setSessionGenerating(isGenerating)
|
|
161
212
|
},
|
|
162
213
|
...(optionsRef.current.queue !== undefined && {
|
|
@@ -166,10 +217,32 @@ export function useChat<
|
|
|
166
217
|
if (activeClientRef.current !== instance) return
|
|
167
218
|
setQueue(nextQueue)
|
|
168
219
|
},
|
|
220
|
+
onRunIdChange: (nextRunId) => {
|
|
221
|
+
if (!getActiveInstance()) return
|
|
222
|
+
setRunId(nextRunId)
|
|
223
|
+
},
|
|
224
|
+
onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
|
|
225
|
+
if (!getActiveInstance()) return
|
|
226
|
+
setInterruptState((current) => ({
|
|
227
|
+
...current,
|
|
228
|
+
interrupts: nextPendingInterrupts,
|
|
229
|
+
pendingInterrupts: nextPendingInterrupts,
|
|
230
|
+
}))
|
|
231
|
+
},
|
|
232
|
+
onInterruptStateChange: (nextInterruptState) => {
|
|
233
|
+
if (!getActiveInstance()) return
|
|
234
|
+
setInterruptState(nextInterruptState)
|
|
235
|
+
optionsRef.current.onInterruptStateChange?.(nextInterruptState)
|
|
236
|
+
},
|
|
169
237
|
})
|
|
238
|
+
instanceHolder.current = instance
|
|
170
239
|
activeClientRef.current = instance
|
|
240
|
+
for (const error of pendingInitializationErrors) {
|
|
241
|
+
if (activeClientRef.current !== instance) break
|
|
242
|
+
optionsRef.current.onError?.(error)
|
|
243
|
+
}
|
|
171
244
|
return instance
|
|
172
|
-
}, [clientId])
|
|
245
|
+
}, [clientId, syncResumeState])
|
|
173
246
|
|
|
174
247
|
useEffect(() => {
|
|
175
248
|
const clientMessages = client.getMessages()
|
|
@@ -211,18 +284,55 @@ export function useChat<
|
|
|
211
284
|
useEffect(() => {
|
|
212
285
|
if (options.live) {
|
|
213
286
|
client.subscribe()
|
|
214
|
-
|
|
287
|
+
subscribedRef.current = true
|
|
288
|
+
} else if (subscribedRef.current) {
|
|
289
|
+
// Only tear down a subscription we actually started. Calling
|
|
290
|
+
// `unsubscribe()` on initial mount (when `live` was never enabled) would
|
|
291
|
+
// abort an in-flight delivery resume — `resumeInFlightRun` is kicked off
|
|
292
|
+
// in the client constructor, and `unsubscribe()` cancels the shared
|
|
293
|
+
// in-flight stream — so a mid-stream reload would drop its rejoin before
|
|
294
|
+
// it delivers a single chunk. This is exactly why a reload froze instead
|
|
295
|
+
// of continuing.
|
|
215
296
|
client.unsubscribe()
|
|
297
|
+
subscribedRef.current = false
|
|
216
298
|
}
|
|
217
299
|
}, [client, options.live])
|
|
218
300
|
|
|
301
|
+
// ONLY THE VIEW ON SCREEN HOLDS A STREAM.
|
|
302
|
+
//
|
|
303
|
+
// A page can own many chats — 40 sandboxes, 40 conversations — and a browser
|
|
304
|
+
// allows only ~6 connections per origin. One long-lived stream per chat reaches
|
|
305
|
+
// that ceiling after a handful of views, and every request after it QUEUES:
|
|
306
|
+
// measured, an in-page fetch took over two minutes while the same request from
|
|
307
|
+
// outside the browser took 17ms. So the connection follows the view.
|
|
308
|
+
//
|
|
309
|
+
// Immediate, not deferred: the deferred teardown below can be skipped when the
|
|
310
|
+
// same client remounts, which is right for disposal but useless for a
|
|
311
|
+
// connection. `attach` is idempotent and `detach` keeps the transcript and the
|
|
312
|
+
// resume pointer, so a Strict Mode remount is just detach-then-attach and the
|
|
313
|
+
// run is picked straight back up from the durable log.
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
client.attach()
|
|
316
|
+
return () => {
|
|
317
|
+
client.detach()
|
|
318
|
+
}
|
|
319
|
+
}, [client])
|
|
320
|
+
|
|
219
321
|
useEffect(() => {
|
|
322
|
+
if (cleanupDisposalRef.current?.client === client) {
|
|
323
|
+
clearTimeout(cleanupDisposalRef.current.timeout)
|
|
324
|
+
cleanupDisposalRef.current = null
|
|
325
|
+
}
|
|
220
326
|
if (cleanupInvalidationRef.current) {
|
|
221
327
|
clearTimeout(cleanupInvalidationRef.current)
|
|
222
328
|
cleanupInvalidationRef.current = null
|
|
223
329
|
}
|
|
224
330
|
activeClientRef.current = client
|
|
225
331
|
client.mountDevtools()
|
|
332
|
+
// Delivery-durability resume is transparent: the resumable SSE connection
|
|
333
|
+
// adapter re-attaches via the browser's native Last-Event-ID on reconnect.
|
|
334
|
+
// We only seed interrupt (state) resume from the client here.
|
|
335
|
+
syncResumeState(client)
|
|
226
336
|
|
|
227
337
|
return () => {
|
|
228
338
|
cleanupInvalidationRef.current = setTimeout(() => {
|
|
@@ -231,46 +341,71 @@ export function useChat<
|
|
|
231
341
|
}
|
|
232
342
|
cleanupInvalidationRef.current = null
|
|
233
343
|
}, 0)
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
344
|
+
// Soft cleanup only: do NOT stop/unsubscribe here. React Strict Mode
|
|
345
|
+
// remounts fire this cleanup then re-attach the same client one tick
|
|
346
|
+
// later; calling `stop()` would abort a constructor rejoin
|
|
347
|
+
// (`resumeInFlightRun`) and can wipe the durable resume pointer before
|
|
348
|
+
// the first chunk. Real teardown lives in the deferred dispose path
|
|
349
|
+
// below, which only runs when the client is not remounted.
|
|
350
|
+
// Subscribe/unsubscribe on `options.live` is still owned by the
|
|
351
|
+
// dedicated effect above for live toggles.
|
|
352
|
+
const disposal = {
|
|
353
|
+
client,
|
|
354
|
+
timeout: setTimeout(() => {
|
|
355
|
+
if (optionsRef.current.live) {
|
|
356
|
+
client.unsubscribe()
|
|
357
|
+
} else {
|
|
358
|
+
client.stop()
|
|
359
|
+
}
|
|
360
|
+
client.dispose()
|
|
361
|
+
if (cleanupDisposalRef.current === disposal) {
|
|
362
|
+
cleanupDisposalRef.current = null
|
|
363
|
+
}
|
|
364
|
+
}, 0),
|
|
242
365
|
}
|
|
243
|
-
|
|
366
|
+
cleanupDisposalRef.current = disposal
|
|
244
367
|
}
|
|
245
|
-
}, [client])
|
|
368
|
+
}, [client, syncResumeState])
|
|
246
369
|
|
|
247
370
|
const sendMessage = useCallback(
|
|
248
371
|
async (
|
|
249
372
|
content: string | MultimodalContent,
|
|
250
373
|
sendOptions?: SendMessageOptions,
|
|
251
374
|
) => {
|
|
252
|
-
|
|
375
|
+
try {
|
|
376
|
+
await client.sendMessage(content, undefined, sendOptions)
|
|
377
|
+
} finally {
|
|
378
|
+
syncResumeState(client)
|
|
379
|
+
}
|
|
253
380
|
},
|
|
254
|
-
[client],
|
|
381
|
+
[client, syncResumeState],
|
|
255
382
|
)
|
|
256
383
|
|
|
257
384
|
const cancelQueued = useCallback(
|
|
258
385
|
(id: string) => {
|
|
259
386
|
client.cancelQueued(id)
|
|
260
387
|
},
|
|
261
|
-
[client],
|
|
388
|
+
[client, syncResumeState],
|
|
262
389
|
)
|
|
263
390
|
|
|
264
391
|
const append = useCallback(
|
|
265
392
|
async (message: ModelMessage | UIMessage) => {
|
|
266
|
-
|
|
393
|
+
try {
|
|
394
|
+
await client.append(message)
|
|
395
|
+
} finally {
|
|
396
|
+
syncResumeState(client)
|
|
397
|
+
}
|
|
267
398
|
},
|
|
268
|
-
[client],
|
|
399
|
+
[client, syncResumeState],
|
|
269
400
|
)
|
|
270
401
|
|
|
271
402
|
const reload = useCallback(async () => {
|
|
272
|
-
|
|
273
|
-
|
|
403
|
+
try {
|
|
404
|
+
await client.reload()
|
|
405
|
+
} finally {
|
|
406
|
+
syncResumeState(client)
|
|
407
|
+
}
|
|
408
|
+
}, [client, syncResumeState])
|
|
274
409
|
|
|
275
410
|
const stop = useCallback(() => {
|
|
276
411
|
client.stop()
|
|
@@ -278,7 +413,8 @@ export function useChat<
|
|
|
278
413
|
|
|
279
414
|
const clear = useCallback(() => {
|
|
280
415
|
client.clear()
|
|
281
|
-
|
|
416
|
+
syncResumeState(client)
|
|
417
|
+
}, [client, syncResumeState])
|
|
282
418
|
|
|
283
419
|
const setMessagesManually = useCallback(
|
|
284
420
|
(newMessages: Array<UIMessage<TTools>>) => {
|
|
@@ -303,10 +439,50 @@ export function useChat<
|
|
|
303
439
|
const addToolApprovalResponse = useCallback(
|
|
304
440
|
async (response: { id: string; approved: boolean }) => {
|
|
305
441
|
await client.addToolApprovalResponse(response)
|
|
442
|
+
syncResumeState(client)
|
|
443
|
+
},
|
|
444
|
+
[client, syncResumeState],
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
const resumeInterrupts = useCallback(
|
|
448
|
+
async (resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) => {
|
|
449
|
+
const result = await client.resumeInterrupts(
|
|
450
|
+
resumeItems,
|
|
451
|
+
state ?? undefined,
|
|
452
|
+
)
|
|
453
|
+
syncResumeState(client)
|
|
454
|
+
return result
|
|
455
|
+
},
|
|
456
|
+
[client, syncResumeState],
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
const resolveInterrupts = useCallback(
|
|
460
|
+
(
|
|
461
|
+
resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),
|
|
462
|
+
) => {
|
|
463
|
+
if (typeof resolution === 'boolean') {
|
|
464
|
+
client.resolveInterrupts(resolution)
|
|
465
|
+
} else {
|
|
466
|
+
client.resolveInterrupts(resolution)
|
|
467
|
+
}
|
|
306
468
|
},
|
|
307
469
|
[client],
|
|
308
470
|
)
|
|
309
471
|
|
|
472
|
+
const cancelInterrupts = useCallback(() => {
|
|
473
|
+
client.cancelInterrupts()
|
|
474
|
+
}, [client])
|
|
475
|
+
|
|
476
|
+
const retryInterrupts = useCallback(() => {
|
|
477
|
+
client.retryInterrupts()
|
|
478
|
+
}, [client])
|
|
479
|
+
|
|
480
|
+
const resumeInterruptsUnsafe = useCallback(
|
|
481
|
+
(resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) =>
|
|
482
|
+
client.resumeInterruptsUnsafe(resumeItems, state),
|
|
483
|
+
[client],
|
|
484
|
+
)
|
|
485
|
+
|
|
310
486
|
// The "active" structured-output part is the one on the assistant message
|
|
311
487
|
// that follows the latest user message. No such message exists between
|
|
312
488
|
// sendMessage() and the first chunk, so partial/final naturally read as
|
|
@@ -355,7 +531,7 @@ export function useChat<
|
|
|
355
531
|
// The runtime shape unconditionally exposes partial/final; the public
|
|
356
532
|
// return type hides them when no outputSchema was supplied. TS can't
|
|
357
533
|
// structurally narrow across that conditional, so the `as` is the seam.
|
|
358
|
-
//
|
|
534
|
+
// oxlint-disable-next-line eslint-js/no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow
|
|
359
535
|
return {
|
|
360
536
|
messages: renderedMessages,
|
|
361
537
|
sendMessage,
|
|
@@ -374,6 +550,16 @@ export function useChat<
|
|
|
374
550
|
addToolApprovalResponse,
|
|
375
551
|
queue,
|
|
376
552
|
cancelQueued,
|
|
553
|
+
runId,
|
|
554
|
+
interrupts: interruptState.interrupts,
|
|
555
|
+
pendingInterrupts: interruptState.pendingInterrupts,
|
|
556
|
+
interruptErrors: interruptState.interruptErrors,
|
|
557
|
+
resuming: interruptState.resuming,
|
|
558
|
+
resolveInterrupts,
|
|
559
|
+
cancelInterrupts,
|
|
560
|
+
retryInterrupts,
|
|
561
|
+
resumeInterruptsUnsafe,
|
|
562
|
+
resumeInterrupts,
|
|
377
563
|
partial,
|
|
378
564
|
final,
|
|
379
565
|
} as unknown as UseChatReturn<TTools, TSchema>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useGeneration } from './use-generation'
|
|
2
|
+
import { reconstructAudioResult } from '@tanstack/ai-client'
|
|
2
3
|
import type { AudioGenerationResult, StreamChunk } from '@tanstack/ai'
|
|
3
4
|
import type {
|
|
4
5
|
AIDevtoolsDisplayOptions,
|
|
@@ -6,6 +7,7 @@ import type {
|
|
|
6
7
|
ConnectConnectionAdapter,
|
|
7
8
|
GenerationClientState,
|
|
8
9
|
GenerationFetcher,
|
|
10
|
+
GenerationPersistenceOptions,
|
|
9
11
|
InferGenerationOutputFromReturn,
|
|
10
12
|
} from '@tanstack/ai-client'
|
|
11
13
|
|
|
@@ -19,12 +21,51 @@ export interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {
|
|
|
19
21
|
connection?: ConnectConnectionAdapter
|
|
20
22
|
/** Direct async function for audio generation */
|
|
21
23
|
fetcher?: GenerationFetcher<AudioGenerateInput, AudioGenerationResult>
|
|
22
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
|
|
26
|
+
*/
|
|
23
27
|
id?: string
|
|
24
28
|
/** Additional body parameters to send with connect-based adapter requests */
|
|
25
29
|
body?: Record<string, any>
|
|
26
30
|
/** Display options for TanStack AI Devtools. */
|
|
27
31
|
devtools?: AIDevtoolsDisplayOptions
|
|
32
|
+
/**
|
|
33
|
+
* How this generation persists across reloads.
|
|
34
|
+
* - Omit / `false`: ephemeral, in-memory only.
|
|
35
|
+
* - `true`: server-driven — on mount the client hydrates the last generation
|
|
36
|
+
* for its `threadId` from the server (needs a connection with a
|
|
37
|
+
* `hydrateGeneration` handler) and repaints it; it never auto-starts a run.
|
|
38
|
+
*/
|
|
39
|
+
persistence?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* The **scope** this generation belongs to: a stable, app-chosen name for the
|
|
42
|
+
* slot successive runs fill — not a link to a chat conversation.
|
|
43
|
+
*
|
|
44
|
+
* The hook starts empty and produces many runs over its life; each gets its
|
|
45
|
+
* own `runId`, but all belong to one scope. Persistence keys on this, so
|
|
46
|
+
* derive it from your own domain and keep it identical across reloads (e.g.
|
|
47
|
+
* `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread
|
|
48
|
+
* id on the wire, which the protocol requires.
|
|
49
|
+
*
|
|
50
|
+
* **Required whenever `persistence` is set** — an app that cannot name the
|
|
51
|
+
* scope has nothing to restore to. Optional for ephemeral generations, where
|
|
52
|
+
* it falls back to `id` purely to satisfy the wire.
|
|
53
|
+
*/
|
|
54
|
+
threadId?: string
|
|
55
|
+
/**
|
|
56
|
+
* Server-driven hydration handler for `persistence: true` when the
|
|
57
|
+
* connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /
|
|
58
|
+
* `rpcStream()` adapter built without handlers) — typically a one-line
|
|
59
|
+
* server-function call. The connection's own handler takes precedence.
|
|
60
|
+
*/
|
|
61
|
+
hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']
|
|
62
|
+
/**
|
|
63
|
+
* Re-attach handler that replays a run still generating to completion on
|
|
64
|
+
* mount, when the connection doesn't carry one. Without it, a restored
|
|
65
|
+
* `running` snapshot surfaces as an (interrupted) error. The connection's
|
|
66
|
+
* own handler takes precedence.
|
|
67
|
+
*/
|
|
68
|
+
joinRun?: ConnectConnectionAdapter['joinRun']
|
|
28
69
|
/**
|
|
29
70
|
* Callback when audio is generated. Can optionally return a transformed value.
|
|
30
71
|
*
|
|
@@ -61,6 +102,13 @@ export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
|
|
|
61
102
|
stop: () => void
|
|
62
103
|
/** Clear result, error, and return to idle */
|
|
63
104
|
reset: () => void
|
|
105
|
+
/**
|
|
106
|
+
* The id of the generation job currently running, or `null` when nothing is in
|
|
107
|
+
* flight. Each call to `generate` is one job with its own id. Pass it to your
|
|
108
|
+
* own endpoint to cancel or poll the provider job — `stop()` only aborts the
|
|
109
|
+
* local stream, it does not stop work already running on the provider.
|
|
110
|
+
*/
|
|
111
|
+
runId: string | null
|
|
64
112
|
}
|
|
65
113
|
|
|
66
114
|
/**
|
|
@@ -94,9 +142,12 @@ export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
|
|
|
94
142
|
* ```
|
|
95
143
|
*/
|
|
96
144
|
export function useGenerateAudio<TTransformed = void>(
|
|
97
|
-
options: Omit<
|
|
145
|
+
options: Omit<
|
|
146
|
+
UseGenerateAudioOptions,
|
|
147
|
+
'onResult' | 'persistence' | 'threadId' | 'id'
|
|
148
|
+
> & {
|
|
98
149
|
onResult?: (result: AudioGenerationResult) => TTransformed
|
|
99
|
-
},
|
|
150
|
+
} & GenerationPersistenceOptions,
|
|
100
151
|
): UseGenerateAudioReturn<
|
|
101
152
|
InferGenerationOutputFromReturn<AudioGenerationResult, TTransformed>
|
|
102
153
|
> {
|
|
@@ -106,19 +157,15 @@ export function useGenerateAudio<TTransformed = void>(
|
|
|
106
157
|
hookName: 'useGenerateAudio',
|
|
107
158
|
outputKind: 'audio' as const,
|
|
108
159
|
}
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
const generation = useGeneration<
|
|
161
|
+
AudioGenerateInput,
|
|
162
|
+
AudioGenerationResult,
|
|
163
|
+
TTransformed
|
|
164
|
+
>({
|
|
165
|
+
...options,
|
|
166
|
+
devtools,
|
|
167
|
+
reconstructResult: reconstructAudioResult,
|
|
168
|
+
})
|
|
114
169
|
|
|
115
|
-
return
|
|
116
|
-
generate: generate as (input: AudioGenerateInput) => Promise<void>,
|
|
117
|
-
result,
|
|
118
|
-
isLoading,
|
|
119
|
-
error,
|
|
120
|
-
status,
|
|
121
|
-
stop,
|
|
122
|
-
reset,
|
|
123
|
-
}
|
|
170
|
+
return generation
|
|
124
171
|
}
|