@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
@@ -1,6 +1,6 @@
1
- import { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai/client';
1
+ import { AnyClientTool, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
2
2
  import { ConnectionAdapter } from './connection-adapters.js';
3
- import { ChatClientOptions, ChatClientState, ChatFetcher, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
3
+ import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterrupt, ChatInterruptState, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
4
4
  type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>> = {
5
5
  connection?: ConnectionAdapter;
6
6
  fetcher?: ChatFetcher;
@@ -17,6 +17,13 @@ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClien
17
17
  onConnectionStatusChange?: (status: ConnectionStatus) => void;
18
18
  onSessionGeneratingChange?: (isGenerating: boolean) => void;
19
19
  onQueueChange?: (queue: Array<QueuedMessage>) => void;
20
+ onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools>) => void;
21
+ /**
22
+ * Fires whenever the id of the run in flight changes: the new id when a run
23
+ * starts (including a rejoin), `null` when it settles.
24
+ */
25
+ onRunIdChange?: (runId: string | null) => void;
26
+ onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void;
20
27
  onCustomEvent?: (eventType: string, data: unknown, context: {
21
28
  toolCallId?: string;
22
29
  }) => void;
@@ -42,7 +49,19 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
42
49
  private readonly uniqueId;
43
50
  private readonly threadId;
44
51
  private readonly persistor?;
52
+ private readonly clearedStreamTracker;
45
53
  private currentRunId;
54
+ private lastResume;
55
+ private rejoinedRunId;
56
+ private readonly interruptManager;
57
+ private activeInterruptSubmission;
58
+ private interruptSubmissionFailure;
59
+ private readonly joinedRunWaiters;
60
+ private pendingResumeParentRunId;
61
+ private pendingResumeThreadId;
62
+ private pendingResumeItems;
63
+ private activeResumeThreadId;
64
+ private activeResumeRunId;
46
65
  private bodyOption;
47
66
  private forwardedPropsOption;
48
67
  private context;
@@ -102,9 +121,74 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
102
121
  private draining;
103
122
  private sessionGenerating;
104
123
  private readonly activeRunIds;
124
+ /** Latched by `dispose()`; stops any late async callback starting new work. */
125
+ private disposed;
126
+ /** Whether a view is currently watching. See `attach` / `detach`. */
127
+ private tailing;
128
+ /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
129
+ private readonly rejoinRunId;
130
+ private readonly cachesMessages;
105
131
  private devtoolsMounted;
106
132
  private readonly callbacksRef;
107
133
  constructor(options: ChatClientOptions<TTools, TContext>);
134
+ /**
135
+ * START TAILING: re-attach to an in-flight run so its chunks arrive here.
136
+ *
137
+ * Called by the constructor, and again by a UI wrapper every time its view
138
+ * mounts. Idempotent — attaching while already attached does nothing — so the
139
+ * constructor call and a wrapper's first mount cost one attach between them.
140
+ *
141
+ * Pairs with {@link detach}. The pair exists because tailing used to begin ONLY
142
+ * in the constructor, which meant a view could never stop tailing and then
143
+ * resume: unmount had to either keep the connection open or lose it for good.
144
+ * Keeping it open is what starved the page — a browser allows ~6 connections per
145
+ * origin, and one long-lived stream per view reaches that after a handful of
146
+ * views, after which every other request queues (measured: an in-page fetch took
147
+ * over two minutes while the same request from outside the browser took 17ms).
148
+ */
149
+ attach(): void;
150
+ /**
151
+ * STOP TAILING: drop the connection, keep everything else.
152
+ *
153
+ * Called by a UI wrapper when its view unmounts. The transcript, the resume
154
+ * pointer and the run id all stay, so a later {@link attach} repaints instantly
155
+ * and re-tails from the durable log — nothing is lost, because the run keeps
156
+ * going server-side and its log holds every chunk.
157
+ *
158
+ * Deliberately NOT `dispose()`: this client is expected back. And deliberately
159
+ * not `stop()`, which means "the user ended this run" — detaching says only that
160
+ * nobody is watching right now.
161
+ *
162
+ * `rejoinedRunId` is cleared so the next `attach` can re-join the same run;
163
+ * without that reset the guard in {@link maybeRejoinInFlight} would treat the
164
+ * run as already joined and the view would come back silent.
165
+ */
166
+ detach(): void;
167
+ private applyResumeSnapshot;
168
+ /**
169
+ * Apply a resume snapshot read from durable storage. Restores interrupt state,
170
+ * and for a bare in-flight run (no pending interrupts) also rejoins it. This is
171
+ * the async-store counterpart to the synchronous rejoin in the constructor:
172
+ * `applyResumeSnapshot` alone only handles interrupts, so an async store
173
+ * (`indexedDBPersistence`) would otherwise never rejoin a mid-stream run.
174
+ */
175
+ private applyPersistedResume;
176
+ /**
177
+ * Rejoin a persisted in-flight run, guarded so it fires at most once and never
178
+ * while another run is already active. Skipped when the connection is not
179
+ * resumable (`joinRun` absent), so a non-durable transport is a no-op.
180
+ */
181
+ private maybeRejoinInFlight;
182
+ /**
183
+ * Server-authoritative mount hydration (`persistence: true`). The client holds
184
+ * no transcript and no run pointer; on mount it asks the server — keyed by the
185
+ * stable threadId — for the stored transcript and whether a run is still
186
+ * generating. The transcript repaints immediately; an in-flight run is tailed
187
+ * through the same durability rejoin as a reload. Best-effort and
188
+ * non-blocking: a failure leaves the client empty rather than throwing, and a
189
+ * send that starts first owns the client (hydration then backs off).
190
+ */
191
+ private hydrateFromServer;
108
192
  mountDevtools(): void;
109
193
  /**
110
194
  * Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
@@ -112,13 +196,57 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
112
196
  * owns the active-run / session / processing state.
113
197
  */
114
198
  private drainIgnoredRunlessChunk;
199
+ private retireIgnoredClearedTerminalChunk;
115
200
  private updateRunLifecycle;
201
+ /**
202
+ * Track interrupt state off the stream's terminal events. A RUN_FINISHED with
203
+ * an interrupt outcome records the pending interrupts + the run/thread to
204
+ * resume; any other terminal event for the tracked/current run clears that
205
+ * state. This is interrupt (state) resume — there is no delivery cursor.
206
+ */
207
+ private observeInterruptState;
208
+ /**
209
+ * The interrupt-resume state for the active/interrupted run (its run/thread
210
+ * ids), or null when there is nothing to resume. Apps can persist this to
211
+ * resume interrupts across a full reload.
212
+ */
213
+ getResumeState(): ChatResumeState | null;
214
+ /**
215
+ * The id of the run this client has in flight — one it started via a send or
216
+ * rejoined via `joinRun` — or null when there is none. Unlike
217
+ * {@link getResumeState}, this tracks ordinary runs too, not only one that is
218
+ * interrupted or being resumed. A run another client started and that arrives
219
+ * over a live subscription is not this client's run and is not reported here.
220
+ */
221
+ getCurrentRunId(): string | null;
222
+ private setCurrentRunId;
223
+ getInterruptState(): ChatInterruptState<TTools>;
224
+ getInterrupts(): BoundInterrupts<TTools>;
225
+ /** @deprecated Use getInterrupts(). */
226
+ getPendingInterrupts(): BoundInterrupts<TTools>;
227
+ resolveInterrupts(approved: boolean): void;
228
+ resolveInterrupts(resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
229
+ cancelInterrupts(): void;
230
+ retryInterrupts(): void;
231
+ /** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
232
+ resumeInterruptsUnsafe(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
233
+ /** @deprecated Use bound interrupt methods or resumeInterruptsUnsafe(). */
234
+ resumeInterrupts(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
235
+ private submitInterruptBatch;
236
+ private takeInterruptSubmissionFailure;
237
+ private interruptGeneration;
116
238
  private generateUniqueId;
117
239
  private setIsLoading;
118
240
  private setStatus;
119
241
  private setIsSubscribed;
120
242
  private setConnectionStatus;
121
243
  private setSessionGenerating;
244
+ private notifyResumeStateChange;
245
+ /**
246
+ * Build the durable resume snapshot from the current resume state + pending
247
+ * interrupt descriptors and hand it to the persistor (null clears it).
248
+ */
249
+ private persistResumeSnapshot;
122
250
  private resetSessionGenerating;
123
251
  private setError;
124
252
  private buildDevtoolsBridgeOptions;
@@ -136,6 +264,40 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
136
264
  * Consume chunks from the connection subscription.
137
265
  */
138
266
  private consumeSubscription;
267
+ /**
268
+ * Re-attach to an in-flight run after a full page reload, replaying its stream
269
+ * from the server's delivery-durability log via `joinRun` (which returns the
270
+ * whole run so far, then tails live to completion).
271
+ *
272
+ * The log is the single source of truth for the run, so we rebuild the
273
+ * in-flight assistant bubble from it rather than trying to reconcile the
274
+ * server-hydrated partial with the replay: on the first chunk that actually
275
+ * (re)builds a message we drop the hydrated in-flight assistant, and the
276
+ * replay reconstructs one clean bubble. Dropping only on real content (not on
277
+ * `RUN_STARTED`) means a rejoin that connects but delivers nothing can never
278
+ * leave an empty bubble behind.
279
+ *
280
+ * Bounded connect: a durable backend keeps a from-start join open waiting for
281
+ * a producer, so a stale pointer to an unknown/evicted run would otherwise pin
282
+ * the UI in a loading state for the backend's full first-chunk deadline. We
283
+ * give up after {@link REJOIN_CONNECT_DEADLINE_MS} if no chunk arrives and
284
+ * clear the dead pointer so it does not retry on the next load.
285
+ *
286
+ * Replay chunks are processed WITHOUT the per-chunk yield the live path uses,
287
+ * so the buffered prefix snaps in and only the genuinely-live tail streams at
288
+ * network speed — a reload looks like the run continued, not like it re-typed.
289
+ */
290
+ private resumeInFlightRun;
291
+ /**
292
+ * Drop a hydrated, still-in-flight assistant turn so a resume replay can
293
+ * rebuild it cleanly. Only touches a trailing assistant message (the shape a
294
+ * reload-mid-stream leaves); a thread whose last turn is a user message (run
295
+ * never produced, or already settled) is left untouched.
296
+ */
297
+ private dropTrailingInFlightAssistant;
298
+ private processIncomingChunk;
299
+ private isActiveInterruptSubmissionFailure;
300
+ private resolveJoinedRun;
139
301
  /**
140
302
  * Ensure subscription loop is running, starting it if needed.
141
303
  */
@@ -185,11 +347,18 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
185
347
  * ],
186
348
  * id: 'custom-message-id'
187
349
  * },
188
- * { model: 'gpt-4-audio' }
350
+ * { model: 'gpt-5.5' }
189
351
  * )
190
352
  * ```
191
353
  */
192
354
  sendMessage(content: string | MultimodalContent, body?: Record<string, any>, sendOptions?: SendMessageOptions): Promise<void>;
355
+ /**
356
+ * True when the client still has user-actionable interrupts (or is mid
357
+ * resume submission). Staged/submitting items that are already being
358
+ * continued do not block a later turn once the resume stream has cleared
359
+ * resume state.
360
+ */
361
+ private hasBlockingInterrupts;
193
362
  /** True while a stream is active, a send is claiming the client, or the queue is draining. */
194
363
  private isSendBusy;
195
364
  private resolveBusyReason;