@tanstack/ai-client 0.22.0 → 0.23.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.
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
@@ -0,0 +1,242 @@
1
+ import type { ChatPersistedState, ChatStorageAdapter } from './types'
2
+
3
+ export interface WebStoragePersistenceOptions {
4
+ keyPrefix?: string
5
+ /**
6
+ * Defaults to `JSON.stringify`. Override only for values JSON can't
7
+ * round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a
8
+ * `Date` rather than an ISO string).
9
+ */
10
+ serialize?: (value: ChatPersistedState) => string
11
+ /** Defaults to `JSON.parse`. */
12
+ deserialize?: (value: string) => ChatPersistedState
13
+ }
14
+
15
+ export interface IndexedDBPersistenceOptions {
16
+ databaseName?: string
17
+ objectStoreName?: string
18
+ keyPrefix?: string
19
+ }
20
+
21
+ type StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB'
22
+
23
+ /**
24
+ * Thrown by a storage adapter when its backing store is absent — most commonly
25
+ * during server-side rendering, where `localStorage` / `sessionStorage` /
26
+ * `indexedDB` do not exist on `globalThis`. The adapters check availability
27
+ * lazily, **per operation**, so constructing an adapter never throws; the error
28
+ * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for
29
+ * IndexedDB). The chat persistence layer treats adapter failures as best-effort
30
+ * (storage errors never break chat setup or streaming).
31
+ */
32
+ export class StorageUnavailableError extends Error {
33
+ constructor(storageName: StorageName) {
34
+ super(`${storageName} is not available in this environment.`)
35
+ this.name = 'StorageUnavailableError'
36
+ }
37
+ }
38
+
39
+ function stringifyJson(value: ChatPersistedState): string {
40
+ const stringify: (input: unknown) => unknown = JSON.stringify
41
+ const serialized = stringify(value)
42
+ if (typeof serialized !== 'string') {
43
+ throw new TypeError('The value is not JSON serializable.')
44
+ }
45
+ return serialized
46
+ }
47
+
48
+ function createWebStoragePersistence(
49
+ storageName: 'localStorage' | 'sessionStorage',
50
+ options: WebStoragePersistenceOptions,
51
+ ): ChatStorageAdapter<ChatPersistedState> {
52
+ const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'
53
+ const serialize = options.serialize ?? stringifyJson
54
+ const deserialize = options.deserialize ?? JSON.parse
55
+ const key = (id: string) => `${keyPrefix}${id}`
56
+
57
+ const getStorage = (): Storage => {
58
+ const browserGlobals: {
59
+ localStorage?: Storage
60
+ sessionStorage?: Storage
61
+ } = globalThis
62
+ const storage = browserGlobals[storageName]
63
+ if (!storage) {
64
+ throw new StorageUnavailableError(storageName)
65
+ }
66
+ return storage
67
+ }
68
+
69
+ return {
70
+ getItem(id) {
71
+ const item = getStorage().getItem(key(id))
72
+ return item === null ? null : deserialize(item)
73
+ },
74
+ setItem(id, value) {
75
+ getStorage().setItem(key(id), serialize(value))
76
+ },
77
+ removeItem(id) {
78
+ getStorage().removeItem(key(id))
79
+ },
80
+ }
81
+ }
82
+
83
+ /**
84
+ * A `ChatStorageAdapter` backed by `window.localStorage` (persists across
85
+ * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which
86
+ * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and
87
+ * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the
88
+ * adapter can be constructed safely on the server.
89
+ *
90
+ * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /
91
+ * `JSON.parse`, so the common case needs no codec.
92
+ */
93
+ export function localStoragePersistence(
94
+ options: WebStoragePersistenceOptions = {},
95
+ ): ChatStorageAdapter<ChatPersistedState> {
96
+ return createWebStoragePersistence('localStorage', options)
97
+ }
98
+
99
+ /**
100
+ * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab
101
+ * and cleared when it closes). Identical to {@link localStoragePersistence} in
102
+ * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy
103
+ * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that
104
+ * defaults to `JSON.stringify` / `JSON.parse`.
105
+ */
106
+ export function sessionStoragePersistence(
107
+ options: WebStoragePersistenceOptions = {},
108
+ ): ChatStorageAdapter<ChatPersistedState> {
109
+ return createWebStoragePersistence('sessionStorage', options)
110
+ }
111
+
112
+ /**
113
+ * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web
114
+ * Storage or that benefit from structured-clone storage. All operations are
115
+ * async and the database opens lazily on first use; keys are namespaced with
116
+ * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.
117
+ * SSR) each operation rejects with {@link StorageUnavailableError}.
118
+ *
119
+ * No serialize/deserialize codec is needed or accepted — values are stored via
120
+ * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.
121
+ * round-trip without a JSON step.
122
+ */
123
+ export function indexedDBPersistence(
124
+ options: IndexedDBPersistenceOptions = {},
125
+ ): ChatStorageAdapter<ChatPersistedState> {
126
+ const databaseName = options.databaseName ?? 'tanstack-ai'
127
+ const objectStoreName = options.objectStoreName ?? 'persistence'
128
+ const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'
129
+ let databasePromise: Promise<IDBDatabase> | undefined
130
+
131
+ const openDatabase = (): Promise<IDBDatabase> => {
132
+ if (databasePromise) {
133
+ return databasePromise
134
+ }
135
+
136
+ databasePromise = new Promise<IDBDatabase>((resolve, reject) => {
137
+ const browserGlobals: { indexedDB?: IDBFactory } = globalThis
138
+ const factory = browserGlobals.indexedDB
139
+ if (!factory) {
140
+ reject(new StorageUnavailableError('indexedDB'))
141
+ return
142
+ }
143
+
144
+ let request: IDBOpenDBRequest
145
+ let openFailed = false
146
+ try {
147
+ request = factory.open(databaseName)
148
+ } catch (error) {
149
+ reject(error)
150
+ return
151
+ }
152
+
153
+ request.onupgradeneeded = () => {
154
+ if (!request.result.objectStoreNames.contains(objectStoreName)) {
155
+ request.result.createObjectStore(objectStoreName)
156
+ }
157
+ }
158
+ request.onerror = () => {
159
+ openFailed = true
160
+ reject(request.error ?? new Error(`Failed to open ${databaseName}.`))
161
+ }
162
+ request.onblocked = () => {
163
+ openFailed = true
164
+ reject(
165
+ new Error(
166
+ `Opening IndexedDB database "${databaseName}" was blocked.`,
167
+ ),
168
+ )
169
+ }
170
+ request.onsuccess = () => {
171
+ const database = request.result
172
+ if (openFailed) {
173
+ database.close()
174
+ return
175
+ }
176
+ database.onversionchange = () => {
177
+ database.close()
178
+ databasePromise = undefined
179
+ }
180
+ resolve(database)
181
+ }
182
+ }).catch((error: unknown) => {
183
+ databasePromise = undefined
184
+ throw error
185
+ })
186
+
187
+ return databasePromise
188
+ }
189
+
190
+ const runRequest = async <TResult>(
191
+ mode: IDBTransactionMode,
192
+ createRequest: (store: IDBObjectStore) => IDBRequest<TResult>,
193
+ ): Promise<TResult> => {
194
+ const database = await openDatabase()
195
+ return new Promise<TResult>((resolve, reject) => {
196
+ let request: IDBRequest<TResult>
197
+ let result: TResult
198
+ try {
199
+ const transaction = database.transaction(objectStoreName, mode)
200
+ request = createRequest(transaction.objectStore(objectStoreName))
201
+ request.onsuccess = () => {
202
+ result = request.result
203
+ }
204
+ request.onerror = () => {
205
+ reject(request.error ?? new Error('IndexedDB request failed.'))
206
+ }
207
+ transaction.oncomplete = () => {
208
+ resolve(result)
209
+ }
210
+ transaction.onerror = () => {
211
+ reject(
212
+ transaction.error ?? new Error('IndexedDB transaction failed.'),
213
+ )
214
+ }
215
+ transaction.onabort = () => {
216
+ reject(
217
+ transaction.error ?? new Error('IndexedDB transaction aborted.'),
218
+ )
219
+ }
220
+ } catch (error) {
221
+ reject(error)
222
+ }
223
+ })
224
+ }
225
+
226
+ const key = (id: string) => `${keyPrefix}${id}`
227
+ return {
228
+ getItem(id) {
229
+ return runRequest('readonly', (store) => store.get(key(id)))
230
+ },
231
+ setItem(id, value) {
232
+ return runRequest('readwrite', (store) => store.put(value, key(id))).then(
233
+ () => undefined,
234
+ )
235
+ },
236
+ removeItem(id) {
237
+ return runRequest('readwrite', (store) => store.delete(key(id))).then(
238
+ () => undefined,
239
+ )
240
+ },
241
+ }
242
+ }
package/src/types.ts CHANGED
@@ -1,13 +1,24 @@
1
1
  import type {
2
2
  AnyClientTool,
3
+ ApprovalCapabilityOf,
4
+ ApprovalSchemaOf,
3
5
  AudioPart,
6
+ BatchInterruptError,
4
7
  ChunkStrategy,
5
8
  ContentPart,
6
9
  DocumentPart,
7
10
  ImagePart,
11
+ InferSchemaType,
8
12
  InferToolInput,
9
13
  InferToolOutput,
14
+ InputSchemaOf,
15
+ Interrupt,
16
+ InterruptBinding,
17
+ ItemInterruptError,
10
18
  ModelMessage,
19
+ NoSchema,
20
+ RunAgentResumeItem,
21
+ SchemaInput,
11
22
  StreamChunk,
12
23
  StructuredOutputPart,
13
24
  UIResourcePart,
@@ -17,7 +28,181 @@ import type { ConnectionAdapter } from './connection-adapters'
17
28
  import type { AIDevtoolsClientMetadata } from './devtools'
18
29
  import type { ChatDevtoolsBridgeFactory } from './devtools-noop'
19
30
 
20
- export type { StructuredOutputPart } from '@tanstack/ai/client'
31
+ export type { StructuredOutputPart }
32
+
33
+ export interface ChatResumeState {
34
+ threadId: string
35
+ runId: string
36
+ }
37
+
38
+ export type ChatPendingInterrupt = Interrupt
39
+
40
+ /**
41
+ * The durable pointer a chat keeps for the run it may need to rejoin, plus any
42
+ * interrupt that run is waiting on.
43
+ *
44
+ * @internal
45
+ */
46
+ export interface ChatResumeSnapshot {
47
+ resumeState: ChatResumeState
48
+ pendingInterrupts?: Array<ChatPendingInterrupt>
49
+ }
50
+
51
+ export type InterruptItemStatus =
52
+ | 'pending'
53
+ | 'validating'
54
+ | 'staged'
55
+ | 'submitting'
56
+ | 'error'
57
+
58
+ export interface BoundInterruptBase {
59
+ readonly id: string
60
+ readonly interruptId: string
61
+ readonly reason: string
62
+ readonly message?: string
63
+ readonly responseSchema?: Readonly<Record<string, unknown>>
64
+ readonly expiresAt?: string
65
+ readonly metadata?: Readonly<Record<string, unknown>>
66
+ readonly threadId: string
67
+ readonly interruptedRunId: string
68
+ readonly generation: number
69
+ readonly status: InterruptItemStatus
70
+ readonly errors: ReadonlyArray<ItemInterruptError>
71
+ /** @deprecated Use `errors[0]`. */
72
+ readonly error?: ItemInterruptError
73
+ /**
74
+ * Whether the binding/schema allows resolution at hydrate time.
75
+ * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and
76
+ * `errors` for those lifecycle states.
77
+ */
78
+ readonly canResolve: boolean
79
+ cancel: () => void
80
+ clearResolution: () => void
81
+ }
82
+
83
+ export interface GenericAGUIInterrupt extends BoundInterruptBase {
84
+ readonly kind: 'generic'
85
+ readonly binding: Readonly<Extract<InterruptBinding, { kind: 'generic' }>>
86
+ resolveInterrupt: (payload: unknown) => void
87
+ }
88
+
89
+ /**
90
+ * An interrupt that arrived on the stream carrying no resume binding this
91
+ * client understands — no `tanstack:interruptBinding`, or one written at a
92
+ * protocol version we don't recognise.
93
+ *
94
+ * These are surfaced rather than hidden so a UI can show that the run is
95
+ * paused, but they are never resolvable here: something else owns them. A
96
+ * workflow engine's durable approval projected into the same AG-UI stream
97
+ * lands in this bucket, and resolving it through the chat resume path would
98
+ * send an answer no one is waiting for. Render it, or route it to whatever
99
+ * actually owns the pause.
100
+ */
101
+ export interface UnboundInterrupt extends BoundInterruptBase {
102
+ readonly kind: 'unbound'
103
+ readonly binding?: undefined
104
+ readonly canResolve: false
105
+ }
106
+
107
+ type ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> =
108
+ ApprovalSchemaOf<TTool> extends infer TApproval
109
+ ? TApproval extends { approve?: SchemaInput; reject?: SchemaInput }
110
+ ? Exclude<TApproval[TBranch], undefined>
111
+ : TApproval extends SchemaInput
112
+ ? TApproval
113
+ : never
114
+ : never
115
+
116
+ type ApprovalEdits<TTool> =
117
+ InputSchemaOf<TTool> extends NoSchema
118
+ ? { editedArgs?: never }
119
+ : { editedArgs?: InferToolInput<TTool> }
120
+
121
+ type ApprovalPayload<TSchema> = [TSchema] extends [never]
122
+ ? { payload?: never }
123
+ : TSchema extends SchemaInput
124
+ ? { payload: InferSchemaType<TSchema> }
125
+ : { payload?: never }
126
+
127
+ type ApproveArguments<TTool> = [
128
+ ApprovalBranchSchema<TTool, 'approve'>,
129
+ ] extends [never]
130
+ ? InputSchemaOf<TTool> extends NoSchema
131
+ ? [options?: never]
132
+ : [options?: ApprovalEdits<TTool> & { payload?: never }]
133
+ : [
134
+ options: ApprovalEdits<TTool> &
135
+ ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>,
136
+ ]
137
+
138
+ type RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [
139
+ never,
140
+ ]
141
+ ? [options?: never]
142
+ : [
143
+ options: { editedArgs?: never } & ApprovalPayload<
144
+ ApprovalBranchSchema<TTool, 'reject'>
145
+ >,
146
+ ]
147
+
148
+ export type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> =
149
+ TTool extends AnyClientTool
150
+ ? BoundInterruptBase & {
151
+ readonly kind: 'tool-approval'
152
+ readonly binding: Readonly<
153
+ Extract<InterruptBinding, { kind: 'tool-approval' }>
154
+ >
155
+ readonly toolName: TTool['name']
156
+ readonly toolCallId: string
157
+ readonly originalArgs: InferToolInput<TTool>
158
+ // A single generic call signature — not two overloads. Overloads break
159
+ // editor autocomplete: a half-typed options literal (e.g.
160
+ // `resolveInterrupt(true, { payload: {` ) satisfies neither overload,
161
+ // so TS resolves no signature and offers no contextual completions.
162
+ // Making `approved` a generic discriminant lets TS infer it from the
163
+ // first argument and pick the matching branch for the rest params, so
164
+ // `payload` / `editedArgs` / the correct schema's fields complete
165
+ // per-branch (a plain union-of-tuples would offer both branches'
166
+ // fields) while still enforcing the right shape.
167
+ resolveInterrupt: <TApproved extends boolean>(
168
+ approved: TApproved,
169
+ ...args: TApproved extends true
170
+ ? ApproveArguments<TTool>
171
+ : RejectArguments<TTool>
172
+ ) => void
173
+ }
174
+ : never
175
+
176
+ type ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> =
177
+ TTools[number] extends infer TTool
178
+ ? TTool extends AnyClientTool
179
+ ? ApprovalCapabilityOf<TTool> extends true
180
+ ? ToolApprovalInterrupt<TTool>
181
+ : never
182
+ : never
183
+ : never
184
+
185
+ // Client tools resolve through their `.client()` implementation (auto-run) or
186
+ // `addToolResult` — never as a bound interrupt. The `client-tool-execution`
187
+ // pause is handled internally and is intentionally absent from this public
188
+ // union.
189
+ export type ChatInterrupt<
190
+ TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
191
+ > = GenericAGUIInterrupt | UnboundInterrupt | ApprovalInterrupts<TTools>
192
+
193
+ export type BoundInterrupts<
194
+ TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
195
+ > = ReadonlyArray<ChatInterrupt<TTools>>
196
+
197
+ export interface ChatInterruptState<
198
+ TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
199
+ > {
200
+ readonly interrupts: BoundInterrupts<TTools>
201
+ /** @deprecated Use `interrupts`. Same snapshot today. */
202
+ readonly pendingInterrupts: BoundInterrupts<TTools>
203
+ readonly interruptErrors: ReadonlyArray<BatchInterruptError>
204
+ readonly resuming: boolean
205
+ }
21
206
 
22
207
  /**
23
208
  * `messages` is the full UIMessage history (not a delta). `data` is the
@@ -31,6 +216,8 @@ export interface ChatFetcherInput {
31
216
  data?: Record<string, unknown>
32
217
  threadId: string
33
218
  runId: string
219
+ parentRunId?: string
220
+ resume?: Array<RunAgentResumeItem>
34
221
  }
35
222
 
36
223
  export interface ChatFetcherOptions {
@@ -363,23 +550,84 @@ export interface UIMessage<
363
550
  createdAt?: Date
364
551
  }
365
552
 
553
+ /**
554
+ * A generic key/value storage adapter. `getItem` may be sync or async; the
555
+ * chat persistence layer treats every call as best-effort. The provided
556
+ * `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
557
+ * factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`
558
+ * is assignable to {@link ChatClientPersistence}.
559
+ */
560
+ export interface ChatStorageAdapter<TValue> {
561
+ getItem: (
562
+ id: string,
563
+ ) => TValue | null | undefined | Promise<TValue | null | undefined>
564
+ setItem: (id: string, value: TValue) => void | Promise<void>
565
+ removeItem: (id: string) => void | Promise<void>
566
+ }
567
+
568
+ /**
569
+ * The single record a `ChatClientPersistence` adapter stores per chat. It folds
570
+ * the two things that must survive a full page reload into one blob under one
571
+ * key: the message transcript and the optional resume snapshot (which run to
572
+ * rejoin / which interrupts to rehydrate). One adapter, one key — see
573
+ * {@link ChatClientPersistence}.
574
+ */
575
+ export interface ChatPersistedState<
576
+ TTools extends ReadonlyArray<AnyClientTool> = any,
577
+ > {
578
+ messages: Array<UIMessage<TTools>>
579
+ /** Present while a run is in flight or paused on an interrupt; absent otherwise. */
580
+ resume?: ChatResumeSnapshot
581
+ }
582
+
583
+ /**
584
+ * Storage adapter for durable chat state. A single adapter persists both the
585
+ * message transcript and the resume snapshot as one {@link ChatPersistedState}
586
+ * record, so a full page reload restores the conversation AND can rejoin an
587
+ * in-flight run / rehydrate pending interrupts.
588
+ *
589
+ * For backward compatibility `getItem` may also return a bare `UIMessage[]`
590
+ * (the legacy messages-only format); the client normalizes it to
591
+ * `{ messages }`. `setItem` always writes the combined record.
592
+ */
366
593
  export interface ChatClientPersistence<
367
594
  TTools extends ReadonlyArray<AnyClientTool> = any,
368
595
  > {
369
596
  getItem: (
370
597
  id: string,
371
598
  ) =>
599
+ | ChatPersistedState<TTools>
372
600
  | Array<UIMessage<TTools>>
373
601
  | null
374
602
  | undefined
375
- | Promise<Array<UIMessage<TTools>> | null | undefined>
603
+ | Promise<
604
+ ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined
605
+ >
376
606
  setItem: (
377
607
  id: string,
378
- messages: Array<UIMessage<TTools>>,
608
+ state: ChatPersistedState<TTools>,
379
609
  ) => void | Promise<void>
380
610
  removeItem: (id: string) => void | Promise<void>
381
611
  }
382
612
 
613
+ /**
614
+ * The `persistence` option for a chat.
615
+ *
616
+ * - `false` (default): ephemeral. Messages live in memory only; a reload starts
617
+ * from empty.
618
+ * - `true`: server-authoritative. Nothing is cached in the browser. On mount the
619
+ * client hydrates the thread from the server by its `threadId` (paints the
620
+ * stored transcript and tails any run still generating), so a reload or the
621
+ * same thread opened on another device both just resume. Requires a connection
622
+ * with a `hydrate` handler.
623
+ * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
624
+ * {@link ChatPersistedState} record (transcript plus resume pointer) is cached
625
+ * in the browser and restored on reload with no network.
626
+ */
627
+ export type ChatPersistenceOption<
628
+ TTools extends ReadonlyArray<AnyClientTool> = any,
629
+ > = boolean | ChatClientPersistence<TTools>
630
+
383
631
  type IsUnknown<T> = unknown extends T
384
632
  ? [T] extends [unknown]
385
633
  ? true
@@ -471,22 +719,49 @@ export interface ChatClientBaseOptions<
471
719
  initialMessages?: Array<UIMessage<TTools>>
472
720
 
473
721
  /**
474
- * Optional persistence adapter for chat messages.
722
+ * How this chat persists across reloads. See {@link ChatPersistenceOption}.
723
+ *
724
+ * - Omit or `false`: ephemeral, in-memory only.
725
+ * - `true`: server-authoritative. The client caches nothing and hydrates the
726
+ * thread from the server by its `threadId` on mount (needs a connection with
727
+ * a `hydrate` handler). Big transcripts never touch the browser, and the same
728
+ * thread opens the same way on another device.
729
+ * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
730
+ * {@link ChatPersistedState} record (transcript plus resume pointer) is cached
731
+ * in the browser, restoring the transcript, pending interrupts, and an
732
+ * in-flight run on reload.
733
+ *
734
+ * Use `initialResumeSnapshot` for a host-supplied in-memory rehydrate instead.
475
735
  */
476
- persistence?: ChatClientPersistence<TTools>
736
+ persistence?: ChatPersistenceOption<TTools>
477
737
 
478
738
  /**
479
- * Unique identifier for this chat instance
480
- * Used for managing multiple chats
739
+ * Optional storage-key override for this chat instance, and the devtools
740
+ * instance id. Persistence keys on `threadId` by default; set `id` only when
741
+ * you need the persisted record keyed separately from the wire thread.
742
+ * Prefer a stable `threadId` for the common case.
743
+ *
744
+ * The framework hooks (`useChat` / `createChat`) do NOT expose `id`: a hook's
745
+ * identity is its `threadId`. This lower-level escape hatch exists only for
746
+ * direct `ChatClient` construction.
481
747
  */
482
748
  id?: string
483
749
 
484
750
  /**
485
- * Thread ID to use for this chat session. Persists across sends within
486
- * the session. If omitted, a unique thread ID is generated.
751
+ * The conversation id for this chat, stable across sends and reloads. It is
752
+ * the AG-UI thread key on the wire AND the key client persistence stores the
753
+ * conversation under, so set a stable `threadId` to have a reload restore the
754
+ * same conversation. If omitted, a unique thread id is generated per session.
487
755
  */
488
756
  threadId?: string
489
757
 
758
+ /**
759
+ * Initial resumable run state, useful when rehydrating a persisted client
760
+ * after a full page reload. This restores the client-side interrupt
761
+ * descriptors needed to send AG-UI resume entries.
762
+ */
763
+ initialResumeSnapshot?: ChatResumeSnapshot
764
+
490
765
  /**
491
766
  * Arbitrary client-controlled JSON forwarded to the server in the
492
767
  * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
@@ -591,6 +866,23 @@ export interface ChatClientBaseOptions<
591
866
  */
592
867
  onQueueChange?: (queue: Array<QueuedMessage>) => void
593
868
 
869
+ /**
870
+ * Callback when resumable run state or pending interrupts change.
871
+ */
872
+ onResumeStateChange?: (
873
+ resumeState: ChatResumeState | null,
874
+ pendingInterrupts: BoundInterrupts<TTools>,
875
+ ) => void
876
+
877
+ /**
878
+ * Callback when the id of the run this client has in flight changes: the new
879
+ * id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.
880
+ */
881
+ onRunIdChange?: (runId: string | null) => void
882
+
883
+ /** Callback when the immutable interrupt state snapshot changes. */
884
+ onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void
885
+
594
886
  /**
595
887
  * Callback when a custom event is received from a server-side tool.
596
888
  * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.