@tanstack/ai-octane 0.0.0

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/src/types.ts ADDED
@@ -0,0 +1,305 @@
1
+ import type {
2
+ AnyClientTool,
3
+ InterruptDefinition,
4
+ InferSchemaType,
5
+ ModelMessage,
6
+ RunAgentResumeItem,
7
+ SchemaInput,
8
+ } from '@tanstack/ai/client'
9
+ import type {
10
+ AIDevtoolsDisplayOptions,
11
+ BoundInterrupts,
12
+ ChatClientOptions,
13
+ ChatClientState,
14
+ ResolvableChatInterrupt,
15
+ ChatInterruptState,
16
+ ChatRequestBody,
17
+ ChatResumeState,
18
+ ClientContextOptionFromTools,
19
+ ConnectionStatus,
20
+ DistributedOmit,
21
+ InferredClientContext,
22
+ MultimodalContent,
23
+ QueueConfig,
24
+ QueueOption,
25
+ QueueStrategy,
26
+ QueuedMessage,
27
+ SendMessageOptions,
28
+ UIMessage,
29
+ WhenBusy,
30
+ } from '@tanstack/ai-client'
31
+
32
+ // Re-export types from ai-client
33
+ export type {
34
+ ChatRequestBody,
35
+ MultimodalContent,
36
+ QueueConfig,
37
+ QueuedMessage,
38
+ QueueOption,
39
+ QueueStrategy,
40
+ SendMessageOptions,
41
+ UIMessage,
42
+ WhenBusy,
43
+ }
44
+
45
+ /**
46
+ * Recursive partial — every property and every nested array element is optional.
47
+ * Used to type the in-flight `partial` value the hook exposes while a structured
48
+ * output stream is still arriving (the JSON has shape but is incomplete).
49
+ */
50
+ export type DeepPartial<T> =
51
+ T extends ReadonlyArray<infer U>
52
+ ? Array<DeepPartial<U>>
53
+ : T extends object
54
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
55
+ : T
56
+
57
+ /**
58
+ * Options for the useChat hook.
59
+ *
60
+ * Pass either `connection` or `fetcher` — the XOR is enforced at the type
61
+ * level via `ChatTransport`.
62
+ *
63
+ * This extends ChatClientOptions but omits the state change callbacks that are
64
+ * managed internally by hook state:
65
+ * - `onMessagesChange` - Managed by hook state (exposed as `messages`)
66
+ * - `onLoadingChange` - Managed by hook state (exposed as `isLoading`)
67
+ * - `onErrorChange` - Managed by hook state (exposed as `error`)
68
+ * - `onStatusChange` - Managed by hook state (exposed as `status`)
69
+ *
70
+ * All other callbacks (onResponse, onChunk, onFinish, onError) are
71
+ * passed through to the underlying ChatClient and can be used for side effects.
72
+ *
73
+ * When `outputSchema` is supplied, the hook returns a typed `partial` (live
74
+ * progressive object, updated from `TEXT_MESSAGE_CONTENT` deltas via
75
+ * `parsePartialJSON`) and `final` (validated terminal payload from the
76
+ * `structured-output.complete` event). The schema is used purely for type
77
+ * inference on the client — server-side validation still runs against the
78
+ * schema you pass to `chat({ outputSchema })` on the server route.
79
+ *
80
+ * Changing `connection` or `fetcher` updates the active ChatClient in place,
81
+ * preserving its state. Changing `threadId` creates a fresh client.
82
+ */
83
+ export type UseChatOptions<
84
+ TTools extends ReadonlyArray<AnyClientTool> = any,
85
+ TSchema extends SchemaInput | undefined = undefined,
86
+ TContext = InferredClientContext<TTools>,
87
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
88
+ readonly [],
89
+ > = DistributedOmit<
90
+ ChatClientOptions<TTools, TContext, TInterrupts>,
91
+ | 'onMessagesChange'
92
+ | 'onLoadingChange'
93
+ | 'onErrorChange'
94
+ | 'onStatusChange'
95
+ | 'onSubscriptionChange'
96
+ | 'onConnectionStatusChange'
97
+ | 'onSessionGeneratingChange'
98
+ | 'onQueueChange'
99
+ | 'onResumeStateChange'
100
+ | 'onRunIdChange'
101
+ | 'context'
102
+ | 'devtools'
103
+ > & {
104
+ /** Display options for TanStack AI Devtools. */
105
+ devtools?: AIDevtoolsDisplayOptions
106
+ /**
107
+ * Opt into mount-time live subscription behavior.
108
+ * When enabled, the hook subscribes on mount and unsubscribes on unmount.
109
+ */
110
+ live?: boolean
111
+ /**
112
+ * Standard-schema-compatible schema (Zod, Valibot, ArkType, or a plain JSON
113
+ * Schema). Used to infer the shape of `partial` and `final` in the return.
114
+ * The schema is **not** sent to the server — server-side validation runs
115
+ * against the schema passed to `chat({ outputSchema })` on the server route.
116
+ */
117
+ outputSchema?: TSchema
118
+ } & ClientContextOptionFromTools<TTools, TContext>
119
+
120
+ /**
121
+ * Discriminated return shape: when `outputSchema` is supplied, the hook adds
122
+ * typed `partial` / `final` fields; when it is omitted (default), the return
123
+ * is unchanged.
124
+ */
125
+ export type UseChatReturn<
126
+ TTools extends ReadonlyArray<AnyClientTool> = any,
127
+ TSchema extends SchemaInput | undefined = undefined,
128
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
129
+ readonly [],
130
+ > = BaseUseChatReturn<
131
+ TTools,
132
+ TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown,
133
+ TInterrupts
134
+ > &
135
+ (TSchema extends SchemaInput
136
+ ? {
137
+ /**
138
+ * Live, progressively-parsed structured output. Updated from
139
+ * `TEXT_MESSAGE_CONTENT` deltas via `parsePartialJSON` while the stream
140
+ * is still arriving, and snapped to the validated payload when
141
+ * `structured-output.complete` fires. Resets on every new run
142
+ * (`sendMessage` / `reload`).
143
+ */
144
+ partial: DeepPartial<InferSchemaType<TSchema>>
145
+ /**
146
+ * Final, schema-validated structured output. `null` until the terminal
147
+ * `structured-output.complete` event arrives. Resets on every new run.
148
+ */
149
+ final: InferSchemaType<TSchema> | null
150
+ }
151
+ : Record<never, never>)
152
+
153
+ interface BaseUseChatReturn<
154
+ TTools extends ReadonlyArray<AnyClientTool> = any,
155
+ TData = unknown,
156
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
157
+ readonly [],
158
+ > {
159
+ /**
160
+ * Current messages in the conversation. When `outputSchema` is supplied,
161
+ * `messages[i].parts.find(p => p.type === 'structured-output')` is typed
162
+ * with the schema's inferred shape — `data: T`, `partial: DeepPartial<T>`.
163
+ */
164
+ messages: Array<UIMessage<TTools, TData>>
165
+
166
+ /**
167
+ * Send a message and get a response.
168
+ * Can be a simple string or multimodal content with images, audio, etc.
169
+ * By default, sends while busy are queued until the run settles successfully
170
+ * (`queue: 'drop'` restores the old drop-while-busy behavior).
171
+ * Pass `{ whenBusy }` to override the policy for a single send.
172
+ */
173
+ sendMessage: (
174
+ content: string | MultimodalContent,
175
+ options?: SendMessageOptions,
176
+ ) => Promise<void>
177
+
178
+ /**
179
+ * Pending messages queued while the client is busy (streaming, claiming a
180
+ * send, or draining). Separate from `messages` until they drain.
181
+ */
182
+ queue: Array<QueuedMessage>
183
+
184
+ /**
185
+ * Cancel a queued message before it drains. No-op if already sent.
186
+ */
187
+ cancelQueued: (id: string) => void
188
+
189
+ /**
190
+ * Append a message to the conversation
191
+ */
192
+ append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>
193
+
194
+ /**
195
+ * Add the result of a client-side tool execution
196
+ */
197
+ addToolResult: (result: {
198
+ toolCallId: string
199
+ tool: string
200
+ output: any
201
+ state?: 'output-available' | 'output-error'
202
+ errorText?: string
203
+ }) => Promise<void>
204
+
205
+ /**
206
+ * Respond to a tool approval request
207
+ */
208
+ addToolApprovalResponse: (response: {
209
+ id: string // approval.id, not toolCallId
210
+ approved: boolean
211
+ }) => Promise<void>
212
+
213
+ /**
214
+ * The id of the run this client has in flight — one it started or rejoined —
215
+ * or `null` when there is none (including while a run sits paused on an
216
+ * interrupt, waiting on approval).
217
+ *
218
+ * A run is one turn of the conversation, so this changes from turn to turn. A
219
+ * whole tool loop stays inside one run, while resuming after an interrupt
220
+ * continues the turn under a new id — so one user message can produce several
221
+ * run ids. Use it to talk to your own server about that run (cancel it, poll
222
+ * it, correlate a log line).
223
+ */
224
+ runId: string | null
225
+ interrupts: BoundInterrupts<TTools, TInterrupts>
226
+ /** @deprecated Use `interrupts`. */
227
+ pendingInterrupts: BoundInterrupts<TTools, TInterrupts>
228
+ interruptErrors: ChatInterruptState<TTools, TInterrupts>['interruptErrors']
229
+ resuming: boolean
230
+ resolveInterrupts: {
231
+ (approved: boolean): void
232
+ (
233
+ resolver: (
234
+ interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
235
+ ) => undefined,
236
+ ): void
237
+ }
238
+ cancelInterrupts: () => void
239
+ retryInterrupts: () => void
240
+ resumeInterruptsUnsafe: (
241
+ resume: Array<RunAgentResumeItem>,
242
+ state?: ChatResumeState,
243
+ ) => Promise<boolean>
244
+ /** @deprecated Use bound interrupt methods or `resumeInterruptsUnsafe`. */
245
+ resumeInterrupts: (
246
+ resume: Array<RunAgentResumeItem>,
247
+ state?: ChatResumeState,
248
+ ) => Promise<boolean>
249
+
250
+ /**
251
+ * Reload the last assistant message
252
+ */
253
+ reload: () => Promise<void>
254
+
255
+ /**
256
+ * Stop the current response generation
257
+ */
258
+ stop: () => void
259
+
260
+ /**
261
+ * Whether a response is currently being generated
262
+ */
263
+ isLoading: boolean
264
+
265
+ /**
266
+ * Current error, if any
267
+ */
268
+ error: Error | undefined
269
+
270
+ /**
271
+ * Current status of the chat client
272
+ */
273
+ status: ChatClientState
274
+
275
+ /**
276
+ * Whether the subscription loop is currently active
277
+ */
278
+ isSubscribed: boolean
279
+
280
+ /**
281
+ * Current connection lifecycle status
282
+ */
283
+ connectionStatus: ConnectionStatus
284
+
285
+ /**
286
+ * Whether the shared session is actively generating.
287
+ * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
288
+ * Unlike `isLoading` (request-local), this reflects shared generation
289
+ * activity visible to all subscribers (e.g. across tabs/devices).
290
+ */
291
+ sessionGenerating: boolean
292
+
293
+ /**
294
+ * Set messages manually
295
+ */
296
+ setMessages: (messages: Array<UIMessage<TTools, TData>>) => void
297
+
298
+ /**
299
+ * Clear all messages
300
+ */
301
+ clear: () => void
302
+ }
303
+
304
+ // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client
305
+ // and re-exported from there for convenience
@@ -0,0 +1,118 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'octane'
2
+ import { AudioRecorder } from '@tanstack/ai-client'
3
+ import type {
4
+ AudioRecorderOptions,
5
+ AudioRecording,
6
+ InferAudioRecordingOutput,
7
+ } from '@tanstack/ai-client'
8
+
9
+ export type UseAudioRecorderOptions<TOnComplete> = AudioRecorderOptions & {
10
+ /**
11
+ * Optional transform applied to the recording when `stop()` resolves. Its
12
+ * (awaited) return value becomes `recording` and the resolved value of
13
+ * `stop()`. Return nothing to keep the raw `AudioRecording`.
14
+ */
15
+ onComplete?: TOnComplete
16
+ }
17
+
18
+ export interface UseAudioRecorderReturn<TOutput> {
19
+ /** Latest recording (transformed if `onComplete` provided), or null. */
20
+ recording: TOutput | null
21
+ /** True while actively capturing audio. */
22
+ isRecording: boolean
23
+ /** Whether the browser supports recording (getUserMedia + MediaRecorder). */
24
+ isSupported: boolean
25
+ /** Acquire the mic and begin recording. */
26
+ start: () => Promise<void>
27
+ /** Stop and resolve with the completed recording (transformed if `onComplete` provided). */
28
+ stop: () => Promise<TOutput>
29
+ /** Discard the in-progress recording and release the mic. */
30
+ cancel: () => void
31
+ }
32
+
33
+ /**
34
+ * Octane hook for recording an audio message. The resolved
35
+ * {@link AudioRecording} carries `.part` (an audio content part for
36
+ * `useChat.sendMessage`) and `.base64` (for the generation hooks).
37
+ *
38
+ * Errors are delivered via `onError`. `start()` and `stop()` also reject on
39
+ * failure (and `stop()` rejects with `Recording cancelled` if the component
40
+ * unmounts while a stop is in flight) — handle one channel, not both.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const { isRecording, start, stop, recording } = useAudioRecorder()
45
+ * const { sendMessage } = useChat({ connection })
46
+ * // ...
47
+ * const rec = await stop()
48
+ * sendMessage({ content: [rec.part] })
49
+ * ```
50
+ */
51
+ // The transforming overload requires `onComplete`. Without that constraint an
52
+ // options object carrying only unrelated keys (`useAudioRecorder({ onError })`)
53
+ // still matches it, `TOnComplete` infers as `unknown`, and `recording`/`stop()`
54
+ // collapse to `unknown` — so passing any option would silently cost you the
55
+ // `AudioRecording` type. Requiring it here sends those calls to the second
56
+ // overload instead.
57
+ export function useAudioRecorder<
58
+ TOnComplete extends (recording: AudioRecording) => unknown,
59
+ >(
60
+ options: UseAudioRecorderOptions<TOnComplete> & { onComplete: TOnComplete },
61
+ ): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>
62
+ export function useAudioRecorder(
63
+ options?: UseAudioRecorderOptions<undefined>,
64
+ ): UseAudioRecorderReturn<AudioRecording>
65
+ export function useAudioRecorder(
66
+ options: UseAudioRecorderOptions<(recording: AudioRecording) => unknown> = {},
67
+ ): UseAudioRecorderReturn<unknown> {
68
+ const [isRecording, setIsRecording] = useState(false)
69
+ const [recording, setRecording] = useState<unknown>(null)
70
+ // Read the freshest callbacks at fire time without recreating the recorder.
71
+ const optionsRef = useRef(options)
72
+ optionsRef.current = options
73
+
74
+ const recorder = useMemo(
75
+ () =>
76
+ new AudioRecorder({
77
+ ...(options.audio !== undefined && { audio: options.audio }),
78
+ ...(options.mimeType !== undefined && { mimeType: options.mimeType }),
79
+ onError: (err) => optionsRef.current.onError?.(err),
80
+ }),
81
+ // Recorder config (audio/mimeType) is captured once at mount, matching the
82
+ // other hooks' create-once pattern.
83
+ [],
84
+ )
85
+
86
+ useEffect(() => {
87
+ const unsubscribe = recorder.subscribe((state) => {
88
+ setIsRecording(state === 'recording')
89
+ })
90
+ return () => {
91
+ unsubscribe()
92
+ recorder.cancel()
93
+ }
94
+ }, [recorder])
95
+
96
+ const start = useCallback(() => recorder.start(), [recorder])
97
+ const stop = useCallback(async () => {
98
+ const recording = await recorder.stop()
99
+ const transformed = await optionsRef.current.onComplete?.(recording)
100
+ // Only `undefined` (returning nothing) falls back to the raw recording, so
101
+ // a transform that returns null is preserved — matching the inferred type,
102
+ // which excludes only undefined/void/null from the transform's return.
103
+ const output = transformed === undefined ? recording : transformed
104
+ setRecording(() => output)
105
+ return output
106
+ }, [recorder])
107
+ const cancel = useCallback(() => recorder.cancel(), [recorder])
108
+
109
+ return {
110
+ recording,
111
+ isRecording,
112
+ // recording is client-only; if SSR'd, gate UI on a mounted flag.
113
+ isSupported: AudioRecorder.isSupported(),
114
+ start,
115
+ stop,
116
+ cancel,
117
+ }
118
+ }
@@ -0,0 +1,54 @@
1
+ // Declaration companion generated from use-audio-recorder.tsrx.
2
+ import type {
3
+ AudioRecorderOptions,
4
+ AudioRecording,
5
+ InferAudioRecordingOutput,
6
+ } from '@tanstack/ai-client'
7
+ export type UseAudioRecorderOptions<TOnComplete> = AudioRecorderOptions & {
8
+ /**
9
+ * Optional transform applied to the recording when `stop()` resolves. Its
10
+ * (awaited) return value becomes `recording` and the resolved value of
11
+ * `stop()`. Return nothing to keep the raw `AudioRecording`.
12
+ */
13
+ onComplete?: TOnComplete
14
+ }
15
+ export interface UseAudioRecorderReturn<TOutput> {
16
+ /** Latest recording (transformed if `onComplete` provided), or null. */
17
+ recording: TOutput | null
18
+ /** True while actively capturing audio. */
19
+ isRecording: boolean
20
+ /** Whether the browser supports recording (getUserMedia + MediaRecorder). */
21
+ isSupported: boolean
22
+ /** Acquire the mic and begin recording. */
23
+ start: () => Promise<void>
24
+ /** Stop and resolve with the completed recording (transformed if `onComplete` provided). */
25
+ stop: () => Promise<TOutput>
26
+ /** Discard the in-progress recording and release the mic. */
27
+ cancel: () => void
28
+ }
29
+ /**
30
+ * Octane hook for recording an audio message. The resolved
31
+ * {@link AudioRecording} carries `.part` (an audio content part for
32
+ * `useChat.sendMessage`) and `.base64` (for the generation hooks).
33
+ *
34
+ * Errors are delivered via `onError`. `start()` and `stop()` also reject on
35
+ * failure (and `stop()` rejects with `Recording cancelled` if the component
36
+ * unmounts while a stop is in flight) — handle one channel, not both.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const { isRecording, start, stop, recording } = useAudioRecorder()
41
+ * const { sendMessage } = useChat({ connection })
42
+ * // ...
43
+ * const rec = await stop()
44
+ * sendMessage({ content: [rec.part] })
45
+ * ```
46
+ */
47
+ export declare function useAudioRecorder<
48
+ TOnComplete extends (recording: AudioRecording) => unknown,
49
+ >(
50
+ options: UseAudioRecorderOptions<TOnComplete> & { onComplete: TOnComplete },
51
+ ): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>
52
+ export declare function useAudioRecorder(
53
+ options?: UseAudioRecorderOptions<undefined>,
54
+ ): UseAudioRecorderReturn<AudioRecording>