@tanstack/ai-client 0.22.1 → 0.23.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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/audio-recorder.js +190 -213
  3. package/dist/esm/audio-recorder.js.map +1 -1
  4. package/dist/esm/chat-client.d.ts +172 -3
  5. package/dist/esm/chat-client.js +1656 -1386
  6. package/dist/esm/chat-client.js.map +1 -1
  7. package/dist/esm/cleared-stream-tracker.d.ts +23 -0
  8. package/dist/esm/cleared-stream-tracker.js +97 -0
  9. package/dist/esm/cleared-stream-tracker.js.map +1 -0
  10. package/dist/esm/client-persistor.d.ts +25 -12
  11. package/dist/esm/client-persistor.js +260 -235
  12. package/dist/esm/client-persistor.js.map +1 -1
  13. package/dist/esm/connection-adapters.d.ts +231 -10
  14. package/dist/esm/connection-adapters.js +989 -574
  15. package/dist/esm/connection-adapters.js.map +1 -1
  16. package/dist/esm/devtools-noop.d.ts +1 -0
  17. package/dist/esm/devtools-noop.js +79 -139
  18. package/dist/esm/devtools-noop.js.map +1 -1
  19. package/dist/esm/devtools.d.ts +31 -1
  20. package/dist/esm/devtools.js +977 -1127
  21. package/dist/esm/devtools.js.map +1 -1
  22. package/dist/esm/events.js +224 -226
  23. package/dist/esm/events.js.map +1 -1
  24. package/dist/esm/generation-client.d.ts +145 -2
  25. package/dist/esm/generation-client.js +659 -321
  26. package/dist/esm/generation-client.js.map +1 -1
  27. package/dist/esm/generation-reconstruct.d.ts +21 -0
  28. package/dist/esm/generation-reconstruct.js +85 -0
  29. package/dist/esm/generation-reconstruct.js.map +1 -0
  30. package/dist/esm/generation-types.d.ts +289 -3
  31. package/dist/esm/generation-types.js +356 -13
  32. package/dist/esm/generation-types.js.map +1 -1
  33. package/dist/esm/index.d.ts +9 -4
  34. package/dist/esm/index.js +7 -39
  35. package/dist/esm/interrupt-manager.d.ts +77 -0
  36. package/dist/esm/interrupt-manager.js +787 -0
  37. package/dist/esm/interrupt-manager.js.map +1 -0
  38. package/dist/esm/mcp-app-bridge.js +56 -64
  39. package/dist/esm/mcp-app-bridge.js.map +1 -1
  40. package/dist/esm/realtime-client.js +366 -440
  41. package/dist/esm/realtime-client.js.map +1 -1
  42. package/dist/esm/response-stream.js +19 -26
  43. package/dist/esm/response-stream.js.map +1 -1
  44. package/dist/esm/sse-parser.js +44 -47
  45. package/dist/esm/sse-parser.js.map +1 -1
  46. package/dist/esm/sse-utils.js +8 -9
  47. package/dist/esm/sse-utils.js.map +1 -1
  48. package/dist/esm/storage-adapters.d.ts +62 -0
  49. package/dist/esm/storage-adapters.js +174 -0
  50. package/dist/esm/storage-adapters.js.map +1 -0
  51. package/dist/esm/types.d.ts +212 -10
  52. package/dist/esm/types.js +38 -7
  53. package/dist/esm/types.js.map +1 -1
  54. package/dist/esm/video-generation-client.d.ts +113 -2
  55. package/dist/esm/video-generation-client.js +665 -379
  56. package/dist/esm/video-generation-client.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/chat-client.ts +1079 -61
  59. package/src/cleared-stream-tracker.ts +151 -0
  60. package/src/client-persistor.ts +102 -33
  61. package/src/connection-adapters.ts +1185 -142
  62. package/src/devtools-noop.ts +4 -3
  63. package/src/devtools.ts +121 -3
  64. package/src/generation-client.ts +563 -13
  65. package/src/generation-reconstruct.ts +121 -0
  66. package/src/generation-types.ts +727 -3
  67. package/src/index.ts +56 -1
  68. package/src/interrupt-manager.ts +1440 -0
  69. package/src/storage-adapters.ts +242 -0
  70. package/src/types.ts +301 -9
  71. package/src/video-generation-client.ts +479 -13
  72. package/dist/esm/index.js.map +0 -1
@@ -1,8 +1,122 @@
1
- import { AnyClientTool, AudioPart, ChunkStrategy, ContentPart, DocumentPart, ImagePart, InferToolInput, InferToolOutput, ModelMessage, StreamChunk, StructuredOutputPart, UIResourcePart, VideoPart } from '@tanstack/ai/client';
1
+ import { AnyClientTool, ApprovalCapabilityOf, ApprovalSchemaOf, AudioPart, BatchInterruptError, ChunkStrategy, ContentPart, DocumentPart, ImagePart, InferSchemaType, InferToolInput, InferToolOutput, InputSchemaOf, Interrupt, InterruptBinding, ItemInterruptError, ModelMessage, NoSchema, RunAgentResumeItem, SchemaInput, StreamChunk, StructuredOutputPart, UIResourcePart, VideoPart } from '@tanstack/ai/client';
2
2
  import { ConnectionAdapter } from './connection-adapters.js';
3
3
  import { AIDevtoolsClientMetadata } from './devtools.js';
4
4
  import { ChatDevtoolsBridgeFactory } from './devtools-noop.js';
