@tanstack/ai-client 0.6.0 → 0.7.2

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 (37) hide show
  1. package/README.md +6 -6
  2. package/dist/esm/chat-client.d.ts +62 -3
  3. package/dist/esm/chat-client.js +264 -54
  4. package/dist/esm/chat-client.js.map +1 -1
  5. package/dist/esm/connection-adapters.d.ts +28 -12
  6. package/dist/esm/connection-adapters.js +93 -0
  7. package/dist/esm/connection-adapters.js.map +1 -1
  8. package/dist/esm/events.js +1 -1
  9. package/dist/esm/events.js.map +1 -1
  10. package/dist/esm/generation-client.d.ts +5 -5
  11. package/dist/esm/generation-client.js +1 -1
  12. package/dist/esm/generation-client.js.map +1 -1
  13. package/dist/esm/generation-types.d.ts +5 -5
  14. package/dist/esm/generation-types.js.map +1 -1
  15. package/dist/esm/index.d.ts +4 -2
  16. package/dist/esm/index.js +2 -0
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/realtime-client.d.ts +98 -0
  19. package/dist/esm/realtime-client.js +384 -0
  20. package/dist/esm/realtime-client.js.map +1 -0
  21. package/dist/esm/realtime-types.d.ts +118 -0
  22. package/dist/esm/types.d.ts +23 -2
  23. package/dist/esm/types.js.map +1 -1
  24. package/dist/esm/video-generation-client.d.ts +5 -5
  25. package/dist/esm/video-generation-client.js +1 -1
  26. package/dist/esm/video-generation-client.js.map +1 -1
  27. package/package.json +3 -2
  28. package/src/chat-client.ts +355 -86
  29. package/src/connection-adapters.ts +147 -12
  30. package/src/events.ts +1 -1
  31. package/src/generation-client.ts +7 -7
  32. package/src/generation-types.ts +5 -5
  33. package/src/index.ts +11 -0
  34. package/src/realtime-client.ts +526 -0
  35. package/src/realtime-types.ts +180 -0
  36. package/src/types.ts +31 -2
  37. package/src/video-generation-client.ts +6 -6
@@ -62,15 +62,9 @@ async function* readStreamLines(
62
62
  }
63
63
  }
64
64
 
