@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/dist/esm/events.js
CHANGED
|
@@ -15,13 +15,13 @@ class ChatClientEventEmitter {
|
|
|
15
15
|
* Emit loading state changed event
|
|
16
16
|
*/
|
|
17
17
|
loadingChanged(isLoading) {
|
|
18
|
-
this.emitEvent("client:loading
|
|
18
|
+
this.emitEvent("client:loading:changed", { isLoading });
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Emit error state changed event
|
|
22
22
|
*/
|
|
23
23
|
errorChanged(error) {
|
|
24
|
-
this.emitEvent("client:error
|
|
24
|
+
this.emitEvent("client:error:changed", {
|
|
25
25
|
error
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -29,11 +29,8 @@ class ChatClientEventEmitter {
|
|
|
29
29
|
* Emit text update events (combines processor and client events)
|
|
30
30
|
*/
|
|
31
31
|
textUpdated(streamId, messageId, content) {
|
|
32
|
-
this.emitEvent("
|
|
32
|
+
this.emitEvent("text:chunk:content", {
|
|
33
33
|
streamId,
|
|
34
|
-
content
|
|
35
|
-
});
|
|
36
|
-
this.emitEvent("client:assistant-message-updated", {
|
|
37
34
|
messageId,
|
|
38
35
|
content
|
|
39
36
|
});
|
|
@@ -42,14 +39,8 @@ class ChatClientEventEmitter {
|
|
|
42
39
|
* Emit tool call state change events (combines processor and client events)
|
|
43
40
|
*/
|
|
44
41
|
toolCallStateChanged(streamId, messageId, toolCallId, toolName, state, args) {
|
|
45
|
-
this.emitEvent("
|
|
42
|
+
this.emitEvent("tools:call:updated", {
|
|
46
43
|
streamId,
|
|
47
|
-
toolCallId,
|
|
48
|
-
toolName,
|
|
49
|
-
state,
|
|
50
|
-
arguments: args
|
|
51
|
-
});
|
|
52
|
-
this.emitEvent("client:tool-call-updated", {
|
|
53
44
|
messageId,
|
|
54
45
|
toolCallId,
|
|
55
46
|
toolName,
|
|
@@ -60,20 +51,11 @@ class ChatClientEventEmitter {
|
|
|
60
51
|
/**
|
|
61
52
|
* Emit tool result state change event
|
|
62
53
|
*/
|
|
63
|
-
toolResultStateChanged(streamId, toolCallId, content, state, error) {
|
|
64
|
-
this.emitEvent("processor:tool-result-state-changed", {
|
|
65
|
-
streamId,
|
|
66
|
-
toolCallId,
|
|
67
|
-
content,
|
|
68
|
-
state,
|
|
69
|
-
error
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
54
|
/**
|
|
73
55
|
* Emit thinking update event
|
|
74
56
|
*/
|
|
75
57
|
thinkingUpdated(streamId, messageId, content, delta) {
|
|
76
|
-
this.emitEvent("
|
|
58
|
+
this.emitEvent("text:chunk:thinking", {
|
|
77
59
|
streamId,
|
|
78
60
|
messageId,
|
|
79
61
|
content,
|
|
@@ -83,8 +65,9 @@ class ChatClientEventEmitter {
|
|
|
83
65
|
/**
|
|
84
66
|
* Emit approval requested event
|
|
85
67
|
*/
|
|
86
|
-
approvalRequested(messageId, toolCallId, toolName, input, approvalId) {
|
|
87
|
-
this.emitEvent("
|
|
68
|
+
approvalRequested(streamId, messageId, toolCallId, toolName, input, approvalId) {
|
|
69
|
+
this.emitEvent("tools:approval:requested", {
|
|
70
|
+
streamId,
|
|
88
71
|
messageId,
|
|
89
72
|
toolCallId,
|
|
90
73
|
toolName,
|
|
@@ -95,20 +78,28 @@ class ChatClientEventEmitter {
|
|
|
95
78
|
/**
|
|
96
79
|
* Emit message appended event
|
|
97
80
|
*/
|
|
98
|
-
messageAppended(uiMessage) {
|
|
99
|
-
const
|
|
100
|
-
this.emitEvent("
|
|
81
|
+
messageAppended(uiMessage, streamId) {
|
|
82
|
+
const content = uiMessage.parts.filter((part) => part.type === "text").map((part) => part.content).join(" ");
|
|
83
|
+
this.emitEvent("text:message:created", {
|
|
84
|
+
streamId,
|
|
101
85
|
messageId: uiMessage.id,
|
|
102
86
|
role: uiMessage.role,
|
|
103
|
-
|
|
87
|
+
content,
|
|
88
|
+
parts: uiMessage.parts
|
|
104
89
|
});
|
|
105
90
|
}
|
|
106
91
|
/**
|
|
107
92
|
* Emit message sent event
|
|
108
93
|
*/
|
|
109
94
|
messageSent(messageId, content) {
|
|
110
|
-
this.emitEvent("
|
|
95
|
+
this.emitEvent("text:message:created", {
|
|
96
|
+
messageId,
|
|
97
|
+
role: "user",
|
|
98
|
+
content
|
|
99
|
+
});
|
|
100
|
+
this.emitEvent("text:message:user", {
|
|
111
101
|
messageId,
|
|
102
|
+
role: "user",
|
|
112
103
|
content
|
|
113
104
|
});
|
|
114
105
|
}
|
|
@@ -130,13 +121,13 @@ class ChatClientEventEmitter {
|
|
|
130
121
|
* Emit messages cleared event
|
|
131
122
|
*/
|
|
132
123
|
messagesCleared() {
|
|
133
|
-
this.emitEvent("client:messages
|
|
124
|
+
this.emitEvent("client:messages:cleared");
|
|
134
125
|
}
|
|
135
126
|
/**
|
|
136
127
|
* Emit tool result added event
|
|
137
128
|
*/
|
|
138
129
|
toolResultAdded(toolCallId, toolName, output, state) {
|
|
139
|
-
this.emitEvent("
|
|
130
|
+
this.emitEvent("tools:result:added", {
|
|
140
131
|
toolCallId,
|
|
141
132
|
toolName,
|
|
142
133
|
output,
|
|
@@ -147,7 +138,7 @@ class ChatClientEventEmitter {
|
|
|
147
138
|
* Emit tool approval responded event
|
|
148
139
|
*/
|
|
149
140
|
toolApprovalResponded(approvalId, toolCallId, approved) {
|
|
150
|
-
this.emitEvent("
|
|
141
|
+
this.emitEvent("tools:approval:responded", {
|
|
151
142
|
approvalId,
|
|
152
143
|
toolCallId,
|
|
153
144
|
approved
|
|
@@ -159,10 +150,11 @@ class DefaultChatClientEventEmitter extends ChatClientEventEmitter {
|
|
|
159
150
|
* Emit an event with automatic clientId and timestamp for client/tool events
|
|
160
151
|
*/
|
|
161
152
|
emitEvent(eventName, data) {
|
|
162
|
-
if (eventName.startsWith("client:") || eventName.startsWith("
|
|
153
|
+
if (eventName.startsWith("client:") || eventName.startsWith("tools:") || eventName.startsWith("text:")) {
|
|
163
154
|
aiEventClient.emit(eventName, {
|
|
164
155
|
...data,
|
|
165
156
|
clientId: this.clientId,
|
|
157
|
+
source: "client",
|
|
166
158
|
timestamp: Date.now()
|
|
167
159
|
});
|
|
168
160
|
} else {
|
package/dist/esm/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sources":["../../src/events.ts"],"sourcesContent":["import { aiEventClient } from '@tanstack/ai/event-client'\nimport type { UIMessage } from './types'\n\n/**\n * Abstract base class for ChatClient event emission\n */\nexport abstract class ChatClientEventEmitter {\n protected clientId: string\n\n constructor(clientId: string) {\n this.clientId = clientId\n }\n\n /**\n * Protected abstract method for emitting events\n * Implementations should handle adding clientId and timestamp\n */\n protected abstract emitEvent(\n eventName: string,\n data?: Record<string,
|
|
1
|
+
{"version":3,"file":"events.js","sources":["../../src/events.ts"],"sourcesContent":["import { aiEventClient } from '@tanstack/ai/event-client'\nimport type { UIMessage } from './types'\n\n/**\n * Abstract base class for ChatClient event emission\n */\nexport abstract class ChatClientEventEmitter {\n protected clientId: string\n\n constructor(clientId: string) {\n this.clientId = clientId\n }\n\n /**\n * Protected abstract method for emitting events\n * Implementations should handle adding clientId and timestamp\n */\n protected abstract emitEvent(\n eventName: string,\n data?: Record<string, unknown>,\n ): void\n\n /**\n * Emit client created event\n */\n clientCreated(initialMessageCount: number): void {\n this.emitEvent('client:created', {\n initialMessageCount,\n })\n }\n\n /**\n * Emit loading state changed event\n */\n loadingChanged(isLoading: boolean): void {\n this.emitEvent('client:loading:changed', { isLoading })\n }\n\n /**\n * Emit error state changed event\n */\n errorChanged(error: string | null): void {\n this.emitEvent('client:error:changed', {\n error,\n })\n }\n\n /**\n * Emit text update events (combines processor and client events)\n */\n textUpdated(streamId: string, messageId: string, content: string): void {\n this.emitEvent('text:chunk:content', {\n streamId,\n messageId,\n content,\n })\n }\n\n /**\n * Emit tool call state change events (combines processor and client events)\n */\n toolCallStateChanged(\n streamId: string,\n messageId: string,\n toolCallId: string,\n toolName: string,\n state: string,\n args: string,\n ): void {\n this.emitEvent('tools:call:updated', {\n streamId,\n messageId,\n toolCallId,\n toolName,\n state,\n arguments: args,\n })\n }\n\n /**\n * Emit tool result state change event\n */\n /**\n * Emit thinking update event\n */\n thinkingUpdated(\n streamId: string,\n messageId: string,\n content: string,\n delta?: string,\n ): void {\n this.emitEvent('text:chunk:thinking', {\n streamId,\n messageId,\n content,\n delta,\n })\n }\n\n /**\n * Emit approval requested event\n */\n approvalRequested(\n streamId: string,\n messageId: string,\n toolCallId: string,\n toolName: string,\n input: unknown,\n approvalId: string,\n ): void {\n this.emitEvent('tools:approval:requested', {\n streamId,\n messageId,\n toolCallId,\n toolName,\n input,\n approvalId,\n })\n }\n\n /**\n * Emit message appended event\n */\n messageAppended(uiMessage: UIMessage, streamId?: string): void {\n const content = uiMessage.parts\n .filter((part) => part.type === 'text')\n .map((part) => part.content)\n .join(' ')\n\n this.emitEvent('text:message:created', {\n streamId,\n messageId: uiMessage.id,\n role: uiMessage.role,\n content,\n parts: uiMessage.parts,\n })\n }\n\n /**\n * Emit message sent event\n */\n messageSent(messageId: string, content: string): void {\n this.emitEvent('text:message:created', {\n messageId,\n role: 'user',\n content,\n })\n\n this.emitEvent('text:message:user', {\n messageId,\n role: 'user',\n content,\n })\n }\n\n /**\n * Emit reloaded event\n */\n reloaded(fromMessageIndex: number): void {\n this.emitEvent('client:reloaded', {\n fromMessageIndex,\n })\n }\n\n /**\n * Emit stopped event\n */\n stopped(): void {\n this.emitEvent('client:stopped')\n }\n\n /**\n * Emit messages cleared event\n */\n messagesCleared(): void {\n this.emitEvent('client:messages:cleared')\n }\n\n /**\n * Emit tool result added event\n */\n toolResultAdded(\n toolCallId: string,\n toolName: string,\n output: unknown,\n state: string,\n ): void {\n this.emitEvent('tools:result:added', {\n toolCallId,\n toolName,\n output,\n state,\n })\n }\n\n /**\n * Emit tool approval responded event\n */\n toolApprovalResponded(\n approvalId: string,\n toolCallId: string,\n approved: boolean,\n ): void {\n this.emitEvent('tools:approval:responded', {\n approvalId,\n toolCallId,\n approved,\n })\n }\n}\n\n/**\n * Default implementation of ChatClientEventEmitter\n */\nexport class DefaultChatClientEventEmitter extends ChatClientEventEmitter {\n /**\n * Emit an event with automatic clientId and timestamp for client/tool events\n */\n protected emitEvent(eventName: string, data?: Record<string, any>): void {\n // For client:* and tool:* events, automatically add clientId and timestamp\n if (\n eventName.startsWith('client:') ||\n eventName.startsWith('tools:') ||\n eventName.startsWith('text:')\n ) {\n aiEventClient.emit(eventName as any, {\n ...data,\n clientId: this.clientId,\n source: 'client',\n timestamp: Date.now(),\n })\n } else {\n // For other events, just add timestamp\n aiEventClient.emit(eventName as any, {\n ...data,\n timestamp: Date.now(),\n })\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { ChatClient } from './chat-client.js';
|
|
2
|
-
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, ChatClientOptions, ChatRequestBody, InferChatMessages, } from './types.js';
|
|
2
|
+
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, ChatClientOptions, ChatRequestBody, InferChatMessages, ChatClientState, } from './types.js';
|
|
3
3
|
export { clientTools, createChatClientOptions } from './types.js';
|
|
4
4
|
export type { ExtractToolNames, ExtractToolInput, ExtractToolOutput, } from './tool-types.js';
|
|
5
5
|
export type { AnyClientTool } from '@tanstack/ai';
|
|
6
6
|
export { fetchServerSentEvents, fetchHttpStream, stream, rpcStream, type ConnectionAdapter, type FetchConnectionOptions, } from './connection-adapters.js';
|
|
7
7
|
export { uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, convertMessagesToModelMessages, normalizeToUIMessage, generateMessageId, } from '@tanstack/ai';
|
|
8
|
-
export { StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, type ChunkStrategy, type StreamProcessorOptions, type
|
|
8
|
+
export { StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, type ChunkStrategy, type StreamProcessorOptions, type StreamProcessorEvents, type InternalToolCallState, type ToolCallState, type ToolResultState, type JSONParser, type ChunkRecording, type ProcessorResult, type ProcessorState, } from '@tanstack/ai';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export type ToolCallState = 'awaiting-input' | 'input-streaming' | 'input-comple
|
|
|
8
8
|
* Tool result states - track the lifecycle of a tool result
|
|
9
9
|
*/
|
|
10
10
|
export type ToolResultState = 'streaming' | 'complete' | 'error';
|
|
11
|
+
/**
|
|
12
|
+
* ChatClient state - track the lifecycle of a chat
|
|
13
|
+
*/
|
|
14
|
+
export type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error';
|
|
11
15
|
/**
|
|
12
16
|
* Message parts - building blocks of UIMessage
|
|
13
17
|
*/
|
|
@@ -138,6 +142,10 @@ export interface ChatClientOptions<TTools extends ReadonlyArray<AnyClientTool> =
|
|
|
138
142
|
* Callback when error state changes
|
|
139
143
|
*/
|
|
140
144
|
onErrorChange?: (error: Error | undefined) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Callback when chat status changes
|
|
147
|
+
*/
|
|
148
|
+
onStatusChange?: (status: ChatClientState) => void;
|
|
141
149
|
/**
|
|
142
150
|
* Client-side tools with execution logic
|
|
143
151
|
* When provided, tools with execute functions will be called automatically
|
package/dist/esm/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n ChunkStrategy,\n InferToolInput,\n InferToolOutput,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Approval metadata if tool requires user approval */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n }\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n | TextPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results\n */\nexport interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools>>\n createdAt?: Date\n}\n\nexport interface ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n /**\n * Connection adapter for streaming\n * Use fetchServerSentEvents(), fetchHttpStream(), or stream() to create adapters\n */\n connection: ConnectionAdapter\n\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Unique identifier for this chat instance\n * Used for managing multiple chats\n */\n id?: string\n\n /**\n * Additional body parameters to send\n */\n body?: Record<string, any>\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n>(options: ChatClientOptions<TTools>): ChatClientOptions<TTools> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools> ? Array<UIMessage<TTools>> : never\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n ChunkStrategy,\n InferToolInput,\n InferToolOutput,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Approval metadata if tool requires user approval */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n }\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n | TextPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results\n */\nexport interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools>>\n createdAt?: Date\n}\n\nexport interface ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n /**\n * Connection adapter for streaming\n * Use fetchServerSentEvents(), fetchHttpStream(), or stream() to create adapters\n */\n connection: ConnectionAdapter\n\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Unique identifier for this chat instance\n * Used for managing multiple chats\n */\n id?: string\n\n /**\n * Additional body parameters to send\n */\n body?: Record<string, any>\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n>(options: ChatClientOptions<TTools>): ChatClientOptions<TTools> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools> ? Array<UIMessage<TTools>> : never\n"],"names":[],"mappings":"AAkPO,SAAS,eACX,OACA;AACH,SAAO;AACT;AAkBO,SAAS,wBAEd,SAA+D;AAC/D,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai": "0.
|
|
34
|
+
"@tanstack/ai": "0.3.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@vitest/coverage-v8": "4.0.14",
|
package/src/chat-client.ts
CHANGED
|
@@ -4,15 +4,16 @@ import {
|
|
|
4
4
|
normalizeToUIMessage,
|
|
5
5
|
} from '@tanstack/ai'
|
|
6
6
|
import { DefaultChatClientEventEmitter } from './events'
|
|
7
|
+
import type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai'
|
|
8
|
+
import type { ConnectionAdapter } from './connection-adapters'
|
|
9
|
+
import type { ChatClientEventEmitter } from './events'
|
|
7
10
|
import type {
|
|
8
11
|
ChatClientOptions,
|
|
12
|
+
ChatClientState,
|
|
9
13
|
MessagePart,
|
|
10
14
|
ToolCallPart,
|
|
11
15
|
UIMessage,
|
|
12
16
|
} from './types'
|
|
13
|
-
import type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai'
|
|
14
|
-
import type { ConnectionAdapter } from './connection-adapters'
|
|
15
|
-
import type { ChatClientEventEmitter } from './events'
|
|
16
17
|
|
|
17
18
|
export class ChatClient {
|
|
18
19
|
private processor: StreamProcessor
|
|
@@ -21,12 +22,17 @@ export class ChatClient {
|
|
|
21
22
|
private body: Record<string, any> = {}
|
|
22
23
|
private isLoading = false
|
|
23
24
|
private error: Error | undefined = undefined
|
|
25
|
+
private status: ChatClientState = 'ready'
|
|
24
26
|
private abortController: AbortController | null = null
|
|
25
27
|
private events: ChatClientEventEmitter
|
|
26
28
|
private clientToolsRef: { current: Map<string, AnyClientTool> }
|
|
27
29
|
private currentStreamId: string | null = null
|
|
28
30
|
private currentMessageId: string | null = null
|
|
29
31
|
private postStreamActions: Array<() => Promise<void>> = []
|
|
32
|
+
// Track pending client tool executions to await them before stream finalization
|
|
33
|
+
private pendingToolExecutions: Map<string, Promise<void>> = new Map()
|
|
34
|
+
// Flag to deduplicate continuation checks during action draining
|
|
35
|
+
private continuationPending = false
|
|
30
36
|
|
|
31
37
|
private callbacksRef: {
|
|
32
38
|
current: {
|
|
@@ -37,6 +43,7 @@ export class ChatClient {
|
|
|
37
43
|
onMessagesChange: (messages: Array<UIMessage>) => void
|
|
38
44
|
onLoadingChange: (isLoading: boolean) => void
|
|
39
45
|
onErrorChange: (error: Error | undefined) => void
|
|
46
|
+
onStatusChange: (status: ChatClientState) => void
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -63,6 +70,7 @@ export class ChatClient {
|
|
|
63
70
|
onMessagesChange: options.onMessagesChange || (() => {}),
|
|
64
71
|
onLoadingChange: options.onLoadingChange || (() => {}),
|
|
65
72
|
onErrorChange: options.onErrorChange || (() => {}),
|
|
73
|
+
onStatusChange: options.onStatusChange || (() => {}),
|
|
66
74
|
},
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -75,13 +83,15 @@ export class ChatClient {
|
|
|
75
83
|
this.callbacksRef.current.onMessagesChange(messages)
|
|
76
84
|
},
|
|
77
85
|
onStreamStart: () => {
|
|
78
|
-
|
|
86
|
+
this.setStatus('streaming')
|
|
79
87
|
},
|
|
80
88
|
onStreamEnd: (message: UIMessage) => {
|
|
81
89
|
this.callbacksRef.current.onFinish(message)
|
|
90
|
+
this.setStatus('ready')
|
|
82
91
|
},
|
|
83
92
|
onError: (error: Error) => {
|
|
84
93
|
this.setError(error)
|
|
94
|
+
this.setStatus('error')
|
|
85
95
|
this.callbacksRef.current.onError(error)
|
|
86
96
|
},
|
|
87
97
|
onTextUpdate: (messageId: string, content: string) => {
|
|
@@ -127,31 +137,41 @@ export class ChatClient {
|
|
|
127
137
|
)
|
|
128
138
|
}
|
|
129
139
|
},
|
|
130
|
-
onToolCall:
|
|
140
|
+
onToolCall: (args: {
|
|
131
141
|
toolCallId: string
|
|
132
142
|
toolName: string
|
|
133
143
|
input: any
|
|
134
144
|
}) => {
|
|
135
145
|
// Handle client-side tool execution automatically
|
|
136
146
|
const clientTool = this.clientToolsRef.current.get(args.toolName)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
const executeFunc = clientTool?.execute
|
|
148
|
+
if (executeFunc) {
|
|
149
|
+
// Create and track the execution promise
|
|
150
|
+
const executionPromise = (async () => {
|
|
151
|
+
try {
|
|
152
|
+
const output = await executeFunc(args.input)
|
|
153
|
+
await this.addToolResult({
|
|
154
|
+
toolCallId: args.toolCallId,
|
|
155
|
+
tool: args.toolName,
|
|
156
|
+
output,
|
|
157
|
+
state: 'output-available',
|
|
158
|
+
})
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
await this.addToolResult({
|
|
161
|
+
toolCallId: args.toolCallId,
|
|
162
|
+
tool: args.toolName,
|
|
163
|
+
output: null,
|
|
164
|
+
state: 'output-error',
|
|
165
|
+
errorText: error.message,
|
|
166
|
+
})
|
|
167
|
+
} finally {
|
|
168
|
+
// Remove from pending when complete
|
|
169
|
+
this.pendingToolExecutions.delete(args.toolCallId)
|
|
170
|
+
}
|
|
171
|
+
})()
|
|
172
|
+
|
|
173
|
+
// Track the pending execution
|
|
174
|
+
this.pendingToolExecutions.set(args.toolCallId, executionPromise)
|
|
155
175
|
}
|
|
156
176
|
},
|
|
157
177
|
onApprovalRequest: (args: {
|
|
@@ -160,13 +180,16 @@ export class ChatClient {
|
|
|
160
180
|
input: any
|
|
161
181
|
approvalId: string
|
|
162
182
|
}) => {
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
if (this.currentStreamId) {
|
|
184
|
+
this.events.approvalRequested(
|
|
185
|
+
this.currentStreamId,
|
|
186
|
+
this.currentMessageId || '',
|
|
187
|
+
args.toolCallId,
|
|
188
|
+
args.toolName,
|
|
189
|
+
args.input,
|
|
190
|
+
args.approvalId,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
170
193
|
},
|
|
171
194
|
},
|
|
172
195
|
})
|
|
@@ -184,6 +207,11 @@ export class ChatClient {
|
|
|
184
207
|
this.events.loadingChanged(isLoading)
|
|
185
208
|
}
|
|
186
209
|
|
|
210
|
+
private setStatus(status: ChatClientState): void {
|
|
211
|
+
this.status = status
|
|
212
|
+
this.callbacksRef.current.onStatusChange(status)
|
|
213
|
+
}
|
|
214
|
+
|
|
187
215
|
private setError(error: Error | undefined): void {
|
|
188
216
|
this.error = error
|
|
189
217
|
this.callbacksRef.current.onErrorChange(error)
|
|
@@ -210,7 +238,10 @@ export class ChatClient {
|
|
|
210
238
|
parts: [],
|
|
211
239
|
createdAt: new Date(),
|
|
212
240
|
}
|
|
213
|
-
this.events.messageAppended(
|
|
241
|
+
this.events.messageAppended(
|
|
242
|
+
assistantMessage,
|
|
243
|
+
this.currentStreamId || undefined,
|
|
244
|
+
)
|
|
214
245
|
|
|
215
246
|
// Process each chunk
|
|
216
247
|
for await (const chunk of source) {
|
|
@@ -221,6 +252,12 @@ export class ChatClient {
|
|
|
221
252
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
222
253
|
}
|
|
223
254
|
|
|
255
|
+
// Wait for all pending tool executions to complete before finalizing
|
|
256
|
+
// This ensures client tools finish before we check for continuation
|
|
257
|
+
if (this.pendingToolExecutions.size > 0) {
|
|
258
|
+
await Promise.all(this.pendingToolExecutions.values())
|
|
259
|
+
}
|
|
260
|
+
|
|
224
261
|
// Finalize the stream
|
|
225
262
|
this.processor.finalizeStream()
|
|
226
263
|
|
|
@@ -281,6 +318,12 @@ export class ChatClient {
|
|
|
281
318
|
const messages = this.processor.getMessages()
|
|
282
319
|
this.processor.setMessages([...messages, uiMessage])
|
|
283
320
|
|
|
321
|
+
// If stream is in progress, queue the response for after it ends
|
|
322
|
+
if (this.isLoading) {
|
|
323
|
+
this.queuePostStreamAction(() => this.streamResponse())
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
284
327
|
await this.streamResponse()
|
|
285
328
|
}
|
|
286
329
|
|
|
@@ -288,13 +331,22 @@ export class ChatClient {
|
|
|
288
331
|
* Stream a response from the LLM
|
|
289
332
|
*/
|
|
290
333
|
private async streamResponse(): Promise<void> {
|
|
334
|
+
// Guard against concurrent streams - if already loading, skip
|
|
335
|
+
if (this.isLoading) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
291
339
|
this.setIsLoading(true)
|
|
340
|
+
this.setStatus('submitted')
|
|
292
341
|
this.setError(undefined)
|
|
293
342
|
this.abortController = new AbortController()
|
|
343
|
+
// Reset pending tool executions for the new stream
|
|
344
|
+
this.pendingToolExecutions.clear()
|
|
345
|
+
let streamCompletedSuccessfully = false
|
|
294
346
|
|
|
295
347
|
try {
|
|
296
|
-
// Get
|
|
297
|
-
const
|
|
348
|
+
// Get UIMessages with parts (preserves approval state and client tool results)
|
|
349
|
+
const messages = this.processor.getMessages()
|
|
298
350
|
|
|
299
351
|
// Call onResponse callback
|
|
300
352
|
await this.callbacksRef.current.onResponse()
|
|
@@ -307,18 +359,20 @@ export class ChatClient {
|
|
|
307
359
|
|
|
308
360
|
// Connect and stream
|
|
309
361
|
const stream = this.connection.connect(
|
|
310
|
-
|
|
362
|
+
messages,
|
|
311
363
|
bodyWithConversationId,
|
|
312
364
|
this.abortController.signal,
|
|
313
365
|
)
|
|
314
366
|
|
|
315
367
|
await this.processStream(stream)
|
|
368
|
+
streamCompletedSuccessfully = true
|
|
316
369
|
} catch (err) {
|
|
317
370
|
if (err instanceof Error) {
|
|
318
371
|
if (err.name === 'AbortError') {
|
|
319
372
|
return
|
|
320
373
|
}
|
|
321
374
|
this.setError(err)
|
|
375
|
+
this.setStatus('error')
|
|
322
376
|
this.callbacksRef.current.onError(err)
|
|
323
377
|
}
|
|
324
378
|
} finally {
|
|
@@ -327,6 +381,20 @@ export class ChatClient {
|
|
|
327
381
|
|
|
328
382
|
// Drain any actions that were queued while the stream was in progress
|
|
329
383
|
await this.drainPostStreamActions()
|
|
384
|
+
|
|
385
|
+
// Continue conversation if the stream ended with a tool result (server tool completed)
|
|
386
|
+
if (streamCompletedSuccessfully) {
|
|
387
|
+
const messages = this.processor.getMessages()
|
|
388
|
+
const lastPart = messages.at(-1)?.parts.at(-1)
|
|
389
|
+
|
|
390
|
+
if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {
|
|
391
|
+
try {
|
|
392
|
+
await this.checkForContinuation()
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error('Failed to continue flow after tool result:', error)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
330
398
|
}
|
|
331
399
|
}
|
|
332
400
|
|
|
@@ -362,6 +430,7 @@ export class ChatClient {
|
|
|
362
430
|
this.abortController = null
|
|
363
431
|
}
|
|
364
432
|
this.setIsLoading(false)
|
|
433
|
+
this.setStatus('ready')
|
|
365
434
|
this.events.stopped()
|
|
366
435
|
}
|
|
367
436
|
|
|
@@ -470,8 +539,18 @@ export class ChatClient {
|
|
|
470
539
|
* Check if we should continue the flow and do so if needed
|
|
471
540
|
*/
|
|
472
541
|
private async checkForContinuation(): Promise<void> {
|
|
542
|
+
// Prevent duplicate continuation attempts
|
|
543
|
+
if (this.continuationPending || this.isLoading) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
473
547
|
if (this.shouldAutoSend()) {
|
|
474
|
-
|
|
548
|
+
this.continuationPending = true
|
|
549
|
+
try {
|
|
550
|
+
await this.streamResponse()
|
|
551
|
+
} finally {
|
|
552
|
+
this.continuationPending = false
|
|
553
|
+
}
|
|
475
554
|
}
|
|
476
555
|
}
|
|
477
556
|
|
|
@@ -496,6 +575,13 @@ export class ChatClient {
|
|
|
496
575
|
return this.isLoading
|
|
497
576
|
}
|
|
498
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Get current status
|
|
580
|
+
*/
|
|
581
|
+
getStatus(): ChatClientState {
|
|
582
|
+
return this.status
|
|
583
|
+
}
|
|
584
|
+
|
|
499
585
|
/**
|
|
500
586
|
* Get current error
|
|
501
587
|
*/
|