@tanstack/ai 0.2.2 → 0.3.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.
Files changed (41) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/index.js +206 -172
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/stream/index.d.ts +1 -1
  5. package/dist/esm/activities/chat/stream/processor.d.ts +22 -43
  6. package/dist/esm/activities/chat/stream/processor.js +89 -156
  7. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  8. package/dist/esm/activities/chat/tools/tool-calls.d.ts +22 -21
  9. package/dist/esm/activities/chat/tools/tool-calls.js +40 -27
  10. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  11. package/dist/esm/activities/generateImage/index.js +41 -1
  12. package/dist/esm/activities/generateImage/index.js.map +1 -1
  13. package/dist/esm/activities/generateSpeech/index.js +33 -1
  14. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  15. package/dist/esm/activities/generateTranscription/index.js +29 -1
  16. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  17. package/dist/esm/activities/generateVideo/index.js +50 -0
  18. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  19. package/dist/esm/activities/summarize/index.js +9 -8
  20. package/dist/esm/activities/summarize/index.js.map +1 -1
  21. package/dist/esm/event-client.d.ts +378 -254
  22. package/dist/esm/event-client.js.map +1 -1
  23. package/dist/esm/index.d.ts +2 -2
  24. package/dist/esm/stream-to-response.d.ts +1 -1
  25. package/dist/esm/stream-to-response.js +5 -3
  26. package/dist/esm/stream-to-response.js.map +1 -1
  27. package/dist/esm/types.d.ts +162 -46
  28. package/package.json +1 -1
  29. package/src/activities/chat/index.ts +267 -191
  30. package/src/activities/chat/stream/index.ts +1 -5
  31. package/src/activities/chat/stream/processor.ts +128 -269
  32. package/src/activities/chat/tools/tool-calls.ts +55 -49
  33. package/src/activities/generateImage/index.ts +47 -2
  34. package/src/activities/generateSpeech/index.ts +37 -1
  35. package/src/activities/generateTranscription/index.ts +33 -1
  36. package/src/activities/generateVideo/index.ts +54 -0
  37. package/src/activities/summarize/index.ts +10 -9
  38. package/src/event-client.ts +460 -256
  39. package/src/index.ts +2 -3
  40. package/src/stream-to-response.ts +8 -6
  41. package/src/types.ts +209 -65
@@ -23,14 +23,18 @@ import type { AnyTextAdapter } from './adapter'
23
23
  import type {
24
24
  AgentLoopStrategy,
25
25
  ConstrainedModelMessage,
26
- DoneStreamChunk,
27
26
  InferSchemaType,
28
27
  ModelMessage,
28
+ RunFinishedEvent,
29
29
  SchemaInput,
30
30
  StreamChunk,
31
+ TextMessageContentEvent,
31
32
  TextOptions,
32
33
  Tool,
33
34
  ToolCall,
35
+ ToolCallArgsEvent,
36
+ ToolCallEndEvent,
37
+ ToolCallStartEvent,
34
38
  } from '../../types'
35
39
 
36
40
  // ===========================
@@ -213,7 +217,9 @@ class TextEngine<
213
217
  private totalChunkCount = 0
214
218
  private currentMessageId: string | null = null
215
219
  private accumulatedContent = ''
216
- private doneChunk: DoneStreamChunk | null = null
220
+ private eventOptions?: Record<string, unknown>
221
+ private eventToolNames?: Array<string>
222
+ private finishedEvent: RunFinishedEvent | null = null
217
223
  private shouldEmitStreamEnd = true
218
224
  private earlyTermination = false
219
225
  private toolPhase: ToolPhaseResult = 'continue'
@@ -278,16 +284,7 @@ class TextEngine<
278
284
 