65
- /**
66
- * Connection adapter interface - converts a connection into a stream of chunks
67
- */
68
- export interface ConnectionAdapter {
65
+ export interface ConnectConnectionAdapter {
69
66
  /**
70
- * Connect and return an async iterable of StreamChunks
71
- * @param messages - The messages to send (UIMessages or ModelMessages)
72
- * @param data - Additional data to send
73
- * @param abortSignal - Optional abort signal for request cancellation
67
+ * Connect and return an async iterable of StreamChunks.
74
68
  */
75
69
  connect: (
76
70
  messages: Array<UIMessage> | Array<ModelMessage>,
@@ -79,6 +73,147 @@ export interface ConnectionAdapter {
79
73
  ) => AsyncIterable<StreamChunk>
80
74
  }
81
75
 
76
+ export interface SubscribeConnectionAdapter {
77
+ /**
78
+ * Subscribe to stream chunks.
79
+ */
80
+ subscribe: (abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>
81
+ /**
82
+ * Send a request; chunks arrive through subscribe().
83
+ */
84
+ send: (
85
+ messages: Array<UIMessage> | Array<ModelMessage>,
86
+ data?: Record<string, any>,
87
+ abortSignal?: AbortSignal,
88
+ ) => Promise<void>
89
+ }
90
+
91
+ /**
92
+ * Connection adapter union.
93
+ * Provide either `connect`, or `subscribe` + `send`.
94
+ */
95
+ export type ConnectionAdapter =
96
+ | ConnectConnectionAdapter
97
+ | SubscribeConnectionAdapter
98
+
99
+ /**
100
+ * Normalize a ConnectionAdapter to subscribe/send operations.
101
+ *
102
+ * If a connection provides native subscribe/send, that mode is used.
103
+ * Otherwise, connect() is wrapped using an async queue.
104
+ */
105
+ export function normalizeConnectionAdapter(
106
+ connection: ConnectionAdapter | undefined,
107
+ ): SubscribeConnectionAdapter {
108
+ if (!connection) {
109
+ throw new Error('Connection adapter is required')
110
+ }
111
+
112
+ const hasConnect = 'connect' in connection
113
+ const hasSubscribe = 'subscribe' in connection
114
+ const hasSend = 'send' in connection
115
+
116
+ if (hasConnect && (hasSubscribe || hasSend)) {
117
+ throw new Error(
118
+ 'Connection adapter must provide either connect or both subscribe and send, not both modes',
119
+ )
120
+ }
121
+
122
+ if (hasSubscribe && hasSend) {
123
+ return {
124
+ subscribe: connection.subscribe.bind(connection),
125
+ send: connection.send.bind(connection),
126
+ }
127
+ }
128
+
129
+ if (!hasConnect) {
130
+ throw new Error(
131
+ 'Connection adapter must provide either connect or both subscribe and send',
132
+ )
133
+ }
134
+
135
+ // Legacy connect() wrapper
136
+ let activeBuffer: Array<StreamChunk> = []
137
+ let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []
138
+
139
+ function push(chunk: StreamChunk): void {
140
+ const waiter = activeWaiters.shift()
141
+ if (waiter) {
142
+ waiter(chunk)
143
+ } else {
144
+ activeBuffer.push(chunk)
145
+ }
146
+ }
147
+
148
+ return {
149
+ subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {
150
+ // Transfer ownership to the latest subscriber so only one active
151
+ // subscribe() call receives chunks from the shared connect-wrapper queue.
152
+ const myBuffer: Array<StreamChunk> = activeBuffer.splice(0)
153
+ const myWaiters: Array<(chunk: StreamChunk | null) => void> = []
154
+ activeBuffer = myBuffer
155
+ activeWaiters = myWaiters
156
+
157
+ return (async function* () {
158
+ while (!abortSignal?.aborted) {
159
+ let chunk: StreamChunk | null
160
+ if (myBuffer.length > 0) {
161
+ chunk = myBuffer.shift()!
162
+ } else {
163
+ chunk = await new Promise<StreamChunk | null>((resolve) => {
164
+ const onAbort = () => resolve(null)
165
+ myWaiters.push((c) => {
166
+ abortSignal?.removeEventListener('abort', onAbort)
167
+ resolve(c)
168
+ })
169
+ abortSignal?.addEventListener('abort', onAbort, { once: true })
170
+ })
171
+ }
172
+ if (chunk !== null) yield chunk
173
+ }
174
+ })()
175
+ },
176
+ async send(messages, data, abortSignal) {
177
+ let hasTerminalEvent = false
178
+ try {
179
+ const stream = connection.connect(messages, data, abortSignal)
180
+ for await (const chunk of stream) {
181
+ if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
182
+ hasTerminalEvent = true
183
+ }
184
+ push(chunk)
185
+ }
186
+
187
+ // If the connect stream ended cleanly without a terminal event,
188
+ // synthesize RUN_FINISHED so request-scoped consumers can complete.
189
+ if (!abortSignal?.aborted && !hasTerminalEvent) {
190
+ push({
191
+ type: 'RUN_FINISHED',
192
+ runId: `run-${Date.now()}`,
193
+ model: 'connect-wrapper',
194
+ timestamp: Date.now(),
195
+ finishReason: 'stop',
196
+ })
197
+ }
198
+ } catch (err) {
199
+ if (!abortSignal?.aborted && !hasTerminalEvent) {
200
+ push({
201
+ type: 'RUN_ERROR',
202
+ timestamp: Date.now(),
203
+ error: {
204
+ message:
205
+ err instanceof Error
206
+ ? err.message
207
+ : 'Unknown error in connect()',
208
+ },
209
+ })
210
+ }
211
+ throw err
212
+ }
213
+ },
214
+ }
215
+ }
216
+
82
217
  /**
83
218
  * Options for fetch-based connection adapters
84
219
  */
@@ -129,7 +264,7 @@ export function fetchServerSentEvents(
129
264
  options:
130
265
  | FetchConnectionOptions
131
266
  | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
132
- ): ConnectionAdapter {
267
+ ): ConnectConnectionAdapter {
133
268
  return {
134
269
  async *connect(messages, data, abortSignal) {
135
270
  // Resolve URL and options if they are functions
@@ -228,7 +363,7 @@ export function fetchHttpStream(
228
363
  options:
229
364
  | FetchConnectionOptions
230
365
  | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
231
- ): ConnectionAdapter {
366
+ ): ConnectConnectionAdapter {
232
367
  return {
233
368
  async *connect(messages, data, abortSignal) {
234
369
  // Resolve URL and options if they are functions
@@ -301,7 +436,7 @@ export function stream(
301
436
  messages: Array<UIMessage> | Array<ModelMessage>,
302
437
  data?: Record<string, any>,
303
438
  ) => AsyncIterable<StreamChunk>,
304
- ): ConnectionAdapter {
439
+ ): ConnectConnectionAdapter {
305
440
  return {
306
441
  async *connect(messages, data) {
307
442
  // Pass messages as-is (UIMessages with parts preserved)
@@ -332,7 +467,7 @@ export function rpcStream(
332
467
  messages: Array<UIMessage> | Array<ModelMessage>,
333
468
  data?: Record<string, any>,
334
469
  ) => AsyncIterable<StreamChunk>,
335
- ): ConnectionAdapter {
470
+ ): ConnectConnectionAdapter {
336
471
  return {
337
472
  async *connect(messages, data) {
338
473
  // Pass messages as-is (UIMessages with parts preserved)
package/src/events.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { aiEventClient } from '@tanstack/ai/event-client'
1
+ import { aiEventClient } from '@tanstack/ai-event-client'
2
2
  import type { ContentPart } from '@tanstack/ai'
3
3
  import type { UIMessage } from './types'
4
4
 
@@ -1,7 +1,7 @@
1
1
  import { GENERATION_EVENTS } from './generation-types'
2
2
  import { parseSSEResponse } from './sse-parser'
3
3
  import type { StreamChunk } from '@tanstack/ai'
4
- import type { ConnectionAdapter } from './connection-adapters'
4
+ import type { ConnectConnectionAdapter } from './connection-adapters'
5
5
  import type {
6
6
  GenerationClientOptions,
7
7
  GenerationClientState,
@@ -27,7 +27,7 @@ interface GenerationCallbacks<TResult, TOutput> {
27
27
  * (image, speech, transcription, summarize).
28
28
  *
29
29
  * Supports two transport modes:
30
- * - **ConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).
30
+ * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).
31
31
  * Server wraps results in StreamChunk events with CUSTOM event names.
32
32
  * - **Fetcher** — Direct async function call. No streaming protocol needed.
33
33
  *
@@ -36,7 +36,7 @@ interface GenerationCallbacks<TResult, TOutput> {
36
36
  *
37
37
  * @example
38
38
  * ```typescript
39
- * // With ConnectionAdapter (streaming)
39
+ * // With streaming connection adapter
40
40
  * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({
41
41
  * connection: fetchServerSentEvents('/api/generate/image'),
42
42
  * onResultChange: setResult,
@@ -62,7 +62,7 @@ export class GenerationClient<
62
62
  TResult,
63
63
  TOutput = TResult,
64
64
  > {
65
- private connection: ConnectionAdapter | undefined
65
+ private connection: ConnectConnectionAdapter | undefined
66
66
  private fetcher: GenerationFetcher<TInput, TResult> | undefined
67
67
  private body: Record<string, any>
68
68
  private result: TOutput | null = null
@@ -75,7 +75,7 @@ export class GenerationClient<
75
75
  constructor(
76
76
  options: GenerationClientOptions<TInput, TResult, TOutput> &
77
77
  (
78
- | { connection: ConnectionAdapter; fetcher?: never }
78
+ | { connection: ConnectConnectionAdapter; fetcher?: never }
79
79
  | {
80
80
  fetcher: GenerationFetcher<TInput, TResult>
81
81
  connection?: never
@@ -127,7 +127,7 @@ export class GenerationClient<
127
127
  this.setStatus('success')
128
128
  }
129
129
  } else if (this.connection) {
130
- // ConnectionAdapter streaming path
130
+ // Streaming adapter path
131
131
  const mergedData = { ...this.body, ...input }
132
132
  const stream = this.connection.connect([], mergedData, signal)
133
133
  await this.processStream(stream)
@@ -149,7 +149,7 @@ export class GenerationClient<
149
149
  }
150
150
 
151
151
  /**
152
- * Process a stream of AG-UI events from the ConnectionAdapter.
152
+ * Process a stream of AG-UI events from the streaming connection adapter.
153
153
  */
154
154
  private async processStream(
155
155
  source: AsyncIterable<StreamChunk>,
@@ -1,5 +1,5 @@
1
1
  import type { StreamChunk } from '@tanstack/ai'
2
- import type { ConnectionAdapter } from './connection-adapters'
2
+ import type { ConnectConnectionAdapter } from './connection-adapters'
3
3
 
4
4
  // ===========================
5
5
  // Inference Utilities
@@ -81,10 +81,10 @@ export type GenerationFetcher<TInput, TResult> = (
81
81
 
82
82
  /**
83
83
  * Transport configuration for generation clients.
84
- * Supports either a ConnectionAdapter (streaming) or a direct fetcher function.
84
+ * Supports either a connect-based streaming adapter or a direct fetcher function.
85
85
  */
86
86
  export type GenerationTransport<TInput, TResult> =
87
- | { connection: ConnectionAdapter; fetcher?: never }
87
+ | { connection: ConnectConnectionAdapter; fetcher?: never }
88
88
  | { fetcher: GenerationFetcher<TInput, TResult>; connection?: never }
89
89
 
90
90
  // ===========================
@@ -103,7 +103,7 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
103
103
  /** Unique identifier for this generation client instance */
104
104
  id?: string
105
105
 
106
- /** Additional body parameters to send with ConnectionAdapter requests */
106
+ /** Additional body parameters to send with connect-based adapter requests */
107
107
  body?: Record<string, any>
108
108
 
109
109
  /**
@@ -119,7 +119,7 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
119
119
  onError?: (error: Error) => void
120
120
  /** Callback when progress is reported (0-100) */
121
121
  onProgress?: (progress: number, message?: string) => void
122
- /** Callback for each stream chunk (ConnectionAdapter mode only) */
122
+ /** Callback for each stream chunk (connect-based adapter mode only) */
123
123
  onChunk?: (chunk: StreamChunk) => void
124
124
 
125
125
  // Framework state callbacks (set by hooks, not users)
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ChatClient } from './chat-client'
2
+ export { RealtimeClient } from './realtime-client'
2
3
  export { GenerationClient } from './generation-client'
3
4
  export { VideoGenerationClient } from './video-generation-client'
4
5
  export type {
@@ -14,6 +15,7 @@ export type {
14
15
  ChatRequestBody,
15
16
  InferChatMessages,
16
17
  ChatClientState,
18
+ ConnectionStatus,
17
19
  // Multimodal content input type
18
20
  MultimodalContent,
19
21
  } from './types'
@@ -42,13 +44,22 @@ export type {
42
44
  ExtractToolOutput,
43
45
  } from './tool-types'
44
46
  export type { AnyClientTool } from '@tanstack/ai'
47
+ export type {
48
+ RealtimeAdapter,
49
+ RealtimeConnection,
50
+ RealtimeClientOptions,
51
+ RealtimeClientState,
52
+ RealtimeStateChangeCallback,
53
+ } from './realtime-types'
45
54
  export {
46
55
  fetchServerSentEvents,
47
56
  fetchHttpStream,
48
57
  stream,
49
58
  rpcStream,
59
+ type ConnectConnectionAdapter,
50
60
  type ConnectionAdapter,
51
61
  type FetchConnectionOptions,
62
+ type SubscribeConnectionAdapter,
52
63
  } from './connection-adapters'
53
64
 
54
65
  // Re-export message converters from @tanstack/ai