5
- export type { StructuredOutputPart } from '@tanstack/ai/client';
5
+ export type { StructuredOutputPart };
6
+ export interface ChatResumeState {
7
+ threadId: string;
8
+ runId: string;
9
+ }
10
+ export type ChatPendingInterrupt = Interrupt;
11
+ /**
12
+ * The durable pointer a chat keeps for the run it may need to rejoin, plus any
13
+ * interrupt that run is waiting on.
14
+ *
15
+ * @internal
16
+ */
17
+ export interface ChatResumeSnapshot {
18
+ resumeState: ChatResumeState;
19
+ pendingInterrupts?: Array<ChatPendingInterrupt>;
20
+ }
21
+ export type InterruptItemStatus = 'pending' | 'validating' | 'staged' | 'submitting' | 'error';
22
+ export interface BoundInterruptBase {
23
+ readonly id: string;
24
+ readonly interruptId: string;
25
+ readonly reason: string;
26
+ readonly message?: string;
27
+ readonly responseSchema?: Readonly<Record<string, unknown>>;
28
+ readonly expiresAt?: string;
29
+ readonly metadata?: Readonly<Record<string, unknown>>;
30
+ readonly threadId: string;
31
+ readonly interruptedRunId: string;
32
+ readonly generation: number;
33
+ readonly status: InterruptItemStatus;
34
+ readonly errors: ReadonlyArray<ItemInterruptError>;
35
+ /** @deprecated Use `errors[0]`. */
36
+ readonly error?: ItemInterruptError;
37
+ /**
38
+ * Whether the binding/schema allows resolution at hydrate time.
39
+ * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and
40
+ * `errors` for those lifecycle states.
41
+ */
42
+ readonly canResolve: boolean;
43
+ cancel: () => void;
44
+ clearResolution: () => void;
45
+ }
46
+ export interface GenericAGUIInterrupt extends BoundInterruptBase {
47
+ readonly kind: 'generic';
48
+ readonly binding: Readonly<Extract<InterruptBinding, {
49
+ kind: 'generic';
50
+ }>>;
51
+ resolveInterrupt: (payload: unknown) => void;
52
+ }
53
+ /**
54
+ * An interrupt that arrived on the stream carrying no resume binding this
55
+ * client understands — no `tanstack:interruptBinding`, or one written at a
56
+ * protocol version we don't recognise.
57
+ *
58
+ * These are surfaced rather than hidden so a UI can show that the run is
59
+ * paused, but they are never resolvable here: something else owns them. A
60
+ * workflow engine's durable approval projected into the same AG-UI stream
61
+ * lands in this bucket, and resolving it through the chat resume path would
62
+ * send an answer no one is waiting for. Render it, or route it to whatever
63
+ * actually owns the pause.
64
+ */
65
+ export interface UnboundInterrupt extends BoundInterruptBase {
66
+ readonly kind: 'unbound';
67
+ readonly binding?: undefined;
68
+ readonly canResolve: false;
69
+ }
70
+ type ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> = ApprovalSchemaOf<TTool> extends infer TApproval ? TApproval extends {
71
+ approve?: SchemaInput;
72
+ reject?: SchemaInput;
73
+ } ? Exclude<TApproval[TBranch], undefined> : TApproval extends SchemaInput ? TApproval : never : never;
74
+ type ApprovalEdits<TTool> = InputSchemaOf<TTool> extends NoSchema ? {
75
+ editedArgs?: never;
76
+ } : {
77
+ editedArgs?: InferToolInput<TTool>;
78
+ };
79
+ type ApprovalPayload<TSchema> = [TSchema] extends [never] ? {
80
+ payload?: never;
81
+ } : TSchema extends SchemaInput ? {
82
+ payload: InferSchemaType<TSchema>;
83
+ } : {
84
+ payload?: never;
85
+ };
86
+ type ApproveArguments<TTool> = [
87
+ ApprovalBranchSchema<TTool, 'approve'>
88
+ ] extends [never] ? InputSchemaOf<TTool> extends NoSchema ? [options?: never] : [options?: ApprovalEdits<TTool> & {
89
+ payload?: never;
90
+ }] : [
91
+ options: ApprovalEdits<TTool> & ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>
92
+ ];
93
+ type RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [
94
+ never
95
+ ] ? [options?: never] : [
96
+ options: {
97
+ editedArgs?: never;
98
+ } & ApprovalPayload<ApprovalBranchSchema<TTool, 'reject'>>
99
+ ];
100
+ export type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> = TTool extends AnyClientTool ? BoundInterruptBase & {
101
+ readonly kind: 'tool-approval';
102
+ readonly binding: Readonly<Extract<InterruptBinding, {
103
+ kind: 'tool-approval';
104
+ }>>;
105
+ readonly toolName: TTool['name'];
106
+ readonly toolCallId: string;
107
+ readonly originalArgs: InferToolInput<TTool>;
108
+ resolveInterrupt: <TApproved extends boolean>(approved: TApproved, ...args: TApproved extends true ? ApproveArguments<TTool> : RejectArguments<TTool>) => void;
109
+ } : never;
110
+ type ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> = TTools[number] extends infer TTool ? TTool extends AnyClientTool ? ApprovalCapabilityOf<TTool> extends true ? ToolApprovalInterrupt<TTool> : never : never : never;
111
+ export type ChatInterrupt<TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>> = GenericAGUIInterrupt | UnboundInterrupt | ApprovalInterrupts<TTools>;
112
+ export type BoundInterrupts<TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>> = ReadonlyArray<ChatInterrupt<TTools>>;
113
+ export interface ChatInterruptState<TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>> {
114
+ readonly interrupts: BoundInterrupts<TTools>;
115
+ /** @deprecated Use `interrupts`. Same snapshot today. */
116
+ readonly pendingInterrupts: BoundInterrupts<TTools>;
117
+ readonly interruptErrors: ReadonlyArray<BatchInterruptError>;
118
+ readonly resuming: boolean;
119
+ }
6
120
  /**
7
121
  * `messages` is the full UIMessage history (not a delta). `data` is the
8
122
  * merged body — `ChatClientOptions.body` plus any per-call data passed to
@@ -15,6 +129,8 @@ export interface ChatFetcherInput {
15
129
  data?: Record<string, unknown>;
16
130
  threadId: string;
17
131
  runId: string;
132
+ parentRunId?: string;
133
+ resume?: Array<RunAgentResumeItem>;
18
134
  }
19
135
  export interface ChatFetcherOptions {
20
136
  /** Fires when `stop()` is called or the request is superseded. */
@@ -275,11 +391,60 @@ export interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any, TD
275
391
  parts: Array<MessagePart<TTools, TData>>;
276
392
  createdAt?: Date;
277
393
  }
394
+ /**
395
+ * A generic key/value storage adapter. `getItem` may be sync or async; the
396
+ * chat persistence layer treats every call as best-effort. The provided
397
+ * `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
398
+ * factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`
399
+ * is assignable to {@link ChatClientPersistence}.
400
+ */
401
+ export interface ChatStorageAdapter<TValue> {
402
+ getItem: (id: string) => TValue | null | undefined | Promise<TValue | null | undefined>;
403
+ setItem: (id: string, value: TValue) => void | Promise<void>;
404
+ removeItem: (id: string) => void | Promise<void>;
405
+ }
406
+ /**
407
+ * The single record a `ChatClientPersistence` adapter stores per chat. It folds
408
+ * the two things that must survive a full page reload into one blob under one
409
+ * key: the message transcript and the optional resume snapshot (which run to
410
+ * rejoin / which interrupts to rehydrate). One adapter, one key — see
411
+ * {@link ChatClientPersistence}.
412
+ */
413
+ export interface ChatPersistedState<TTools extends ReadonlyArray<AnyClientTool> = any> {
414
+ messages: Array<UIMessage<TTools>>;
415
+ /** Present while a run is in flight or paused on an interrupt; absent otherwise. */
416
+ resume?: ChatResumeSnapshot;
417
+ }
418
+ /**
419
+ * Storage adapter for durable chat state. A single adapter persists both the
420
+ * message transcript and the resume snapshot as one {@link ChatPersistedState}
421
+ * record, so a full page reload restores the conversation AND can rejoin an
422
+ * in-flight run / rehydrate pending interrupts.
423
+ *
424
+ * For backward compatibility `getItem` may also return a bare `UIMessage[]`
425
+ * (the legacy messages-only format); the client normalizes it to
426
+ * `{ messages }`. `setItem` always writes the combined record.
427
+ */
278
428
  export interface ChatClientPersistence<TTools extends ReadonlyArray<AnyClientTool> = any> {
279
- getItem: (id: string) => Array<UIMessage<TTools>> | null | undefined | Promise<Array<UIMessage<TTools>> | null | undefined>;
280
- setItem: (id: string, messages: Array<UIMessage<TTools>>) => void | Promise<void>;
429
+ getItem: (id: string) => ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined | Promise<ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined>;
430
+ setItem: (id: string, state: ChatPersistedState<TTools>) => void | Promise<void>;
281
431
  removeItem: (id: string) => void | Promise<void>;
282
432
  }
433
+ /**
434
+ * The `persistence` option for a chat.
435
+ *
436
+ * - `false` (default): ephemeral. Messages live in memory only; a reload starts
437
+ * from empty.
438
+ * - `true`: server-authoritative. Nothing is cached in the browser. On mount the
439
+ * client hydrates the thread from the server by its `threadId` (paints the
440
+ * stored transcript and tails any run still generating), so a reload or the
441
+ * same thread opened on another device both just resume. Requires a connection
442
+ * with a `hydrate` handler.
443
+ * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
444
+ * {@link ChatPersistedState} record (transcript plus resume pointer) is cached
445
+ * in the browser and restored on reload with no network.
446
+ */
447
+ export type ChatPersistenceOption<TTools extends ReadonlyArray<AnyClientTool> = any> = boolean | ChatClientPersistence<TTools>;
283
448
  type IsUnknown<T> = unknown extends T ? [T] extends [unknown] ? true : false : false;
284
449
  type KnownContext<T> = IsUnknown<T> extends true ? never : T;
285
450
  type MergeContext<TLeft, TRight> = [TLeft] extends [never] ? TRight : [TRight] extends [never] ? TLeft : TLeft & TRight;
@@ -319,19 +484,45 @@ export interface ChatClientBaseOptions<TTools extends ReadonlyArray<AnyClientToo
319
484
  */
320
485
  initialMessages?: Array<UIMessage<TTools>>;
