@tanstack/ai-client 0.2.2 → 0.4.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/README.md +1 -0
- package/dist/esm/chat-client.d.ts +9 -1
- package/dist/esm/chat-client.js +95 -30
- 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 +4 -5
- package/dist/esm/events.js +26 -34
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/types.d.ts +8 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-client.ts +121 -35
- package/src/connection-adapters.ts +14 -18
- package/src/events.ts +37 -49
- package/src/index.ts +1 -1
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ A powerful, type-safe AI SDK for building AI-powered applications.
|
|
|
45
45
|
- Headless chat state management with adapters (SSE, HTTP stream, custom)
|
|
46
46
|
- Isomorphic type-safe tools with server/client execution
|
|
47
47
|
- **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
|
|
48
|
+
- **Observability events** - Structured, typed events for text, tools, image, speech, transcription, and video ([docs](./docs/guides/observability.md))
|
|
48
49
|
|
|
49
50
|
### <a href="https://tanstack.com/ai">Read the docs →</b></a>
|
|
50
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ChatClientOptions, UIMessage } from './types.js';
|
|
2
1
|
import { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai';
|
|
3
2
|
import { ConnectionAdapter } from './connection-adapters.js';
|
|
3
|
+
import { ChatClientOptions, ChatClientState, UIMessage } from './types.js';
|
|
4
4
|
export declare class ChatClient {
|
|
5
5
|
private processor;
|
|
6
6
|
private connection;
|
|
@@ -8,16 +8,20 @@ export declare class ChatClient {
|
|
|
8
8
|
private body;
|
|
9
9
|
private isLoading;
|
|
10
10
|
private error;
|
|
11
|
+
private status;
|
|
11
12
|
private abortController;
|
|
12
13
|
private events;
|
|
13
14
|
private clientToolsRef;
|
|
14
15
|
private currentStreamId;
|
|
15
16
|
private currentMessageId;
|
|
16
17
|
private postStreamActions;
|
|
18
|
+
private pendingToolExecutions;
|
|
19
|
+
private continuationPending;
|
|
17
20
|
private callbacksRef;
|
|
18
21
|
constructor(options: ChatClientOptions);
|
|
19
22
|
private generateUniqueId;
|
|
20
23
|
private setIsLoading;
|
|
24
|
+
private setStatus;
|
|
21
25
|
private setError;
|
|
22
26
|
/**
|
|
23
27
|
* Process a stream through the StreamProcessor
|
|
@@ -88,6 +92,10 @@ export declare class ChatClient {
|
|
|
88
92
|
* Get loading state
|
|
89
93
|
*/
|
|
90
94
|
getIsLoading(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Get current status
|
|
97
|
+
*/
|
|
98
|
+
getStatus(): ChatClientState;
|
|
91
99
|
/**
|
|
92
100
|
* Get current error
|
|
93
101
|
*/
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -5,10 +5,13 @@ class ChatClient {
|
|
|
5
5
|
this.body = {};
|
|
6
6
|
this.isLoading = false;
|
|
7
7
|
this.error = void 0;
|
|
8
|
+
this.status = "ready";
|
|
8
9
|
this.abortController = null;
|
|
9
10
|
this.currentStreamId = null;
|
|
10
11
|
this.currentMessageId = null;
|
|
11
12
|
this.postStreamActions = [];
|
|
13
|
+
this.pendingToolExecutions = /* @__PURE__ */ new Map();
|
|
14
|
+
this.continuationPending = false;
|
|
12
15
|
this.uniqueId = options.id || this.generateUniqueId("chat");
|
|
13
16
|
this.body = options.body || {};
|
|
14
17
|
this.connection = options.connection;
|
|
@@ -34,6 +37,8 @@ class ChatClient {
|
|
|
34
37
|
onLoadingChange: options.onLoadingChange || (() => {
|
|
35
38
|
}),
|
|
36
39
|
onErrorChange: options.onErrorChange || (() => {
|
|
40
|
+
}),
|
|
41
|
+
onStatusChange: options.onStatusChange || (() => {
|
|
37
42
|
})
|
|
38
43
|
}
|
|
39
44
|
};
|
|
@@ -45,12 +50,15 @@ class ChatClient {
|
|
|
45
50
|
this.callbacksRef.current.onMessagesChange(messages);
|
|
46
51
|
},
|
|
47
52
|
onStreamStart: () => {
|
|
53
|
+
this.setStatus("streaming");
|
|
48
54
|
},
|
|
49
55
|
onStreamEnd: (message) => {
|
|
50
56
|
this.callbacksRef.current.onFinish(message);
|
|
57
|
+
this.setStatus("ready");
|
|
51
58
|
},
|
|
52
59
|
onError: (error) => {
|
|
53
60
|
this.setError(error);
|
|
61
|
+
this.setStatus("error");
|
|
54
62
|
this.callbacksRef.current.onError(error);
|
|
55
63
|
},
|
|
56
64
|
onTextUpdate: (messageId, content) => {
|
|
@@ -85,36 +93,45 @@ class ChatClient {
|
|
|
85
93
|
);
|
|
86
94
|
}
|
|
87
95
|
},
|
|
88
|
-
onToolCall:
|
|
96
|
+
onToolCall: (args) => {
|
|
89
97
|
const clientTool = this.clientToolsRef.current.get(args.toolName);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
const executeFunc = clientTool?.execute;
|
|
99
|
+
if (executeFunc) {
|
|
100
|
+
const executionPromise = (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const output = await executeFunc(args.input);
|
|
103
|
+
await this.addToolResult({
|
|
104
|
+
toolCallId: args.toolCallId,
|
|
105
|
+
tool: args.toolName,
|
|
106
|
+
output,
|
|
107
|
+
state: "output-available"
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await this.addToolResult({
|
|
111
|
+
toolCallId: args.toolCallId,
|
|
112
|
+
tool: args.toolName,
|
|
113
|
+
output: null,
|
|
114
|
+
state: "output-error",
|
|
115
|
+
errorText: error.message
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
this.pendingToolExecutions.delete(args.toolCallId);
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
this.pendingToolExecutions.set(args.toolCallId, executionPromise);
|
|
108
122
|
}
|
|
109
123
|
},
|
|
110
124
|
onApprovalRequest: (args) => {
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
if (this.currentStreamId) {
|
|
126
|
+
this.events.approvalRequested(
|
|
127
|
+
this.currentStreamId,
|
|
128
|
+
this.currentMessageId || "",
|
|
129
|
+
args.toolCallId,
|
|
130
|
+
args.toolName,
|
|
131
|
+
args.input,
|
|
132
|
+
args.approvalId
|
|
133
|
+
);
|
|
134
|
+
}
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
});
|
|
@@ -128,6 +145,10 @@ class ChatClient {
|
|
|
128
145
|
this.callbacksRef.current.onLoadingChange(isLoading);
|
|
129
146
|
this.events.loadingChanged(isLoading);
|
|
130
147
|
}
|
|
148
|
+
setStatus(status) {
|
|
149
|
+
this.status = status;
|
|
150
|
+
this.callbacksRef.current.onStatusChange(status);
|
|
151
|
+
}
|
|
131
152
|
setError(error) {
|
|
132
153
|
this.error = error;
|
|
133
154
|
this.callbacksRef.current.onErrorChange(error);
|
|
@@ -146,12 +167,18 @@ class ChatClient {
|
|
|
146
167
|
parts: [],
|
|
147
168
|
createdAt: /* @__PURE__ */ new Date()
|
|
148
169
|
};
|
|
149
|
-
this.events.messageAppended(
|
|
170
|
+
this.events.messageAppended(
|
|
171
|
+
assistantMessage,
|
|
172
|
+
this.currentStreamId || void 0
|
|
173
|
+
);
|
|
150
174
|
for await (const chunk of source) {
|
|
151
175
|
this.callbacksRef.current.onChunk(chunk);
|
|
152
176
|
this.processor.processChunk(chunk);
|
|
153
177
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
154
178
|
}
|
|
179
|
+
if (this.pendingToolExecutions.size > 0) {
|
|
180
|
+
await Promise.all(this.pendingToolExecutions.values());
|
|
181
|
+
}
|
|
155
182
|
this.processor.finalizeStream();
|
|
156
183
|
this.currentStreamId = null;
|
|
157
184
|
this.currentMessageId = null;
|
|
@@ -189,40 +216,63 @@ class ChatClient {
|
|
|
189
216
|
this.events.messageAppended(uiMessage);
|
|
190
217
|
const messages = this.processor.getMessages();
|
|
191
218
|
this.processor.setMessages([...messages, uiMessage]);
|
|
219
|
+
if (this.isLoading) {
|
|
220
|
+
this.queuePostStreamAction(() => this.streamResponse());
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
192
223
|
await this.streamResponse();
|
|
193
224
|
}
|
|
194
225
|
/**
|
|
195
226
|
* Stream a response from the LLM
|
|
196
227
|
*/
|
|
197
228
|
async streamResponse() {
|
|
229
|
+
if (this.isLoading) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
198
232
|
this.setIsLoading(true);
|
|
233
|
+
this.setStatus("submitted");
|
|
199
234
|
this.setError(void 0);
|
|
200
235
|
this.abortController = new AbortController();
|
|
236
|
+
this.pendingToolExecutions.clear();
|
|
237
|
+
let streamCompletedSuccessfully = false;
|
|
201
238
|
try {
|
|
202
|
-
const
|
|
239
|
+
const messages = this.processor.getMessages();
|
|
203
240
|
await this.callbacksRef.current.onResponse();
|
|
204
241
|
const bodyWithConversationId = {
|
|
205
242
|
...this.body,
|
|
206
243
|
conversationId: this.uniqueId
|
|
207
244
|
};
|
|
208
245
|
const stream = this.connection.connect(
|
|
209
|
-
|
|
246
|
+
messages,
|
|
210
247
|
bodyWithConversationId,
|
|
211
248
|
this.abortController.signal
|
|
212
249
|
);
|
|
213
250
|
await this.processStream(stream);
|
|
251
|
+
streamCompletedSuccessfully = true;
|
|
214
252
|
} catch (err) {
|
|
215
253
|
if (err instanceof Error) {
|
|
216
254
|
if (err.name === "AbortError") {
|
|
217
255
|
return;
|
|
218
256
|
}
|
|
219
257
|
this.setError(err);
|
|
258
|
+
this.setStatus("error");
|
|
220
259
|
this.callbacksRef.current.onError(err);
|
|
221
260
|
}
|
|
222
261
|
} finally {
|
|
223
262
|
this.abortController = null;
|
|
224
263
|
this.setIsLoading(false);
|
|
225
264
|
await this.drainPostStreamActions();
|
|
265
|
+
if (streamCompletedSuccessfully) {
|
|
266
|
+
const messages = this.processor.getMessages();
|
|
267
|
+
const lastPart = messages.at(-1)?.parts.at(-1);
|
|
268
|
+
if (lastPart?.type === "tool-result" && this.shouldAutoSend()) {
|
|
269
|
+
try {
|
|
270
|
+
await this.checkForContinuation();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error("Failed to continue flow after tool result:", error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
226
276
|
}
|
|
227
277
|
}
|
|
228
278
|
/**
|
|
@@ -248,6 +298,7 @@ class ChatClient {
|
|
|
248
298
|
this.abortController = null;
|
|
249
299
|
}
|
|
250
300
|
this.setIsLoading(false);
|
|
301
|
+
this.setStatus("ready");
|
|
251
302
|
this.events.stopped();
|
|
252
303
|
}
|
|
253
304
|
/**
|
|
@@ -327,8 +378,16 @@ class ChatClient {
|
|
|
327
378
|
* Check if we should continue the flow and do so if needed
|
|
328
379
|
*/
|
|
329
380
|
async checkForContinuation() {
|
|
381
|
+
if (this.continuationPending || this.isLoading) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
330
384
|
if (this.shouldAutoSend()) {
|
|
331
|
-
|
|
385
|
+
this.continuationPending = true;
|
|
386
|
+
try {
|
|
387
|
+
await this.streamResponse();
|
|
388
|
+
} finally {
|
|
389
|
+
this.continuationPending = false;
|
|
390
|
+
}
|
|
332
391
|
}
|
|
333
392
|
}
|
|
334
393
|
/**
|
|
@@ -349,6 +408,12 @@ class ChatClient {
|
|
|
349
408
|
getIsLoading() {
|
|
350
409
|
return this.isLoading;
|
|
351
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Get current status
|
|
413
|
+
*/
|
|
414
|
+
getStatus() {
|
|
415
|
+
return this.status;
|
|
416
|
+
}
|
|
352
417
|
/**
|
|
353
418
|
* Get current error
|
|
354
419
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-client.js","sources":["../../src/chat-client.ts"],"sourcesContent":["import {\n StreamProcessor,\n generateMessageId,\n normalizeToUIMessage,\n} from '@tanstack/ai'\nimport { DefaultChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n MessagePart,\n ToolCallPart,\n UIMessage,\n} from './types'\nimport type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: ConnectionAdapter\n private uniqueId: string\n private body: Record<string, any> = {}\n private isLoading = false\n private error: Error | undefined = undefined\n private abortController: AbortController | null = null\n private events: ChatClientEventEmitter\n private clientToolsRef: { current: Map<string, AnyClientTool> }\n private currentStreamId: string | null = null\n private currentMessageId: string | null = null\n private postStreamActions: Array<() => Promise<void>> = []\n\n private callbacksRef: {\n current: {\n onResponse: (response?: Response) => void | Promise<void>\n onChunk: (chunk: StreamChunk) => void\n onFinish: (message: UIMessage) => void\n onError: (error: Error) => void\n onMessagesChange: (messages: Array<UIMessage>) => void\n onLoadingChange: (isLoading: boolean) => void\n onErrorChange: (error: Error | undefined) => void\n }\n }\n\n constructor(options: ChatClientOptions) {\n this.uniqueId = options.id || this.generateUniqueId('chat')\n this.body = options.body || {}\n this.connection = options.connection\n this.events = new DefaultChatClientEventEmitter(this.uniqueId)\n\n // Build client tools map\n this.clientToolsRef = { current: new Map() }\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n\n this.callbacksRef = {\n current: {\n onResponse: options.onResponse || (() => {}),\n onChunk: options.onChunk || (() => {}),\n onFinish: options.onFinish || (() => {}),\n onError: options.onError || (() => {}),\n onMessagesChange: options.onMessagesChange || (() => {}),\n onLoadingChange: options.onLoadingChange || (() => {}),\n onErrorChange: options.onErrorChange || (() => {}),\n },\n }\n\n // Create StreamProcessor with event handlers\n this.processor = new StreamProcessor({\n chunkStrategy: options.streamProcessor?.chunkStrategy,\n initialMessages: options.initialMessages,\n events: {\n onMessagesChange: (messages: Array<UIMessage>) => {\n this.callbacksRef.current.onMessagesChange(messages)\n },\n onStreamStart: () => {\n // Stream started\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n },\n onError: (error: Error) => {\n this.setError(error)\n this.callbacksRef.current.onError(error)\n },\n onTextUpdate: (messageId: string, content: string) => {\n // Emit text update to devtools\n if (this.currentStreamId) {\n this.events.textUpdated(this.currentStreamId, messageId, content)\n }\n },\n onThinkingUpdate: (messageId: string, content: string) => {\n // Emit thinking update to devtools\n if (this.currentStreamId) {\n this.events.thinkingUpdated(\n this.currentStreamId,\n messageId,\n content,\n )\n }\n },\n onToolCallStateChange: (\n messageId: string,\n toolCallId: string,\n state: string,\n args: string,\n ) => {\n // Get the tool name from the messages\n const messages = this.processor.getMessages()\n const message = messages.find((m: UIMessage) => m.id === messageId)\n const toolCallPart = message?.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.id === toolCallId,\n )\n const toolName = toolCallPart?.name || 'unknown'\n\n // Emit tool call state change to devtools\n if (this.currentStreamId) {\n this.events.toolCallStateChanged(\n this.currentStreamId,\n messageId,\n toolCallId,\n toolName,\n state,\n args,\n )\n }\n },\n onToolCall: async (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => {\n // Handle client-side tool execution automatically\n const clientTool = this.clientToolsRef.current.get(args.toolName)\n if (clientTool?.execute) {\n try {\n const output = await clientTool.execute(args.input)\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output,\n state: 'output-available',\n })\n } catch (error: any) {\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output: null,\n state: 'output-error',\n errorText: error.message,\n })\n }\n }\n },\n onApprovalRequest: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => {\n this.events.approvalRequested(\n this.currentMessageId || '',\n args.toolCallId,\n args.toolName,\n args.input,\n args.approvalId,\n )\n },\n },\n })\n\n this.events.clientCreated(this.processor.getMessages().length)\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.current.onLoadingChange(isLoading)\n this.events.loadingChanged(isLoading)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.current.onErrorChange(error)\n this.events.errorChanged(error?.message || null)\n }\n\n /**\n * Process a stream through the StreamProcessor\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<UIMessage> {\n // Generate a stream ID for this streaming operation\n this.currentStreamId = this.generateUniqueId('stream')\n\n // Start a new assistant message\n const messageId = this.processor.startAssistantMessage()\n this.currentMessageId = messageId\n\n // Emit message appended event for the new assistant message\n const assistantMessage: UIMessage = {\n id: messageId,\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n this.events.messageAppended(assistantMessage)\n\n // Process each chunk\n for await (const chunk of source) {\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n\n // Yield control back to event loop to allow UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n\n // Finalize the stream\n this.processor.finalizeStream()\n\n // Clear the current stream and message IDs\n this.currentStreamId = null\n this.currentMessageId = null\n\n // Return the assistant message\n const messages = this.processor.getMessages()\n const finalAssistantMessage = messages.find(\n (m: UIMessage) => m.id === messageId,\n )\n\n return (\n finalAssistantMessage || {\n id: messageId,\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n )\n }\n\n /**\n * Send a message and stream the response\n */\n async sendMessage(content: string): Promise<void> {\n if (!content.trim() || this.isLoading) {\n return\n }\n\n // Add user message via processor\n const userMessage = this.processor.addUserMessage(content.trim())\n this.events.messageSent(userMessage.id, content.trim())\n\n await this.streamResponse()\n }\n\n /**\n * Append a message and stream the response\n */\n async append(message: UIMessage | ModelMessage): Promise<void> {\n // Normalize the message to ensure it has id and createdAt\n const normalizedMessage = normalizeToUIMessage(message, generateMessageId)\n\n // Skip system messages - they're handled via systemPrompts, not UIMessages\n if (normalizedMessage.role === 'system') {\n return\n }\n\n // Type assertion: after checking for system, we know it's user or assistant\n const uiMessage = normalizedMessage as UIMessage\n\n // Emit message appended event\n this.events.messageAppended(uiMessage)\n\n // Add to messages\n const messages = this.processor.getMessages()\n this.processor.setMessages([...messages, uiMessage])\n\n await this.streamResponse()\n }\n\n /**\n * Stream a response from the LLM\n */\n private async streamResponse(): Promise<void> {\n this.setIsLoading(true)\n this.setError(undefined)\n this.abortController = new AbortController()\n\n try {\n // Get model messages for the LLM\n const modelMessages = this.processor.toModelMessages()\n\n // Call onResponse callback\n await this.callbacksRef.current.onResponse()\n\n // Include conversationId in the body for server-side event correlation\n const bodyWithConversationId = {\n ...this.body,\n conversationId: this.uniqueId,\n }\n\n // Connect and stream\n const stream = this.connection.connect(\n modelMessages,\n bodyWithConversationId,\n this.abortController.signal,\n )\n\n await this.processStream(stream)\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n this.setError(err)\n this.callbacksRef.current.onError(err)\n }\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n\n // Drain any actions that were queued while the stream was in progress\n await this.drainPostStreamActions()\n }\n }\n\n /**\n * Reload the last assistant message\n */\n async reload(): Promise<void> {\n const messages = this.processor.getMessages()\n if (messages.length === 0) return\n\n // Find the last user message\n const lastUserMessageIndex = messages.findLastIndex(\n (m: UIMessage) => m.role === 'user',\n )\n\n if (lastUserMessageIndex === -1) return\n\n this.events.reloaded(lastUserMessageIndex)\n\n // Remove all messages after the last user message\n this.processor.removeMessagesAfter(lastUserMessageIndex)\n\n // Resend\n await this.streamResponse()\n }\n\n /**\n * Stop the current stream\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.events.stopped()\n }\n\n /**\n * Clear all messages\n */\n clear(): void {\n this.processor.clearMessages()\n this.setError(undefined)\n this.events.messagesCleared()\n }\n\n /**\n * Add the result of a client-side tool execution\n */\n async addToolResult(result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }): Promise<void> {\n this.events.toolResultAdded(\n result.toolCallId,\n result.tool,\n result.output,\n result.state || 'output-available',\n )\n\n // Add result via processor\n this.processor.addToolResult(\n result.toolCallId,\n result.output,\n result.errorText,\n )\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Respond to a tool approval request\n */\n async addToolApprovalResponse(response: {\n id: string // approval.id, not toolCallId\n approved: boolean\n }): Promise<void> {\n // Find the tool call ID from the approval ID\n const messages = this.processor.getMessages()\n let foundToolCallId: string | undefined\n\n for (const msg of messages) {\n const toolCallPart = msg.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.approval?.id === response.id,\n )\n if (toolCallPart) {\n foundToolCallId = toolCallPart.id\n break\n }\n }\n\n if (foundToolCallId) {\n this.events.toolApprovalResponded(\n response.id,\n foundToolCallId,\n response.approved,\n )\n }\n\n // Add response via processor\n this.processor.addToolApprovalResponse(response.id, response.approved)\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Queue an action to be executed after the current stream ends\n */\n private queuePostStreamAction(action: () => Promise<void>): void {\n this.postStreamActions.push(action)\n }\n\n /**\n * Drain and execute all queued post-stream actions\n */\n private async drainPostStreamActions(): Promise<void> {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n }\n\n /**\n * Check if we should continue the flow and do so if needed\n */\n private async checkForContinuation(): Promise<void> {\n if (this.shouldAutoSend()) {\n await this.streamResponse()\n }\n }\n\n /**\n * Check if all tool calls are complete and we should auto-send\n */\n private shouldAutoSend(): boolean {\n return this.processor.areAllToolsComplete()\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.processor.getMessages()\n }\n\n /**\n * Get loading state\n */\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n /**\n * Get current error\n */\n getError(): Error | undefined {\n return this.error\n }\n\n /**\n * Manually set messages\n */\n setMessagesManually(messages: Array<UIMessage>): void {\n this.processor.setMessages(messages)\n }\n\n /**\n * Update options refs (for use in React hooks to avoid recreating client)\n */\n updateOptions(options: {\n connection?: ConnectionAdapter\n body?: Record<string, any>\n tools?: ReadonlyArray<AnyClientTool>\n onResponse?: (response?: Response) => void | Promise<void>\n onChunk?: (chunk: StreamChunk) => void\n onFinish?: (message: UIMessage) => void\n onError?: (error: Error) => void\n }): void {\n if (options.connection !== undefined) {\n this.connection = options.connection\n }\n if (options.body !== undefined) {\n this.body = options.body\n }\n if (options.tools !== undefined) {\n this.clientToolsRef.current = new Map()\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n if (options.onResponse !== undefined) {\n this.callbacksRef.current.onResponse = options.onResponse\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.current.onChunk = options.onChunk\n }\n if (options.onFinish !== undefined) {\n this.callbacksRef.current.onFinish = options.onFinish\n }\n if (options.onError !== undefined) {\n this.callbacksRef.current.onError = options.onError\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-client.js","sources":["../../src/chat-client.ts"],"sourcesContent":["import {\n StreamProcessor,\n generateMessageId,\n normalizeToUIMessage,\n} from '@tanstack/ai'\nimport { DefaultChatClientEventEmitter } from './events'\nimport type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n ChatClientState,\n MessagePart,\n ToolCallPart,\n UIMessage,\n} from './types'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: ConnectionAdapter\n private uniqueId: string\n private body: Record<string, any> = {}\n private isLoading = false\n private error: Error | undefined = undefined\n private status: ChatClientState = 'ready'\n private abortController: AbortController | null = null\n private events: ChatClientEventEmitter\n private clientToolsRef: { current: Map<string, AnyClientTool> }\n private currentStreamId: string | null = null\n private currentMessageId: string | null = null\n private postStreamActions: Array<() => Promise<void>> = []\n // Track pending client tool executions to await them before stream finalization\n private pendingToolExecutions: Map<string, Promise<void>> = new Map()\n // Flag to deduplicate continuation checks during action draining\n private continuationPending = false\n\n private callbacksRef: {\n current: {\n onResponse: (response?: Response) => void | Promise<void>\n onChunk: (chunk: StreamChunk) => void\n onFinish: (message: UIMessage) => void\n onError: (error: Error) => void\n onMessagesChange: (messages: Array<UIMessage>) => void\n onLoadingChange: (isLoading: boolean) => void\n onErrorChange: (error: Error | undefined) => void\n onStatusChange: (status: ChatClientState) => void\n }\n }\n\n constructor(options: ChatClientOptions) {\n this.uniqueId = options.id || this.generateUniqueId('chat')\n this.body = options.body || {}\n this.connection = options.connection\n this.events = new DefaultChatClientEventEmitter(this.uniqueId)\n\n // Build client tools map\n this.clientToolsRef = { current: new Map() }\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n\n this.callbacksRef = {\n current: {\n onResponse: options.onResponse || (() => {}),\n onChunk: options.onChunk || (() => {}),\n onFinish: options.onFinish || (() => {}),\n onError: options.onError || (() => {}),\n onMessagesChange: options.onMessagesChange || (() => {}),\n onLoadingChange: options.onLoadingChange || (() => {}),\n onErrorChange: options.onErrorChange || (() => {}),\n onStatusChange: options.onStatusChange || (() => {}),\n },\n }\n\n // Create StreamProcessor with event handlers\n this.processor = new StreamProcessor({\n chunkStrategy: options.streamProcessor?.chunkStrategy,\n initialMessages: options.initialMessages,\n events: {\n onMessagesChange: (messages: Array<UIMessage>) => {\n this.callbacksRef.current.onMessagesChange(messages)\n },\n onStreamStart: () => {\n this.setStatus('streaming')\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n this.setStatus('ready')\n },\n onError: (error: Error) => {\n this.setError(error)\n this.setStatus('error')\n this.callbacksRef.current.onError(error)\n },\n onTextUpdate: (messageId: string, content: string) => {\n // Emit text update to devtools\n if (this.currentStreamId) {\n this.events.textUpdated(this.currentStreamId, messageId, content)\n }\n },\n onThinkingUpdate: (messageId: string, content: string) => {\n // Emit thinking update to devtools\n if (this.currentStreamId) {\n this.events.thinkingUpdated(\n this.currentStreamId,\n messageId,\n content,\n )\n }\n },\n onToolCallStateChange: (\n messageId: string,\n toolCallId: string,\n state: string,\n args: string,\n ) => {\n // Get the tool name from the messages\n const messages = this.processor.getMessages()\n const message = messages.find((m: UIMessage) => m.id === messageId)\n const toolCallPart = message?.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.id === toolCallId,\n )\n const toolName = toolCallPart?.name || 'unknown'\n\n // Emit tool call state change to devtools\n if (this.currentStreamId) {\n this.events.toolCallStateChanged(\n this.currentStreamId,\n messageId,\n toolCallId,\n toolName,\n state,\n args,\n )\n }\n },\n onToolCall: (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => {\n // Handle client-side tool execution automatically\n const clientTool = this.clientToolsRef.current.get(args.toolName)\n const executeFunc = clientTool?.execute\n if (executeFunc) {\n // Create and track the execution promise\n const executionPromise = (async () => {\n try {\n const output = await executeFunc(args.input)\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output,\n state: 'output-available',\n })\n } catch (error: any) {\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output: null,\n state: 'output-error',\n errorText: error.message,\n })\n } finally {\n // Remove from pending when complete\n this.pendingToolExecutions.delete(args.toolCallId)\n }\n })()\n\n // Track the pending execution\n this.pendingToolExecutions.set(args.toolCallId, executionPromise)\n }\n },\n onApprovalRequest: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => {\n if (this.currentStreamId) {\n this.events.approvalRequested(\n this.currentStreamId,\n this.currentMessageId || '',\n args.toolCallId,\n args.toolName,\n args.input,\n args.approvalId,\n )\n }\n },\n },\n })\n\n this.events.clientCreated(this.processor.getMessages().length)\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.current.onLoadingChange(isLoading)\n this.events.loadingChanged(isLoading)\n }\n\n private setStatus(status: ChatClientState): void {\n this.status = status\n this.callbacksRef.current.onStatusChange(status)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.current.onErrorChange(error)\n this.events.errorChanged(error?.message || null)\n }\n\n /**\n * Process a stream through the StreamProcessor\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<UIMessage> {\n // Generate a stream ID for this streaming operation\n this.currentStreamId = this.generateUniqueId('stream')\n\n // Start a new assistant message\n const messageId = this.processor.startAssistantMessage()\n this.currentMessageId = messageId\n\n // Emit message appended event for the new assistant message\n const assistantMessage: UIMessage = {\n id: messageId,\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n this.events.messageAppended(\n assistantMessage,\n this.currentStreamId || undefined,\n )\n\n // Process each chunk\n for await (const chunk of source) {\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n\n // Yield control back to event loop to allow UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n\n // Wait for all pending tool executions to complete before finalizing\n // This ensures client tools finish before we check for continuation\n if (this.pendingToolExecutions.size > 0) {\n await Promise.all(this.pendingToolExecutions.values())\n }\n\n // Finalize the stream\n this.processor.finalizeStream()\n\n // Clear the current stream and message IDs\n this.currentStreamId = null\n this.currentMessageId = null\n\n // Return the assistant message\n const messages = this.processor.getMessages()\n const finalAssistantMessage = messages.find(\n (m: UIMessage) => m.id === messageId,\n )\n\n return (\n finalAssistantMessage || {\n id: messageId,\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n )\n }\n\n /**\n * Send a message and stream the response\n */\n async sendMessage(content: string): Promise<void> {\n if (!content.trim() || this.isLoading) {\n return\n }\n\n // Add user message via processor\n const userMessage = this.processor.addUserMessage(content.trim())\n this.events.messageSent(userMessage.id, content.trim())\n\n await this.streamResponse()\n }\n\n /**\n * Append a message and stream the response\n */\n async append(message: UIMessage | ModelMessage): Promise<void> {\n // Normalize the message to ensure it has id and createdAt\n const normalizedMessage = normalizeToUIMessage(message, generateMessageId)\n\n // Skip system messages - they're handled via systemPrompts, not UIMessages\n if (normalizedMessage.role === 'system') {\n return\n }\n\n // Type assertion: after checking for system, we know it's user or assistant\n const uiMessage = normalizedMessage as UIMessage\n\n // Emit message appended event\n this.events.messageAppended(uiMessage)\n\n // Add to messages\n const messages = this.processor.getMessages()\n this.processor.setMessages([...messages, uiMessage])\n\n // If stream is in progress, queue the response for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.streamResponse())\n return\n }\n\n await this.streamResponse()\n }\n\n /**\n * Stream a response from the LLM\n */\n private async streamResponse(): Promise<void> {\n // Guard against concurrent streams - if already loading, skip\n if (this.isLoading) {\n return\n }\n\n this.setIsLoading(true)\n this.setStatus('submitted')\n this.setError(undefined)\n this.abortController = new AbortController()\n // Reset pending tool executions for the new stream\n this.pendingToolExecutions.clear()\n let streamCompletedSuccessfully = false\n\n try {\n // Get UIMessages with parts (preserves approval state and client tool results)\n const messages = this.processor.getMessages()\n\n // Call onResponse callback\n await this.callbacksRef.current.onResponse()\n\n // Include conversationId in the body for server-side event correlation\n const bodyWithConversationId = {\n ...this.body,\n conversationId: this.uniqueId,\n }\n\n // Connect and stream\n const stream = this.connection.connect(\n messages,\n bodyWithConversationId,\n this.abortController.signal,\n )\n\n await this.processStream(stream)\n streamCompletedSuccessfully = true\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n this.setError(err)\n this.setStatus('error')\n this.callbacksRef.current.onError(err)\n }\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n\n // Drain any actions that were queued while the stream was in progress\n await this.drainPostStreamActions()\n\n // Continue conversation if the stream ended with a tool result (server tool completed)\n if (streamCompletedSuccessfully) {\n const messages = this.processor.getMessages()\n const lastPart = messages.at(-1)?.parts.at(-1)\n\n if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {\n try {\n await this.checkForContinuation()\n } catch (error) {\n console.error('Failed to continue flow after tool result:', error)\n }\n }\n }\n }\n }\n\n /**\n * Reload the last assistant message\n */\n async reload(): Promise<void> {\n const messages = this.processor.getMessages()\n if (messages.length === 0) return\n\n // Find the last user message\n const lastUserMessageIndex = messages.findLastIndex(\n (m: UIMessage) => m.role === 'user',\n )\n\n if (lastUserMessageIndex === -1) return\n\n this.events.reloaded(lastUserMessageIndex)\n\n // Remove all messages after the last user message\n this.processor.removeMessagesAfter(lastUserMessageIndex)\n\n // Resend\n await this.streamResponse()\n }\n\n /**\n * Stop the current stream\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.setStatus('ready')\n this.events.stopped()\n }\n\n /**\n * Clear all messages\n */\n clear(): void {\n this.processor.clearMessages()\n this.setError(undefined)\n this.events.messagesCleared()\n }\n\n /**\n * Add the result of a client-side tool execution\n */\n async addToolResult(result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }): Promise<void> {\n this.events.toolResultAdded(\n result.toolCallId,\n result.tool,\n result.output,\n result.state || 'output-available',\n )\n\n // Add result via processor\n this.processor.addToolResult(\n result.toolCallId,\n result.output,\n result.errorText,\n )\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Respond to a tool approval request\n */\n async addToolApprovalResponse(response: {\n id: string // approval.id, not toolCallId\n approved: boolean\n }): Promise<void> {\n // Find the tool call ID from the approval ID\n const messages = this.processor.getMessages()\n let foundToolCallId: string | undefined\n\n for (const msg of messages) {\n const toolCallPart = msg.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.approval?.id === response.id,\n )\n if (toolCallPart) {\n foundToolCallId = toolCallPart.id\n break\n }\n }\n\n if (foundToolCallId) {\n this.events.toolApprovalResponded(\n response.id,\n foundToolCallId,\n response.approved,\n )\n }\n\n // Add response via processor\n this.processor.addToolApprovalResponse(response.id, response.approved)\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Queue an action to be executed after the current stream ends\n */\n private queuePostStreamAction(action: () => Promise<void>): void {\n this.postStreamActions.push(action)\n }\n\n /**\n * Drain and execute all queued post-stream actions\n */\n private async drainPostStreamActions(): Promise<void> {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n }\n\n /**\n * Check if we should continue the flow and do so if needed\n */\n private async checkForContinuation(): Promise<void> {\n // Prevent duplicate continuation attempts\n if (this.continuationPending || this.isLoading) {\n return\n }\n\n if (this.shouldAutoSend()) {\n this.continuationPending = true\n try {\n await this.streamResponse()\n } finally {\n this.continuationPending = false\n }\n }\n }\n\n /**\n * Check if all tool calls are complete and we should auto-send\n */\n private shouldAutoSend(): boolean {\n return this.processor.areAllToolsComplete()\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.processor.getMessages()\n }\n\n /**\n * Get loading state\n */\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n /**\n * Get current status\n */\n getStatus(): ChatClientState {\n return this.status\n }\n\n /**\n * Get current error\n */\n getError(): Error | undefined {\n return this.error\n }\n\n /**\n * Manually set messages\n */\n setMessagesManually(messages: Array<UIMessage>): void {\n this.processor.setMessages(messages)\n }\n\n /**\n * Update options refs (for use in React hooks to avoid recreating client)\n */\n updateOptions(options: {\n connection?: ConnectionAdapter\n body?: Record<string, any>\n tools?: ReadonlyArray<AnyClientTool>\n onResponse?: (response?: Response) => void | Promise<void>\n onChunk?: (chunk: StreamChunk) => void\n onFinish?: (message: UIMessage) => void\n onError?: (error: Error) => void\n }): void {\n if (options.connection !== undefined) {\n this.connection = options.connection\n }\n if (options.body !== undefined) {\n this.body = options.body\n }\n if (options.tools !== undefined) {\n this.clientToolsRef.current = new Map()\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n if (options.onResponse !== undefined) {\n this.callbacksRef.current.onResponse = options.onResponse\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.current.onChunk = options.onChunk\n }\n if (options.onFinish !== undefined) {\n this.callbacksRef.current.onFinish = options.onFinish\n }\n if (options.onError !== undefined) {\n this.callbacksRef.current.onError = options.onError\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -105,7 +105,7 @@ export declare function fetchHttpStream(url: string | (() => string), options?:
|
|
|
105
105
|
* const client = new ChatClient({ connection });
|
|
106
106
|
* ```
|
|
107
107
|
*/
|
|
108
|
-
export declare function stream(streamFactory: (messages: Array<ModelMessage>, data?: Record<string, any>) => AsyncIterable<StreamChunk>): ConnectionAdapter;
|
|
108
|
+
export declare function stream(streamFactory: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>) => AsyncIterable<StreamChunk>): ConnectionAdapter;
|
|
109
109
|
/**
|
|
110
110
|
* Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)
|
|
111
111
|
*
|
|
@@ -122,4 +122,4 @@ export declare function stream(streamFactory: (messages: Array<ModelMessage>, da
|
|
|
122
122
|
* const client = new ChatClient({ connection });
|
|
123
123
|
* ```
|
|
124
124
|
*/
|
|
125
|
-
export declare function rpcStream(rpcCall: (messages: Array<ModelMessage>, data?: Record<string, any>) => AsyncIterable<StreamChunk>): ConnectionAdapter;
|
|
125
|
+
export declare function rpcStream(rpcCall: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>) => AsyncIterable<StreamChunk>): ConnectionAdapter;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { convertMessagesToModelMessages } from "@tanstack/ai";
|
|
2
1
|
function mergeHeaders(customHeaders) {
|
|
3
2
|
if (!customHeaders) {
|
|
4
3
|
return {};
|
|
@@ -43,13 +42,12 @@ function fetchServerSentEvents(url, options = {}) {
|
|
|
43
42
|
async *connect(messages, data, abortSignal) {
|
|
44
43
|
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
45
44
|
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
46
|
-
const modelMessages = convertMessagesToModelMessages(messages);
|
|
47
45
|
const requestHeaders = {
|
|
48
46
|
"Content-Type": "application/json",
|
|
49
47
|
...mergeHeaders(resolvedOptions.headers)
|
|
50
48
|
};
|
|
51
49
|
const requestBody = {
|
|
52
|
-
messages
|
|
50
|
+
messages,
|
|
53
51
|
data,
|
|
54
52
|
...resolvedOptions.body
|
|
55
53
|
};
|
|
@@ -88,13 +86,12 @@ function fetchHttpStream(url, options = {}) {
|
|
|
88
86
|
async *connect(messages, data, abortSignal) {
|
|
89
87
|
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
90
88
|
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
91
|
-
const modelMessages = convertMessagesToModelMessages(messages);
|
|
92
89
|
const requestHeaders = {
|
|
93
90
|
"Content-Type": "application/json",
|
|
94
91
|
...mergeHeaders(resolvedOptions.headers)
|
|
95
92
|
};
|
|
96
93
|
const requestBody = {
|
|
97
|
-
messages
|
|
94
|
+
messages,
|
|
98
95
|
data,
|
|
99
96
|
...resolvedOptions.body
|
|
100
97
|
};
|
|
@@ -129,16 +126,14 @@ function fetchHttpStream(url, options = {}) {
|
|
|
129
126
|
function stream(streamFactory) {
|
|
130
127
|
return {
|
|
131
128
|
async *connect(messages, data) {
|
|
132
|
-
|
|
133
|
-
yield* streamFactory(modelMessages, data);
|
|
129
|
+
yield* streamFactory(messages, data);
|
|
134
130
|
}
|
|
135
131
|
};
|
|
136
132
|
}
|
|
137
133
|
function rpcStream(rpcCall) {
|
|
138
134
|
return {
|
|
139
135
|
async *connect(messages, data) {
|
|
140
|
-
|
|
141
|
-
yield* rpcCall(modelMessages, data);
|
|
136
|
+
yield* rpcCall(messages, data);
|
|
142
137
|
}
|
|
143
138
|
};
|
|
144
139
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-adapters.js","sources":["../../src/connection-adapters.ts"],"sourcesContent":["import { convertMessagesToModelMessages } from '@tanstack/ai'\nimport type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = new TextDecoder()\n let buffer = ''\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n // Check if aborted before reading\n if (abortSignal?.aborted) {\n break\n }\n\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n // Process any remaining data in the buffer\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Connection adapter interface - converts a connection into a stream of chunks\n */\nexport interface ConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks\n * @param messages - The messages to send (UIMessages or ModelMessages)\n * @param data - Additional data to send\n * @param abortSignal - Optional abort signal for request cancellation\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const modelMessages = convertMessagesToModelMessages(messages)\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Merge body from options with messages and data\n const requestBody = {\n messages: modelMessages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse Server-Sent Events format\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n // Handle Server-Sent Events format\n const data = line.startsWith('data: ') ? line.slice(6) : line\n\n if (data === '[DONE]') continue\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n // Skip non-JSON lines or malformed chunks\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n // Convert UIMessages to ModelMessages if needed\n const modelMessages = convertMessagesToModelMessages(messages)\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Merge body from options with messages and data\n const requestBody = {\n messages: modelMessages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse raw HTTP stream (newline-delimited JSON)\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n try {\n const parsed: StreamChunk = JSON.parse(line)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse HTTP stream chunk:', line)\n }\n }\n },\n }\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectionAdapter {\n return {\n async *connect(messages, data) {\n const modelMessages = convertMessagesToModelMessages(messages)\n yield* streamFactory(modelMessages, data)\n },\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectionAdapter {\n return {\n async *connect(messages, data) {\n const modelMessages = convertMessagesToModelMessages(messages)\n // Simply yield from the RPC call\n // The RPC layer handles WebSocket transport\n yield* rpcCall(modelMessages, data)\n },\n }\n}\n"],"names":["data"],"mappings":";AAMA,SAAS,aACP,eACwB;AACxB,MAAI,CAAC,eAAe;AAClB,WAAO,CAAA;AAAA,EACT;AACA,MAAI,yBAAyB,SAAS;AACpC,UAAM,SAAiC,CAAA;AACvC,kBAAc,QAAQ,CAAC,OAAO,QAAQ;AACpC,aAAO,GAAG,IAAI;AAAA,IAChB,CAAC;AACD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKA,gBAAgB,gBACd,QACA,aACwB;AACxB,MAAI;AACF,UAAM,UAAU,IAAI,YAAA;AACpB,QAAI,SAAS;AAGb,WAAO,MAAM;AAEX,UAAI,aAAa,SAAS;AACxB;AAAA,MACF;AAEA,YAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,UAAI,KAAM;AAEV,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM;AAChD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAG/B,eAAS,MAAM,SAAS;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,QAAQ;AACf,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,QAAI,OAAO,QAAQ;AACjB,YAAM;AAAA,IACR;AAAA,EACF,UAAA;AACE,WAAO,YAAA;AAAA,EACT;AACF;AAgEO,SAAS,sBACd,KACA,UAEuE,IACpD;AACnB,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,aAAa;AAE1C,YAAM,cAAc,OAAO,QAAQ,aAAa,QAAQ;AACxD,YAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,YAAY;AAEpD,YAAM,gBAAgB,+BAA+B,QAAQ;AAE7D,YAAM,iBAAyC;AAAA,QAC7C,gBAAgB;AAAA,QAChB,GAAG,aAAa,gBAAgB,OAAO;AAAA,MAAA;AAIzC,YAAM,cAAc;AAAA,QAClB,UAAU;AAAA,QACV;AAAA,QACA,GAAG,gBAAgB;AAAA,MAAA;AAGrB,YAAM,cAAc,gBAAgB,eAAe;AACnD,YAAM,WAAW,MAAM,YAAY,aAAa;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAM,KAAK,UAAU,WAAW;AAAA,QAChC,aAAa,gBAAgB,eAAe;AAAA,QAC5C,QAAQ,eAAe,gBAAgB;AAAA,MAAA,CACxC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI;AAAA,UACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QAAA;AAAA,MAEjE;AAGA,YAAM,SAAS,SAAS,MAAM,UAAA;AAC9B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAEA,uBAAiB,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;AAE7D,cAAMA,QAAO,KAAK,WAAW,QAAQ,IAAI,KAAK,MAAM,CAAC,IAAI;AAEzD,YAAIA,UAAS,SAAU;AAEvB,YAAI;AACF,gBAAM,SAAsB,KAAK,MAAMA,KAAI;AAC3C,gBAAM;AAAA,QACR,SAAS,YAAY;AAEnB,kBAAQ,KAAK,8BAA8BA,KAAI;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAoCO,SAAS,gBACd,KACA,UAEuE,IACpD;AACnB,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,aAAa;AAE1C,YAAM,cAAc,OAAO,QAAQ,aAAa,QAAQ;AACxD,YAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,YAAY;AAGpD,YAAM,gBAAgB,+BAA+B,QAAQ;AAE7D,YAAM,iBAAyC;AAAA,QAC7C,gBAAgB;AAAA,QAChB,GAAG,aAAa,gBAAgB,OAAO;AAAA,MAAA;AAIzC,YAAM,cAAc;AAAA,QAClB,UAAU;AAAA,QACV;AAAA,QACA,GAAG,gBAAgB;AAAA,MAAA;AAGrB,YAAM,cAAc,gBAAgB,eAAe;AACnD,YAAM,WAAW,MAAM,YAAY,aAAa;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAM,KAAK,UAAU,WAAW;AAAA,QAChC,aAAa,gBAAgB,eAAe;AAAA,QAC5C,QAAQ,eAAe,gBAAgB;AAAA,MAAA,CACxC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI;AAAA,UACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QAAA;AAAA,MAEjE;AAGA,YAAM,SAAS,SAAS,MAAM,UAAA;AAC9B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAEA,uBAAiB,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;AAC7D,YAAI;AACF,gBAAM,SAAsB,KAAK,MAAM,IAAI;AAC3C,gBAAM;AAAA,QACR,SAAS,YAAY;AACnB,kBAAQ,KAAK,sCAAsC,IAAI;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAgBO,SAAS,OACd,eAImB;AACnB,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM;AAC7B,YAAM,gBAAgB,+BAA+B,QAAQ;AAC7D,aAAO,cAAc,eAAe,IAAI;AAAA,IAC1C;AAAA,EAAA;AAEJ;AAkBO,SAAS,UACd,SAImB;AACnB,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM;AAC7B,YAAM,gBAAgB,+BAA+B,QAAQ;AAG7D,aAAO,QAAQ,eAAe,IAAI;AAAA,IACpC;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"connection-adapters.js","sources":["../../src/connection-adapters.ts"],"sourcesContent":["import type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = new TextDecoder()\n let buffer = ''\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n // Check if aborted before reading\n if (abortSignal?.aborted) {\n break\n }\n\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n // Process any remaining data in the buffer\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Connection adapter interface - converts a connection into a stream of chunks\n */\nexport interface ConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks\n * @param messages - The messages to send (UIMessages or ModelMessages)\n * @param data - Additional data to send\n * @param abortSignal - Optional abort signal for request cancellation\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Send messages as-is (UIMessages with parts preserved)\n // Server-side TextEngine handles conversion to ModelMessages\n const requestBody = {\n messages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse Server-Sent Events format\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n // Handle Server-Sent Events format\n const data = line.startsWith('data: ') ? line.slice(6) : line\n\n if (data === '[DONE]') continue\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n // Skip non-JSON lines or malformed chunks\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Send messages as-is (UIMessages with parts preserved)\n // Server-side TextEngine handles conversion to ModelMessages\n const requestBody = {\n messages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse raw HTTP stream (newline-delimited JSON)\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n try {\n const parsed: StreamChunk = JSON.parse(line)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse HTTP stream chunk:', line)\n }\n }\n },\n }\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectionAdapter {\n return {\n async *connect(messages, data) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* streamFactory(messages, data)\n },\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectionAdapter {\n return {\n async *connect(messages, data) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* rpcCall(messages, data)\n },\n }\n}\n"],"names":["data"],"mappings":"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"}
|
package/dist/esm/events.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare abstract class ChatClientEventEmitter {
|
|
|
9
9
|
* Protected abstract method for emitting events
|
|
10
10
|
* Implementations should handle adding clientId and timestamp
|
|
11
11
|
*/
|
|
12
|
-
protected abstract emitEvent(eventName: string, data?: Record<string,
|
|
12
|
+
protected abstract emitEvent(eventName: string, data?: Record<string, unknown>): void;
|
|
13
13
|
/**
|
|
14
14
|
* Emit client created event
|
|
15
15
|
*/
|
|
@@ -33,7 +33,6 @@ export declare abstract class ChatClientEventEmitter {
|
|
|
33
33
|
/**
|
|
34
34
|
* Emit tool result state change event
|
|
35
35
|
*/
|
|
36
|
-
toolResultStateChanged(streamId: string, toolCallId: string, content: string, state: string, error?: string): void;
|
|
37
36
|
/**
|
|
38
37
|
* Emit thinking update event
|
|
39
38
|
*/
|
|
@@ -41,11 +40,11 @@ export declare abstract class ChatClientEventEmitter {
|
|
|
41
40
|
/**
|
|
42
41
|
* Emit approval requested event
|
|
43
42
|
*/
|
|
44
|
-
approvalRequested(messageId: string, toolCallId: string, toolName: string, input:
|
|
43
|
+
approvalRequested(streamId: string, messageId: string, toolCallId: string, toolName: string, input: unknown, approvalId: string): void;
|
|
45
44
|
/**
|
|
46
45
|
* Emit message appended event
|
|
47
46
|
*/
|
|
48
|
-
messageAppended(uiMessage: UIMessage): void;
|
|
47
|
+
messageAppended(uiMessage: UIMessage, streamId?: string): void;
|
|
49
48
|
/**
|
|
50
49
|
* Emit message sent event
|
|
51
50
|
*/
|
|
@@ -65,7 +64,7 @@ export declare abstract class ChatClientEventEmitter {
|
|
|
65
64
|
/**
|
|
66
65
|
* Emit tool result added event
|
|
67
66
|
*/
|
|
68
|
-
toolResultAdded(toolCallId: string, toolName: string, output:
|
|
67
|
+
toolResultAdded(toolCallId: string, toolName: string, output: unknown, state: string): void;
|
|
69
68
|
/**
|
|
70
69
|
* Emit tool approval responded event
|
|
71
70
|
*/
|