279
285
  private beforeRun(): void {
280
286
  this.streamStartTime = Date.now()
281
- const {
282
- model,
283
- tools,
284
- temperature,
285
- topP,
286
- maxTokens,
287
- metadata,
288
- modelOptions,
289
- conversationId,
290
- } = this.params
287
+ const { tools, temperature, topP, maxTokens, metadata } = this.params
291
288
 
292
289
  // Gather flattened options into an object for event emission
293
290
  const options: Record<string, unknown> = {}
@@ -296,26 +293,49 @@ class TextEngine<
296
293
  if (maxTokens !== undefined) options.maxTokens = maxTokens
297
294
  if (metadata !== undefined) options.metadata = metadata
298
295
 
299
- aiEventClient.emit('text:started', {
300
- requestId: this.requestId,
301
- streamId: this.streamId,
302
- model: model,
303
- provider: this.adapter.name,
304
- messageCount: this.initialMessageCount,
305
- hasTools: !!tools && tools.length > 0,
306
- streaming: true,
296
+ this.eventOptions = Object.keys(options).length > 0 ? options : undefined
297
+ this.eventToolNames = tools?.map((t) => t.name)
298
+
299
+ aiEventClient.emit('text:request:started', {
300
+ ...this.buildTextEventContext(),
307
301
  timestamp: Date.now(),
308
- clientId: conversationId,
309
- toolNames: tools?.map((t) => t.name),
310
- options: Object.keys(options).length > 0 ? options : undefined,
311
- modelOptions: modelOptions as Record<string, unknown> | undefined,
312
302
  })
313
303
 
314
- aiEventClient.emit('stream:started', {
315
- streamId: this.streamId,
316
- model,
317
- provider: this.adapter.name,
318
- timestamp: Date.now(),
304
+ // Always emit messages for tracking:
305
+ // - For existing conversations (with conversationId): only emit the latest user message
306
+ // - For new conversations (without conversationId): emit all messages for reconstruction
307
+ const messagesToEmit = this.params.conversationId
308
+ ? this.messages.slice(-1).filter((m) => m.role === 'user')
309
+ : this.messages
310
+
311
+ messagesToEmit.forEach((message, index) => {
312
+ const messageIndex = this.params.conversationId
313
+ ? this.messages.length - 1
314
+ : index
315
+ const messageId = this.createId('msg')
316
+ const baseContext = this.buildTextEventContext()
317
+ const content = this.getContentString(message.content)
318
+
319
+ aiEventClient.emit('text:message:created', {
320
+ ...baseContext,
321
+ messageId,
322
+ role: message.role,
323
+ content,
324
+ toolCalls: message.toolCalls,
325
+ messageIndex,
326
+ timestamp: Date.now(),
327
+ })
328
+
329
+ if (message.role === 'user') {
330
+ aiEventClient.emit('text:message:user', {
331
+ ...baseContext,
332
+ messageId,
333
+ role: 'user',
334
+ content,
335
+ messageIndex,
336
+ timestamp: Date.now(),
337
+ })
338
+ }
319
339
  })
320
340
  }
321
341
 
@@ -325,23 +345,13 @@ class TextEngine<
325
345
  }
326
346
 
327
347
  const now = Date.now()
328
-
329
- // Emit text:completed with final state
330
- aiEventClient.emit('text:completed', {
331
- requestId: this.requestId,
332
- streamId: this.streamId,
333
- model: this.params.model,
348
+ // Emit text:request:completed with final state
349
+ aiEventClient.emit('text:request:completed', {
350
+ ...this.buildTextEventContext(),
334
351
  content: this.accumulatedContent,
335
352
  messageId: this.currentMessageId || undefined,
336
353
  finishReason: this.lastFinishReason || undefined,
337
- usage: this.doneChunk?.usage,
338
- timestamp: now,
339
- })
340
-
341
- aiEventClient.emit('stream:ended', {
342
- requestId: this.requestId,
343
- streamId: this.streamId,
344
- totalChunks: this.totalChunkCount,
354
+ usage: this.finishedEvent?.usage,
345
355
  duration: now - this.streamStartTime,
346
356
  timestamp: now,
347
357
  })
@@ -366,7 +376,16 @@ class TextEngine<
366
376
  private beginIteration(): void {
367
377
  this.currentMessageId = this.createId('msg')
368
378
  this.accumulatedContent = ''
369
- this.doneChunk = null
379
+ this.finishedEvent = null
380
+
381
+ const baseContext = this.buildTextEventContext()
382
+ aiEventClient.emit('text:message:created', {
383
+ ...baseContext,
384
+ messageId: this.currentMessageId,
385
+ role: 'assistant',
386
+ content: '',
387
+ timestamp: Date.now(),
388
+ })
370
389
  }
371
390
 
372
391
  private async *streamModelResponse(): AsyncGenerator<StreamChunk> {
@@ -413,101 +432,96 @@ class TextEngine<
413
432
 
414
433
  private handleStreamChunk(chunk: StreamChunk): void {
415
434
  switch (chunk.type) {
416
- case 'content':
417
- this.handleContentChunk(chunk)
435
+ // AG-UI Events
436
+ case 'TEXT_MESSAGE_CONTENT':
437
+ this.handleTextMessageContentEvent(chunk)
438
+ break
439
+ case 'TOOL_CALL_START':
440
+ this.handleToolCallStartEvent(chunk)
418
441
  break
419
- case 'tool_call':
420
- this.handleToolCallChunk(chunk)
442
+ case 'TOOL_CALL_ARGS':
443
+ this.handleToolCallArgsEvent(chunk)
421
444
  break
422
- case 'tool_result':
423
- this.handleToolResultChunk(chunk)
445
+ case 'TOOL_CALL_END':
446
+ this.handleToolCallEndEvent(chunk)
424
447
  break
425
- case 'done':
426
- this.handleDoneChunk(chunk)
448
+ case 'RUN_FINISHED':
449
+ this.handleRunFinishedEvent(chunk)
427
450
  break
428
- case 'error':
429
- this.handleErrorChunk(chunk)
451
+ case 'RUN_ERROR':
452
+ this.handleRunErrorEvent(chunk)
430
453
  break
431
- case 'thinking':
432
- this.handleThinkingChunk(chunk)
454
+ case 'STEP_FINISHED':
455
+ this.handleStepFinishedEvent(chunk)
433
456
  break
457
+
434
458
  default:
459
+ // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
460
+ // STATE_SNAPSHOT, STATE_DELTA, CUSTOM
461
+ // - no special handling needed in chat activity
435
462
  break
436
463
  }
437
464
  }
438
465
 
439
- private handleContentChunk(chunk: Extract<StreamChunk, { type: 'content' }>) {
440
- this.accumulatedContent = chunk.content
441
- aiEventClient.emit('stream:chunk:content', {
442
- streamId: this.streamId,
466
+ // ===========================
467
+ // AG-UI Event Handlers
468
+ // ===========================
469
+
470
+ private handleTextMessageContentEvent(chunk: TextMessageContentEvent): void {
471
+ if (chunk.content) {
472
+ this.accumulatedContent = chunk.content
473
+ } else {
474
+ this.accumulatedContent += chunk.delta
475
+ }
476
+ aiEventClient.emit('text:chunk:content', {
477
+ ...this.buildTextEventContext(),
443
478
  messageId: this.currentMessageId || undefined,
444
- content: chunk.content,
479
+ content: this.accumulatedContent,
445
480
  delta: chunk.delta,
446
481
  timestamp: Date.now(),
447
482
  })
448
483
  }
449
484
 
450
- private handleToolCallChunk(
451
- chunk: Extract<StreamChunk, { type: 'tool_call' }>,
452
- ): void {
453
- this.toolCallManager.addToolCallChunk(chunk)
454
- aiEventClient.emit('stream:chunk:tool-call', {
455
- streamId: this.streamId,
485
+ private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
486
+ this.toolCallManager.addToolCallStartEvent(chunk)
487
+ aiEventClient.emit('text:chunk:tool-call', {
488
+ ...this.buildTextEventContext(),
456
489
  messageId: this.currentMessageId || undefined,
457
- toolCallId: chunk.toolCall.id,
458
- toolName: chunk.toolCall.function.name,
459
- index: chunk.index,
460
- arguments: chunk.toolCall.function.arguments,
490
+ toolCallId: chunk.toolCallId,
491
+ toolName: chunk.toolName,
492
+ index: chunk.index ?? 0,
493
+ arguments: '',
461
494
  timestamp: Date.now(),
462
495
  })
463
496
  }
464
497
 
465
- private handleToolResultChunk(
466
- chunk: Extract<StreamChunk, { type: 'tool_result' }>,
467
- ): void {
468
- aiEventClient.emit('stream:chunk:tool-result', {
469
- streamId: this.streamId,
498
+ private handleToolCallArgsEvent(chunk: ToolCallArgsEvent): void {
499
+ this.toolCallManager.addToolCallArgsEvent(chunk)
500
+ aiEventClient.emit('text:chunk:tool-call', {
501
+ ...this.buildTextEventContext(),
470
502
  messageId: this.currentMessageId || undefined,
471
503
  toolCallId: chunk.toolCallId,
472
- result: chunk.content,
504
+ toolName: '',
505
+ index: 0,
506
+ arguments: chunk.delta,
473
507
  timestamp: Date.now(),
474
508
  })
475
509
  }
476
510
 
477
- private handleDoneChunk(chunk: DoneStreamChunk): void {
478
- // Don't overwrite a tool_calls finishReason with a stop finishReason
479
- // This can happen when adapters send multiple done chunks
480
- if (
481
- this.doneChunk?.finishReason === 'tool_calls' &&
482
- chunk.finishReason === 'stop'
483
- ) {
484
- // Still emit the event and update lastFinishReason, but don't overwrite doneChunk
485
- this.lastFinishReason = chunk.finishReason
486
- aiEventClient.emit('stream:chunk:done', {
487
- streamId: this.streamId,
488
- messageId: this.currentMessageId || undefined,
489
- finishReason: chunk.finishReason,
490
- usage: chunk.usage,
491
- timestamp: Date.now(),
492
- })
493
-
494
- if (chunk.usage) {
495
- aiEventClient.emit('usage:tokens', {
496
- requestId: this.requestId,
497
- streamId: this.streamId,
498
- messageId: this.currentMessageId || undefined,
499
- model: this.params.model,
500
- usage: chunk.usage,
501
- timestamp: Date.now(),
502
- })
503
- }
504
- return
505
- }
511
+ private handleToolCallEndEvent(chunk: ToolCallEndEvent): void {
512
+ this.toolCallManager.completeToolCall(chunk)
513
+ aiEventClient.emit('text:chunk:tool-result', {
514
+ ...this.buildTextEventContext(),
515
+ messageId: this.currentMessageId || undefined,
516
+ toolCallId: chunk.toolCallId,
517
+ result: chunk.result || '',
518
+ timestamp: Date.now(),
519
+ })
520
+ }
506
521
 
507
- this.doneChunk = chunk
508
- this.lastFinishReason = chunk.finishReason
509
- aiEventClient.emit('stream:chunk:done', {
510
- streamId: this.streamId,
522
+ private handleRunFinishedEvent(chunk: RunFinishedEvent): void {
523
+ aiEventClient.emit('text:chunk:done', {
524
+ ...this.buildTextEventContext(),
511
525
  messageId: this.currentMessageId || undefined,
512
526
  finishReason: chunk.finishReason,
513
527
  usage: chunk.usage,
@@ -515,22 +529,32 @@ class TextEngine<
515
529
  })
516
530
 
517
531
  if (chunk.usage) {
518
- aiEventClient.emit('usage:tokens', {
519
- requestId: this.requestId,
520
- streamId: this.streamId,
532
+ aiEventClient.emit('text:usage', {
533
+ ...this.buildTextEventContext(),
521
534
  messageId: this.currentMessageId || undefined,
522
- model: this.params.model,
523
535
  usage: chunk.usage,
524
536
  timestamp: Date.now(),
525
537
  })
526
538
  }
539
+
540
+ // Don't overwrite a tool_calls finishReason with a stop finishReason
541
+ if (
542
+ this.finishedEvent?.finishReason === 'tool_calls' &&
543
+ chunk.finishReason === 'stop'
544
+ ) {
545
+ this.lastFinishReason = chunk.finishReason
546
+ return
547
+ }
548
+
549
+ this.finishedEvent = chunk
550
+ this.lastFinishReason = chunk.finishReason
527
551
  }
528
552
 
529
- private handleErrorChunk(
530
- chunk: Extract<StreamChunk, { type: 'error' }>,
553
+ private handleRunErrorEvent(
554
+ chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
531
555
  ): void {
532
- aiEventClient.emit('stream:chunk:error', {
533
- streamId: this.streamId,
556
+ aiEventClient.emit('text:chunk:error', {
557
+ ...this.buildTextEventContext(),
534
558
  messageId: this.currentMessageId || undefined,
535
559
  error: chunk.error.message,
536
560
  timestamp: Date.now(),
@@ -539,16 +563,19 @@ class TextEngine<
539
563
  this.shouldEmitStreamEnd = false
540
564
  }
541
565
 
542
- private handleThinkingChunk(
543
- chunk: Extract<StreamChunk, { type: 'thinking' }>,
566
+ private handleStepFinishedEvent(
567
+ chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
544
568
  ): void {
545
- aiEventClient.emit('stream:chunk:thinking', {
546
- streamId: this.streamId,
547
- messageId: this.currentMessageId || undefined,
548
- content: chunk.content,
549
- delta: chunk.delta,
550
- timestamp: Date.now(),
551
- })
569
+ // Handle thinking/reasoning content from STEP_FINISHED events
570
+ if (chunk.content || chunk.delta) {
571
+ aiEventClient.emit('text:chunk:thinking', {
572
+ ...this.buildTextEventContext(),
573
+ messageId: this.currentMessageId || undefined,
574
+ content: chunk.content || '',
575
+ delta: chunk.delta,
576
+ timestamp: Date.now(),
577
+ })
578
+ }
552
579
  }
553
580
 
554
581
  private async *checkForPendingToolCalls(): AsyncGenerator<
@@ -561,16 +588,7 @@ class TextEngine<
561
588
  return 'continue'
562
589
  }
563
590
 
564
- const doneChunk = this.createSyntheticDoneChunk()
565
-
566
- aiEventClient.emit('text:iteration', {
567
- requestId: this.requestId,
568
- streamId: this.streamId,
569
- iterationNumber: this.iterationCount + 1,
570
- messageCount: this.messages.length,
571
- toolCallCount: pendingToolCalls.length,
572
- timestamp: Date.now(),
573
- })
591
+ const finishEvent = this.createSyntheticFinishedEvent()
574
592
 
575
593
  const { approvals, clientToolResults } = this.collectClientState()
576
594
 
@@ -587,14 +605,14 @@ class TextEngine<
587
605
  ) {
588
606
  for (const chunk of this.emitApprovalRequests(
589
607
  executionResult.needsApproval,
590
- doneChunk,
608
+ finishEvent,
591
609
  )) {
592
610
  yield chunk
593
611
  }
594
612
 
595
613
  for (const chunk of this.emitClientToolInputs(
596
614
  executionResult.needsClientExecution,
597
- doneChunk,
615
+ finishEvent,
598
616
  )) {
599
617
  yield chunk
600
618
  }
@@ -605,7 +623,7 @@ class TextEngine<
605
623
 
606
624
  const toolResultChunks = this.emitToolResults(
607
625
  executionResult.results,
608
- doneChunk,
626
+ finishEvent,
609
627
  )
610
628
 
611
629
  for (const chunk of toolResultChunks) {
@@ -622,22 +640,13 @@ class TextEngine<
622
640
  }
623
641
 
624
642
  const toolCalls = this.toolCallManager.getToolCalls()
625
- const doneChunk = this.doneChunk
643
+ const finishEvent = this.finishedEvent
626
644
 
627
- if (!doneChunk || toolCalls.length === 0) {
645
+ if (!finishEvent || toolCalls.length === 0) {
628
646
  this.setToolPhase('stop')
629
647
  return
630
648
  }
631
649
 
632
- aiEventClient.emit('text:iteration', {
633
- requestId: this.requestId,
634
- streamId: this.streamId,
635
- iterationNumber: this.iterationCount + 1,
636
- messageCount: this.messages.length,
637
- toolCallCount: toolCalls.length,
638
- timestamp: Date.now(),
639
- })
640
-
641
650
  this.addAssistantToolCallMessage(toolCalls)
642
651
 
643
652
  const { approvals, clientToolResults } = this.collectClientState()
@@ -655,14 +664,14 @@ class TextEngine<
655
664
  ) {
656
665
  for (const chunk of this.emitApprovalRequests(
657
666
  executionResult.needsApproval,
658
- doneChunk,
667
+ finishEvent,
659
668
  )) {
660
669
  yield chunk
661
670
  }
662
671
 
663
672
  for (const chunk of this.emitClientToolInputs(
664
673
  executionResult.needsClientExecution,
665
- doneChunk,
674
+ finishEvent,
666
675
  )) {
667
676
  yield chunk
668
677
  }
@@ -673,7 +682,7 @@ class TextEngine<
673
682
 
674
683
  const toolResultChunks = this.emitToolResults(
675
684
  executionResult.results,
676
- doneChunk,
685
+ finishEvent,
677
686
  )
678
687
 
679
688
  for (const chunk of toolResultChunks) {
@@ -687,13 +696,14 @@ class TextEngine<
687
696
 
688
697
  private shouldExecuteToolPhase(): boolean {
689
698
  return (
690
- this.doneChunk?.finishReason === 'tool_calls' &&
699
+ this.finishedEvent?.finishReason === 'tool_calls' &&
691
700
  this.tools.length > 0 &&
692
701
  this.toolCallManager.hasToolCalls()
693
702
  )
694
703
  }
695
704
 
696
705
  private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
706
+ const messageId = this.currentMessageId ?? this.createId('msg')
697
707
  this.messages = [
698
708
  ...this.messages,
699
709
  {
@@ -702,6 +712,15 @@ class TextEngine<
702
712
  toolCalls,
703
713
  },
704
714
  ]
715
+
716
+ aiEventClient.emit('text:message:created', {
717
+ ...this.buildTextEventContext(),
718
+ messageId,
719
+ role: 'assistant',
720
+ content: this.accumulatedContent || '',
721
+ toolCalls,
722
+ timestamp: Date.now(),
723
+ })
705
724
  }
706
725
 
707
726
  private collectClientState(): {
@@ -740,13 +759,13 @@ class TextEngine<
740
759
 
741
760
  private emitApprovalRequests(
742
761
  approvals: Array<ApprovalRequest>,
743
- doneChunk: DoneStreamChunk,
762
+ finishEvent: RunFinishedEvent,
744
763
  ): Array<StreamChunk> {
745
764
  const chunks: Array<StreamChunk> = []
746
765
 
747
766
  for (const approval of approvals) {
748
- aiEventClient.emit('stream:approval-requested', {
749
- streamId: this.streamId,
767
+ aiEventClient.emit('tools:approval:requested', {
768
+ ...this.buildTextEventContext(),
750
769
  messageId: this.currentMessageId || undefined,
751
770
  toolCallId: approval.toolCallId,
752
771
  toolName: approval.toolName,
@@ -755,17 +774,20 @@ class TextEngine<
755
774
  timestamp: Date.now(),
756
775
  })
757
776
 
777
+ // Emit a CUSTOM event for approval requests
758
778
  chunks.push({
759
- type: 'approval-requested',
760
- id: doneChunk.id,
761
- model: doneChunk.model,
779
+ type: 'CUSTOM',
762
780
  timestamp: Date.now(),
763
- toolCallId: approval.toolCallId,
764
- toolName: approval.toolName,
765
- input: approval.input,
766
- approval: {
767
- id: approval.approvalId,
768
- needsApproval: true,
781
+ model: finishEvent.model,
782
+ name: 'approval-requested',
783
+ data: {
784
+ toolCallId: approval.toolCallId,
785
+ toolName: approval.toolName,
786
+ input: approval.input,
787
+ approval: {
788
+ id: approval.approvalId,
789
+ needsApproval: true,
790
+ },
769
791
  },
770
792
  })
771
793
  }
@@ -775,13 +797,13 @@ class TextEngine<
775
797
 
776
798
  private emitClientToolInputs(
777
799
  clientRequests: Array<ClientToolRequest>,
778
- doneChunk: DoneStreamChunk,
800
+ finishEvent: RunFinishedEvent,
779
801
  ): Array<StreamChunk> {
780
802
  const chunks: Array<StreamChunk> = []
781
803
 
782
804
  for (const clientTool of clientRequests) {
783
- aiEventClient.emit('stream:tool-input-available', {
784
- streamId: this.streamId,
805
+ aiEventClient.emit('tools:input:available', {
806
+ ...this.buildTextEventContext(),
785
807
  messageId: this.currentMessageId || undefined,
786
808
  toolCallId: clientTool.toolCallId,
787
809
  toolName: clientTool.toolName,
@@ -789,14 +811,17 @@ class TextEngine<
789
811
  timestamp: Date.now(),
790
812
  })
791
813
 
814
+ // Emit a CUSTOM event for client tool inputs
792
815
  chunks.push({
793
- type: 'tool-input-available',
794
- id: doneChunk.id,
795
- model: doneChunk.model,
816
+ type: 'CUSTOM',
796
817
  timestamp: Date.now(),
797
- toolCallId: clientTool.toolCallId,
798
- toolName: clientTool.toolName,
799
- input: clientTool.input,
818
+ model: finishEvent.model,
819
+ name: 'tool-input-available',
820
+ data: {
821
+ toolCallId: clientTool.toolCallId,
822
+ toolName: clientTool.toolName,
823
+ input: clientTool.input,
824
+ },
800
825
  })
801
826
  }
802
827
 
@@ -805,14 +830,13 @@ class TextEngine<
805
830
 
806
831
  private emitToolResults(
807
832
  results: Array<ToolResult>,
808
- doneChunk: DoneStreamChunk,
833
+ finishEvent: RunFinishedEvent,
809
834
  ): Array<StreamChunk> {
810
835
  const chunks: Array<StreamChunk> = []
811
836
 
812
837
  for (const result of results) {
813
- aiEventClient.emit('tool:call-completed', {
814
- requestId: this.requestId,
815
- streamId: this.streamId,
838
+ aiEventClient.emit('tools:call:completed', {
839
+ ...this.buildTextEventContext(),
816
840
  messageId: this.currentMessageId || undefined,
817
841
  toolCallId: result.toolCallId,
818
842
  toolName: result.toolName,
@@ -822,16 +846,16 @@ class TextEngine<
822
846
  })
823
847
 
824
848
  const content = JSON.stringify(result.result)
825
- const chunk: Extract<StreamChunk, { type: 'tool_result' }> = {
826
- type: 'tool_result',
827
- id: doneChunk.id,
828
- model: doneChunk.model,
849
+
850
+ // Emit TOOL_CALL_END event
851
+ chunks.push({
852
+ type: 'TOOL_CALL_END',
829
853
  timestamp: Date.now(),
854
+ model: finishEvent.model,
830
855
  toolCallId: result.toolCallId,
831
- content,
832
- }
833
-
834
- chunks.push(chunk)
856
+ toolName: result.toolName,
857
+ result: content,
858
+ })
835
859
 
836
860
  this.messages = [
837
861
  ...this.messages,
@@ -841,6 +865,14 @@ class TextEngine<
841
865
  toolCallId: result.toolCallId,
842
866
  },
843
867
  ]
868
+
869
+ aiEventClient.emit('text:message:created', {
870
+ ...this.buildTextEventContext(),
871
+ messageId: this.createId('msg'),
872
+ role: 'tool',
873
+ content,
874
+ timestamp: Date.now(),
875
+ })
844
876
  }
845
877
 
846
878
  return chunks
@@ -868,10 +900,10 @@ class TextEngine<
868
900
  return pending
869
901
  }
870
902
 
871
- private createSyntheticDoneChunk(): DoneStreamChunk {
903
+ private createSyntheticFinishedEvent(): RunFinishedEvent {
872
904
  return {
873
- type: 'done',
874
- id: this.createId('pending'),
905
+ type: 'RUN_FINISHED',
906
+ runId: this.createId('pending'),
875
907
  model: this.params.model,
876
908
  timestamp: Date.now(),
877
909
  finishReason: 'tool_calls',
@@ -896,6 +928,50 @@ class TextEngine<
896
928
  return !!this.effectiveSignal?.aborted
897
929
  }
898
930
 
931
+ private buildTextEventContext(): {
932
+ requestId: string
933
+ streamId: string
934
+ provider: string
935
+ model: string
936
+ clientId?: string
937
+ source?: 'client' | 'server'
938
+ systemPrompts?: Array<string>
939
+ toolNames?: Array<string>
940
+ options?: Record<string, unknown>
941
+ modelOptions?: Record<string, unknown>
942
+ messageCount: number
943
+ hasTools: boolean
944
+ streaming: boolean
945
+ } {
946
+ return {
947
+ requestId: this.requestId,
948
+ streamId: this.streamId,
949
+ provider: this.adapter.name,
950
+ model: this.params.model,
951
+ clientId: this.params.conversationId,
952
+ source: 'server',
953
+ systemPrompts:
954
+ this.systemPrompts.length > 0 ? this.systemPrompts : undefined,
955
+ toolNames: this.eventToolNames,
956
+ options: this.eventOptions,
957
+ modelOptions: this.params.modelOptions as
958
+ | Record<string, unknown>
959
+ | undefined,
960
+ messageCount: this.initialMessageCount,
961
+ hasTools: this.tools.length > 0,
962
+ streaming: true,
963
+ }
964
+ }
965
+
966
+ private getContentString(content: ModelMessage['content']): string {
967
+ if (typeof content === 'string') return content
968
+ const text =
969
+ content
970
+ ?.map((part) => (part.type === 'text' ? part.content : ''))
971
+ .join('') || ''
972
+ return text
973
+ }
974
+
899
975
  private setToolPhase(phase: ToolPhaseResult): void {
900
976
  this.toolPhase = phase
901
977
  if (phase === 'wait') {