321
486
  /**
322
- * Optional persistence adapter for chat messages.
487
+ * How this chat persists across reloads. See {@link ChatPersistenceOption}.
488
+ *
489
+ * - Omit or `false`: ephemeral, in-memory only.
490
+ * - `true`: server-authoritative. The client caches nothing and hydrates the
491
+ * thread from the server by its `threadId` on mount (needs a connection with
492
+ * a `hydrate` handler). Big transcripts never touch the browser, and the same
493
+ * thread opens the same way on another device.
494
+ * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
495
+ * {@link ChatPersistedState} record (transcript plus resume pointer) is cached
496
+ * in the browser, restoring the transcript, pending interrupts, and an
497
+ * in-flight run on reload.
498
+ *
499
+ * Use `initialResumeSnapshot` for a host-supplied in-memory rehydrate instead.
323
500
  */
324
- persistence?: ChatClientPersistence<TTools>;
501
+ persistence?: ChatPersistenceOption<TTools>;
325
502
  /**
326
- * Unique identifier for this chat instance
327
- * Used for managing multiple chats
503
+ * Optional storage-key override for this chat instance, and the devtools
504
+ * instance id. Persistence keys on `threadId` by default; set `id` only when
505
+ * you need the persisted record keyed separately from the wire thread.
506
+ * Prefer a stable `threadId` for the common case.
507
+ *
508
+ * The framework hooks (`useChat` / `createChat`) do NOT expose `id`: a hook's
509
+ * identity is its `threadId`. This lower-level escape hatch exists only for
510
+ * direct `ChatClient` construction.
328
511
  */
329
512
  id?: string;
330
513
  /**
331
- * Thread ID to use for this chat session. Persists across sends within
332
- * the session. If omitted, a unique thread ID is generated.
514
+ * The conversation id for this chat, stable across sends and reloads. It is
515
+ * the AG-UI thread key on the wire AND the key client persistence stores the
516
+ * conversation under, so set a stable `threadId` to have a reload restore the
517
+ * same conversation. If omitted, a unique thread id is generated per session.
333
518
  */
334
519
  threadId?: string;
520
+ /**
521
+ * Initial resumable run state, useful when rehydrating a persisted client
522
+ * after a full page reload. This restores the client-side interrupt
523
+ * descriptors needed to send AG-UI resume entries.
524
+ */
525
+ initialResumeSnapshot?: ChatResumeSnapshot;
335
526
  /**
336
527
  * Arbitrary client-controlled JSON forwarded to the server in the
337
528
  * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
@@ -420,6 +611,17 @@ export interface ChatClientBaseOptions<TTools extends ReadonlyArray<AnyClientToo
420
611
  * or flush).
421
612
  */
422
613
  onQueueChange?: (queue: Array<QueuedMessage>) => void;
614
+ /**
615
+ * Callback when resumable run state or pending interrupts change.
616
+ */
617
+ onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools>) => void;
618
+ /**
619
+ * Callback when the id of the run this client has in flight changes: the new
620
+ * id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.
621
+ */
622
+ onRunIdChange?: (runId: string | null) => void;
623
+ /** Callback when the immutable interrupt state snapshot changes. */
624
+ onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void;
423
625
  /**
424
626
  * Callback when a custom event is received from a server-side tool.
425
627
  * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.
package/dist/esm/types.js CHANGED
@@ -1,11 +1,42 @@
1
+ //#region src/types.ts
2
+ /**
3
+ * Create a typed array of client tools with proper type inference.
4
+ * This eliminates the need for `as const` when defining tool arrays.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const tools = clientTools(
9
+ * myTool1.client(() => result1),
10
+ * myTool2.client(() => result2),
11
+ * )
12
+ *
13
+ * // tools is now properly typed as a tuple with literal tool names
14
+ * // This enables type narrowing when checking part.name === 'toolName'
15
+ * ```
16
+ */
1
17
  function clientTools(...tools) {
2
- return tools;
18
+ return tools;
3
19
  }
20
+ /**
21
+ * Helper to create typed chat client options
22
+ * Use this to get proper type inference for messages
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const tools = clientTools(myTool1, myTool2)
27
+ *
28
+ * const chatOptions = createChatClientOptions({
29
+ * connection: fetchServerSentEvents('/api/chat'),
30
+ * tools,
31
+ * })
32
+ *
33
+ * type MyMessages = InferChatMessages<typeof chatOptions>
34
+ * ```
35
+ */
4
36
  function createChatClientOptions(options) {
5
- return options;
37
+ return options;
6
38
  }
