@tanstack/ai-client 0.20.0 → 0.22.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/dist/esm/chat-client.d.ts +96 -2
- package/dist/esm/chat-client.js +312 -13
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/devtools.d.ts +2 -1
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/realtime-client.d.ts +6 -1
- package/dist/esm/realtime-client.js +40 -1
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/realtime-types.d.ts +7 -0
- package/dist/esm/types.d.ts +104 -4
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-client.ts +407 -17
- package/src/devtools.ts +2 -0
- package/src/index.ts +7 -0
- package/src/realtime-client.ts +54 -3
- package/src/realtime-types.ts +8 -0
- package/src/types.ts +118 -7
package/src/realtime-client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
AudioVisualization,
|
|
5
5
|
RealtimeMessage,
|
|
6
6
|
RealtimeMode,
|
|
7
|
+
RealtimeSessionConfig,
|
|
7
8
|
RealtimeStatus,
|
|
8
9
|
RealtimeToken,
|
|
9
10
|
} from '@tanstack/ai/client'
|
|
@@ -101,6 +102,7 @@ export class RealtimeClient {
|
|
|
101
102
|
this.clientTools.size > 0
|
|
102
103
|
? Array.from(this.clientTools.values())
|
|
103
104
|
: undefined
|
|
105
|
+
|
|
104
106
|
this.connection = await this.options.adapter.connect(
|
|
105
107
|
this.token,
|
|
106
108
|
toolsList,
|
|
@@ -282,6 +284,38 @@ export class RealtimeClient {
|
|
|
282
284
|
return this.connection?.getAudioVisualization() ?? null
|
|
283
285
|
}
|
|
284
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Update the session configuration.
|
|
289
|
+
* This applies changes to the active connection and persists them for future reconnections.
|
|
290
|
+
*/
|
|
291
|
+
updateSession(config: Partial<RealtimeSessionConfig>): void {
|
|
292
|
+
// Persist type-compatible fields so future (re)connections use the updated
|
|
293
|
+
// config. `tools` is intentionally excluded: a session's tool configs are
|
|
294
|
+
// serialized descriptors, whereas `options.tools` holds executable client
|
|
295
|
+
// tools tracked separately via `clientTools`.
|
|
296
|
+
const o = this.options
|
|
297
|
+
if (config.instructions !== undefined) o.instructions = config.instructions
|
|
298
|
+
if (config.voice !== undefined) o.voice = config.voice
|
|
299
|
+
if (config.vadMode !== undefined) o.vadMode = config.vadMode
|
|
300
|
+
if (config.outputModalities !== undefined) {
|
|
301
|
+
o.outputModalities = config.outputModalities
|
|
302
|
+
}
|
|
303
|
+
if (config.temperature !== undefined) o.temperature = config.temperature
|
|
304
|
+
if (config.maxOutputTokens !== undefined) {
|
|
305
|
+
o.maxOutputTokens = config.maxOutputTokens
|
|
306
|
+
}
|
|
307
|
+
if (config.semanticEagerness !== undefined) {
|
|
308
|
+
o.semanticEagerness = config.semanticEagerness
|
|
309
|
+
}
|
|
310
|
+
if (config.providerOptions !== undefined) {
|
|
311
|
+
o.providerOptions = config.providerOptions
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.connection) {
|
|
315
|
+
this.applySessionConfig()
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
// ============================================================================
|
|
286
320
|
// State Subscription
|
|
287
321
|
// ============================================================================
|
|
@@ -353,6 +387,7 @@ export class RealtimeClient {
|
|
|
353
387
|
try {
|
|
354
388
|
this.token = await this.options.getToken()
|
|
355
389
|
this.scheduleTokenRefresh()
|
|
390
|
+
this.connection?.updateToken?.(this.token)
|
|
356
391
|
// Note: Some providers may require reconnection with new token
|
|
357
392
|
// This is handled by the adapter implementation
|
|
358
393
|
} catch (error) {
|
|
@@ -487,6 +522,18 @@ export class RealtimeClient {
|
|
|
487
522
|
this.options.onError?.(error)
|
|
488
523
|
}),
|
|
489
524
|
)
|
|
525
|
+
|
|
526
|
+
this.unsubscribers.push(
|
|
527
|
+
this.connection.on('go_away', ({ timeLeft }) => {
|
|
528
|
+
this.options.onGoAway?.(timeLeft)
|
|
529
|
+
}),
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
this.unsubscribers.push(
|
|
533
|
+
this.connection.on('usage', (usage) => {
|
|
534
|
+
this.options.onUsage?.(usage)
|
|
535
|
+
}),
|
|
536
|
+
)
|
|
490
537
|
}
|
|
491
538
|
|
|
492
539
|
private applySessionConfig(): void {
|
|
@@ -513,18 +560,22 @@ export class RealtimeClient {
|
|
|
513
560
|
semanticEagerness
|
|
514
561
|
if (!hasConfig) return
|
|
515
562
|
|
|
516
|
-
// `RealtimeToolConfig.inputSchema`
|
|
517
|
-
// `undefined` under `exactOptionalPropertyTypes`). Conditionally spread
|
|
518
|
-
//
|
|
563
|
+
// `RealtimeToolConfig.inputSchema`/`outputSchema` are `Record<string, any>`
|
|
564
|
+
// (no `undefined` under `exactOptionalPropertyTypes`). Conditionally spread
|
|
565
|
+
// each so we don't pass `undefined` when the tool has no such schema.
|
|
519
566
|
const toolsConfig = tools
|
|
520
567
|
? Array.from(this.clientTools.values()).map((t) => {
|
|
521
568
|
const inputSchema = t.inputSchema
|
|
522
569
|
? convertSchemaToJsonSchema(t.inputSchema)
|
|
523
570
|
: undefined
|
|
571
|
+
const outputSchema = t.outputSchema
|
|
572
|
+
? convertSchemaToJsonSchema(t.outputSchema)
|
|
573
|
+
: undefined
|
|
524
574
|
return {
|
|
525
575
|
name: t.name,
|
|
526
576
|
description: t.description,
|
|
527
577
|
...(inputSchema ? { inputSchema } : {}),
|
|
578
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
528
579
|
}
|
|
529
580
|
})
|
|
530
581
|
: undefined
|
package/src/realtime-types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RealtimeStatus,
|
|
7
7
|
RealtimeToken,
|
|
8
8
|
} from '@tanstack/ai/client'
|
|
9
|
+
import type { UsageInfo } from '@tanstack/ai'
|
|
9
10
|
|
|
10
11
|
// The realtime adapter contract lives in `@tanstack/ai` (the shared layer both
|
|
11
12
|
// providers and this client depend on) so provider packages don't need to
|
|
@@ -82,6 +83,11 @@ export interface RealtimeClientOptions {
|
|
|
82
83
|
*/
|
|
83
84
|
semanticEagerness?: 'low' | 'medium' | 'high'
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Provider-specific options
|
|
88
|
+
*/
|
|
89
|
+
providerOptions?: Record<string, unknown>
|
|
90
|
+
|
|
85
91
|
// Callbacks
|
|
86
92
|
onStatusChange?: (status: RealtimeStatus) => void
|
|
87
93
|
onModeChange?: (mode: RealtimeMode) => void
|
|
@@ -90,6 +96,8 @@ export interface RealtimeClientOptions {
|
|
|
90
96
|
onConnect?: () => void
|
|
91
97
|
onDisconnect?: () => void
|
|
92
98
|
onInterrupted?: () => void
|
|
99
|
+
onUsage?: (usage: UsageInfo) => void
|
|
100
|
+
onGoAway?: (timeLeft?: string) => void
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
// ============================================================================
|
package/src/types.ts
CHANGED
|
@@ -144,6 +144,88 @@ export interface MultimodalContent {
|
|
|
144
144
|
id?: string
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Action taken when `sendMessage` is called while the client is busy
|
|
149
|
+
* (streaming, claiming a send, or draining the queue).
|
|
150
|
+
* - `queue`: hold the message; it auto-sends when the current run settles
|
|
151
|
+
* **successfully**.
|
|
152
|
+
* - `drop`: ignore the send (promise still resolves; does not throw).
|
|
153
|
+
* - `interrupt`: abort the current stream and send immediately. Unlike
|
|
154
|
+
* `stop()`, does **not** flush already-queued messages — they still drain
|
|
155
|
+
* after the interrupting send settles successfully.
|
|
156
|
+
*/
|
|
157
|
+
export type WhenBusy = 'queue' | 'drop' | 'interrupt'
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Why the client is busy when a {@link QueueStrategy} runs.
|
|
161
|
+
* - `streaming` — an LLM stream is active (`isLoading`).
|
|
162
|
+
* - `sendInFlight` — a send has claimed the client but is not yet loading.
|
|
163
|
+
* - `draining` — the queue drain loop is delivering pending messages.
|
|
164
|
+
*/
|
|
165
|
+
export type QueueBusyReason = 'streaming' | 'sendInFlight' | 'draining'
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A user message held in the send queue while a stream is active.
|
|
169
|
+
* Rendered separately from `messages`; cancellable via `cancelQueued(id)`
|
|
170
|
+
* until it drains.
|
|
171
|
+
*/
|
|
172
|
+
export interface QueuedMessage {
|
|
173
|
+
id: string
|
|
174
|
+
content: string | MultimodalContent
|
|
175
|
+
createdAt: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Declarative queue policy.
|
|
180
|
+
*/
|
|
181
|
+
export interface QueueConfig {
|
|
182
|
+
/**
|
|
183
|
+
* Action when the client is busy (streaming, claiming a send, or draining).
|
|
184
|
+
* Default `'queue'`.
|
|
185
|
+
*/
|
|
186
|
+
whenBusy?: WhenBusy
|
|
187
|
+
/**
|
|
188
|
+
* How queued items leave the queue.
|
|
189
|
+
* - `'fifo'`: one at a time, in order (default).
|
|
190
|
+
* - `'batch'`: merge all queued items into one send when the run settles
|
|
191
|
+
* successfully.
|
|
192
|
+
*/
|
|
193
|
+
drain?: 'fifo' | 'batch'
|
|
194
|
+
/** Max queued items. Unlimited when omitted. `0` means never queue. */
|
|
195
|
+
maxSize?: number
|
|
196
|
+
/**
|
|
197
|
+
* Behavior when `maxSize` is reached. Default `'reject'`.
|
|
198
|
+
* `'reject'` silently discards the new send (does not throw);
|
|
199
|
+
* `'drop-oldest'` evicts the oldest queued item to make room.
|
|
200
|
+
* Only meaningful when `maxSize` is set.
|
|
201
|
+
*/
|
|
202
|
+
onOverflow?: 'reject' | 'drop-oldest'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Escape hatch: decide the action for a single send. Drain stays FIFO for the
|
|
207
|
+
* function form (no `batch` via function). Per-call `sendOptions.whenBusy`
|
|
208
|
+
* overrides the strategy for that send.
|
|
209
|
+
*
|
|
210
|
+
* Actions match {@link WhenBusy}: `'queue' | 'drop' | 'interrupt'`. Concurrent
|
|
211
|
+
* streams are not supported. `pending.id` is the id that will be stored if the
|
|
212
|
+
* action is `'queue'` (safe to pass to `cancelQueued`).
|
|
213
|
+
*/
|
|
214
|
+
export type QueueStrategy = (ctx: {
|
|
215
|
+
pending: QueuedMessage
|
|
216
|
+
busyReason: QueueBusyReason
|
|
217
|
+
queued: ReadonlyArray<QueuedMessage>
|
|
218
|
+
}) => { action: WhenBusy }
|
|
219
|
+
|
|
220
|
+
/** A `WhenBusy` shorthand, a full config, or a strategy function. */
|
|
221
|
+
export type QueueOption = WhenBusy | QueueConfig | QueueStrategy
|
|
222
|
+
|
|
223
|
+
/** Per-call overrides for `sendMessage`. */
|
|
224
|
+
export interface SendMessageOptions {
|
|
225
|
+
/** Overrides the configured `whenBusy` for this one send. */
|
|
226
|
+
whenBusy?: WhenBusy
|
|
227
|
+
}
|
|
228
|
+
|
|
147
229
|
/**
|
|
148
230
|
* Message parts - building blocks of UIMessage
|
|
149
231
|
*/
|
|
@@ -166,15 +248,27 @@ type ToolCallPartForTool<T> = T extends AnyClientTool
|
|
|
166
248
|
/** Parsed tool input (typed from inputSchema) */
|
|
167
249
|
input?: InferToolInput<T>
|
|
168
250
|
state: ToolCallState
|
|
169
|
-
/** Approval metadata if tool requires user approval */
|
|
170
|
-
approval?: {
|
|
171
|
-
id: string // Unique approval ID
|
|
172
|
-
needsApproval: boolean // Always true if present
|
|
173
|
-
approved?: boolean // User's decision (undefined until responded)
|
|
174
|
-
}
|
|
175
251
|
/** Tool execution output (for client tools or after approval) */
|
|
176
252
|
output?: InferToolOutput<T>
|
|
177
|
-
}
|
|
253
|
+
} & (NonNullable<T['needsApproval']> extends true
|
|
254
|
+
? {
|
|
255
|
+
/**
|
|
256
|
+
* Approval metadata — present only on tools defined with
|
|
257
|
+
* `needsApproval: true`. Populated once the call reaches
|
|
258
|
+
* `state: 'approval-requested'`. `needsApproval` is an optional
|
|
259
|
+
* property on the tool, so we index into it (rather than
|
|
260
|
+
* `T extends { needsApproval: true }`, which an optional property
|
|
261
|
+
* never satisfies) and strip `undefined` before comparing to `true`.
|
|
262
|
+
*/
|
|
263
|
+
approval?: {
|
|
264
|
+
id: string // Unique approval ID
|
|
265
|
+
needsApproval: boolean // Always true if present
|
|
266
|
+
approved?: boolean // User's decision (undefined until responded)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
: // Tools without `needsApproval: true` never carry an approval field.
|
|
270
|
+
// `& unknown` is a no-op intersection (adds nothing).
|
|
271
|
+
unknown)
|
|
178
272
|
: never
|
|
179
273
|
|
|
180
274
|
/**
|
|
@@ -480,6 +574,23 @@ export interface ChatClientBaseOptions<
|
|
|
480
574
|
*/
|
|
481
575
|
onSessionGeneratingChange?: (isGenerating: boolean) => void
|
|
482
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Policy for messages sent while the client is busy (streaming, claiming
|
|
579
|
+
* a send, or draining the queue). Accepts a `WhenBusy` string, a
|
|
580
|
+
* `QueueConfig`, or a `QueueStrategy` function.
|
|
581
|
+
* Default: `{ whenBusy: 'queue', drain: 'fifo' }`.
|
|
582
|
+
* Queued items auto-send only after a **successful** settle; they are
|
|
583
|
+
* discarded on error/abort, `stop()`, `clear()`, `unsubscribe()`, and
|
|
584
|
+
* `reload()`.
|
|
585
|
+
*/
|
|
586
|
+
queue?: QueueOption
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Callback when the pending send queue changes (enqueue, cancel, drain,
|
|
590
|
+
* or flush).
|
|
591
|
+
*/
|
|
592
|
+
onQueueChange?: (queue: Array<QueuedMessage>) => void
|
|
593
|
+
|
|
483
594
|
/**
|
|
484
595
|
* Callback when a custom event is received from a server-side tool.
|
|
485
596
|
* Custom events are emitted by tools using `context.emitCustomEvent()` during execution.
|