@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/esm/base-adapter.d.ts +35 -0
  4. package/dist/esm/base-adapter.js +12 -0
  5. package/dist/esm/base-adapter.js.map +1 -0
  6. package/dist/esm/core/chat-common-options.d.ts +52 -0
  7. package/dist/esm/core/chat.d.ts +30 -0
  8. package/dist/esm/core/chat.js +533 -0
  9. package/dist/esm/core/chat.js.map +1 -0
  10. package/dist/esm/core/embedding.d.ts +8 -0
  11. package/dist/esm/core/embedding.js +33 -0
  12. package/dist/esm/core/embedding.js.map +1 -0
  13. package/dist/esm/core/summarize.d.ts +9 -0
  14. package/dist/esm/core/summarize.js +36 -0
  15. package/dist/esm/core/summarize.js.map +1 -0
  16. package/dist/esm/event-client.d.ts +311 -0
  17. package/dist/esm/event-client.js +62 -0
  18. package/dist/esm/event-client.js.map +1 -0
  19. package/dist/esm/index.d.ts +16 -0
  20. package/dist/esm/index.js +50 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/message-converters.d.ts +52 -0
  23. package/dist/esm/message-converters.js +162 -0
  24. package/dist/esm/message-converters.js.map +1 -0
  25. package/dist/esm/stream/index.d.ts +11 -0
  26. package/dist/esm/stream/json-parser.d.ts +38 -0
  27. package/dist/esm/stream/json-parser.js +28 -0
  28. package/dist/esm/stream/json-parser.js.map +1 -0
  29. package/dist/esm/stream/message-updaters.d.ts +44 -0
  30. package/dist/esm/stream/message-updaters.js +141 -0
  31. package/dist/esm/stream/message-updaters.js.map +1 -0
  32. package/dist/esm/stream/processor.d.ts +242 -0
  33. package/dist/esm/stream/processor.js +693 -0
  34. package/dist/esm/stream/processor.js.map +1 -0
  35. package/dist/esm/stream/strategies.d.ts +43 -0
  36. package/dist/esm/stream/strategies.js +54 -0
  37. package/dist/esm/stream/strategies.js.map +1 -0
  38. package/dist/esm/stream/types.d.ts +71 -0
  39. package/dist/esm/tools/tool-calls.d.ts +112 -0
  40. package/dist/esm/tools/tool-calls.js +302 -0
  41. package/dist/esm/tools/tool-calls.js.map +1 -0
  42. package/dist/esm/tools/tool-definition.d.ts +125 -0
  43. package/dist/esm/tools/tool-definition.js +25 -0
  44. package/dist/esm/tools/tool-definition.js.map +1 -0
  45. package/dist/esm/tools/zod-converter.d.ts +30 -0
  46. package/dist/esm/tools/zod-converter.js +36 -0
  47. package/dist/esm/tools/zod-converter.js.map +1 -0
  48. package/dist/esm/types.d.ts +619 -0
  49. package/dist/esm/utilities/agent-loop-strategies.d.ts +59 -0
  50. package/dist/esm/utilities/agent-loop-strategies.js +23 -0
  51. package/dist/esm/utilities/agent-loop-strategies.js.map +1 -0
  52. package/dist/esm/utilities/chat-options.d.ts +6 -0
  53. package/dist/esm/utilities/chat-options.js +7 -0
  54. package/dist/esm/utilities/chat-options.js.map +1 -0
  55. package/dist/esm/utilities/messages.d.ts +30 -0
  56. package/dist/esm/utilities/messages.js +7 -0
  57. package/dist/esm/utilities/messages.js.map +1 -0
  58. package/dist/esm/utilities/stream-to-response.d.ts +48 -0
  59. package/dist/esm/utilities/stream-to-response.js +62 -0
  60. package/dist/esm/utilities/stream-to-response.js.map +1 -0
  61. package/package.json +65 -0
  62. package/src/base-adapter.ts +85 -0
  63. package/src/core/chat-common-options.ts +55 -0
  64. package/src/core/chat.ts +771 -0
  65. package/src/core/embedding.ts +54 -0
  66. package/src/core/summarize.ts +56 -0
  67. package/src/event-client.ts +389 -0
  68. package/src/index.ts +68 -0
  69. package/src/message-converters.ts +285 -0
  70. package/src/stream/index.ts +41 -0
  71. package/src/stream/json-parser.ts +58 -0
  72. package/src/stream/message-updaters.ts +275 -0
  73. package/src/stream/processor.ts +1092 -0
  74. package/src/stream/strategies.ts +78 -0
  75. package/src/stream/types.ts +94 -0
  76. package/src/tools/tool-calls.ts +471 -0
  77. package/src/tools/tool-definition.ts +206 -0
  78. package/src/tools/zod-converter.ts +85 -0
  79. package/src/types.ts +872 -0
  80. package/src/utilities/agent-loop-strategies.ts +85 -0
  81. package/src/utilities/chat-options.ts +35 -0
  82. package/src/utilities/messages.ts +63 -0
  83. package/src/utilities/stream-to-response.ts +116 -0
@@ -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
+ }