7
- export {
8
- clientTools,
9
- createChatClientOptions
10
- };
11
- //# sourceMappingURL=types.js.map
39
+ //#endregion
40
+ export { clientTools, createChatClientOptions };
41
+
42
+ //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n AudioPart,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferToolInput,\n InferToolOutput,\n ModelMessage,\n StreamChunk,\n StructuredOutputPart,\n UIResourcePart,\n VideoPart,\n} from '@tanstack/ai/client'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type { ChatDevtoolsBridgeFactory } from './devtools-noop'\n\nexport type { StructuredOutputPart } from '@tanstack/ai/client'\n\n/**\n * `messages` is the full UIMessage history (not a delta). `data` is the\n * merged body — `ChatClientOptions.body` plus any per-call data passed to\n * `sendMessage(...)`. `threadId` / `runId` are the AG-UI correlation ids\n * the chat client uses to track this turn — forward them to your server\n * if it needs to correlate requests.\n */\nexport interface ChatFetcherInput {\n messages: Array<UIMessage>\n data?: Record<string, unknown>\n threadId: string\n runId: string\n}\n\nexport interface ChatFetcherOptions {\n /** Fires when `stop()` is called or the request is superseded. */\n signal: AbortSignal\n}\n\n/**\n * Direct function that performs a chat request. Mirrors\n * `GenerationFetcher`. Returns either a `Response` (SSE body parsed by the\n * chat client) or an `AsyncIterable<StreamChunk>` (yielded directly). May\n * return the value synchronously, as a `Promise`, or as an async generator\n * (`async function*`) — the chat client awaits whichever shape is returned.\n *\n * @example\n * ```ts\n * useChat({\n * fetcher: ({ messages }, { signal }) =>\n * chatFn({ data: { messages }, signal }),\n * })\n * ```\n */\nexport type ChatFetcher = (\n input: ChatFetcherInput,\n options: ChatFetcherOptions,\n) =>\n | Response\n | AsyncIterable<StreamChunk>\n | Promise<Response | AsyncIterable<StreamChunk>>\n\n/**\n * Distributive `Omit` — applies `Omit<O, K>` per branch of a union so\n * discriminated unions survive omission. Plain `Omit` collapses unions\n * into a single object shape, which would erase the `ChatTransport` XOR\n * when framework hooks omit React-managed callbacks from\n * `ChatClientOptions`.\n */\nexport type DistributedOmit<\n TObject,\n TKeys extends keyof any,\n> = TObject extends unknown ? Omit<TObject, TKeys> : never\n\n/**\n * Discriminated union enforcing that exactly one of `connection` or\n * `fetcher` is provided. Mirrors `GenerationTransport`.\n */\nexport type ChatTransport =\n | { connection: ConnectionAdapter; fetcher?: never }\n | { fetcher: ChatFetcher; connection?: never }\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n | 'complete' // Result is complete\n | 'error' // Tool execution failed (terminal)\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n}\n\n/**\n * Action taken when `sendMessage` is called while the client is busy\n * (streaming, claiming a send, or draining the queue).\n * - `queue`: hold the message; it auto-sends when the current run settles\n * **successfully**.\n * - `drop`: ignore the send (promise still resolves; does not throw).\n * - `interrupt`: abort the current stream and send immediately. Unlike\n * `stop()`, does **not** flush already-queued messages — they still drain\n * after the interrupting send settles successfully.\n */\nexport type WhenBusy = 'queue' | 'drop' | 'interrupt'\n\n/**\n * Why the client is busy when a {@link QueueStrategy} runs.\n * - `streaming` — an LLM stream is active (`isLoading`).\n * - `sendInFlight` — a send has claimed the client but is not yet loading.\n * - `draining` — the queue drain loop is delivering pending messages.\n */\nexport type QueueBusyReason = 'streaming' | 'sendInFlight' | 'draining'\n\n/**\n * A user message held in the send queue while a stream is active.\n * Rendered separately from `messages`; cancellable via `cancelQueued(id)`\n * until it drains.\n */\nexport interface QueuedMessage {\n id: string\n content: string | MultimodalContent\n createdAt: number\n}\n\n/**\n * Declarative queue policy.\n */\nexport interface QueueConfig {\n /**\n * Action when the client is busy (streaming, claiming a send, or draining).\n * Default `'queue'`.\n */\n whenBusy?: WhenBusy\n /**\n * How queued items leave the queue.\n * - `'fifo'`: one at a time, in order (default).\n * - `'batch'`: merge all queued items into one send when the run settles\n * successfully.\n */\n drain?: 'fifo' | 'batch'\n /** Max queued items. Unlimited when omitted. `0` means never queue. */\n maxSize?: number\n /**\n * Behavior when `maxSize` is reached. Default `'reject'`.\n * `'reject'` silently discards the new send (does not throw);\n * `'drop-oldest'` evicts the oldest queued item to make room.\n * Only meaningful when `maxSize` is set.\n */\n onOverflow?: 'reject' | 'drop-oldest'\n}\n\n/**\n * Escape hatch: decide the action for a single send. Drain stays FIFO for the\n * function form (no `batch` via function). Per-call `sendOptions.whenBusy`\n * overrides the strategy for that send.\n *\n * Actions match {@link WhenBusy}: `'queue' | 'drop' | 'interrupt'`. Concurrent\n * streams are not supported. `pending.id` is the id that will be stored if the\n * action is `'queue'` (safe to pass to `cancelQueued`).\n */\nexport type QueueStrategy = (ctx: {\n pending: QueuedMessage\n busyReason: QueueBusyReason\n queued: ReadonlyArray<QueuedMessage>\n}) => { action: WhenBusy }\n\n/** A `WhenBusy` shorthand, a full config, or a strategy function. */\nexport type QueueOption = WhenBusy | QueueConfig | QueueStrategy\n\n/** Per-call overrides for `sendMessage`. */\nexport interface SendMessageOptions {\n /** Overrides the configured `whenBusy` for this one send. */\n whenBusy?: WhenBusy\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n } & (NonNullable<T['needsApproval']> extends true\n ? {\n /**\n * Approval metadata — present only on tools defined with\n * `needsApproval: true`. Populated once the call reaches\n * `state: 'approval-requested'`. `needsApproval` is an optional\n * property on the tool, so we index into it (rather than\n * `T extends { needsApproval: true }`, which an optional property\n * never satisfies) and strip `undefined` before comparing to `true`.\n */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n }\n : // Tools without `needsApproval: true` never carry an approval field.\n // `& unknown` is a no-op intersection (adds nothing).\n unknown)\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string | Array<ContentPart>\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n | StructuredOutputPart<TData>\n | UIResourcePart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results.\n *\n * `TTools` narrows the tool-call/result part types based on the registered\n * tools. `TData` is the schema-inferred type for any `structured-output` part\n * on the message — defaulted to `unknown` so untyped consumers (the core\n * stream processor, the wire converter) don't need to thread a schema generic\n * everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on\n * the public return so `m.parts.find(p => p.type === 'structured-output').data`\n * is typed without manual casts.\n */\nexport interface UIMessage<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools, TData>>\n createdAt?: Date\n}\n\nexport interface ChatClientPersistence<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n getItem: (\n id: string,\n ) =>\n | Array<UIMessage<TTools>>\n | null\n | undefined\n | Promise<Array<UIMessage<TTools>> | null | undefined>\n setItem: (\n id: string,\n messages: Array<UIMessage<TTools>>,\n ) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\ntype IsUnknown<T> = unknown extends T\n ? [T] extends [unknown]\n ? true\n : false\n : false\n\ntype KnownContext<T> = IsUnknown<T> extends true ? never : T\n\ntype MergeContext<TLeft, TRight> = [TLeft] extends [never]\n ? TRight\n : [TRight] extends [never]\n ? TLeft\n : TLeft & TRight\n\ntype UnionToIntersection<T> = [T] extends [never]\n ? never\n : (T extends unknown ? (value: T) => void : never) extends (\n value: infer TIntersection,\n ) => void\n ? TIntersection\n : never\n\ntype DefinedContext<T> = Exclude<T, undefined>\n\ntype ContextFromExecute<T> = T extends (...args: any) => any\n ? NonNullable<Parameters<T>[1]> extends { context: infer TContext }\n ? KnownContext<TContext>\n : never\n : never\n\ntype ContextFromClientTool<T> = T extends AnyClientTool\n ? T extends { execute?: infer TExecute }\n ? ContextFromExecute<TExecute>\n : never\n : never\n\ntype RequiredContextFromClientToolUnion<T> = T extends unknown\n ? undefined extends ContextFromClientTool<T>\n ? never\n : ContextFromClientTool<T>\n : never\n\ntype ContextFromClientToolUnion<T> = [\n UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>,\n] extends [never]\n ? never\n : [RequiredContextFromClientToolUnion<T>] extends [never]\n ? UnionToIntersection<DefinedContext<ContextFromClientTool<T>>> | undefined\n : UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>\n\ntype ContextFromClientTools<TTools> =\n IsUnknown<TTools> extends true\n ? never\n : TTools extends readonly [infer THead, ...infer TTail]\n ? MergeContext<\n ContextFromClientTool<THead>,\n ContextFromClientTools<TTail>\n >\n : TTools extends ReadonlyArray<infer TItem>\n ? ContextFromClientToolUnion<TItem>\n : never\n\nexport type InferredClientContext<TTools> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? unknown\n : ContextFromClientTools<TTools>\n\nexport type ClientContextOptionFromTools<TTools, TContext> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? { context?: TContext }\n : undefined extends ContextFromClientTools<TTools>\n ? { context?: TContext & ContextFromClientTools<TTools> }\n : { context: TContext & ContextFromClientTools<TTools> }\n\n/**\n * Base options for `ChatClient`, excluding the transport (`connection` or\n * `fetcher`) which is supplied separately via `ChatTransport` so the XOR\n * is preserved when composing the final `ChatClientOptions` type.\n */\nexport interface ChatClientBaseOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = unknown,\n> {\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Optional persistence adapter for chat messages.\n */\n persistence?: ChatClientPersistence<TTools>\n\n /**\n * Unique identifier for this chat instance\n * Used for managing multiple chats\n */\n id?: string\n\n /**\n * Thread ID to use for this chat session. Persists across sends within\n * the session. If omitted, a unique thread ID is generated.\n */\n threadId?: string\n\n /**\n * Arbitrary client-controlled JSON forwarded to the server in the\n * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session\n * options like provider/model selection or feature flags that the\n * server endpoint should read.\n *\n * Replaces the legacy `body` option. If both are provided,\n * `forwardedProps` wins on key collision.\n */\n forwardedProps?: Record<string, any>\n\n /**\n * @deprecated Use `forwardedProps` instead. `body` continues to work\n * unchanged — its values are merged into the AG-UI\n * `RunAgentInput.forwardedProps` field on the wire and are also\n * mirrored under the legacy `data` field for servers that have not\n * migrated yet. Will be removed in a future major release.\n */\n body?: Record<string, any>\n\n /**\n * Client-local runtime context passed to client tool implementations.\n *\n * This value is not serialized to the server. Use `forwardedProps` for\n * explicit client-to-server handoff of serializable values.\n */\n context?: TContext\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Callback when subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n\n /**\n * Policy for messages sent while the client is busy (streaming, claiming\n * a send, or draining the queue). Accepts a `WhenBusy` string, a\n * `QueueConfig`, or a `QueueStrategy` function.\n * Default: `{ whenBusy: 'queue', drain: 'fifo' }`.\n * Queued items auto-send only after a **successful** settle; they are\n * discarded on error/abort, `stop()`, `clear()`, `unsubscribe()`, and\n * `reload()`.\n */\n queue?: QueueOption\n\n /**\n * Callback when the pending send queue changes (enqueue, cancel, drain,\n * or flush).\n */\n onQueueChange?: (queue: Array<QueuedMessage>) => void\n\n /**\n * Callback when a custom event is received from a server-side tool.\n * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.\n *\n * @param eventType - The name of the custom event\n * @param data - The event payload data\n * @param context - Additional context including the toolCallId that emitted the event\n */\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Devtools hook metadata for this client instance.\n */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory, which keeps `@tanstack/ai-client/devtools` (the heavy\n * bridge implementation) out of the main entry's bundle. Frameworks\n * that need live devtools should pass the real factory from\n * `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: ChatDevtoolsBridgeFactory\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\n/**\n * Options for `ChatClient`. Exactly one of `connection` or `fetcher` must be\n * provided — the type-level XOR is enforced via `ChatTransport`.\n */\nexport type ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n> = DistributedOmit<ChatClientBaseOptions<TTools, TContext>, 'context'> &\n ClientContextOptionFromTools<TTools, TContext> &\n ChatTransport\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n>(\n options: ChatClientOptions<TTools, TContext>,\n): ChatClientOptions<TTools, TContext> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools, any>\n ? Array<UIMessage<TTools>>\n : never\n"],"names":[],"mappings":"AA+pBO,SAAS,eACX,OACA;AACH,SAAO;AACT;AAkBO,SAAS,wBAId,SACqC;AACrC,SAAO;AACT;"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n ApprovalCapabilityOf,\n ApprovalSchemaOf,\n AudioPart,\n BatchInterruptError,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferSchemaType,\n InferToolInput,\n InferToolOutput,\n InputSchemaOf,\n Interrupt,\n InterruptBinding,\n ItemInterruptError,\n ModelMessage,\n NoSchema,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n StructuredOutputPart,\n UIResourcePart,\n VideoPart,\n} from '@tanstack/ai/client'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type { ChatDevtoolsBridgeFactory } from './devtools-noop'\n\nexport type { StructuredOutputPart }\n\nexport interface ChatResumeState {\n threadId: string\n runId: string\n}\n\nexport type ChatPendingInterrupt = Interrupt\n\n/**\n * The durable pointer a chat keeps for the run it may need to rejoin, plus any\n * interrupt that run is waiting on.\n *\n * @internal\n */\nexport interface ChatResumeSnapshot {\n resumeState: ChatResumeState\n pendingInterrupts?: Array<ChatPendingInterrupt>\n}\n\nexport type InterruptItemStatus =\n | 'pending'\n | 'validating'\n | 'staged'\n | 'submitting'\n | 'error'\n\nexport interface BoundInterruptBase {\n readonly id: string\n readonly interruptId: string\n readonly reason: string\n readonly message?: string\n readonly responseSchema?: Readonly<Record<string, unknown>>\n readonly expiresAt?: string\n readonly metadata?: Readonly<Record<string, unknown>>\n readonly threadId: string\n readonly interruptedRunId: string\n readonly generation: number\n readonly status: InterruptItemStatus\n readonly errors: ReadonlyArray<ItemInterruptError>\n /** @deprecated Use `errors[0]`. */\n readonly error?: ItemInterruptError\n /**\n * Whether the binding/schema allows resolution at hydrate time.\n * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and\n * `errors` for those lifecycle states.\n */\n readonly canResolve: boolean\n cancel: () => void\n clearResolution: () => void\n}\n\nexport interface GenericAGUIInterrupt extends BoundInterruptBase {\n readonly kind: 'generic'\n readonly binding: Readonly<Extract<InterruptBinding, { kind: 'generic' }>>\n resolveInterrupt: (payload: unknown) => void\n}\n\n/**\n * An interrupt that arrived on the stream carrying no resume binding this\n * client understands — no `tanstack:interruptBinding`, or one written at a\n * protocol version we don't recognise.\n *\n * These are surfaced rather than hidden so a UI can show that the run is\n * paused, but they are never resolvable here: something else owns them. A\n * workflow engine's durable approval projected into the same AG-UI stream\n * lands in this bucket, and resolving it through the chat resume path would\n * send an answer no one is waiting for. Render it, or route it to whatever\n * actually owns the pause.\n */\nexport interface UnboundInterrupt extends BoundInterruptBase {\n readonly kind: 'unbound'\n readonly binding?: undefined\n readonly canResolve: false\n}\n\ntype ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> =\n ApprovalSchemaOf<TTool> extends infer TApproval\n ? TApproval extends { approve?: SchemaInput; reject?: SchemaInput }\n ? Exclude<TApproval[TBranch], undefined>\n : TApproval extends SchemaInput\n ? TApproval\n : never\n : never\n\ntype ApprovalEdits<TTool> =\n InputSchemaOf<TTool> extends NoSchema\n ? { editedArgs?: never }\n : { editedArgs?: InferToolInput<TTool> }\n\ntype ApprovalPayload<TSchema> = [TSchema] extends [never]\n ? { payload?: never }\n : TSchema extends SchemaInput\n ? { payload: InferSchemaType<TSchema> }\n : { payload?: never }\n\ntype ApproveArguments<TTool> = [\n ApprovalBranchSchema<TTool, 'approve'>,\n] extends [never]\n ? InputSchemaOf<TTool> extends NoSchema\n ? [options?: never]\n : [options?: ApprovalEdits<TTool> & { payload?: never }]\n : [\n options: ApprovalEdits<TTool> &\n ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>,\n ]\n\ntype RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [\n never,\n]\n ? [options?: never]\n : [\n options: { editedArgs?: never } & ApprovalPayload<\n ApprovalBranchSchema<TTool, 'reject'>\n >,\n ]\n\nexport type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> =\n TTool extends AnyClientTool\n ? BoundInterruptBase & {\n readonly kind: 'tool-approval'\n readonly binding: Readonly<\n Extract<InterruptBinding, { kind: 'tool-approval' }>\n >\n readonly toolName: TTool['name']\n readonly toolCallId: string\n readonly originalArgs: InferToolInput<TTool>\n // A single generic call signature — not two overloads. Overloads break\n // editor autocomplete: a half-typed options literal (e.g.\n // `resolveInterrupt(true, { payload: {` ) satisfies neither overload,\n // so TS resolves no signature and offers no contextual completions.\n // Making `approved` a generic discriminant lets TS infer it from the\n // first argument and pick the matching branch for the rest params, so\n // `payload` / `editedArgs` / the correct schema's fields complete\n // per-branch (a plain union-of-tuples would offer both branches'\n // fields) while still enforcing the right shape.\n resolveInterrupt: <TApproved extends boolean>(\n approved: TApproved,\n ...args: TApproved extends true\n ? ApproveArguments<TTool>\n : RejectArguments<TTool>\n ) => void\n }\n : never\n\ntype ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> =\n TTools[number] extends infer TTool\n ? TTool extends AnyClientTool\n ? ApprovalCapabilityOf<TTool> extends true\n ? ToolApprovalInterrupt<TTool>\n : never\n : never\n : never\n\n// Client tools resolve through their `.client()` implementation (auto-run) or\n// `addToolResult` — never as a bound interrupt. The `client-tool-execution`\n// pause is handled internally and is intentionally absent from this public\n// union.\nexport type ChatInterrupt<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n> = GenericAGUIInterrupt | UnboundInterrupt | ApprovalInterrupts<TTools>\n\nexport type BoundInterrupts<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n> = ReadonlyArray<ChatInterrupt<TTools>>\n\nexport interface ChatInterruptState<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n> {\n readonly interrupts: BoundInterrupts<TTools>\n /** @deprecated Use `interrupts`. Same snapshot today. */\n readonly pendingInterrupts: BoundInterrupts<TTools>\n readonly interruptErrors: ReadonlyArray<BatchInterruptError>\n readonly resuming: boolean\n}\n\n/**\n * `messages` is the full UIMessage history (not a delta). `data` is the\n * merged body — `ChatClientOptions.body` plus any per-call data passed to\n * `sendMessage(...)`. `threadId` / `runId` are the AG-UI correlation ids\n * the chat client uses to track this turn — forward them to your server\n * if it needs to correlate requests.\n */\nexport interface ChatFetcherInput {\n messages: Array<UIMessage>\n data?: Record<string, unknown>\n threadId: string\n runId: string\n parentRunId?: string\n resume?: Array<RunAgentResumeItem>\n}\n\nexport interface ChatFetcherOptions {\n /** Fires when `stop()` is called or the request is superseded. */\n signal: AbortSignal\n}\n\n/**\n * Direct function that performs a chat request. Mirrors\n * `GenerationFetcher`. Returns either a `Response` (SSE body parsed by the\n * chat client) or an `AsyncIterable<StreamChunk>` (yielded directly). May\n * return the value synchronously, as a `Promise`, or as an async generator\n * (`async function*`) — the chat client awaits whichever shape is returned.\n *\n * @example\n * ```ts\n * useChat({\n * fetcher: ({ messages }, { signal }) =>\n * chatFn({ data: { messages }, signal }),\n * })\n * ```\n */\nexport type ChatFetcher = (\n input: ChatFetcherInput,\n options: ChatFetcherOptions,\n) =>\n | Response\n | AsyncIterable<StreamChunk>\n | Promise<Response | AsyncIterable<StreamChunk>>\n\n/**\n * Distributive `Omit` — applies `Omit<O, K>` per branch of a union so\n * discriminated unions survive omission. Plain `Omit` collapses unions\n * into a single object shape, which would erase the `ChatTransport` XOR\n * when framework hooks omit React-managed callbacks from\n * `ChatClientOptions`.\n */\nexport type DistributedOmit<\n TObject,\n TKeys extends keyof any,\n> = TObject extends unknown ? Omit<TObject, TKeys> : never\n\n/**\n * Discriminated union enforcing that exactly one of `connection` or\n * `fetcher` is provided. Mirrors `GenerationTransport`.\n */\nexport type ChatTransport =\n | { connection: ConnectionAdapter; fetcher?: never }\n | { fetcher: ChatFetcher; connection?: never }\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n | 'complete' // Result is complete\n | 'error' // Tool execution failed (terminal)\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n}\n\n/**\n * Action taken when `sendMessage` is called while the client is busy\n * (streaming, claiming a send, or draining the queue).\n * - `queue`: hold the message; it auto-sends when the current run settles\n * **successfully**.\n * - `drop`: ignore the send (promise still resolves; does not throw).\n * - `interrupt`: abort the current stream and send immediately. Unlike\n * `stop()`, does **not** flush already-queued messages — they still drain\n * after the interrupting send settles successfully.\n */\nexport type WhenBusy = 'queue' | 'drop' | 'interrupt'\n\n/**\n * Why the client is busy when a {@link QueueStrategy} runs.\n * - `streaming` — an LLM stream is active (`isLoading`).\n * - `sendInFlight` — a send has claimed the client but is not yet loading.\n * - `draining` — the queue drain loop is delivering pending messages.\n */\nexport type QueueBusyReason = 'streaming' | 'sendInFlight' | 'draining'\n\n/**\n * A user message held in the send queue while a stream is active.\n * Rendered separately from `messages`; cancellable via `cancelQueued(id)`\n * until it drains.\n */\nexport interface QueuedMessage {\n id: string\n content: string | MultimodalContent\n createdAt: number\n}\n\n/**\n * Declarative queue policy.\n */\nexport interface QueueConfig {\n /**\n * Action when the client is busy (streaming, claiming a send, or draining).\n * Default `'queue'`.\n */\n whenBusy?: WhenBusy\n /**\n * How queued items leave the queue.\n * - `'fifo'`: one at a time, in order (default).\n * - `'batch'`: merge all queued items into one send when the run settles\n * successfully.\n */\n drain?: 'fifo' | 'batch'\n /** Max queued items. Unlimited when omitted. `0` means never queue. */\n maxSize?: number\n /**\n * Behavior when `maxSize` is reached. Default `'reject'`.\n * `'reject'` silently discards the new send (does not throw);\n * `'drop-oldest'` evicts the oldest queued item to make room.\n * Only meaningful when `maxSize` is set.\n */\n onOverflow?: 'reject' | 'drop-oldest'\n}\n\n/**\n * Escape hatch: decide the action for a single send. Drain stays FIFO for the\n * function form (no `batch` via function). Per-call `sendOptions.whenBusy`\n * overrides the strategy for that send.\n *\n * Actions match {@link WhenBusy}: `'queue' | 'drop' | 'interrupt'`. Concurrent\n * streams are not supported. `pending.id` is the id that will be stored if the\n * action is `'queue'` (safe to pass to `cancelQueued`).\n */\nexport type QueueStrategy = (ctx: {\n pending: QueuedMessage\n busyReason: QueueBusyReason\n queued: ReadonlyArray<QueuedMessage>\n}) => { action: WhenBusy }\n\n/** A `WhenBusy` shorthand, a full config, or a strategy function. */\nexport type QueueOption = WhenBusy | QueueConfig | QueueStrategy\n\n/** Per-call overrides for `sendMessage`. */\nexport interface SendMessageOptions {\n /** Overrides the configured `whenBusy` for this one send. */\n whenBusy?: WhenBusy\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n } & (NonNullable<T['needsApproval']> extends true\n ? {\n /**\n * Approval metadata — present only on tools defined with\n * `needsApproval: true`. Populated once the call reaches\n * `state: 'approval-requested'`. `needsApproval` is an optional\n * property on the tool, so we index into it (rather than\n * `T extends { needsApproval: true }`, which an optional property\n * never satisfies) and strip `undefined` before comparing to `true`.\n */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n }\n : // Tools without `needsApproval: true` never carry an approval field.\n // `& unknown` is a no-op intersection (adds nothing).\n unknown)\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string | Array<ContentPart>\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n | StructuredOutputPart<TData>\n | UIResourcePart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results.\n *\n * `TTools` narrows the tool-call/result part types based on the registered\n * tools. `TData` is the schema-inferred type for any `structured-output` part\n * on the message — defaulted to `unknown` so untyped consumers (the core\n * stream processor, the wire converter) don't need to thread a schema generic\n * everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on\n * the public return so `m.parts.find(p => p.type === 'structured-output').data`\n * is typed without manual casts.\n */\nexport interface UIMessage<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools, TData>>\n createdAt?: Date\n}\n\n/**\n * A generic key/value storage adapter. `getItem` may be sync or async; the\n * chat persistence layer treats every call as best-effort. The provided\n * `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`\n * factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`\n * is assignable to {@link ChatClientPersistence}.\n */\nexport interface ChatStorageAdapter<TValue> {\n getItem: (\n id: string,\n ) => TValue | null | undefined | Promise<TValue | null | undefined>\n setItem: (id: string, value: TValue) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The single record a `ChatClientPersistence` adapter stores per chat. It folds\n * the two things that must survive a full page reload into one blob under one\n * key: the message transcript and the optional resume snapshot (which run to\n * rejoin / which interrupts to rehydrate). One adapter, one key — see\n * {@link ChatClientPersistence}.\n */\nexport interface ChatPersistedState<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n messages: Array<UIMessage<TTools>>\n /** Present while a run is in flight or paused on an interrupt; absent otherwise. */\n resume?: ChatResumeSnapshot\n}\n\n/**\n * Storage adapter for durable chat state. A single adapter persists both the\n * message transcript and the resume snapshot as one {@link ChatPersistedState}\n * record, so a full page reload restores the conversation AND can rejoin an\n * in-flight run / rehydrate pending interrupts.\n *\n * For backward compatibility `getItem` may also return a bare `UIMessage[]`\n * (the legacy messages-only format); the client normalizes it to\n * `{ messages }`. `setItem` always writes the combined record.\n */\nexport interface ChatClientPersistence<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n getItem: (\n id: string,\n ) =>\n | ChatPersistedState<TTools>\n | Array<UIMessage<TTools>>\n | null\n | undefined\n | Promise<\n ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined\n >\n setItem: (\n id: string,\n state: ChatPersistedState<TTools>,\n ) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The `persistence` option for a chat.\n *\n * - `false` (default): ephemeral. Messages live in memory only; a reload starts\n * from empty.\n * - `true`: server-authoritative. Nothing is cached in the browser. On mount the\n * client hydrates the thread from the server by its `threadId` (paints the\n * stored transcript and tails any run still generating), so a reload or the\n * same thread opened on another device both just resume. Requires a connection\n * with a `hydrate` handler.\n * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined\n * {@link ChatPersistedState} record (transcript plus resume pointer) is cached\n * in the browser and restored on reload with no network.\n */\nexport type ChatPersistenceOption<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> = boolean | ChatClientPersistence<TTools>\n\ntype IsUnknown<T> = unknown extends T\n ? [T] extends [unknown]\n ? true\n : false\n : false\n\ntype KnownContext<T> = IsUnknown<T> extends true ? never : T\n\ntype MergeContext<TLeft, TRight> = [TLeft] extends [never]\n ? TRight\n : [TRight] extends [never]\n ? TLeft\n : TLeft & TRight\n\ntype UnionToIntersection<T> = [T] extends [never]\n ? never\n : (T extends unknown ? (value: T) => void : never) extends (\n value: infer TIntersection,\n ) => void\n ? TIntersection\n : never\n\ntype DefinedContext<T> = Exclude<T, undefined>\n\ntype ContextFromExecute<T> = T extends (...args: any) => any\n ? NonNullable<Parameters<T>[1]> extends { context: infer TContext }\n ? KnownContext<TContext>\n : never\n : never\n\ntype ContextFromClientTool<T> = T extends AnyClientTool\n ? T extends { execute?: infer TExecute }\n ? ContextFromExecute<TExecute>\n : never\n : never\n\ntype RequiredContextFromClientToolUnion<T> = T extends unknown\n ? undefined extends ContextFromClientTool<T>\n ? never\n : ContextFromClientTool<T>\n : never\n\ntype ContextFromClientToolUnion<T> = [\n UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>,\n] extends [never]\n ? never\n : [RequiredContextFromClientToolUnion<T>] extends [never]\n ? UnionToIntersection<DefinedContext<ContextFromClientTool<T>>> | undefined\n : UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>\n\ntype ContextFromClientTools<TTools> =\n IsUnknown<TTools> extends true\n ? never\n : TTools extends readonly [infer THead, ...infer TTail]\n ? MergeContext<\n ContextFromClientTool<THead>,\n ContextFromClientTools<TTail>\n >\n : TTools extends ReadonlyArray<infer TItem>\n ? ContextFromClientToolUnion<TItem>\n : never\n\nexport type InferredClientContext<TTools> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? unknown\n : ContextFromClientTools<TTools>\n\nexport type ClientContextOptionFromTools<TTools, TContext> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? { context?: TContext }\n : undefined extends ContextFromClientTools<TTools>\n ? { context?: TContext & ContextFromClientTools<TTools> }\n : { context: TContext & ContextFromClientTools<TTools> }\n\n/**\n * Base options for `ChatClient`, excluding the transport (`connection` or\n * `fetcher`) which is supplied separately via `ChatTransport` so the XOR\n * is preserved when composing the final `ChatClientOptions` type.\n */\nexport interface ChatClientBaseOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = unknown,\n> {\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * How this chat persists across reloads. See {@link ChatPersistenceOption}.\n *\n * - Omit or `false`: ephemeral, in-memory only.\n * - `true`: server-authoritative. The client caches nothing and hydrates the\n * thread from the server by its `threadId` on mount (needs a connection with\n * a `hydrate` handler). Big transcripts never touch the browser, and the same\n * thread opens the same way on another device.\n * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined\n * {@link ChatPersistedState} record (transcript plus resume pointer) is cached\n * in the browser, restoring the transcript, pending interrupts, and an\n * in-flight run on reload.\n *\n * Use `initialResumeSnapshot` for a host-supplied in-memory rehydrate instead.\n */\n persistence?: ChatPersistenceOption<TTools>\n\n /**\n * Optional storage-key override for this chat instance, and the devtools\n * instance id. Persistence keys on `threadId` by default; set `id` only when\n * you need the persisted record keyed separately from the wire thread.\n * Prefer a stable `threadId` for the common case.\n *\n * The framework hooks (`useChat` / `createChat`) do NOT expose `id`: a hook's\n * identity is its `threadId`. This lower-level escape hatch exists only for\n * direct `ChatClient` construction.\n */\n id?: string\n\n /**\n * The conversation id for this chat, stable across sends and reloads. It is\n * the AG-UI thread key on the wire AND the key client persistence stores the\n * conversation under, so set a stable `threadId` to have a reload restore the\n * same conversation. If omitted, a unique thread id is generated per session.\n */\n threadId?: string\n\n /**\n * Initial resumable run state, useful when rehydrating a persisted client\n * after a full page reload. This restores the client-side interrupt\n * descriptors needed to send AG-UI resume entries.\n */\n initialResumeSnapshot?: ChatResumeSnapshot\n\n /**\n * Arbitrary client-controlled JSON forwarded to the server in the\n * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session\n * options like provider/model selection or feature flags that the\n * server endpoint should read.\n *\n * Replaces the legacy `body` option. If both are provided,\n * `forwardedProps` wins on key collision.\n */\n forwardedProps?: Record<string, any>\n\n /**\n * @deprecated Use `forwardedProps` instead. `body` continues to work\n * unchanged — its values are merged into the AG-UI\n * `RunAgentInput.forwardedProps` field on the wire and are also\n * mirrored under the legacy `data` field for servers that have not\n * migrated yet. Will be removed in a future major release.\n */\n body?: Record<string, any>\n\n /**\n * Client-local runtime context passed to client tool implementations.\n *\n * This value is not serialized to the server. Use `forwardedProps` for\n * explicit client-to-server handoff of serializable values.\n */\n context?: TContext\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Callback when subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n\n /**\n * Policy for messages sent while the client is busy (streaming, claiming\n * a send, or draining the queue). Accepts a `WhenBusy` string, a\n * `QueueConfig`, or a `QueueStrategy` function.\n * Default: `{ whenBusy: 'queue', drain: 'fifo' }`.\n * Queued items auto-send only after a **successful** settle; they are\n * discarded on error/abort, `stop()`, `clear()`, `unsubscribe()`, and\n * `reload()`.\n */\n queue?: QueueOption\n\n /**\n * Callback when the pending send queue changes (enqueue, cancel, drain,\n * or flush).\n */\n onQueueChange?: (queue: Array<QueuedMessage>) => void\n\n /**\n * Callback when resumable run state or pending interrupts change.\n */\n onResumeStateChange?: (\n resumeState: ChatResumeState | null,\n pendingInterrupts: BoundInterrupts<TTools>,\n ) => void\n\n /**\n * Callback when the id of the run this client has in flight changes: the new\n * id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.\n */\n onRunIdChange?: (runId: string | null) => void\n\n /** Callback when the immutable interrupt state snapshot changes. */\n onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void\n\n /**\n * Callback when a custom event is received from a server-side tool.\n * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.\n *\n * @param eventType - The name of the custom event\n * @param data - The event payload data\n * @param context - Additional context including the toolCallId that emitted the event\n */\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Devtools hook metadata for this client instance.\n */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory, which keeps `@tanstack/ai-client/devtools` (the heavy\n * bridge implementation) out of the main entry's bundle. Frameworks\n * that need live devtools should pass the real factory from\n * `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: ChatDevtoolsBridgeFactory\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\n/**\n * Options for `ChatClient`. Exactly one of `connection` or `fetcher` must be\n * provided — the type-level XOR is enforced via `ChatTransport`.\n */\nexport type ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n> = DistributedOmit<ChatClientBaseOptions<TTools, TContext>, 'context'> &\n ClientContextOptionFromTools<TTools, TContext> &\n ChatTransport\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n>(\n options: ChatClientOptions<TTools, TContext>,\n): ChatClientOptions<TTools, TContext> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools, any>\n ? Array<UIMessage<TTools>>\n : never\n"],"mappings":";;;;;;;;;;;;;;;;AAm8BA,SAAgB,YACd,GAAG,OACA;CACH,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAgB,wBAId,SACqC;CACrC,OAAO;AACT"}
@@ -1,5 +1,5 @@
1
1
  import { ConnectConnectionAdapter } from './connection-adapters.js';
