@tanstack/ai-client 0.23.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,14 @@
36
36
  </a>
37
37
  </div>
38
38
 
39
+ <br />
40
+
41
+ <div align="center">
42
+ <a href="https://tanstack.com/blog/tanstack-open-source-awards-2026">
43
+ <img src="https://raw.githubusercontent.com/TanStack/ai/16826d81cade868956df240d6239a671e689193c/media/js-open-source-award-2026-ai-project-of-the-year.svg" alt="Winner of the 2026 JavaScript Open Source Award for AI Project of the Year" width="250" />
44
+ </a>
45
+ </div>
46
+
39
47
  # TanStack AI
40
48
 
41
49
  Type-safe, provider-agnostic TypeScript SDK for building streaming chat,
@@ -191,7 +199,7 @@ Learn more in the
191
199
  build low-latency realtime voice experiences.
192
200
  - [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
193
201
  models write and execute TypeScript inside a secure isolate.
194
- - [Code Mode with Skills](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-skills) -
202
+ - [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) -
195
203
  give Code Mode reusable runtime capabilities.
196
204
 
197
205
  ## Providers
@@ -208,6 +216,7 @@ Official adapters include:
208
216
  | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
209
217
  | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
210
218
  | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
219
+ | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
211
220
  | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
212
221
 
213
222
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
@@ -46,8 +46,8 @@ export declare function normalizeQueueOption(option: QueueOption | undefined): N
46
46
  export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown> {
47
47
  private readonly processor;
48
48
  private connection;
49
- private readonly uniqueId;
50
- private readonly threadId;
49
+ private uniqueId;
50
+ private threadId;
51
51
  private readonly persistor?;
52
52
  private readonly clearedStreamTracker;
53
53
  private currentRunId;
@@ -190,6 +190,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
190
190
  */
191
191
  private hydrateFromServer;
192
192
  mountDevtools(): void;
193
+ private ensureThreadId;
193
194
  /**
194
195
  * Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
195
196
  * still tracking. The persistor owns the cleared-run bookkeeping; the client
@@ -352,12 +353,9 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
352
353
  * ```
353
354
  */
354
355
  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
- */
356
+ /** True while interrupt descriptors still own continuation. */
357
+ private hasPendingInterrupts;
358
+ /** True while an interrupt batch owns the next user turn. */
361
359
  private hasBlockingInterrupts;
362
360
  /** True while a stream is active, a send is claiming the client, or the queue is draining. */
363
361
  private isSendBusy;
@@ -197,13 +197,15 @@ var ChatClient = class {
197
197
  devtoolsMounted = false;
198
198
  callbacksRef;
199
199
  constructor(options) {
200
- this.threadId = options.threadId || this.generateUniqueId("thread");
201
- this.uniqueId = options.id || this.threadId;
200
+ this.threadId = options.threadId || "";
201
+ this.uniqueId = this.threadId;
202
202
  let cachesMessages = true;
203
- if (options.persistence === true) cachesMessages = false;
204
- else if (options.persistence) {
205
- const persistenceKey = options.id ?? this.threadId;
206
- this.persistor = new ChatPersistor(options.persistence, persistenceKey, (messages) => this.processor.setMessages(messages), (snapshot) => this.applyPersistedResume(snapshot));
203
+ if (options.persistence === true) {
204
+ if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
205
+ cachesMessages = false;
206
+ } else if (options.persistence) {
207
+ if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
208
+ this.persistor = new ChatPersistor(options.persistence, options.threadId, (messages) => this.processor.setMessages(messages), (snapshot) => this.applyPersistedResume(snapshot));
207
209
  }
208
210
  this.bodyOption = options.body || {};
209
211
  this.forwardedPropsOption = options.forwardedProps || {};
@@ -367,6 +369,7 @@ var ChatClient = class {
367
369
  */
368
370
  attach() {
369
371
  if (this.disposed || this.tailing) return;
372
+ this.ensureThreadId();
370
373
  this.tailing = true;
371
374
  if (this.rejoinRunId) this.maybeRejoinInFlight(this.rejoinRunId);
372
375
  if (!this.cachesMessages && this.connection.hydrate) this.hydrateFromServer();
@@ -474,10 +477,16 @@ var ChatClient = class {
474
477
  })();
475
478
  }
476
479
  mountDevtools() {
480
+ this.ensureThreadId();
477
481
  if (this.devtoolsMounted) return;
478
482
  this.devtoolsMounted = true;
479
483
  this.devtoolsBridge.mountWithTools(this.processor.getMessages().length);
480
484
  }
485
+ ensureThreadId() {
486
+ if (!this.threadId) this.threadId = this.generateUniqueId("thread");
487
+ this.uniqueId = this.threadId;
488
+ return this.threadId;
489
+ }
481
490
  /**
482
491
  * Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
483
492
  * still tracking. The persistor owns the cleared-run bookkeeping; the client
@@ -732,10 +741,17 @@ var ChatClient = class {
732
741
  this.events.errorChanged(error?.message || null);
733
742
  }
734
743
  buildDevtoolsBridgeOptions(devtools) {
744
+ const client = this;
735
745
  return {
736
- hookId: this.uniqueId,
737
- clientId: this.uniqueId,
738
- threadId: this.threadId,
746
+ get hookId() {
747
+ return client.uniqueId;
748
+ },
749
+ get clientId() {
750
+ return client.uniqueId;
751
+ },
752
+ get threadId() {
753
+ return client.threadId;
754
+ },
739
755
  metadata: {
740
756
  hookName: devtools?.hookName ?? "useChat",
741
757
  outputKind: devtools?.outputKind ?? "chat",
@@ -1038,16 +1054,13 @@ var ChatClient = class {
1038
1054
  this.sendInFlight = false;
1039
1055
  }
1040
1056
  }
1041
- /**
1042
- * True when the client still has user-actionable interrupts (or is mid
1043
- * resume submission). Staged/submitting items that are already being
1044
- * continued do not block a later turn once the resume stream has cleared
1045
- * resume state.
1046
- */
1057
+ /** True while interrupt descriptors still own continuation. */
1058
+ hasPendingInterrupts() {
1059
+ return this.interruptManager.getDescriptors().length > 0;
1060
+ }
1061
+ /** True while an interrupt batch owns the next user turn. */
1047
1062
  hasBlockingInterrupts() {
1048
- if (!this.lastResume && !this.activeInterruptSubmission) return false;
1049
- if (this.activeInterruptSubmission) return true;
1050
- return this.interruptManager.getInterrupts().some((item) => item.status === "pending" || item.status === "validating" || item.status === "error" || item.status === "staged");
1063
+ return this.activeInterruptSubmission !== void 0 || this.hasPendingInterrupts();
1051
1064
  }
1052
1065
  /** True while a stream is active, a send is claiming the client, or the queue is draining. */
1053
1066
  isSendBusy() {
@@ -1445,6 +1458,7 @@ var ChatClient = class {
1445
1458
  * Check if we should continue the flow and do so if needed
1446
1459
  */
1447
1460
  async checkForContinuation() {
1461
+ if (this.hasPendingInterrupts()) return;
1448
1462
  if (this.continuationPending || this.isLoading) {
1449
1463
  this.continuationSkipped = true;
1450
1464
  return;