@tanstack/ai-client 0.3.0 → 0.4.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/dist/esm/chat-client.d.ts +54 -4
- package/dist/esm/chat-client.js +144 -29
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -2
- package/dist/esm/connection-adapters.js +4 -9
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/events.d.ts +7 -2
- package/dist/esm/events.js +12 -3
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/types.d.ts +38 -2
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-client.ts +190 -35
- package/src/connection-adapters.ts +14 -18
- package/src/events.ts +22 -5
- package/src/index.ts +3 -0
- package/src/types.ts +48 -0
package/src/chat-client.ts
CHANGED
|
@@ -4,29 +4,42 @@ import {
|
|
|
4
4
|
normalizeToUIMessage,
|
|
5
5
|
} from '@tanstack/ai'
|
|
6
6
|
import { DefaultChatClientEventEmitter } from './events'
|
|
7
|
+
import type {
|
|
8
|
+
AnyClientTool,
|
|
9
|
+
ContentPart,
|
|
10
|
+
ModelMessage,
|
|
11
|
+
StreamChunk,
|
|
12
|
+
} from '@tanstack/ai'
|
|
13
|
+
import type { ConnectionAdapter } from './connection-adapters'
|
|
14
|
+
import type { ChatClientEventEmitter } from './events'
|
|
7
15
|
import type {
|
|
8
16
|
ChatClientOptions,
|
|
17
|
+
ChatClientState,
|
|
9
18
|
MessagePart,
|
|
19
|
+
MultimodalContent,
|
|
10
20
|
ToolCallPart,
|
|
11
21
|
UIMessage,
|
|
12
22
|
} from './types'
|
|
13
|
-
import type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai'
|
|
14
|
-
import type { ConnectionAdapter } from './connection-adapters'
|
|
15
|
-
import type { ChatClientEventEmitter } from './events'
|
|
16
23
|
|
|
17
24
|
export class ChatClient {
|
|
18
25
|
private processor: StreamProcessor
|
|
19
26
|
private connection: ConnectionAdapter
|
|
20
27
|
private uniqueId: string
|
|
21
28
|
private body: Record<string, any> = {}
|
|
29
|
+
private pendingMessageBody: Record<string, any> | undefined = undefined
|
|
22
30
|
private isLoading = false
|
|
23
31
|
private error: Error | undefined = undefined
|
|
32
|
+
private status: ChatClientState = 'ready'
|
|
24
33
|
private abortController: AbortController | null = null
|
|
25
34
|
private events: ChatClientEventEmitter
|
|
26
35
|
private clientToolsRef: { current: Map<string, AnyClientTool> }
|
|
27
36
|
private currentStreamId: string | null = null
|
|
28
37
|
private currentMessageId: string | null = null
|
|
29
38
|
private postStreamActions: Array<() => Promise<void>> = []
|
|
39
|
+
// Track pending client tool executions to await them before stream finalization
|
|
40
|
+
private pendingToolExecutions: Map<string, Promise<void>> = new Map()
|
|
41
|
+
// Flag to deduplicate continuation checks during action draining
|
|
42
|
+
private continuationPending = false
|
|
30
43
|
|
|
31
44
|
private callbacksRef: {
|
|
32
45
|
current: {
|
|
@@ -37,6 +50,7 @@ export class ChatClient {
|
|
|
37
50
|
onMessagesChange: (messages: Array<UIMessage>) => void
|
|
38
51
|
onLoadingChange: (isLoading: boolean) => void
|
|
39
52
|
onErrorChange: (error: Error | undefined) => void
|
|
53
|
+
onStatusChange: (status: ChatClientState) => void
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -63,6 +77,7 @@ export class ChatClient {
|
|
|
63
77
|
onMessagesChange: options.onMessagesChange || (() => {}),
|
|
64
78
|
onLoadingChange: options.onLoadingChange || (() => {}),
|
|
65
79
|
onErrorChange: options.onErrorChange || (() => {}),
|
|
80
|
+
onStatusChange: options.onStatusChange || (() => {}),
|
|
66
81
|
},
|
|
67
82
|
}
|
|
68
83
|
|
|
@@ -75,13 +90,15 @@ export class ChatClient {
|
|
|
75
90
|
this.callbacksRef.current.onMessagesChange(messages)
|
|
76
91
|
},
|
|
77
92
|
onStreamStart: () => {
|
|
78
|
-
|
|
93
|
+
this.setStatus('streaming')
|
|
79
94
|
},
|
|
80
95
|
onStreamEnd: (message: UIMessage) => {
|
|
81
96
|
this.callbacksRef.current.onFinish(message)
|
|
97
|
+
this.setStatus('ready')
|
|
82
98
|
},
|
|
83
99
|
onError: (error: Error) => {
|
|
84
100
|
this.setError(error)
|
|
101
|
+
this.setStatus('error')
|
|
85
102
|
this.callbacksRef.current.onError(error)
|
|
86
103
|
},
|
|
87
104
|
onTextUpdate: (messageId: string, content: string) => {
|
|
@@ -127,31 +144,41 @@ export class ChatClient {
|
|
|
127
144
|
)
|
|
128
145
|
}
|
|
129
146
|
},
|
|
130
|
-
onToolCall:
|
|
147
|
+
onToolCall: (args: {
|
|
131
148
|
toolCallId: string
|
|
132
149
|
toolName: string
|
|
133
150
|
input: any
|
|
134
151
|
}) => {
|
|
135
152
|
// Handle client-side tool execution automatically
|
|
136
153
|
const clientTool = this.clientToolsRef.current.get(args.toolName)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
const executeFunc = clientTool?.execute
|
|
155
|
+
if (executeFunc) {
|
|
156
|
+
// Create and track the execution promise
|
|
157
|
+
const executionPromise = (async () => {
|
|
158
|
+
try {
|
|
159
|
+
const output = await executeFunc(args.input)
|
|
160
|
+
await this.addToolResult({
|
|
161
|
+
toolCallId: args.toolCallId,
|
|
162
|
+
tool: args.toolName,
|
|
163
|
+
output,
|
|
164
|
+
state: 'output-available',
|
|
165
|
+
})
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
await this.addToolResult({
|
|
168
|
+
toolCallId: args.toolCallId,
|
|
169
|
+
tool: args.toolName,
|
|
170
|
+
output: null,
|
|
171
|
+
state: 'output-error',
|
|
172
|
+
errorText: error.message,
|
|
173
|
+
})
|
|
174
|
+
} finally {
|
|
175
|
+
// Remove from pending when complete
|
|
176
|
+
this.pendingToolExecutions.delete(args.toolCallId)
|
|
177
|
+
}
|
|
178
|
+
})()
|
|
179
|
+
|
|
180
|
+
// Track the pending execution
|
|
181
|
+
this.pendingToolExecutions.set(args.toolCallId, executionPromise)
|
|
155
182
|
}
|
|
156
183
|
},
|
|
157
184
|
onApprovalRequest: (args: {
|
|
@@ -187,6 +214,11 @@ export class ChatClient {
|
|
|
187
214
|
this.events.loadingChanged(isLoading)
|
|
188
215
|
}
|
|
189
216
|
|
|
217
|
+
private setStatus(status: ChatClientState): void {
|
|
218
|
+
this.status = status
|
|
219
|
+
this.callbacksRef.current.onStatusChange(status)
|
|
220
|
+
}
|
|
221
|
+
|
|
190
222
|
private setError(error: Error | undefined): void {
|
|
191
223
|
this.error = error
|
|
192
224
|
this.callbacksRef.current.onErrorChange(error)
|
|
@@ -227,6 +259,12 @@ export class ChatClient {
|
|
|
227
259
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
228
260
|
}
|
|
229
261
|
|
|
262
|
+
// Wait for all pending tool executions to complete before finalizing
|
|
263
|
+
// This ensures client tools finish before we check for continuation
|
|
264
|
+
if (this.pendingToolExecutions.size > 0) {
|
|
265
|
+
await Promise.all(this.pendingToolExecutions.values())
|
|
266
|
+
}
|
|
267
|
+
|
|
230
268
|
// Finalize the stream
|
|
231
269
|
this.processor.finalizeStream()
|
|
232
270
|
|
|
@@ -251,20 +289,82 @@ export class ChatClient {
|
|
|
251
289
|
}
|
|
252
290
|
|
|
253
291
|
/**
|
|
254
|
-
* Send a message and stream the response
|
|
292
|
+
* Send a message and stream the response.
|
|
293
|
+
* Supports both simple string content and multimodal content (images, audio, video, documents).
|
|
294
|
+
*
|
|
295
|
+
* @param content - The message content. Can be:
|
|
296
|
+
* - A simple string for text-only messages
|
|
297
|
+
* - A MultimodalContent object with content array and optional custom ID
|
|
298
|
+
* @param body - Optional body parameters to merge with the client's base body for this request.
|
|
299
|
+
* Uses shallow merge with per-message body taking priority.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* // Simple text message
|
|
304
|
+
* await client.sendMessage('Hello!')
|
|
305
|
+
*
|
|
306
|
+
* // Text message with custom body params
|
|
307
|
+
* await client.sendMessage('Hello!', { temperature: 0.7 })
|
|
308
|
+
*
|
|
309
|
+
* // Multimodal message with image
|
|
310
|
+
* await client.sendMessage({
|
|
311
|
+
* content: [
|
|
312
|
+
* { type: 'text', content: 'What is in this image?' },
|
|
313
|
+
* { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
|
|
314
|
+
* ]
|
|
315
|
+
* })
|
|
316
|
+
*
|
|
317
|
+
* // Multimodal message with custom ID and body params
|
|
318
|
+
* await client.sendMessage(
|
|
319
|
+
* {
|
|
320
|
+
* content: [
|
|
321
|
+
* { type: 'text', content: 'Describe this audio' },
|
|
322
|
+
* { type: 'audio', source: { type: 'data', value: 'base64...' } }
|
|
323
|
+
* ],
|
|
324
|
+
* id: 'custom-message-id'
|
|
325
|
+
* },
|
|
326
|
+
* { model: 'gpt-4-audio' }
|
|
327
|
+
* )
|
|
328
|
+
* ```
|
|
255
329
|
*/
|
|
256
|
-
async sendMessage(
|
|
257
|
-
|
|
330
|
+
async sendMessage(
|
|
331
|
+
content: string | MultimodalContent,
|
|
332
|
+
body?: Record<string, any>,
|
|
333
|
+
): Promise<void> {
|
|
334
|
+
const emptyMessage = typeof content === 'string' && !content.trim()
|
|
335
|
+
if (emptyMessage || this.isLoading) {
|
|
258
336
|
return
|
|
259
337
|
}
|
|
338
|
+
// Normalize input to extract content, id, and validate
|
|
339
|
+
const normalizedContent = this.normalizeMessageInput(content)
|
|
340
|
+
|
|
341
|
+
// Store the per-message body for use in streamResponse
|
|
342
|
+
this.pendingMessageBody = body
|
|
260
343
|
|
|
261
344
|
// Add user message via processor
|
|
262
|
-
const userMessage = this.processor.addUserMessage(
|
|
263
|
-
|
|
345
|
+
const userMessage = this.processor.addUserMessage(
|
|
346
|
+
normalizedContent.content,
|
|
347
|
+
normalizedContent.id,
|
|
348
|
+
)
|
|
349
|
+
this.events.messageSent(userMessage.id, normalizedContent.content)
|
|
264
350
|
|
|
265
351
|
await this.streamResponse()
|
|
266
352
|
}
|
|
267
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Normalize the message input to extract content and optional id.
|
|
356
|
+
* Trims string content automatically.
|
|
357
|
+
*/
|
|
358
|
+
private normalizeMessageInput(input: string | MultimodalContent): {
|
|
359
|
+
content: string | Array<ContentPart>
|
|
360
|
+
id?: string
|
|
361
|
+
} {
|
|
362
|
+
if (typeof input === 'string') {
|
|
363
|
+
return { content: input.trim() }
|
|
364
|
+
}
|
|
365
|
+
return { content: input.content, id: input.id }
|
|
366
|
+
}
|
|
367
|
+
|
|
268
368
|
/**
|
|
269
369
|
* Append a message and stream the response
|
|
270
370
|
*/
|
|
@@ -287,6 +387,12 @@ export class ChatClient {
|
|
|
287
387
|
const messages = this.processor.getMessages()
|
|
288
388
|
this.processor.setMessages([...messages, uiMessage])
|
|
289
389
|
|
|
390
|
+
// If stream is in progress, queue the response for after it ends
|
|
391
|
+
if (this.isLoading) {
|
|
392
|
+
this.queuePostStreamAction(() => this.streamResponse())
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
290
396
|
await this.streamResponse()
|
|
291
397
|
}
|
|
292
398
|
|
|
@@ -294,45 +400,76 @@ export class ChatClient {
|
|
|
294
400
|
* Stream a response from the LLM
|
|
295
401
|
*/
|
|
296
402
|
private async streamResponse(): Promise<void> {
|
|
403
|
+
// Guard against concurrent streams - if already loading, skip
|
|
404
|
+
if (this.isLoading) {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
297
408
|
this.setIsLoading(true)
|
|
409
|
+
this.setStatus('submitted')
|
|
298
410
|
this.setError(undefined)
|
|
299
411
|
this.abortController = new AbortController()
|
|
412
|
+
// Reset pending tool executions for the new stream
|
|
413
|
+
this.pendingToolExecutions.clear()
|
|
414
|
+
let streamCompletedSuccessfully = false
|
|
300
415
|
|
|
301
416
|
try {
|
|
302
|
-
// Get
|
|
303
|
-
const
|
|
417
|
+
// Get UIMessages with parts (preserves approval state and client tool results)
|
|
418
|
+
const messages = this.processor.getMessages()
|
|
304
419
|
|
|
305
420
|
// Call onResponse callback
|
|
306
421
|
await this.callbacksRef.current.onResponse()
|
|
307
422
|
|
|
308
|
-
//
|
|
309
|
-
|
|
423
|
+
// Merge body: base body + per-message body (per-message takes priority)
|
|
424
|
+
// Include conversationId for server-side event correlation
|
|
425
|
+
const mergedBody = {
|
|
310
426
|
...this.body,
|
|
427
|
+
...this.pendingMessageBody,
|
|
311
428
|
conversationId: this.uniqueId,
|
|
312
429
|
}
|
|
313
430
|
|
|
431
|
+
// Clear the pending message body after use
|
|
432
|
+
this.pendingMessageBody = undefined
|
|
433
|
+
|
|
314
434
|
// Connect and stream
|
|
315
435
|
const stream = this.connection.connect(
|
|
316
|
-
|
|
317
|
-
|
|
436
|
+
messages,
|
|
437
|
+
mergedBody,
|
|
318
438
|
this.abortController.signal,
|
|
319
439
|
)
|
|
320
440
|
|
|
321
441
|
await this.processStream(stream)
|
|
442
|
+
streamCompletedSuccessfully = true
|
|
322
443
|
} catch (err) {
|
|
323
444
|
if (err instanceof Error) {
|
|
324
445
|
if (err.name === 'AbortError') {
|
|
325
446
|
return
|
|
326
447
|
}
|
|
327
448
|
this.setError(err)
|
|
449
|
+
this.setStatus('error')
|
|
328
450
|
this.callbacksRef.current.onError(err)
|
|
329
451
|
}
|
|
330
452
|
} finally {
|
|
331
453
|
this.abortController = null
|
|
332
454
|
this.setIsLoading(false)
|
|
455
|
+
this.pendingMessageBody = undefined // Ensure it's cleared even on error
|
|
333
456
|
|
|
334
457
|
// Drain any actions that were queued while the stream was in progress
|
|
335
458
|
await this.drainPostStreamActions()
|
|
459
|
+
|
|
460
|
+
// Continue conversation if the stream ended with a tool result (server tool completed)
|
|
461
|
+
if (streamCompletedSuccessfully) {
|
|
462
|
+
const messages = this.processor.getMessages()
|
|
463
|
+
const lastPart = messages.at(-1)?.parts.at(-1)
|
|
464
|
+
|
|
465
|
+
if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {
|
|
466
|
+
try {
|
|
467
|
+
await this.checkForContinuation()
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('Failed to continue flow after tool result:', error)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
336
473
|
}
|
|
337
474
|
}
|
|
338
475
|
|
|
@@ -368,6 +505,7 @@ export class ChatClient {
|
|
|
368
505
|
this.abortController = null
|
|
369
506
|
}
|
|
370
507
|
this.setIsLoading(false)
|
|
508
|
+
this.setStatus('ready')
|
|
371
509
|
this.events.stopped()
|
|
372
510
|
}
|
|
373
511
|
|
|
@@ -476,8 +614,18 @@ export class ChatClient {
|
|
|
476
614
|
* Check if we should continue the flow and do so if needed
|
|
477
615
|
*/
|
|
478
616
|
private async checkForContinuation(): Promise<void> {
|
|
617
|
+
// Prevent duplicate continuation attempts
|
|
618
|
+
if (this.continuationPending || this.isLoading) {
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
|
|
479
622
|
if (this.shouldAutoSend()) {
|
|
480
|
-
|
|
623
|
+
this.continuationPending = true
|
|
624
|
+
try {
|
|
625
|
+
await this.streamResponse()
|
|
626
|
+
} finally {
|
|
627
|
+
this.continuationPending = false
|
|
628
|
+
}
|
|
481
629
|
}
|
|
482
630
|
}
|
|
483
631
|
|
|
@@ -502,6 +650,13 @@ export class ChatClient {
|
|
|
502
650
|
return this.isLoading
|
|
503
651
|
}
|
|
504
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Get current status
|
|
655
|
+
*/
|
|
656
|
+
getStatus(): ChatClientState {
|
|
657
|
+
return this.status
|
|
658
|
+
}
|
|
659
|
+
|
|
505
660
|
/**
|
|
506
661
|
* Get current error
|
|
507
662
|
*/
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { convertMessagesToModelMessages } from '@tanstack/ai'
|
|
2
1
|
import type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -138,16 +137,15 @@ export function fetchServerSentEvents(
|
|
|
138
137
|
const resolvedOptions =
|
|
139
138
|
typeof options === 'function' ? await options() : options
|
|
140
139
|
|
|
141
|
-
const modelMessages = convertMessagesToModelMessages(messages)
|
|
142
|
-
|
|
143
140
|
const requestHeaders: Record<string, string> = {
|
|
144
141
|
'Content-Type': 'application/json',
|
|
145
142
|
...mergeHeaders(resolvedOptions.headers),
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
//
|
|
145
|
+
// Send messages as-is (UIMessages with parts preserved)
|
|
146
|
+
// Server-side TextEngine handles conversion to ModelMessages
|
|
149
147
|
const requestBody = {
|
|
150
|
-
messages
|
|
148
|
+
messages,
|
|
151
149
|
data,
|
|
152
150
|
...resolvedOptions.body,
|
|
153
151
|
}
|
|
@@ -238,17 +236,15 @@ export function fetchHttpStream(
|
|
|
238
236
|
const resolvedOptions =
|
|
239
237
|
typeof options === 'function' ? await options() : options
|
|
240
238
|
|
|
241
|
-
// Convert UIMessages to ModelMessages if needed
|
|
242
|
-
const modelMessages = convertMessagesToModelMessages(messages)
|
|
243
|
-
|
|
244
239
|
const requestHeaders: Record<string, string> = {
|
|
245
240
|
'Content-Type': 'application/json',
|
|
246
241
|
...mergeHeaders(resolvedOptions.headers),
|
|
247
242
|
}
|
|
248
243
|
|
|
249
|
-
//
|
|
244
|
+
// Send messages as-is (UIMessages with parts preserved)
|
|
245
|
+
// Server-side TextEngine handles conversion to ModelMessages
|
|
250
246
|
const requestBody = {
|
|
251
|
-
messages
|
|
247
|
+
messages,
|
|
252
248
|
data,
|
|
253
249
|
...resolvedOptions.body,
|
|
254
250
|
}
|
|
@@ -302,14 +298,15 @@ export function fetchHttpStream(
|
|
|
302
298
|
*/
|
|
303
299
|
export function stream(
|
|
304
300
|
streamFactory: (
|
|
305
|
-
messages: Array<ModelMessage>,
|
|
301
|
+
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
306
302
|
data?: Record<string, any>,
|
|
307
303
|
) => AsyncIterable<StreamChunk>,
|
|
308
304
|
): ConnectionAdapter {
|
|
309
305
|
return {
|
|
310
306
|
async *connect(messages, data) {
|
|
311
|
-
|
|
312
|
-
|
|
307
|
+
// Pass messages as-is (UIMessages with parts preserved)
|
|
308
|
+
// Server-side chat() handles conversion to ModelMessages
|
|
309
|
+
yield* streamFactory(messages, data)
|
|
313
310
|
},
|
|
314
311
|
}
|
|
315
312
|
}
|
|
@@ -332,16 +329,15 @@ export function stream(
|
|
|
332
329
|
*/
|
|
333
330
|
export function rpcStream(
|
|
334
331
|
rpcCall: (
|
|
335
|
-
messages: Array<ModelMessage>,
|
|
332
|
+
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
336
333
|
data?: Record<string, any>,
|
|
337
334
|
) => AsyncIterable<StreamChunk>,
|
|
338
335
|
): ConnectionAdapter {
|
|
339
336
|
return {
|
|
340
337
|
async *connect(messages, data) {
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
yield* rpcCall(modelMessages, data)
|
|
338
|
+
// Pass messages as-is (UIMessages with parts preserved)
|
|
339
|
+
// Server-side chat() handles conversion to ModelMessages
|
|
340
|
+
yield* rpcCall(messages, data)
|
|
345
341
|
},
|
|
346
342
|
}
|
|
347
343
|
}
|
package/src/events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { aiEventClient } from '@tanstack/ai/event-client'
|
|
2
|
+
import type { ContentPart } from '@tanstack/ai'
|
|
2
3
|
import type { UIMessage } from './types'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -137,19 +138,36 @@ export abstract class ChatClientEventEmitter {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
* Emit message sent event
|
|
141
|
+
* Emit message sent event.
|
|
142
|
+
* Supports both simple string content and multimodal content arrays.
|
|
143
|
+
*
|
|
144
|
+
* @param messageId - The ID of the sent message
|
|
145
|
+
* @param content - The message content (string or array of ContentPart for multimodal)
|
|
141
146
|
*/
|
|
142
|
-
messageSent(messageId: string, content: string): void {
|
|
147
|
+
messageSent(messageId: string, content: string | Array<ContentPart>): void {
|
|
148
|
+
// For text content, extract it; for multimodal, provide the array
|
|
149
|
+
const textContent =
|
|
150
|
+
typeof content === 'string'
|
|
151
|
+
? content
|
|
152
|
+
: content
|
|
153
|
+
.filter((part) => part.type === 'text')
|
|
154
|
+
.map((part) => (part as { type: 'text'; content: string }).content)
|
|
155
|
+
.join('')
|
|
156
|
+
|
|
143
157
|
this.emitEvent('text:message:created', {
|
|
144
158
|
messageId,
|
|
145
159
|
role: 'user',
|
|
146
|
-
content,
|
|
160
|
+
content: textContent,
|
|
161
|
+
// Include full content for multimodal messages
|
|
162
|
+
...(Array.isArray(content) && { parts: content }),
|
|
147
163
|
})
|
|
148
164
|
|
|
149
165
|
this.emitEvent('text:message:user', {
|
|
150
166
|
messageId,
|
|
151
167
|
role: 'user',
|
|
152
|
-
content,
|
|
168
|
+
content: textContent,
|
|
169
|
+
// Include full content for multimodal messages
|
|
170
|
+
...(Array.isArray(content) && { parts: content }),
|
|
153
171
|
})
|
|
154
172
|
}
|
|
155
173
|
|
|
@@ -161,7 +179,6 @@ export abstract class ChatClientEventEmitter {
|
|
|
161
179
|
fromMessageIndex,
|
|
162
180
|
})
|
|
163
181
|
}
|
|
164
|
-
|
|
165
182
|
/**
|
|
166
183
|
* Emit stopped event
|
|
167
184
|
*/
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ export type {
|
|
|
11
11
|
ChatClientOptions,
|
|
12
12
|
ChatRequestBody,
|
|
13
13
|
InferChatMessages,
|
|
14
|
+
ChatClientState,
|
|
15
|
+
// Multimodal content input type
|
|
16
|
+
MultimodalContent,
|
|
14
17
|
} from './types'
|
|
15
18
|
export { clientTools, createChatClientOptions } from './types'
|
|
16
19
|
export type {
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyClientTool,
|
|
3
|
+
AudioPart,
|
|
3
4
|
ChunkStrategy,
|
|
5
|
+
ContentPart,
|
|
6
|
+
DocumentPart,
|
|
7
|
+
ImagePart,
|
|
4
8
|
InferToolInput,
|
|
5
9
|
InferToolOutput,
|
|
6
10
|
ModelMessage,
|
|
7
11
|
StreamChunk,
|
|
12
|
+
VideoPart,
|
|
8
13
|
} from '@tanstack/ai'
|
|
9
14
|
import type { ConnectionAdapter } from './connection-adapters'
|
|
10
15
|
|
|
@@ -26,6 +31,40 @@ export type ToolResultState =
|
|
|
26
31
|
| 'complete' // Result is complete
|
|
27
32
|
| 'error' // Error occurred
|
|
28
33
|
|
|
34
|
+
/**
|
|
35
|
+
* ChatClient state - track the lifecycle of a chat
|
|
36
|
+
*/
|
|
37
|
+
export type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Multimodal content input for sending messages with rich media.
|
|
41
|
+
* Allows sending text, images, audio, video, and documents to the LLM.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // Send an image with a question
|
|
46
|
+
* client.sendMessage({
|
|
47
|
+
* content: [
|
|
48
|
+
* { type: 'text', content: 'What is in this image?' },
|
|
49
|
+
* { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
|
|
50
|
+
* ],
|
|
51
|
+
* id: 'custom-message-id' // optional
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export interface MultimodalContent {
|
|
56
|
+
/**
|
|
57
|
+
* The content of the message.
|
|
58
|
+
* Can be a simple string or an array of content parts for multimodal messages.
|
|
59
|
+
*/
|
|
60
|
+
content: string | Array<ContentPart>
|
|
61
|
+
/**
|
|
62
|
+
* Optional custom ID for the message.
|
|
63
|
+
* If not provided, a unique ID will be generated.
|
|
64
|
+
*/
|
|
65
|
+
id?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
/**
|
|
30
69
|
* Message parts - building blocks of UIMessage
|
|
31
70
|
*/
|
|
@@ -116,6 +155,10 @@ export interface ThinkingPart {
|
|
|
116
155
|
|
|
117
156
|
export type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> =
|
|
118
157
|
| TextPart
|
|
158
|
+
| ImagePart
|
|
159
|
+
| AudioPart
|
|
160
|
+
| VideoPart
|
|
161
|
+
| DocumentPart
|
|
119
162
|
| ToolCallPart<TTools>
|
|
120
163
|
| ToolResultPart
|
|
121
164
|
| ThinkingPart
|
|
@@ -191,6 +234,11 @@ export interface ChatClientOptions<
|
|
|
191
234
|
*/
|
|
192
235
|
onErrorChange?: (error: Error | undefined) => void
|
|
193
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Callback when chat status changes
|
|
239
|
+
*/
|
|
240
|
+
onStatusChange?: (status: ChatClientState) => void
|
|
241
|
+
|
|
194
242
|
/**
|
|
195
243
|
* Client-side tools with execution logic
|
|
196
244
|
* When provided, tools with execute functions will be called automatically
|