2
- import { GenerationClientState, GenerationFetcher, VideoGenerateInput, VideoGenerateResult, VideoGenerationClientOptions, VideoStatusInfo } from './generation-types.js';
2
+ import { GenerationClientState, GenerationFetcher, GenerationResumeSnapshot, VideoGenerateInput, VideoGenerateResult, VideoGenerationClientOptions, VideoStatusInfo } from './generation-types.js';
3
3
  /**
4
4
  * A specialized client for job-based video generation.
5
5
  *
@@ -37,11 +37,14 @@ import { GenerationClientState, GenerationFetcher, VideoGenerateInput, VideoGene
37
37
  */
38
38
  export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
39
39
  private readonly connection;
40
+ private readonly hydrateGenerationHandler;
41
+ private readonly joinRunHandler;
40
42
  private readonly fetcher;
41
43
  private readonly uniqueId;
42
44
  private readonly devtoolsMetadata;
43
45
  private readonly devtoolsBridge;
44
46
  private readonly threadId;
47
+ private readonly serverDriven;
45
48
  private body;
46
49
  private result;
47
50
  private input;
@@ -51,9 +54,13 @@ export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
51
54
  private isLoading;
52
55
  private error;
53
56
  private status;
57
+ private resumeSnapshot;
54
58
  private abortController;
