@tanstack/ai-client 0.0.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/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/esm/chat-client.d.ts +102 -0
- package/dist/esm/chat-client.js +375 -0
- package/dist/esm/chat-client.js.map +1 -0
- package/dist/esm/connection-adapters.d.ts +124 -0
- package/dist/esm/connection-adapters.js +149 -0
- package/dist/esm/connection-adapters.js.map +1 -0
- package/dist/esm/events.d.ts +82 -0
- package/dist/esm/events.js +180 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +29 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tool-types.d.ts +20 -0
- package/dist/esm/types.d.ts +210 -0
- package/dist/esm/types.js +11 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +53 -0
- package/src/chat-client.ts +522 -0
- package/src/connection-adapters.ts +344 -0
- package/src/events.ts +252 -0
- package/src/index.ts +63 -0
- package/src/tool-types.ts +41 -0
- package/src/types.ts +276 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { convertMessagesToModelMessages } from '@tanstack/ai'
|
|
2
|
+
import type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge custom headers into request headers
|
|
6
|
+
*/
|
|
7
|
+
function mergeHeaders(
|
|
8
|
+
customHeaders?: Record<string, string> | Headers,
|
|
9
|
+
): Record<string, string> {
|
|
10
|
+
if (!customHeaders) {
|
|
11
|
+
return {}
|
|
12
|
+
}
|
|
13
|
+
if (customHeaders instanceof Headers) {
|
|
14
|
+
const result: Record<string, string> = {}
|
|
15
|
+
customHeaders.forEach((value, key) => {
|
|
16
|
+
result[key] = value
|
|
17
|
+
})
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
return customHeaders
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read lines from a stream (newline-delimited)
|
|
25
|
+
*/
|
|
26
|
+
async function* readStreamLines(
|
|
27
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
28
|
+
abortSignal?: AbortSignal,
|
|
29
|
+
): AsyncGenerator<string> {
|
|
30
|
+
try {
|
|
31
|
+
const decoder = new TextDecoder()
|
|
32
|
+
let buffer = ''
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
35
|
+
while (true) {
|
|
36
|
+
// Check if aborted before reading
|
|
37
|
+
if (abortSignal?.aborted) {
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { done, value } = await reader.read()
|
|
42
|
+
if (done) break
|
|
43
|
+
|
|
44
|
+
buffer += decoder.decode(value, { stream: true })
|
|
45
|
+
const lines = buffer.split('\n')
|
|
46
|
+
|
|
47
|
+
// Keep the last incomplete line in the buffer
|
|
48
|
+
buffer = lines.pop() || ''
|
|
49
|
+
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (line.trim()) {
|
|
52
|
+
yield line
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Process any remaining data in the buffer
|
|
58
|
+
if (buffer.trim()) {
|
|
59
|
+
yield buffer
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
reader.releaseLock()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Connection adapter interface - converts a connection into a stream of chunks
|
|
68
|
+
*/
|
|
69
|
+
export interface ConnectionAdapter {
|
|
70
|
+
/**
|
|
71
|
+
* Connect and return an async iterable of StreamChunks
|
|
72
|
+
* @param messages - The messages to send (UIMessages or ModelMessages)
|
|
73
|
+
* @param data - Additional data to send
|
|
74
|
+
* @param abortSignal - Optional abort signal for request cancellation
|
|
75
|
+
*/
|
|
76
|
+
connect: (
|
|
77
|
+
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
78
|
+
data?: Record<string, any>,
|
|
79
|
+
abortSignal?: AbortSignal,
|
|
80
|
+
) => AsyncIterable<StreamChunk>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for fetch-based connection adapters
|
|
85
|
+
*/
|
|
86
|
+
export interface FetchConnectionOptions {
|
|
87
|
+
headers?: Record<string, string> | Headers
|
|
88
|
+
credentials?: RequestCredentials
|
|
89
|
+
signal?: AbortSignal
|
|
90
|
+
body?: Record<string, any>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a Server-Sent Events connection adapter
|
|
95
|
+
*
|
|
96
|
+
* @param url - The API endpoint URL (or a function that returns the URL)
|
|
97
|
+
* @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)
|
|
98
|
+
* @returns A connection adapter for SSE streams
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // Static URL
|
|
103
|
+
* const connection = fetchServerSentEvents('/api/chat');
|
|
104
|
+
*
|
|
105
|
+
* // Dynamic URL
|
|
106
|
+
* const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);
|
|
107
|
+
*
|
|
108
|
+
* // With options
|
|
109
|
+
* const connection = fetchServerSentEvents('/api/chat', {
|
|
110
|
+
* headers: { 'Authorization': 'Bearer token' }
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // With dynamic options
|
|
114
|
+
* const connection = fetchServerSentEvents('/api/chat', () => ({
|
|
115
|
+
* headers: { 'Authorization': `Bearer ${getToken()}` }
|
|
116
|
+
* }));
|
|
117
|
+
*
|
|
118
|
+
* // With additional body data
|
|
119
|
+
* const connection = fetchServerSentEvents('/api/chat', async () => ({
|
|
120
|
+
* body: {
|
|
121
|
+
* provider: 'openai',
|
|
122
|
+
* model: 'gpt-4o',
|
|
123
|
+
* }
|
|
124
|
+
* }));
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function fetchServerSentEvents(
|
|
128
|
+
url: string | (() => string),
|
|
129
|
+
options:
|
|
130
|
+
| FetchConnectionOptions
|
|
131
|
+
| (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
|
|
132
|
+
): ConnectionAdapter {
|
|
133
|
+
return {
|
|
134
|
+
async *connect(messages, data, abortSignal) {
|
|
135
|
+
// Resolve URL and options if they are functions
|
|
136
|
+
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
137
|
+
const resolvedOptions =
|
|
138
|
+
typeof options === 'function' ? await options() : options
|
|
139
|
+
|
|
140
|
+
const modelMessages = convertMessagesToModelMessages(messages)
|
|
141
|
+
|
|
142
|
+
const requestHeaders: Record<string, string> = {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
...mergeHeaders(resolvedOptions.headers),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Merge body from options with messages and data
|
|
148
|
+
const requestBody = {
|
|
149
|
+
messages: modelMessages,
|
|
150
|
+
data,
|
|
151
|
+
...resolvedOptions.body,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await fetch(resolvedUrl, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: requestHeaders,
|
|
157
|
+
body: JSON.stringify(requestBody),
|
|
158
|
+
credentials: resolvedOptions.credentials || 'same-origin',
|
|
159
|
+
signal: abortSignal || resolvedOptions.signal,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Parse Server-Sent Events format
|
|
169
|
+
const reader = response.body?.getReader()
|
|
170
|
+
if (!reader) {
|
|
171
|
+
throw new Error('Response body is not readable')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
175
|
+
// Handle Server-Sent Events format
|
|
176
|
+
const data = line.startsWith('data: ') ? line.slice(6) : line
|
|
177
|
+
|
|
178
|
+
if (data === '[DONE]') continue
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const parsed: StreamChunk = JSON.parse(data)
|
|
182
|
+
yield parsed
|
|
183
|
+
} catch (parseError) {
|
|
184
|
+
// Skip non-JSON lines or malformed chunks
|
|
185
|
+
console.warn('Failed to parse SSE chunk:', data)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create an HTTP streaming connection adapter (for raw streaming without SSE format)
|
|
194
|
+
*
|
|
195
|
+
* @param url - The API endpoint URL (or a function that returns the URL)
|
|
196
|
+
* @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)
|
|
197
|
+
* @returns A connection adapter for HTTP streams
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* // Static URL
|
|
202
|
+
* const connection = fetchHttpStream('/api/chat');
|
|
203
|
+
*
|
|
204
|
+
* // Dynamic URL
|
|
205
|
+
* const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);
|
|
206
|
+
*
|
|
207
|
+
* // With options
|
|
208
|
+
* const connection = fetchHttpStream('/api/chat', {
|
|
209
|
+
* headers: { 'Authorization': 'Bearer token' }
|
|
210
|
+
* });
|
|
211
|
+
*
|
|
212
|
+
* // With dynamic options
|
|
213
|
+
* const connection = fetchHttpStream('/api/chat', () => ({
|
|
214
|
+
* headers: { 'Authorization': `Bearer ${getToken()}` }
|
|
215
|
+
* }));
|
|
216
|
+
*
|
|
217
|
+
* // With additional body data
|
|
218
|
+
* const connection = fetchHttpStream('/api/chat', async () => ({
|
|
219
|
+
* body: {
|
|
220
|
+
* provider: 'openai',
|
|
221
|
+
* model: 'gpt-4o',
|
|
222
|
+
* }
|
|
223
|
+
* }));
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function fetchHttpStream(
|
|
227
|
+
url: string | (() => string),
|
|
228
|
+
options:
|
|
229
|
+
| FetchConnectionOptions
|
|
230
|
+
| (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
|
|
231
|
+
): ConnectionAdapter {
|
|
232
|
+
return {
|
|
233
|
+
async *connect(messages, data, abortSignal) {
|
|
234
|
+
// Resolve URL and options if they are functions
|
|
235
|
+
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
236
|
+
const resolvedOptions =
|
|
237
|
+
typeof options === 'function' ? await options() : options
|
|
238
|
+
|
|
239
|
+
// Convert UIMessages to ModelMessages if needed
|
|
240
|
+
const modelMessages = convertMessagesToModelMessages(messages)
|
|
241
|
+
|
|
242
|
+
const requestHeaders: Record<string, string> = {
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
...mergeHeaders(resolvedOptions.headers),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Merge body from options with messages and data
|
|
248
|
+
const requestBody = {
|
|
249
|
+
messages: modelMessages,
|
|
250
|
+
data,
|
|
251
|
+
...resolvedOptions.body,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const response = await fetch(resolvedUrl, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: requestHeaders,
|
|
257
|
+
body: JSON.stringify(requestBody),
|
|
258
|
+
credentials: resolvedOptions.credentials || 'same-origin',
|
|
259
|
+
signal: abortSignal || resolvedOptions.signal,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`HTTP error! status: ${response.status} ${response.statusText}`,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Parse raw HTTP stream (newline-delimited JSON)
|
|
269
|
+
const reader = response.body?.getReader()
|
|
270
|
+
if (!reader) {
|
|
271
|
+
throw new Error('Response body is not readable')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
275
|
+
try {
|
|
276
|
+
const parsed: StreamChunk = JSON.parse(line)
|
|
277
|
+
yield parsed
|
|
278
|
+
} catch (parseError) {
|
|
279
|
+
console.warn('Failed to parse HTTP stream chunk:', line)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create a direct stream connection adapter (for server functions or direct streams)
|
|
288
|
+
*
|
|
289
|
+
* @param streamFactory - A function that returns an async iterable of StreamChunks
|
|
290
|
+
* @returns A connection adapter for direct streams
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // With TanStack Start server function
|
|
295
|
+
* const connection = stream(() => serverFunction({ messages }));
|
|
296
|
+
*
|
|
297
|
+
* const client = new ChatClient({ connection });
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
export function stream(
|
|
301
|
+
streamFactory: (
|
|
302
|
+
messages: Array<ModelMessage>,
|
|
303
|
+
data?: Record<string, any>,
|
|
304
|
+
) => AsyncIterable<StreamChunk>,
|
|
305
|
+
): ConnectionAdapter {
|
|
306
|
+
return {
|
|
307
|
+
async *connect(messages, data) {
|
|
308
|
+
const modelMessages = convertMessagesToModelMessages(messages)
|
|
309
|
+
yield* streamFactory(modelMessages, data)
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)
|
|
316
|
+
*
|
|
317
|
+
* @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks
|
|
318
|
+
* @returns A connection adapter for RPC streams
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* // With Cap'n Web RPC
|
|
323
|
+
* const connection = rpcStream((messages, data) =>
|
|
324
|
+
* api.streamMurfResponse(messages, data)
|
|
325
|
+
* );
|
|
326
|
+
*
|
|
327
|
+
* const client = new ChatClient({ connection });
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export function rpcStream(
|
|
331
|
+
rpcCall: (
|
|
332
|
+
messages: Array<ModelMessage>,
|
|
333
|
+
data?: Record<string, any>,
|
|
334
|
+
) => AsyncIterable<StreamChunk>,
|
|
335
|
+
): ConnectionAdapter {
|
|
336
|
+
return {
|
|
337
|
+
async *connect(messages, data) {
|
|
338
|
+
const modelMessages = convertMessagesToModelMessages(messages)
|
|
339
|
+
// Simply yield from the RPC call
|
|
340
|
+
// The RPC layer handles WebSocket transport
|
|
341
|
+
yield* rpcCall(modelMessages, data)
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { aiEventClient } from '@tanstack/ai/event-client'
|
|
2
|
+
import type { UIMessage } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Abstract base class for ChatClient event emission
|
|
6
|
+
*/
|
|
7
|
+
export abstract class ChatClientEventEmitter {
|
|
8
|
+
protected clientId: string
|
|
9
|
+
|
|
10
|
+
constructor(clientId: string) {
|
|
11
|
+
this.clientId = clientId
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Protected abstract method for emitting events
|
|
16
|
+
* Implementations should handle adding clientId and timestamp
|
|
17
|
+
*/
|
|
18
|
+
protected abstract emitEvent(
|
|
19
|
+
eventName: string,
|
|
20
|
+
data?: Record<string, any>,
|
|
21
|
+
): void
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Emit client created event
|
|
25
|
+
*/
|
|
26
|
+
clientCreated(initialMessageCount: number): void {
|
|
27
|
+
this.emitEvent('client:created', {
|
|
28
|
+
initialMessageCount,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Emit loading state changed event
|
|
34
|
+
*/
|
|
35
|
+
loadingChanged(isLoading: boolean): void {
|
|
36
|
+
this.emitEvent('client:loading-changed', { isLoading })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Emit error state changed event
|
|
41
|
+
*/
|
|
42
|
+
errorChanged(error: string | null): void {
|
|
43
|
+
this.emitEvent('client:error-changed', {
|
|
44
|
+
error,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Emit text update events (combines processor and client events)
|
|
50
|
+
*/
|
|
51
|
+
textUpdated(streamId: string, messageId: string, content: string): void {
|
|
52
|
+
this.emitEvent('processor:text-updated', {
|
|
53
|
+
streamId,
|
|
54
|
+
content,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
this.emitEvent('client:assistant-message-updated', {
|
|
58
|
+
messageId,
|
|
59
|
+
content,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emit tool call state change events (combines processor and client events)
|
|
65
|
+
*/
|
|
66
|
+
toolCallStateChanged(
|
|
67
|
+
streamId: string,
|
|
68
|
+
messageId: string,
|
|
69
|
+
toolCallId: string,
|
|
70
|
+
toolName: string,
|
|
71
|
+
state: string,
|
|
72
|
+
args: string,
|
|
73
|
+
): void {
|
|
74
|
+
this.emitEvent('processor:tool-call-state-changed', {
|
|
75
|
+
streamId,
|
|
76
|
+
toolCallId,
|
|
77
|
+
toolName,
|
|
78
|
+
state,
|
|
79
|
+
arguments: args,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
this.emitEvent('client:tool-call-updated', {
|
|
83
|
+
messageId,
|
|
84
|
+
toolCallId,
|
|
85
|
+
toolName,
|
|
86
|
+
state,
|
|
87
|
+
arguments: args,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Emit tool result state change event
|
|
93
|
+
*/
|
|
94
|
+
toolResultStateChanged(
|
|
95
|
+
streamId: string,
|
|
96
|
+
toolCallId: string,
|
|
97
|
+
content: string,
|
|
98
|
+
state: string,
|
|
99
|
+
error?: string,
|
|
100
|
+
): void {
|
|
101
|
+
this.emitEvent('processor:tool-result-state-changed', {
|
|
102
|
+
streamId,
|
|
103
|
+
toolCallId,
|
|
104
|
+
content,
|
|
105
|
+
state,
|
|
106
|
+
error,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Emit thinking update event
|
|
112
|
+
*/
|
|
113
|
+
thinkingUpdated(
|
|
114
|
+
streamId: string,
|
|
115
|
+
messageId: string,
|
|
116
|
+
content: string,
|
|
117
|
+
delta?: string,
|
|
118
|
+
): void {
|
|
119
|
+
this.emitEvent('stream:chunk:thinking', {
|
|
120
|
+
streamId,
|
|
121
|
+
messageId,
|
|
122
|
+
content,
|
|
123
|
+
delta,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Emit approval requested event
|
|
129
|
+
*/
|
|
130
|
+
approvalRequested(
|
|
131
|
+
messageId: string,
|
|
132
|
+
toolCallId: string,
|
|
133
|
+
toolName: string,
|
|
134
|
+
input: any,
|
|
135
|
+
approvalId: string,
|
|
136
|
+
): void {
|
|
137
|
+
this.emitEvent('client:approval-requested', {
|
|
138
|
+
messageId,
|
|
139
|
+
toolCallId,
|
|
140
|
+
toolName,
|
|
141
|
+
input,
|
|
142
|
+
approvalId,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Emit message appended event
|
|
148
|
+
*/
|
|
149
|
+
messageAppended(uiMessage: UIMessage): void {
|
|
150
|
+
const contentPreview = uiMessage.parts
|
|
151
|
+
.filter((p) => p.type === 'text')
|
|
152
|
+
.map((p) => (p as any).content)
|
|
153
|
+
.join(' ')
|
|
154
|
+
.substring(0, 100)
|
|
155
|
+
|
|
156
|
+
this.emitEvent('client:message-appended', {
|
|
157
|
+
messageId: uiMessage.id,
|
|
158
|
+
role: uiMessage.role,
|
|
159
|
+
contentPreview,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Emit message sent event
|
|
165
|
+
*/
|
|
166
|
+
messageSent(messageId: string, content: string): void {
|
|
167
|
+
this.emitEvent('client:message-sent', {
|
|
168
|
+
messageId,
|
|
169
|
+
content,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Emit reloaded event
|
|
175
|
+
*/
|
|
176
|
+
reloaded(fromMessageIndex: number): void {
|
|
177
|
+
this.emitEvent('client:reloaded', {
|
|
178
|
+
fromMessageIndex,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Emit stopped event
|
|
184
|
+
*/
|
|
185
|
+
stopped(): void {
|
|
186
|
+
this.emitEvent('client:stopped')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Emit messages cleared event
|
|
191
|
+
*/
|
|
192
|
+
messagesCleared(): void {
|
|
193
|
+
this.emitEvent('client:messages-cleared')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Emit tool result added event
|
|
198
|
+
*/
|
|
199
|
+
toolResultAdded(
|
|
200
|
+
toolCallId: string,
|
|
201
|
+
toolName: string,
|
|
202
|
+
output: any,
|
|
203
|
+
state: string,
|
|
204
|
+
): void {
|
|
205
|
+
this.emitEvent('tool:result-added', {
|
|
206
|
+
toolCallId,
|
|
207
|
+
toolName,
|
|
208
|
+
output,
|
|
209
|
+
state,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Emit tool approval responded event
|
|
215
|
+
*/
|
|
216
|
+
toolApprovalResponded(
|
|
217
|
+
approvalId: string,
|
|
218
|
+
toolCallId: string,
|
|
219
|
+
approved: boolean,
|
|
220
|
+
): void {
|
|
221
|
+
this.emitEvent('tool:approval-responded', {
|
|
222
|
+
approvalId,
|
|
223
|
+
toolCallId,
|
|
224
|
+
approved,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Default implementation of ChatClientEventEmitter
|
|
231
|
+
*/
|
|
232
|
+
export class DefaultChatClientEventEmitter extends ChatClientEventEmitter {
|
|
233
|
+
/**
|
|
234
|
+
* Emit an event with automatic clientId and timestamp for client/tool events
|
|
235
|
+
*/
|
|
236
|
+
protected emitEvent(eventName: string, data?: Record<string, any>): void {
|
|
237
|
+
// For client:* and tool:* events, automatically add clientId and timestamp
|
|
238
|
+
if (eventName.startsWith('client:') || eventName.startsWith('tool:')) {
|
|
239
|
+
aiEventClient.emit(eventName as any, {
|
|
240
|
+
...data,
|
|
241
|
+
clientId: this.clientId,
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
})
|
|
244
|
+
} else {
|
|
245
|
+
// For other events (e.g., processor:*), just add timestamp
|
|
246
|
+
aiEventClient.emit(eventName as any, {
|
|
247
|
+
...data,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export { ChatClient } from './chat-client'
|
|
2
|
+
export type {
|
|
3
|
+
// Core message types (re-exported from @tanstack/ai via types.ts)
|
|
4
|
+
UIMessage,
|
|
5
|
+
MessagePart,
|
|
6
|
+
TextPart,
|
|
7
|
+
ToolCallPart,
|
|
8
|
+
ToolResultPart,
|
|
9
|
+
ThinkingPart,
|
|
10
|
+
// Client configuration types
|
|
11
|
+
ChatClientOptions,
|
|
12
|
+
ChatRequestBody,
|
|
13
|
+
InferChatMessages,
|
|
14
|
+
} from './types'
|
|
15
|
+
export { clientTools, createChatClientOptions } from './types'
|
|
16
|
+
export type {
|
|
17
|
+
ExtractToolNames,
|
|
18
|
+
ExtractToolInput,
|
|
19
|
+
ExtractToolOutput,
|
|
20
|
+
} from './tool-types'
|
|
21
|
+
export type { AnyClientTool } from '@tanstack/ai'
|
|
22
|
+
export {
|
|
23
|
+
fetchServerSentEvents,
|
|
24
|
+
fetchHttpStream,
|
|
25
|
+
stream,
|
|
26
|
+
rpcStream,
|
|
27
|
+
type ConnectionAdapter,
|
|
28
|
+
type FetchConnectionOptions,
|
|
29
|
+
} from './connection-adapters'
|
|
30
|
+
|
|
31
|
+
// Re-export message converters from @tanstack/ai
|
|
32
|
+
export {
|
|
33
|
+
uiMessageToModelMessages,
|
|
34
|
+
modelMessageToUIMessage,
|
|
35
|
+
modelMessagesToUIMessages,
|
|
36
|
+
convertMessagesToModelMessages,
|
|
37
|
+
normalizeToUIMessage,
|
|
38
|
+
generateMessageId,
|
|
39
|
+
} from '@tanstack/ai'
|
|
40
|
+
|
|
41
|
+
// Re-export stream processing from @tanstack/ai (shared implementation)
|
|
42
|
+
export {
|
|
43
|
+
StreamProcessor,
|
|
44
|
+
ImmediateStrategy,
|
|
45
|
+
PunctuationStrategy,
|
|
46
|
+
BatchStrategy,
|
|
47
|
+
WordBoundaryStrategy,
|
|
48
|
+
CompositeStrategy,
|
|
49
|
+
parsePartialJSON,
|
|
50
|
+
PartialJSONParser,
|
|
51
|
+
defaultJSONParser,
|
|
52
|
+
type ChunkStrategy,
|
|
53
|
+
type StreamProcessorOptions,
|
|
54
|
+
type StreamProcessorHandlers,
|
|
55
|
+
type StreamProcessorEvents,
|
|
56
|
+
type InternalToolCallState,
|
|
57
|
+
type ToolCallState,
|
|
58
|
+
type ToolResultState,
|
|
59
|
+
type JSONParser,
|
|
60
|
+
type ChunkRecording,
|
|
61
|
+
type ProcessorResult,
|
|
62
|
+
type ProcessorState,
|
|
63
|
+
} from '@tanstack/ai'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyClientTool,
|
|
3
|
+
InferToolInput,
|
|
4
|
+
InferToolOutput,
|
|
5
|
+
} from '@tanstack/ai'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract all tool names from a tools array as a union type
|
|
9
|
+
*/
|
|
10
|
+
export type ExtractToolNames<TTools extends ReadonlyArray<AnyClientTool>> =
|
|
11
|
+
TTools[number]['name']
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find a tool by name in the tools array
|
|
15
|
+
*/
|
|
16
|
+
type FindTool<
|
|
17
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
18
|
+
TName extends string,
|
|
19
|
+
> = Extract<TTools[number], { name: TName }>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract the input type for a specific tool by name
|
|
23
|
+
*/
|
|
24
|
+
export type ExtractToolInput<
|
|
25
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
26
|
+
TName extends string,
|
|
27
|
+
> =
|
|
28
|
+
TName extends ExtractToolNames<TTools>
|
|
29
|
+
? InferToolInput<FindTool<TTools, TName>>
|
|
30
|
+
: any
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract the output type for a specific tool by name
|
|
34
|
+
*/
|
|
35
|
+
export type ExtractToolOutput<
|
|
36
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
37
|
+
TName extends string,
|
|
38
|
+
> =
|
|
39
|
+
TName extends ExtractToolNames<TTools>
|
|
40
|
+
? InferToolOutput<FindTool<TTools, TName>>
|
|
41
|
+
: any
|