@tanstack/ai-client 0.23.3 → 0.25.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.
- package/README.md +10 -1
- package/dist/esm/chat-client.d.ts +17 -13
- package/dist/esm/chat-client.js +49 -18
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +49 -0
- package/dist/esm/connection-adapters.js +269 -29
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools.d.ts +0 -1
- package/dist/esm/devtools.js +4 -10
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/generation-client.d.ts +5 -11
- package/dist/esm/generation-client.js +18 -5
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +15 -45
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/interrupt-manager.d.ts +15 -10
- package/dist/esm/interrupt-manager.js +176 -83
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/types.d.ts +66 -48
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +5 -11
- package/dist/esm/video-generation-client.js +18 -5
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +148 -53
- package/src/connection-adapters.ts +505 -47
- package/src/devtools.ts +6 -22
- package/src/generation-client.ts +36 -24
- package/src/generation-types.ts +15 -47
- package/src/index.ts +6 -0
- package/src/interrupt-manager.ts +386 -145
- package/src/types.ts +133 -50
- package/src/video-generation-client.ts +33 -20
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
|
|
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,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { AnyClientTool, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
|
|
1
|
+
import { AnyClientTool, InterruptDefinition, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
|
|
2
2
|
import { ConnectionAdapter } from './connection-adapters.js';
|
|
3
|
-
import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher,
|
|
4
|
-
type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>> = {
|
|
3
|
+
import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
|
|
4
|
+
type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = {
|
|
5
5
|
connection?: ConnectionAdapter;
|
|
6
6
|
fetcher?: ChatFetcher;
|
|
7
7
|
/** @deprecated Use `forwardedProps` instead. */
|
|
8
8
|
body?: Record<string, any>;
|
|
9
9
|
forwardedProps?: Record<string, any>;
|
|
10
10
|
tools?: TTools;
|
|
11
|
+
interrupts?: TInterrupts;
|
|
11
12
|
queue?: QueueOption;
|
|
12
13
|
onResponse?: (response?: Response) => void | Promise<void>;
|
|
13
14
|
onChunk?: (chunk: StreamChunk) => void;
|
|
@@ -17,13 +18,15 @@ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClien
|
|
|
17
18
|
onConnectionStatusChange?: (status: ConnectionStatus) => void;
|
|
18
19
|
onSessionGeneratingChange?: (isGenerating: boolean) => void;
|
|
19
20
|
onQueueChange?: (queue: Array<QueuedMessage>) => void;
|
|
20
|
-
onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools>) => void;
|
|
21
|
+
onResumeStateChange?: (resumeState: ChatResumeState | null, pendingInterrupts: BoundInterrupts<TTools, TInterrupts>) => void;
|
|
21
22
|
/**
|
|
22
23
|
* Fires whenever the id of the run in flight changes: the new id when a run
|
|
23
24
|
* starts (including a rejoin), `null` when it settles.
|
|
24
25
|
*/
|
|
25
26
|
onRunIdChange?: (runId: string | null) => void;
|
|
26
|
-
onInterruptStateChange?: (state: ChatInterruptState<TTools
|
|
27
|
+
onInterruptStateChange?: (state: ChatInterruptState<TTools, TInterrupts>, context: {
|
|
28
|
+
source: 'hydrate' | 'live';
|
|
29
|
+
}) => void;
|
|
27
30
|
onCustomEvent?: (eventType: string, data: unknown, context: {
|
|
28
31
|
toolCallId?: string;
|
|
29
32
|
}) => void;
|
|
@@ -43,11 +46,11 @@ export interface NormalizedQueueConfig {
|
|
|
43
46
|
strategy?: QueueStrategy;
|
|
44
47
|
}
|
|
45
48
|
export declare function normalizeQueueOption(option: QueueOption | undefined): NormalizedQueueConfig;
|
|
46
|
-
export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown> {
|
|
49
|
+
export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = unknown, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = any> {
|
|
47
50
|
private readonly processor;
|
|
48
51
|
private connection;
|
|
49
|
-
private
|
|
50
|
-
private
|
|
52
|
+
private uniqueId;
|
|
53
|
+
private threadId;
|
|
51
54
|
private readonly persistor?;
|
|
52
55
|
private readonly clearedStreamTracker;
|
|
53
56
|
private currentRunId;
|
|
@@ -130,7 +133,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
130
133
|
private readonly cachesMessages;
|
|
131
134
|
private devtoolsMounted;
|
|
132
135
|
private readonly callbacksRef;
|
|
133
|
-
constructor(options: ChatClientOptions<TTools, TContext>);
|
|
136
|
+
constructor(options: ChatClientOptions<TTools, TContext, TInterrupts>);
|
|
134
137
|
/**
|
|
135
138
|
* START TAILING: re-attach to an in-flight run so its chunks arrive here.
|
|
136
139
|
*
|
|
@@ -190,6 +193,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
190
193
|
*/
|
|
191
194
|
private hydrateFromServer;
|
|
192
195
|
mountDevtools(): void;
|
|
196
|
+
private ensureThreadId;
|
|
193
197
|
/**
|
|
194
198
|
* Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
|
|
195
199
|
* still tracking. The persistor owns the cleared-run bookkeeping; the client
|
|
@@ -220,12 +224,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
220
224
|
*/
|
|
221
225
|
getCurrentRunId(): string | null;
|
|
222
226
|
private setCurrentRunId;
|
|
223
|
-
getInterruptState(): ChatInterruptState<TTools>;
|
|
224
|
-
getInterrupts(): BoundInterrupts<TTools>;
|
|
227
|
+
getInterruptState(): ChatInterruptState<TTools, TInterrupts>;
|
|
228
|
+
getInterrupts(): BoundInterrupts<TTools, TInterrupts>;
|
|
225
229
|
/** @deprecated Use getInterrupts(). */
|
|
226
|
-
getPendingInterrupts(): BoundInterrupts<TTools>;
|
|
230
|
+
getPendingInterrupts(): BoundInterrupts<TTools, TInterrupts>;
|
|
227
231
|
resolveInterrupts(approved: boolean): void;
|
|
228
|
-
resolveInterrupts(resolver: (interrupt:
|
|
232
|
+
resolveInterrupts(resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
|
|
229
233
|
cancelInterrupts(): void;
|
|
230
234
|
retryInterrupts(): void;
|
|
231
235
|
/** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -5,6 +5,13 @@ import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
|
|
|
5
5
|
import { InterruptManager } from "./interrupt-manager.js";
|
|
6
6
|
import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, normalizeToUIMessage, parseWithStandardSchema } from "@tanstack/ai/client";
|
|
7
7
|
//#region src/chat-client.ts
|
|
8
|
+
function assertUniqueInterruptDefinitions(interrupts) {
|
|
9
|
+
const ids = /* @__PURE__ */ new Set();
|
|
10
|
+
for (const interrupt of interrupts ?? []) {
|
|
11
|
+
if (ids.has(interrupt.id)) throw new Error(`Duplicate interrupt definition id: ${interrupt.id}`);
|
|
12
|
+
ids.add(interrupt.id);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
8
15
|
function resolveTransport(transport) {
|
|
9
16
|
const { connection, fetcher } = transport;
|
|
10
17
|
if (connection && fetcher) throw new Error("ChatClient: pass either `connection` or `fetcher`, not both.");
|
|
@@ -197,13 +204,16 @@ var ChatClient = class {
|
|
|
197
204
|
devtoolsMounted = false;
|
|
198
205
|
callbacksRef;
|
|
199
206
|
constructor(options) {
|
|
200
|
-
|
|
201
|
-
this.
|
|
207
|
+
assertUniqueInterruptDefinitions(options.interrupts);
|
|
208
|
+
this.threadId = options.threadId || "";
|
|
209
|
+
this.uniqueId = this.threadId;
|
|
202
210
|
let cachesMessages = true;
|
|
203
|
-
if (options.persistence === true)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
if (options.persistence === true) {
|
|
212
|
+
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).");
|
|
213
|
+
cachesMessages = false;
|
|
214
|
+
} else if (options.persistence) {
|
|
215
|
+
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).");
|
|
216
|
+
this.persistor = new ChatPersistor(options.persistence, options.threadId, (messages) => this.processor.setMessages(messages), (snapshot) => this.applyPersistedResume(snapshot));
|
|
207
217
|
}
|
|
208
218
|
this.bodyOption = options.body || {};
|
|
209
219
|
this.forwardedPropsOption = options.forwardedProps || {};
|
|
@@ -234,8 +244,9 @@ var ChatClient = class {
|
|
|
234
244
|
} };
|
|
235
245
|
this.interruptManager = new InterruptManager({
|
|
236
246
|
...options.tools !== void 0 ? { tools: options.tools } : {},
|
|
247
|
+
...options.interrupts !== void 0 ? { interrupts: options.interrupts } : {},
|
|
237
248
|
submit: (submission) => this.submitInterruptBatch(submission),
|
|
238
|
-
onChange: () => this.notifyResumeStateChange()
|
|
249
|
+
onChange: (source) => this.notifyResumeStateChange(source)
|
|
239
250
|
});
|
|
240
251
|
if (options.initialResumeSnapshot) this.applyResumeSnapshot(options.initialResumeSnapshot);
|
|
241
252
|
const persistedState = this.persistor?.readInitial();
|
|
@@ -367,6 +378,7 @@ var ChatClient = class {
|
|
|
367
378
|
*/
|
|
368
379
|
attach() {
|
|
369
380
|
if (this.disposed || this.tailing) return;
|
|
381
|
+
this.ensureThreadId();
|
|
370
382
|
this.tailing = true;
|
|
371
383
|
if (this.rejoinRunId) this.maybeRejoinInFlight(this.rejoinRunId);
|
|
372
384
|
if (!this.cachesMessages && this.connection.hydrate) this.hydrateFromServer();
|
|
@@ -396,13 +408,13 @@ var ChatClient = class {
|
|
|
396
408
|
applyResumeSnapshot(snapshot) {
|
|
397
409
|
const resumeState = readResumeState(snapshot);
|
|
398
410
|
if (resumeState === void 0) {
|
|
399
|
-
this.interruptManager.reset();
|
|
411
|
+
this.interruptManager.reset({ source: "hydrate" });
|
|
400
412
|
return;
|
|
401
413
|
}
|
|
402
414
|
this.lastResume = resumeState;
|
|
403
415
|
const pendingInterrupts = Array.isArray(snapshot.pendingInterrupts) ? snapshot.pendingInterrupts : [];
|
|
404
416
|
if (pendingInterrupts.length === 0) {
|
|
405
|
-
this.interruptManager.reset();
|
|
417
|
+
this.interruptManager.reset({ source: "hydrate" });
|
|
406
418
|
return;
|
|
407
419
|
}
|
|
408
420
|
const generation = this.interruptGeneration(pendingInterrupts);
|
|
@@ -411,7 +423,7 @@ var ChatClient = class {
|
|
|
411
423
|
interruptedRunId: resumeState.runId,
|
|
412
424
|
generation,
|
|
413
425
|
interrupts: pendingInterrupts
|
|
414
|
-
});
|
|
426
|
+
}, "hydrate");
|
|
415
427
|
}
|
|
416
428
|
/**
|
|
417
429
|
* Apply a resume snapshot read from durable storage. Restores interrupt state,
|
|
@@ -474,10 +486,16 @@ var ChatClient = class {
|
|
|
474
486
|
})();
|
|
475
487
|
}
|
|
476
488
|
mountDevtools() {
|
|
489
|
+
this.ensureThreadId();
|
|
477
490
|
if (this.devtoolsMounted) return;
|
|
478
491
|
this.devtoolsMounted = true;
|
|
479
492
|
this.devtoolsBridge.mountWithTools(this.processor.getMessages().length);
|
|
480
493
|
}
|
|
494
|
+
ensureThreadId() {
|
|
495
|
+
if (!this.threadId) this.threadId = this.generateUniqueId("thread");
|
|
496
|
+
this.uniqueId = this.threadId;
|
|
497
|
+
return this.threadId;
|
|
498
|
+
}
|
|
481
499
|
/**
|
|
482
500
|
* Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
|
|
483
501
|
* still tracking. The persistor owns the cleared-run bookkeeping; the client
|
|
@@ -547,7 +565,7 @@ var ChatClient = class {
|
|
|
547
565
|
interruptedRunId,
|
|
548
566
|
generation: this.interruptGeneration(chunk.outcome.interrupts),
|
|
549
567
|
interrupts: chunk.outcome.interrupts
|
|
550
|
-
});
|
|
568
|
+
}, "live");
|
|
551
569
|
return;
|
|
552
570
|
}
|
|
553
571
|
const isRunlessSessionError = chunk.type === "RUN_ERROR" && !runId;
|
|
@@ -562,7 +580,7 @@ var ChatClient = class {
|
|
|
562
580
|
this.interruptManager.reset();
|
|
563
581
|
return;
|
|
564
582
|
}
|
|
565
|
-
this.notifyResumeStateChange();
|
|
583
|
+
this.notifyResumeStateChange("live");
|
|
566
584
|
}
|
|
567
585
|
/**
|
|
568
586
|
* The interrupt-resume state for the active/interrupted run (its run/thread
|
|
@@ -699,11 +717,12 @@ var ChatClient = class {
|
|
|
699
717
|
this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
|
|
700
718
|
this.devtoolsBridge.emitSnapshot();
|
|
701
719
|
}
|
|
702
|
-
notifyResumeStateChange() {
|
|
720
|
+
notifyResumeStateChange(source) {
|
|
703
721
|
const resumeState = this.getResumeState();
|
|
722
|
+
const interruptState = this.interruptManager.getState();
|
|
704
723
|
this.persistResumeSnapshot(resumeState);
|
|
705
|
-
this.callbacksRef.current.onResumeStateChange(resumeState,
|
|
706
|
-
this.callbacksRef.current.onInterruptStateChange(
|
|
724
|
+
this.callbacksRef.current.onResumeStateChange(resumeState, interruptState.interrupts);
|
|
725
|
+
this.callbacksRef.current.onInterruptStateChange(interruptState, { source });
|
|
707
726
|
}
|
|
708
727
|
/**
|
|
709
728
|
* Build the durable resume snapshot from the current resume state + pending
|
|
@@ -732,10 +751,17 @@ var ChatClient = class {
|
|
|
732
751
|
this.events.errorChanged(error?.message || null);
|
|
733
752
|
}
|
|
734
753
|
buildDevtoolsBridgeOptions(devtools) {
|
|
754
|
+
const client = this;
|
|
735
755
|
return {
|
|
736
|
-
hookId
|
|
737
|
-
|
|
738
|
-
|
|
756
|
+
get hookId() {
|
|
757
|
+
return client.uniqueId;
|
|
758
|
+
},
|
|
759
|
+
get clientId() {
|
|
760
|
+
return client.uniqueId;
|
|
761
|
+
},
|
|
762
|
+
get threadId() {
|
|
763
|
+
return client.threadId;
|
|
764
|
+
},
|
|
739
765
|
metadata: {
|
|
740
766
|
hookName: devtools?.hookName ?? "useChat",
|
|
741
767
|
outputKind: devtools?.outputKind ?? "chat",
|
|
@@ -885,6 +911,7 @@ var ChatClient = class {
|
|
|
885
911
|
}
|
|
886
912
|
await this.processIncomingChunk(chunk, { defer: false });
|
|
887
913
|
}
|
|
914
|
+
if (this.pendingToolExecutions.size > 0) await Promise.all(this.pendingToolExecutions.values());
|
|
888
915
|
} catch (error) {
|
|
889
916
|
const isAbort = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
890
917
|
if (!attached && !isAbort) refused = true;
|
|
@@ -899,6 +926,7 @@ var ChatClient = class {
|
|
|
899
926
|
this.abortController = null;
|
|
900
927
|
this.setIsLoading(false);
|
|
901
928
|
if (this.status === "streaming") this.setStatus("ready");
|
|
929
|
+
await this.drainPostStreamActions();
|
|
902
930
|
}
|
|
903
931
|
}
|
|
904
932
|
})();
|
|
@@ -1468,6 +1496,9 @@ var ChatClient = class {
|
|
|
1468
1496
|
* a text-only response has nothing to auto-send.
|
|
1469
1497
|
*/
|
|
1470
1498
|
shouldAutoSend() {
|
|
1499
|
+
if (this.lastResume) return false;
|
|
1500
|
+
if (this.activeInterruptSubmission && this.hasPendingInterrupts()) return false;
|
|
1501
|
+
if (this.interruptManager.getInterrupts().length > 0) return false;
|
|
1471
1502
|
const lastAssistant = this.processor.getMessages().findLast((m) => m.role === "assistant");
|
|
1472
1503
|
if (!lastAssistant) return false;
|
|
1473
1504
|
if (!lastAssistant.parts.some((p) => p.type === "tool-call")) return false;
|