59
+ private rejoinedRunId;
55
60
  private readonly callbacksRef;
56
61
  private devtoolsMounted;
62
+ private disposed;
63
+ private serverHydrationStarted;
57
64
  constructor(options: VideoGenerationClientOptions<TOutput> & ({
58
65
  connection: ConnectConnectionAdapter;
59
66
  fetcher?: never;
@@ -75,6 +82,11 @@ export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
75
82
  /**
76
83
  * Process a stream of AG-UI events from the streaming connection adapter.
77
84
  * The server handles the polling loop and streams status updates.
85
+ *
86
+ * Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends
87
+ * without a terminal chunk — see the note on
88
+ * `GenerationClient.processStream`. Video runs are long enough that a proxy
89
+ * idle timeout mid-poll is the likeliest way to hit it.
78
90
  */
79
91
  private processStream;
80
92
  /**
@@ -82,7 +94,10 @@ export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
82
94
  */
83
95
  stop(): void;
84
96
  /**
85
- * Clear all state and return to idle.
97
+ * Clear all state and return to idle. Also drops the client's in-memory
98
+ * resume snapshot, so a remount restores nothing. The server-side record is
99
+ * untouched — this client no longer writes one — so a full page reload under
100
+ * `persistence: true` re-hydrates the last generation again.
86
101
  */
87
102
  reset(): void;
88
103
  /**
@@ -96,6 +111,7 @@ export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
96
111
  getIsLoading(): boolean;
97
112
  getError(): Error | undefined;
98
113
  getStatus(): GenerationClientState;
114
+ getResumeSnapshot(): GenerationResumeSnapshot | undefined;
99
115
  private setResult;
100
116
  private setJobId;
101
117
  private setVideoStatus;
@@ -107,4 +123,99 @@ export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
107
123
  private createDevtoolsMetadata;
108
124
  private generateUniqueId;
109
125
  private createRunContext;
126
+ private observeResumeSnapshot;
127
+ /** Notify the internal snapshot listener AND emit the public resume state. */
128
+ private notifyResumeSnapshotChanged;
129
+ /** Derive the public `resumeState` from the internal snapshot. */
130
+ private emitResumeState;
131
+ /**
132
+ * Repaint the normal fields from a restored snapshot so a reload presents the
133
+ * video in `result` / `status` / `error` / `jobId`, never a snapshot object.
134
+ * `isLoading` stays false (no auto-tail). Not re-persisted (it came from
135
+ * storage / the server).
136
+ *
137
+ * A `complete` snapshot with no durable video artifact cannot be rebuilt, so
138
+ * it repaints as an error rather than a `success` with a `null` result — see
139
+ * the note on `GenerationClient.repaintFromSnapshot`.
140
+ */
141
+ private repaintFromSnapshot;
142
+ /**
143
+ * Report a `complete` snapshot with no durable video artifact to rebuild
144
+ * from. Runs after the status/error repaint above, so it wins over the
145
+ * snapshot's own `complete` status.
146
+ */
147
+ private reportUnrestorableResult;
148
+ /**
149
+ * Repaint a restored snapshot (client store or server hydrate) and, when it
150
+ * reports a run still in flight, tail that run to completion via `joinRun`
151
+ * (from the connection, or the `joinRun` option when the transport can't
152
+ * carry one).
153
+ *
154
+ * A `running` snapshot that no `joinRun` handler can tail is repainted as an
155
+ * interrupted error instead of a `generating` status that would never
156
+ * settle: an interrupted generation cannot be resumed, only re-run.
157
+ */
158
+ private repaintRestoredSnapshot;
159
+ /**
160
+ * Rebuild a `VideoGenerateResult` from a restored snapshot: the video's bytes
161
+ * are served from the durable artifact URL, so the restored result renders
162
+ * from your own origin. Returns `null` when there is no durable video artifact.
163
+ */
164
+ private reconstructVideoResult;
165
+ /**
166
+ * The plain (non-Response) fetcher path never observes stream chunks, so
167
+ * the terminal snapshot is built here from the fetcher's own result. A
168
+ * stale `error` from a previous run is intentionally dropped — this run
169
+ * succeeded.
170
+ */
171
+ private completePlainFetcherResumeSnapshot;
172
+ /**
173
+ * Records a transport-level failure (network drop, throwing callback) in
174
+ * the snapshot. Without this, only a server-emitted RUN_ERROR chunk would
175
+ * mark the snapshot `error`, leaving a persisted record that claims the
176
+ * run is still in flight.
177
+ */
178
+ private recordResumeSnapshotError;
179
+ /**
180
+ * Drop the client's in-memory snapshot and re-emit. Purely local — this
181
+ * client writes no storage, so nothing persisted is removed.
182
+ */
183
+ private clearResumeSnapshot;
184
+ /**
185
+ * Server-driven mount hydration entry point (`persistence: true`). Runs at
186
+ * most once, from the commit-phase mount path (`mountDevtools`) — never the
187
+ * constructor / render phase — so remounts and speculative renders can't
188
+ * re-fire the hydrate GET.
189
+ */
190
+ private maybeHydrateFromServer;
191
+ /**
192
+ * Server-driven mount hydration (`persistence: true`). The client holds no
193
+ * local snapshot; on mount it asks the server — keyed by the stable threadId —
194
+ * for the last generation's resume snapshot, validates it, and repaints it. It
195
+ * never auto-starts a run, and never blocks: a `generate()` that starts first
196
+ * owns the client and hydration backs off, mirroring the chat client.
197
+ *
198
+ * A genuine **miss** (no record for the thread) is silent; a genuine
199
+ * **failure** (transport error, authorize rejection, malformed body, a record
200
+ * the validator rejects) surfaces through `status` / `error` / `onError` — see
201
+ * the note on `GenerationClient.hydrateFromServer`.
202
+ */
203
+ private hydrateFromServer;
204
+ /**
205
+ * Surface a hydration failure on the observable fields. Skipped when a
206
+ * `generate()` took ownership while the hydrate GET was in flight.
207
+ */
208
+ private failHydration;
209
+ /**
210
+ * Re-attach to an already-loaded `running` snapshot (remount case); see the
211
+ * note in GenerationClient.maybeResumeInFlight. Guarded by `rejoinInFlight`.
212
+ */
213
+ private maybeResumeInFlight;
214
+ /**
215
+ * Re-attach to a video run still generating and stream it to completion,
216
+ * mirroring the chat client's mount-time rejoin. Reuses `processStream`, so
217
+ * the job status and result repaint from the replayed chunks. A live
218
+ * `generate()` owns the client and is never stomped; a run is rejoined once.
219
+ */
220
+ private rejoinInFlight;
110
221
  }