@tanstack/ai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/esm/base-adapter.d.ts +35 -0
- package/dist/esm/base-adapter.js +12 -0
- package/dist/esm/base-adapter.js.map +1 -0
- package/dist/esm/core/chat-common-options.d.ts +52 -0
- package/dist/esm/core/chat.d.ts +30 -0
- package/dist/esm/core/chat.js +533 -0
- package/dist/esm/core/chat.js.map +1 -0
- package/dist/esm/core/embedding.d.ts +8 -0
- package/dist/esm/core/embedding.js +33 -0
- package/dist/esm/core/embedding.js.map +1 -0
- package/dist/esm/core/summarize.d.ts +9 -0
- package/dist/esm/core/summarize.js +36 -0
- package/dist/esm/core/summarize.js.map +1 -0
- package/dist/esm/event-client.d.ts +311 -0
- package/dist/esm/event-client.js +62 -0
- package/dist/esm/event-client.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/message-converters.d.ts +52 -0
- package/dist/esm/message-converters.js +162 -0
- package/dist/esm/message-converters.js.map +1 -0
- package/dist/esm/stream/index.d.ts +11 -0
- package/dist/esm/stream/json-parser.d.ts +38 -0
- package/dist/esm/stream/json-parser.js +28 -0
- package/dist/esm/stream/json-parser.js.map +1 -0
- package/dist/esm/stream/message-updaters.d.ts +44 -0
- package/dist/esm/stream/message-updaters.js +141 -0
- package/dist/esm/stream/message-updaters.js.map +1 -0
- package/dist/esm/stream/processor.d.ts +242 -0
- package/dist/esm/stream/processor.js +693 -0
- package/dist/esm/stream/processor.js.map +1 -0
- package/dist/esm/stream/strategies.d.ts +43 -0
- package/dist/esm/stream/strategies.js +54 -0
- package/dist/esm/stream/strategies.js.map +1 -0
- package/dist/esm/stream/types.d.ts +71 -0
- package/dist/esm/tools/tool-calls.d.ts +112 -0
- package/dist/esm/tools/tool-calls.js +302 -0
- package/dist/esm/tools/tool-calls.js.map +1 -0
- package/dist/esm/tools/tool-definition.d.ts +125 -0
- package/dist/esm/tools/tool-definition.js +25 -0
- package/dist/esm/tools/tool-definition.js.map +1 -0
- package/dist/esm/tools/zod-converter.d.ts +30 -0
- package/dist/esm/tools/zod-converter.js +36 -0
- package/dist/esm/tools/zod-converter.js.map +1 -0
- package/dist/esm/types.d.ts +619 -0
- package/dist/esm/utilities/agent-loop-strategies.d.ts +59 -0
- package/dist/esm/utilities/agent-loop-strategies.js +23 -0
- package/dist/esm/utilities/agent-loop-strategies.js.map +1 -0
- package/dist/esm/utilities/chat-options.d.ts +6 -0
- package/dist/esm/utilities/chat-options.js +7 -0
- package/dist/esm/utilities/chat-options.js.map +1 -0
- package/dist/esm/utilities/messages.d.ts +30 -0
- package/dist/esm/utilities/messages.js +7 -0
- package/dist/esm/utilities/messages.js.map +1 -0
- package/dist/esm/utilities/stream-to-response.d.ts +48 -0
- package/dist/esm/utilities/stream-to-response.js +62 -0
- package/dist/esm/utilities/stream-to-response.js.map +1 -0
- package/package.json +65 -0
- package/src/base-adapter.ts +85 -0
- package/src/core/chat-common-options.ts +55 -0
- package/src/core/chat.ts +771 -0
- package/src/core/embedding.ts +54 -0
- package/src/core/summarize.ts +56 -0
- package/src/event-client.ts +389 -0
- package/src/index.ts +68 -0
- package/src/message-converters.ts +285 -0
- package/src/stream/index.ts +41 -0
- package/src/stream/json-parser.ts +58 -0
- package/src/stream/message-updaters.ts +275 -0
- package/src/stream/processor.ts +1092 -0
- package/src/stream/strategies.ts +78 -0
- package/src/stream/types.ts +94 -0
- package/src/tools/tool-calls.ts +471 -0
- package/src/tools/tool-definition.ts +206 -0
- package/src/tools/zod-converter.ts +85 -0
- package/src/types.ts +872 -0
- package/src/utilities/agent-loop-strategies.ts +85 -0
- package/src/utilities/chat-options.ts +35 -0
- package/src/utilities/messages.ts +63 -0
- package/src/utilities/stream-to-response.ts +116 -0
package/src/core/chat.ts
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { aiEventClient } from '../event-client.js'
|
|
2
|
+
import { ToolCallManager, executeToolCalls } from '../tools/tool-calls'
|
|
3
|
+
import { maxIterations as maxIterationsStrategy } from '../utilities/agent-loop-strategies'
|
|
4
|
+
import type {
|
|
5
|
+
ApprovalRequest,
|
|
6
|
+
ClientToolRequest,
|
|
7
|
+
ToolResult,
|
|
8
|
+
} from '../tools/tool-calls'
|
|
9
|
+
import type {
|
|
10
|
+
AIAdapter,
|
|
11
|
+
AgentLoopStrategy,
|
|
12
|
+
ChatOptions,
|
|
13
|
+
ChatStreamOptionsForModel,
|
|
14
|
+
DoneStreamChunk,
|
|
15
|
+
ModelMessage,
|
|
16
|
+
StreamChunk,
|
|
17
|
+
Tool,
|
|
18
|
+
ToolCall,
|
|
19
|
+
} from '../types'
|
|
20
|
+
|
|
21
|
+
interface ChatEngineConfig<
|
|
22
|
+
TAdapter extends AIAdapter<any, any, any, any>,
|
|
23
|
+
TParams extends ChatOptions<any, any> = ChatOptions<any>,
|
|
24
|
+
> {
|
|
25
|
+
adapter: TAdapter
|
|
26
|
+
systemPrompts?: Array<string>
|
|
27
|
+
params: TParams
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
31
|
+
type CyclePhase = 'processChat' | 'executeToolCalls'
|
|
32
|
+
|
|
33
|
+
class ChatEngine<
|
|
34
|
+
TAdapter extends AIAdapter<any, any, any, any>,
|
|
35
|
+
TParams extends ChatOptions<any, any> = ChatOptions<any>,
|
|
36
|
+
> {
|
|
37
|
+
private readonly adapter: TAdapter
|
|
38
|
+
private readonly params: TParams
|
|
39
|
+
private readonly systemPrompts: Array<string>
|
|
40
|
+
private readonly tools: ReadonlyArray<Tool>
|
|
41
|
+
private readonly loopStrategy: AgentLoopStrategy
|
|
42
|
+
private readonly toolCallManager: ToolCallManager
|
|
43
|
+
private readonly initialMessageCount: number
|
|
44
|
+
private readonly requestId: string
|
|
45
|
+
private readonly streamId: string
|
|
46
|
+
private readonly effectiveRequest?: Request | RequestInit
|
|
47
|
+
private readonly effectiveSignal?: AbortSignal
|
|
48
|
+
|
|
49
|
+
private messages: Array<ModelMessage>
|
|
50
|
+
private iterationCount = 0
|
|
51
|
+
private lastFinishReason: string | null = null
|
|
52
|
+
private streamStartTime = 0
|
|
53
|
+
private totalChunkCount = 0
|
|
54
|
+
private currentMessageId: string | null = null
|
|
55
|
+
private accumulatedContent = ''
|
|
56
|
+
private doneChunk: DoneStreamChunk | null = null
|
|
57
|
+
private shouldEmitStreamEnd = true
|
|
58
|
+
private earlyTermination = false
|
|
59
|
+
private toolPhase: ToolPhaseResult = 'continue'
|
|
60
|
+
private cyclePhase: CyclePhase = 'processChat'
|
|
61
|
+
|
|
62
|
+
constructor(config: ChatEngineConfig<TAdapter, TParams>) {
|
|
63
|
+
this.adapter = config.adapter
|
|
64
|
+
this.params = config.params
|
|
65
|
+
this.systemPrompts = config.params.systemPrompts || []
|
|
66
|
+
this.tools = config.params.tools || []
|
|
67
|
+
this.loopStrategy =
|
|
68
|
+
config.params.agentLoopStrategy || maxIterationsStrategy(5)
|
|
69
|
+
this.toolCallManager = new ToolCallManager(this.tools)
|
|
70
|
+
this.initialMessageCount = config.params.messages.length
|
|
71
|
+
this.messages = config.params.messages
|
|
72
|
+
this.requestId = this.createId('chat')
|
|
73
|
+
this.streamId = this.createId('stream')
|
|
74
|
+
this.effectiveRequest = config.params.abortController
|
|
75
|
+
? { signal: config.params.abortController.signal }
|
|
76
|
+
: undefined
|
|
77
|
+
this.effectiveSignal = config.params.abortController?.signal
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async *chat(): AsyncGenerator<StreamChunk> {
|
|
81
|
+
this.beforeChat()
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const pendingPhase = yield* this.checkForPendingToolCalls()
|
|
85
|
+
if (pendingPhase === 'wait') {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
do {
|
|
90
|
+
if (this.earlyTermination || this.isAborted()) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.beginCycle()
|
|
95
|
+
|
|
96
|
+
if (this.cyclePhase === 'processChat') {
|
|
97
|
+
yield* this.streamModelResponse()
|
|
98
|
+
} else {
|
|
99
|
+
yield* this.processToolCalls()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.endCycle()
|
|
103
|
+
} while (this.shouldContinue())
|
|
104
|
+
} finally {
|
|
105
|
+
this.afterChat()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private beforeChat(): void {
|
|
110
|
+
this.streamStartTime = Date.now()
|
|
111
|
+
const { model, tools, options, providerOptions, conversationId } =
|
|
112
|
+
this.params
|
|
113
|
+
|
|
114
|
+
aiEventClient.emit('chat:started', {
|
|
115
|
+
requestId: this.requestId,
|
|
116
|
+
streamId: this.streamId,
|
|
117
|
+
model: model,
|
|
118
|
+
provider: this.adapter.name,
|
|
119
|
+
messageCount: this.initialMessageCount,
|
|
120
|
+
hasTools: !!tools && tools.length > 0,
|
|
121
|
+
streaming: true,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
clientId: conversationId,
|
|
124
|
+
toolNames: tools?.map((t) => t.name),
|
|
125
|
+
options: options as Record<string, unknown> | undefined,
|
|
126
|
+
providerOptions: providerOptions as Record<string, unknown> | undefined,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
aiEventClient.emit('stream:started', {
|
|
130
|
+
streamId: this.streamId,
|
|
131
|
+
model,
|
|
132
|
+
provider: this.adapter.name,
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private afterChat(): void {
|
|
138
|
+
if (!this.shouldEmitStreamEnd) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const now = Date.now()
|
|
143
|
+
|
|
144
|
+
// Emit chat:completed with final state
|
|
145
|
+
aiEventClient.emit('chat:completed', {
|
|
146
|
+
requestId: this.requestId,
|
|
147
|
+
streamId: this.streamId,
|
|
148
|
+
model: this.params.model,
|
|
149
|
+
content: this.accumulatedContent,
|
|
150
|
+
messageId: this.currentMessageId || undefined,
|
|
151
|
+
finishReason: this.lastFinishReason || undefined,
|
|
152
|
+
usage: this.doneChunk?.usage,
|
|
153
|
+
timestamp: now,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
aiEventClient.emit('stream:ended', {
|
|
157
|
+
requestId: this.requestId,
|
|
158
|
+
streamId: this.streamId,
|
|
159
|
+
totalChunks: this.totalChunkCount,
|
|
160
|
+
duration: now - this.streamStartTime,
|
|
161
|
+
timestamp: now,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private beginCycle(): void {
|
|
166
|
+
if (this.cyclePhase === 'processChat') {
|
|
167
|
+
this.beginIteration()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private endCycle(): void {
|
|
172
|
+
if (this.cyclePhase === 'processChat') {
|
|
173
|
+
this.cyclePhase = 'executeToolCalls'
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.cyclePhase = 'processChat'
|
|
178
|
+
this.iterationCount++
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private beginIteration(): void {
|
|
182
|
+
this.currentMessageId = this.createId('msg')
|
|
183
|
+
this.accumulatedContent = ''
|
|
184
|
+
this.doneChunk = null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async *streamModelResponse(): AsyncGenerator<StreamChunk> {
|
|
188
|
+
const adapterOptions = this.params.options || {}
|
|
189
|
+
const providerOptions = this.params.providerOptions
|
|
190
|
+
const tools = this.params.tools
|
|
191
|
+
|
|
192
|
+
for await (const chunk of this.adapter.chatStream({
|
|
193
|
+
model: this.params.model,
|
|
194
|
+
messages: this.messages,
|
|
195
|
+
tools,
|
|
196
|
+
options: adapterOptions,
|
|
197
|
+
request: this.effectiveRequest,
|
|
198
|
+
providerOptions,
|
|
199
|
+
systemPrompts: this.systemPrompts,
|
|
200
|
+
})) {
|
|
201
|
+
if (this.isAborted()) {
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.totalChunkCount++
|
|
206
|
+
|
|
207
|
+
yield chunk
|
|
208
|
+
this.handleStreamChunk(chunk)
|
|
209
|
+
|
|
210
|
+
if (this.earlyTermination) {
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private handleStreamChunk(chunk: StreamChunk): void {
|
|
217
|
+
switch (chunk.type) {
|
|
218
|
+
case 'content':
|
|
219
|
+
this.handleContentChunk(chunk)
|
|
220
|
+
break
|
|
221
|
+
case 'tool_call':
|
|
222
|
+
this.handleToolCallChunk(chunk)
|
|
223
|
+
break
|
|
224
|
+
case 'tool_result':
|
|
225
|
+
this.handleToolResultChunk(chunk)
|
|
226
|
+
break
|
|
227
|
+
case 'done':
|
|
228
|
+
this.handleDoneChunk(chunk)
|
|
229
|
+
break
|
|
230
|
+
case 'error':
|
|
231
|
+
this.handleErrorChunk(chunk)
|
|
232
|
+
break
|
|
233
|
+
case 'thinking':
|
|
234
|
+
this.handleThinkingChunk(chunk)
|
|
235
|
+
break
|
|
236
|
+
default:
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private handleContentChunk(chunk: Extract<StreamChunk, { type: 'content' }>) {
|
|
242
|
+
this.accumulatedContent = chunk.content
|
|
243
|
+
aiEventClient.emit('stream:chunk:content', {
|
|
244
|
+
streamId: this.streamId,
|
|
245
|
+
messageId: this.currentMessageId || undefined,
|
|
246
|
+
content: chunk.content,
|
|
247
|
+
delta: chunk.delta,
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private handleToolCallChunk(
|
|
253
|
+
chunk: Extract<StreamChunk, { type: 'tool_call' }>,
|
|
254
|
+
): void {
|
|
255
|
+
this.toolCallManager.addToolCallChunk(chunk)
|
|
256
|
+
aiEventClient.emit('stream:chunk:tool-call', {
|
|
257
|
+
streamId: this.streamId,
|
|
258
|
+
messageId: this.currentMessageId || undefined,
|
|
259
|
+
toolCallId: chunk.toolCall.id,
|
|
260
|
+
toolName: chunk.toolCall.function.name,
|
|
261
|
+
index: chunk.index,
|
|
262
|
+
arguments: chunk.toolCall.function.arguments,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private handleToolResultChunk(
|
|
268
|
+
chunk: Extract<StreamChunk, { type: 'tool_result' }>,
|
|
269
|
+
): void {
|
|
270
|
+
aiEventClient.emit('stream:chunk:tool-result', {
|
|
271
|
+
streamId: this.streamId,
|
|
272
|
+
messageId: this.currentMessageId || undefined,
|
|
273
|
+
toolCallId: chunk.toolCallId,
|
|
274
|
+
result: chunk.content,
|
|
275
|
+
timestamp: Date.now(),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private handleDoneChunk(chunk: DoneStreamChunk): void {
|
|
280
|
+
// Don't overwrite a tool_calls finishReason with a stop finishReason
|
|
281
|
+
// This can happen when adapters send multiple done chunks
|
|
282
|
+
if (
|
|
283
|
+
this.doneChunk?.finishReason === 'tool_calls' &&
|
|
284
|
+
chunk.finishReason === 'stop'
|
|
285
|
+
) {
|
|
286
|
+
// Still emit the event and update lastFinishReason, but don't overwrite doneChunk
|
|
287
|
+
this.lastFinishReason = chunk.finishReason
|
|
288
|
+
aiEventClient.emit('stream:chunk:done', {
|
|
289
|
+
streamId: this.streamId,
|
|
290
|
+
messageId: this.currentMessageId || undefined,
|
|
291
|
+
finishReason: chunk.finishReason,
|
|
292
|
+
usage: chunk.usage,
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (chunk.usage) {
|
|
297
|
+
aiEventClient.emit('usage:tokens', {
|
|
298
|
+
requestId: this.requestId,
|
|
299
|
+
streamId: this.streamId,
|
|
300
|
+
messageId: this.currentMessageId || undefined,
|
|
301
|
+
model: this.params.model,
|
|
302
|
+
usage: chunk.usage,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.doneChunk = chunk
|
|
310
|
+
this.lastFinishReason = chunk.finishReason
|
|
311
|
+
aiEventClient.emit('stream:chunk:done', {
|
|
312
|
+
streamId: this.streamId,
|
|
313
|
+
messageId: this.currentMessageId || undefined,
|
|
314
|
+
finishReason: chunk.finishReason,
|
|
315
|
+
usage: chunk.usage,
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
if (chunk.usage) {
|
|
320
|
+
aiEventClient.emit('usage:tokens', {
|
|
321
|
+
requestId: this.requestId,
|
|
322
|
+
streamId: this.streamId,
|
|
323
|
+
messageId: this.currentMessageId || undefined,
|
|
324
|
+
model: this.params.model,
|
|
325
|
+
usage: chunk.usage,
|
|
326
|
+
timestamp: Date.now(),
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private handleErrorChunk(
|
|
332
|
+
chunk: Extract<StreamChunk, { type: 'error' }>,
|
|
333
|
+
): void {
|
|
334
|
+
aiEventClient.emit('stream:chunk:error', {
|
|
335
|
+
streamId: this.streamId,
|
|
336
|
+
messageId: this.currentMessageId || undefined,
|
|
337
|
+
error: chunk.error.message,
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
})
|
|
340
|
+
this.earlyTermination = true
|
|
341
|
+
this.shouldEmitStreamEnd = false
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private handleThinkingChunk(
|
|
345
|
+
chunk: Extract<StreamChunk, { type: 'thinking' }>,
|
|
346
|
+
): void {
|
|
347
|
+
aiEventClient.emit('stream:chunk:thinking', {
|
|
348
|
+
streamId: this.streamId,
|
|
349
|
+
messageId: this.currentMessageId || undefined,
|
|
350
|
+
content: chunk.content,
|
|
351
|
+
delta: chunk.delta,
|
|
352
|
+
timestamp: Date.now(),
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async *checkForPendingToolCalls(): AsyncGenerator<
|
|
357
|
+
StreamChunk,
|
|
358
|
+
ToolPhaseResult,
|
|
359
|
+
void
|
|
360
|
+
> {
|
|
361
|
+
const pendingToolCalls = this.getPendingToolCallsFromMessages()
|
|
362
|
+
if (pendingToolCalls.length === 0) {
|
|
363
|
+
return 'continue'
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const doneChunk = this.createSyntheticDoneChunk()
|
|
367
|
+
|
|
368
|
+
aiEventClient.emit('chat:iteration', {
|
|
369
|
+
requestId: this.requestId,
|
|
370
|
+
streamId: this.streamId,
|
|
371
|
+
iterationNumber: this.iterationCount + 1,
|
|
372
|
+
messageCount: this.messages.length,
|
|
373
|
+
toolCallCount: pendingToolCalls.length,
|
|
374
|
+
timestamp: Date.now(),
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
const { approvals, clientToolResults } = this.collectClientState()
|
|
378
|
+
|
|
379
|
+
const executionResult = await executeToolCalls(
|
|
380
|
+
pendingToolCalls,
|
|
381
|
+
this.tools,
|
|
382
|
+
approvals,
|
|
383
|
+
clientToolResults,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
executionResult.needsApproval.length > 0 ||
|
|
388
|
+
executionResult.needsClientExecution.length > 0
|
|
389
|
+
) {
|
|
390
|
+
for (const chunk of this.emitApprovalRequests(
|
|
391
|
+
executionResult.needsApproval,
|
|
392
|
+
doneChunk,
|
|
393
|
+
)) {
|
|
394
|
+
yield chunk
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const chunk of this.emitClientToolInputs(
|
|
398
|
+
executionResult.needsClientExecution,
|
|
399
|
+
doneChunk,
|
|
400
|
+
)) {
|
|
401
|
+
yield chunk
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.shouldEmitStreamEnd = false
|
|
405
|
+
return 'wait'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const toolResultChunks = this.emitToolResults(
|
|
409
|
+
executionResult.results,
|
|
410
|
+
doneChunk,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
for (const chunk of toolResultChunks) {
|
|
414
|
+
yield chunk
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return 'continue'
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async *processToolCalls(): AsyncGenerator<StreamChunk, void, void> {
|
|
421
|
+
if (!this.shouldExecuteToolPhase()) {
|
|
422
|
+
this.setToolPhase('stop')
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const toolCalls = this.toolCallManager.getToolCalls()
|
|
427
|
+
const doneChunk = this.doneChunk
|
|
428
|
+
|
|
429
|
+
if (!doneChunk || toolCalls.length === 0) {
|
|
430
|
+
this.setToolPhase('stop')
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
aiEventClient.emit('chat:iteration', {
|
|
435
|
+
requestId: this.requestId,
|
|
436
|
+
streamId: this.streamId,
|
|
437
|
+
iterationNumber: this.iterationCount + 1,
|
|
438
|
+
messageCount: this.messages.length,
|
|
439
|
+
toolCallCount: toolCalls.length,
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
this.addAssistantToolCallMessage(toolCalls)
|
|
444
|
+
|
|
445
|
+
const { approvals, clientToolResults } = this.collectClientState()
|
|
446
|
+
|
|
447
|
+
const executionResult = await executeToolCalls(
|
|
448
|
+
toolCalls,
|
|
449
|
+
this.tools,
|
|
450
|
+
approvals,
|
|
451
|
+
clientToolResults,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if (
|
|
455
|
+
executionResult.needsApproval.length > 0 ||
|
|
456
|
+
executionResult.needsClientExecution.length > 0
|
|
457
|
+
) {
|
|
458
|
+
for (const chunk of this.emitApprovalRequests(
|
|
459
|
+
executionResult.needsApproval,
|
|
460
|
+
doneChunk,
|
|
461
|
+
)) {
|
|
462
|
+
yield chunk
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (const chunk of this.emitClientToolInputs(
|
|
466
|
+
executionResult.needsClientExecution,
|
|
467
|
+
doneChunk,
|
|
468
|
+
)) {
|
|
469
|
+
yield chunk
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.setToolPhase('wait')
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const toolResultChunks = this.emitToolResults(
|
|
477
|
+
executionResult.results,
|
|
478
|
+
doneChunk,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
for (const chunk of toolResultChunks) {
|
|
482
|
+
yield chunk
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.toolCallManager.clear()
|
|
486
|
+
|
|
487
|
+
this.setToolPhase('continue')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private shouldExecuteToolPhase(): boolean {
|
|
491
|
+
return (
|
|
492
|
+
this.doneChunk?.finishReason === 'tool_calls' &&
|
|
493
|
+
this.tools.length > 0 &&
|
|
494
|
+
this.toolCallManager.hasToolCalls()
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
|
|
499
|
+
this.messages = [
|
|
500
|
+
...this.messages,
|
|
501
|
+
{
|
|
502
|
+
role: 'assistant',
|
|
503
|
+
content: this.accumulatedContent || null,
|
|
504
|
+
toolCalls,
|
|
505
|
+
},
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private collectClientState(): {
|
|
510
|
+
approvals: Map<string, boolean>
|
|
511
|
+
clientToolResults: Map<string, any>
|
|
512
|
+
} {
|
|
513
|
+
const approvals = new Map<string, boolean>()
|
|
514
|
+
const clientToolResults = new Map<string, any>()
|
|
515
|
+
|
|
516
|
+
for (const message of this.messages) {
|
|
517
|
+
// todo remove any and fix this
|
|
518
|
+
if (message.role === 'assistant' && (message as any).parts) {
|
|
519
|
+
const parts = (message as any).parts
|
|
520
|
+
for (const part of parts) {
|
|
521
|
+
if (
|
|
522
|
+
part.type === 'tool-call' &&
|
|
523
|
+
part.state === 'approval-responded' &&
|
|
524
|
+
part.approval
|
|
525
|
+
) {
|
|
526
|
+
approvals.set(part.approval.id, part.approval.approved)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
part.type === 'tool-call' &&
|
|
531
|
+
part.output !== undefined &&
|
|
532
|
+
!part.approval
|
|
533
|
+
) {
|
|
534
|
+
clientToolResults.set(part.id, part.output)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { approvals, clientToolResults }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private emitApprovalRequests(
|
|
544
|
+
approvals: Array<ApprovalRequest>,
|
|
545
|
+
doneChunk: DoneStreamChunk,
|
|
546
|
+
): Array<StreamChunk> {
|
|
547
|
+
const chunks: Array<StreamChunk> = []
|
|
548
|
+
|
|
549
|
+
for (const approval of approvals) {
|
|
550
|
+
aiEventClient.emit('stream:approval-requested', {
|
|
551
|
+
streamId: this.streamId,
|
|
552
|
+
messageId: this.currentMessageId || undefined,
|
|
553
|
+
toolCallId: approval.toolCallId,
|
|
554
|
+
toolName: approval.toolName,
|
|
555
|
+
input: approval.input,
|
|
556
|
+
approvalId: approval.approvalId,
|
|
557
|
+
timestamp: Date.now(),
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
chunks.push({
|
|
561
|
+
type: 'approval-requested',
|
|
562
|
+
id: doneChunk.id,
|
|
563
|
+
model: doneChunk.model,
|
|
564
|
+
timestamp: Date.now(),
|
|
565
|
+
toolCallId: approval.toolCallId,
|
|
566
|
+
toolName: approval.toolName,
|
|
567
|
+
input: approval.input,
|
|
568
|
+
approval: {
|
|
569
|
+
id: approval.approvalId,
|
|
570
|
+
needsApproval: true,
|
|
571
|
+
},
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return chunks
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private emitClientToolInputs(
|
|
579
|
+
clientRequests: Array<ClientToolRequest>,
|
|
580
|
+
doneChunk: DoneStreamChunk,
|
|
581
|
+
): Array<StreamChunk> {
|
|
582
|
+
const chunks: Array<StreamChunk> = []
|
|
583
|
+
|
|
584
|
+
for (const clientTool of clientRequests) {
|
|
585
|
+
aiEventClient.emit('stream:tool-input-available', {
|
|
586
|
+
streamId: this.streamId,
|
|
587
|
+
messageId: this.currentMessageId || undefined,
|
|
588
|
+
toolCallId: clientTool.toolCallId,
|
|
589
|
+
toolName: clientTool.toolName,
|
|
590
|
+
input: clientTool.input,
|
|
591
|
+
timestamp: Date.now(),
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
chunks.push({
|
|
595
|
+
type: 'tool-input-available',
|
|
596
|
+
id: doneChunk.id,
|
|
597
|
+
model: doneChunk.model,
|
|
598
|
+
timestamp: Date.now(),
|
|
599
|
+
toolCallId: clientTool.toolCallId,
|
|
600
|
+
toolName: clientTool.toolName,
|
|
601
|
+
input: clientTool.input,
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return chunks
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private emitToolResults(
|
|
609
|
+
results: Array<ToolResult>,
|
|
610
|
+
doneChunk: DoneStreamChunk,
|
|
611
|
+
): Array<StreamChunk> {
|
|
612
|
+
const chunks: Array<StreamChunk> = []
|
|
613
|
+
|
|
614
|
+
for (const result of results) {
|
|
615
|
+
aiEventClient.emit('tool:call-completed', {
|
|
616
|
+
requestId: this.requestId,
|
|
617
|
+
streamId: this.streamId,
|
|
618
|
+
messageId: this.currentMessageId || undefined,
|
|
619
|
+
toolCallId: result.toolCallId,
|
|
620
|
+
toolName: result.toolName,
|
|
621
|
+
result: result.result,
|
|
622
|
+
duration: result.duration ?? 0,
|
|
623
|
+
timestamp: Date.now(),
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
const content = JSON.stringify(result.result)
|
|
627
|
+
const chunk: Extract<StreamChunk, { type: 'tool_result' }> = {
|
|
628
|
+
type: 'tool_result',
|
|
629
|
+
id: doneChunk.id,
|
|
630
|
+
model: doneChunk.model,
|
|
631
|
+
timestamp: Date.now(),
|
|
632
|
+
toolCallId: result.toolCallId,
|
|
633
|
+
content,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
chunks.push(chunk)
|
|
637
|
+
|
|
638
|
+
this.messages = [
|
|
639
|
+
...this.messages,
|
|
640
|
+
{
|
|
641
|
+
role: 'tool',
|
|
642
|
+
content,
|
|
643
|
+
toolCallId: result.toolCallId,
|
|
644
|
+
},
|
|
645
|
+
]
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return chunks
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private getPendingToolCallsFromMessages(): Array<ToolCall> {
|
|
652
|
+
const completedToolIds = new Set(
|
|
653
|
+
this.messages
|
|
654
|
+
.filter((message) => message.role === 'tool' && message.toolCallId)
|
|
655
|
+
.map((message) => message.toolCallId!), // toolCallId exists due to filter
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
const pending: Array<ToolCall> = []
|
|
659
|
+
|
|
660
|
+
for (const message of this.messages) {
|
|
661
|
+
if (message.role === 'assistant' && message.toolCalls) {
|
|
662
|
+
for (const toolCall of message.toolCalls) {
|
|
663
|
+
if (!completedToolIds.has(toolCall.id)) {
|
|
664
|
+
pending.push(toolCall)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return pending
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private createSyntheticDoneChunk(): DoneStreamChunk {
|
|
674
|
+
return {
|
|
675
|
+
type: 'done',
|
|
676
|
+
id: this.createId('pending'),
|
|
677
|
+
model: this.params.model,
|
|
678
|
+
timestamp: Date.now(),
|
|
679
|
+
finishReason: 'tool_calls',
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private shouldContinue(): boolean {
|
|
684
|
+
if (this.cyclePhase === 'executeToolCalls') {
|
|
685
|
+
return true
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
this.loopStrategy({
|
|
690
|
+
iterationCount: this.iterationCount,
|
|
691
|
+
messages: this.messages,
|
|
692
|
+
finishReason: this.lastFinishReason,
|
|
693
|
+
}) && this.toolPhase === 'continue'
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private isAborted(): boolean {
|
|
698
|
+
return !!this.effectiveSignal?.aborted
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private setToolPhase(phase: ToolPhaseResult): void {
|
|
702
|
+
this.toolPhase = phase
|
|
703
|
+
if (phase === 'wait') {
|
|
704
|
+
this.shouldEmitStreamEnd = false
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private createId(prefix: string): string {
|
|
709
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Standalone chat streaming function with type inference from adapter
|
|
715
|
+
* Returns an async iterable of StreamChunks for streaming responses
|
|
716
|
+
* Includes automatic tool execution loop
|
|
717
|
+
*
|
|
718
|
+
* @param options Chat options
|
|
719
|
+
* @param options.adapter - AI adapter instance to use
|
|
720
|
+
* @param options.model - Model name (autocompletes based on adapter)
|
|
721
|
+
* @param options.messages - Conversation messages
|
|
722
|
+
* @param options.tools - Optional tools for function calling (auto-executed)
|
|
723
|
+
* @param options.agentLoopStrategy - Optional strategy for controlling tool execution loop
|
|
724
|
+
*
|
|
725
|
+
* @example
|
|
726
|
+
* ```typescript
|
|
727
|
+
* const stream = chat({
|
|
728
|
+
* adapter: openai(),
|
|
729
|
+
* model: 'gpt-4o',
|
|
730
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
731
|
+
* tools: [weatherTool], // Optional: auto-executed when called
|
|
732
|
+
* });
|
|
733
|
+
*
|
|
734
|
+
* for await (const chunk of stream) {
|
|
735
|
+
* if (chunk.type === 'content') {
|
|
736
|
+
* console.log(chunk.delta);
|
|
737
|
+
* }
|
|
738
|
+
* }
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
export async function* chat<
|
|
742
|
+
TAdapter extends AIAdapter<any, any, any, any, any, any>,
|
|
743
|
+
const TModel extends TAdapter extends AIAdapter<
|
|
744
|
+
infer Models,
|
|
745
|
+
any,
|
|
746
|
+
any,
|
|
747
|
+
any,
|
|
748
|
+
any,
|
|
749
|
+
any
|
|
750
|
+
>
|
|
751
|
+
? Models[number]
|
|
752
|
+
: string,
|
|
753
|
+
>(
|
|
754
|
+
options: ChatStreamOptionsForModel<TAdapter, TModel>,
|
|
755
|
+
): AsyncIterable<StreamChunk> {
|
|
756
|
+
const { adapter, ...chatOptions } = options
|
|
757
|
+
|
|
758
|
+
const engine = new ChatEngine({
|
|
759
|
+
adapter,
|
|
760
|
+
params: chatOptions as ChatOptions<
|
|
761
|
+
string,
|
|
762
|
+
Record<string, any>,
|
|
763
|
+
undefined,
|
|
764
|
+
Record<string, any>
|
|
765
|
+
>,
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
for await (const chunk of engine.chat()) {
|
|
769
|
+
yield chunk
|
|
770
|
+
}
|
|
771
|
+
}
|