@tanstack/ai 0.4.2 → 0.5.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.
@@ -12,6 +12,9 @@
12
12
  * - Thinking/reasoning content
13
13
  * - Recording/replay for testing
14
14
  * - Event-driven architecture for UI updates
15
+ *
16
+ * @see docs/chat-architecture.md — Canonical reference for AG-UI chunk ordering,
17
+ * adapter contract, single-shot flows, and expected UIMessage output.
15
18
  */
16
19
  import { generateMessageId, uiMessageToModelMessages } from '../messages.js'
17
20
  import { defaultJSONParser } from './json-parser'
@@ -101,18 +104,18 @@ export interface StreamProcessorOptions {
101
104
  * StreamProcessor - State machine for processing AI response streams
102
105
  *
103
106
  * Manages the full UIMessage[] conversation and emits events on changes.
107
+ * Trusts the adapter contract: adapters emit clean AG-UI events in the
108
+ * correct order.
104
109
  *
105
110
  * State tracking:
106
111
  * - Full message array
107
112
  * - Current assistant message being streamed
108
- * - Text content accumulation
113
+ * - Text content accumulation (reset on TEXT_MESSAGE_START)
109
114
  * - Multiple parallel tool calls
110
- * - Tool call completion detection
115
+ * - Tool call completion via TOOL_CALL_END events
111
116
  *
112
- * Tool call completion is detected when:
113
- * 1. A new tool call starts at a different index
114
- * 2. Text content arrives
115
- * 3. Stream ends
117
+ * @see docs/chat-architecture.md#streamprocessor-internal-state — State field reference
118
+ * @see docs/chat-architecture.md#adapter-contract — What this class expects from adapters
116
119
  */
117
120
  export class StreamProcessor {
118
121
  private chunkStrategy: ChunkStrategy
@@ -134,9 +137,8 @@ export class StreamProcessor {
134
137
  private toolCalls: Map<string, InternalToolCallState> = new Map()
135
138
  private toolCallOrder: Array<string> = []
136
139
  private finishReason: string | null = null
140
+ private hasError = false
137
141
  private isDone = false
138
- // Track if we've had tool calls since the last text segment started
139
- private hasToolCallsSinceTextStart = false
140
142
 
141
143
  // Recording
142
144
  private recording: ChunkRecording | null = null
@@ -213,12 +215,52 @@ export class StreamProcessor {
213
215
  }
214
216
 
215
217
  /**
216
- * Start streaming a new assistant message
217
- * Returns the message ID
218
+ * Prepare for a new assistant message stream.
219
+ * Does NOT create the message immediately -- the message is created lazily
220
+ * when the first content-bearing chunk arrives via ensureAssistantMessage().
221
+ * This prevents empty assistant messages from flickering in the UI when
222
+ * auto-continuation produces no content.
218
223
  */
219
- startAssistantMessage(): string {
224
+ prepareAssistantMessage(): void {
220
225
  // Reset stream state for new message
221
226
  this.resetStreamState()
227
+ // Clear the current assistant message ID so ensureAssistantMessage()
228
+ // will create a fresh message on the first content chunk
229
+ this.currentAssistantMessageId = null
230
+ }
231
+
232
+ /**
233
+ * @deprecated Use prepareAssistantMessage() instead. This eagerly creates
234
+ * an assistant message which can cause empty message flicker.
235
+ */
236
+ startAssistantMessage(): string {
237
+ this.prepareAssistantMessage()
238
+ return this.ensureAssistantMessage()
239
+ }
240
+
241
+ /**
242
+ * Get the current assistant message ID (if one has been created).
243
+ * Returns null if prepareAssistantMessage() was called but no content
244
+ * has arrived yet.
245
+ */
246
+ getCurrentAssistantMessageId(): string | null {
247
+ return this.currentAssistantMessageId
248
+ }
249
+
250
+ /**
251
+ * Lazily create the assistant message if it hasn't been created yet.
252
+ * Called by content handlers on the first content-bearing chunk.
253
+ * Returns the message ID.
254
+ *
255
+ * Content-bearing chunks that trigger this:
256
+ * TEXT_MESSAGE_CONTENT, TOOL_CALL_START, STEP_FINISHED, RUN_ERROR.
257
+ *
258
+ * @see docs/chat-architecture.md#streamprocessor-internal-state — Lazy creation pattern
259
+ */
260
+ private ensureAssistantMessage(): string {
261
+ if (this.currentAssistantMessageId) {
262
+ return this.currentAssistantMessageId
263
+ }
222
264
 
223
265
  const assistantMessage: UIMessage = {
224
266
  id: generateMessageId(),
@@ -398,7 +440,13 @@ export class StreamProcessor {
398
440
  }
399
441
 
400
442
  /**
401
- * Process a single chunk from the stream
443
+ * Process a single chunk from the stream.
444
+ *
445
+ * Central dispatch for all AG-UI events. Each event type maps to a specific
446
+ * handler. Events not listed in the switch are intentionally ignored
447
+ * (RUN_STARTED, TEXT_MESSAGE_END, STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA).
448
+ *
449
+ * @see docs/chat-architecture.md#adapter-contract — Expected event types and ordering
402
450
  */
403
451
  processChunk(chunk: StreamChunk): void {
404
452
  // Record chunk if enabled
@@ -412,6 +460,10 @@ export class StreamProcessor {
412
460
 
413
461
  switch (chunk.type) {
414
462
  // AG-UI Events
463
+ case 'TEXT_MESSAGE_START':
464
+ this.handleTextMessageStartEvent()
465
+ break
466
+
415
467
  case 'TEXT_MESSAGE_CONTENT':
416
468
  this.handleTextMessageContentEvent(chunk)
417
469
  break
@@ -445,67 +497,53 @@ export class StreamProcessor {
445
497
  break
446
498
 
447
499
  default:
448
- // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
500
+ // RUN_STARTED, TEXT_MESSAGE_END, STEP_STARTED,
449
501
  // STATE_SNAPSHOT, STATE_DELTA - no special handling needed
450
502
  break
451
503
  }
452
504
  }
453
505
 
454
506
  /**
455
- * Handle TEXT_MESSAGE_CONTENT event
507
+ * Handle TEXT_MESSAGE_START event — marks the beginning of a new text segment.
508
+ * Resets segment accumulation so text after tool calls starts fresh.
509
+ *
510
+ * This is the key mechanism for multi-segment text (text before and after tool
511
+ * calls becoming separate TextParts). Without this reset, all text would merge
512
+ * into a single TextPart and tool-call interleaving would be lost.
513
+ *
514
+ * @see docs/chat-architecture.md#single-shot-text-response — Step-by-step text processing
515
+ * @see docs/chat-architecture.md#text-then-tool-interleaving-single-shot — Multi-segment text
516
+ */
517
+ private handleTextMessageStartEvent(): void {
518
+ // Emit any pending text from a previous segment before resetting
519
+ if (this.currentSegmentText !== this.lastEmittedText) {
520
+ this.emitTextUpdate()
521
+ }
522
+ this.currentSegmentText = ''
523
+ this.lastEmittedText = ''
524
+ }
525
+
526
+ /**
527
+ * Handle TEXT_MESSAGE_CONTENT event.
528
+ *
529
+ * Accumulates delta into both currentSegmentText (for UI emission) and
530
+ * totalTextContent (for ProcessorResult). Lazily creates the assistant
531
+ * UIMessage on first content. Uses updateTextPart() which replaces the
532
+ * last TextPart or creates a new one depending on part ordering.
533
+ *
534
+ * @see docs/chat-architecture.md#single-shot-text-response — Text accumulation step-by-step
535
+ * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
456
536
  */
457
537
  private handleTextMessageContentEvent(
458
538
  chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
459
539
  ): void {
460
- // Content arriving means all current tool calls are complete
461
- this.completeAllToolCalls()
462
-
463
- const previousSegment = this.currentSegmentText
464
-
465
- // Detect if this is a NEW text segment (after tool calls) vs continuation
466
- const isNewSegment =
467
- this.hasToolCallsSinceTextStart &&
468
- previousSegment.length > 0 &&
469
- this.isNewTextSegment(chunk, previousSegment)
470
-
471
- if (isNewSegment) {
472
- // Emit any accumulated text before starting new segment
473
- if (previousSegment !== this.lastEmittedText) {
474
- this.emitTextUpdate()
475
- }
476
- // Reset SEGMENT text accumulation for the new text segment after tool calls
477
- this.currentSegmentText = ''
478
- this.lastEmittedText = ''
479
- this.hasToolCallsSinceTextStart = false
480
- }
540
+ this.ensureAssistantMessage()
481
541
 
482
- const currentText = this.currentSegmentText
483
- let nextText = currentText
484
-
485
- // Prefer delta over content - delta is the incremental change
486
- // Check for both undefined and empty string to avoid "undefined" string concatenation
487
- if (chunk.delta !== undefined && chunk.delta !== '') {
488
- nextText = currentText + chunk.delta
489
- } else if (chunk.content !== undefined && chunk.content !== '') {
490
- // Fallback: use content if delta is not provided
491
- if (chunk.content.startsWith(currentText)) {
492
- nextText = chunk.content
493
- } else if (currentText.startsWith(chunk.content)) {
494
- nextText = currentText
495
- } else {
496
- nextText = currentText + chunk.content
497
- }
498
- }
499
-
500
- // Calculate the delta for totalTextContent
501
- const textDelta = nextText.slice(currentText.length)
502
- this.currentSegmentText = nextText
503
- this.totalTextContent += textDelta
542
+ this.currentSegmentText += chunk.delta
543
+ this.totalTextContent += chunk.delta
504
544
 
505
- // Use delta for chunk strategy if available
506
- const chunkPortion = chunk.delta || chunk.content || ''
507
545
  const shouldEmit = this.chunkStrategy.shouldEmit(
508
- chunkPortion,
546
+ chunk.delta,
509
547
  this.currentSegmentText,
510
548
  )
511
549
  if (shouldEmit && this.currentSegmentText !== this.lastEmittedText) {
@@ -514,13 +552,22 @@ export class StreamProcessor {
514
552
  }
515
553
 
516
554
  /**
517
- * Handle TOOL_CALL_START event
555
+ * Handle TOOL_CALL_START event.
556
+ *
557
+ * Creates a new InternalToolCallState entry in the toolCalls Map and appends
558
+ * a ToolCallPart to the UIMessage. Duplicate toolCallId is a no-op.
559
+ *
560
+ * CRITICAL: This MUST be received before any TOOL_CALL_ARGS for the same
561
+ * toolCallId. Args for unknown IDs are silently dropped.
562
+ *
563
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Tool call state transitions
564
+ * @see docs/chat-architecture.md#parallel-tool-calls-single-shot — Parallel tracking by ID
565
+ * @see docs/chat-architecture.md#adapter-contract — Ordering requirements
518
566
  */
519
567
  private handleToolCallStartEvent(
520
568
  chunk: Extract<StreamChunk, { type: 'TOOL_CALL_START' }>,
521
569
  ): void {
522
- // Mark that we've seen tool calls since the last text segment
523
- this.hasToolCallsSinceTextStart = true
570
+ this.ensureAssistantMessage()
524
571
 
525
572
  const toolCallId = chunk.toolCallId
526
573
  const existingToolCall = this.toolCalls.get(toolCallId)
@@ -567,7 +614,16 @@ export class StreamProcessor {
567
614
  }
568
615
 
569
616
  /**
570
- * Handle TOOL_CALL_ARGS event
617
+ * Handle TOOL_CALL_ARGS event.
618
+ *
619
+ * Appends the delta to the tool call's accumulated arguments string.
620
+ * Transitions state from awaiting-input → input-streaming on first non-empty delta.
621
+ * Attempts partial JSON parse on each update for UI preview.
622
+ *
623
+ * If toolCallId is not found in the Map (no preceding TOOL_CALL_START),
624
+ * this event is silently dropped.
625
+ *
626
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Step-by-step tool call processing
571
627
  */
572
628
  private handleToolCallArgsEvent(
573
629
  chunk: Extract<StreamChunk, { type: 'TOOL_CALL_ARGS' }>,
@@ -617,15 +673,53 @@ export class StreamProcessor {
617
673
  }
618
674
 
619
675
  /**
620
- * Handle TOOL_CALL_END event
676
+ * Handle TOOL_CALL_END event — authoritative signal that a tool call's input is finalized.
677
+ *
678
+ * This event has a DUAL ROLE:
679
+ * - Without `result`: Signals arguments are done (from adapter). Transitions to input-complete.
680
+ * - With `result`: Signals tool was executed and result is available (from TextEngine).
681
+ * Creates both output on the tool-call part AND a tool-result part.
682
+ *
683
+ * If `input` is provided, it overrides the accumulated string parse as the
684
+ * canonical parsed arguments.
685
+ *
686
+ * @see docs/chat-architecture.md#tool-results-and-the-tool_call_end-dual-role — Full explanation
687
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
621
688
  */
622
689
  private handleToolCallEndEvent(
623
690
  chunk: Extract<StreamChunk, { type: 'TOOL_CALL_END' }>,
624
691
  ): void {
625
- const state: ToolResultState = 'complete'
692
+ // Transition the tool call to input-complete (the authoritative completion signal)
693
+ const existingToolCall = this.toolCalls.get(chunk.toolCallId)
694
+ if (existingToolCall && existingToolCall.state !== 'input-complete') {
695
+ const index = this.toolCallOrder.indexOf(chunk.toolCallId)
696
+ this.completeToolCall(index, existingToolCall)
697
+ // If TOOL_CALL_END provides parsed input, use it as the canonical parsed
698
+ // arguments (overrides the accumulated string parse from completeToolCall)
699
+ if (chunk.input !== undefined) {
700
+ existingToolCall.parsedArguments = chunk.input
701
+ }
702
+ }
626
703
 
627
- // Update UIMessage if we have a current assistant message
704
+ // Update UIMessage if we have a current assistant message and a result
628
705
  if (this.currentAssistantMessageId && chunk.result) {
706
+ const state: ToolResultState = 'complete'
707
+
708
+ // Step 1: Update the tool-call part's output field (for UI consistency
709
+ // with client tools — see GitHub issue #176)
710
+ let output: unknown
711
+ try {
712
+ output = JSON.parse(chunk.result)
713
+ } catch {
714
+ output = chunk.result
715
+ }
716
+ this.messages = updateToolCallWithOutput(
717
+ this.messages,
718
+ chunk.toolCallId,
719
+ output,
720
+ )
721
+
722
+ // Step 2: Create/update the tool-result part (for LLM conversation history)
629
723
  this.messages = updateToolResultPart(
630
724
  this.messages,
631
725
  this.currentAssistantMessageId,
@@ -638,7 +732,14 @@ export class StreamProcessor {
638
732
  }
639
733
 
640
734
  /**
641
- * Handle RUN_FINISHED event
735
+ * Handle RUN_FINISHED event.
736
+ *
737
+ * Records the finishReason and calls completeAllToolCalls() as a safety net
738
+ * to force-complete any tool calls that didn't receive an explicit TOOL_CALL_END.
739
+ * This handles cases like aborted streams or adapter bugs.
740
+ *
741
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — finishReason semantics
742
+ * @see docs/chat-architecture.md#adapter-contract — Why RUN_FINISHED is mandatory
642
743
  */
643
744
  private handleRunFinishedEvent(
644
745
  chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
@@ -654,33 +755,26 @@ export class StreamProcessor {
654
755
  private handleRunErrorEvent(
655
756
  chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
656
757
  ): void {
758
+ this.hasError = true
759
+ this.ensureAssistantMessage()
657
760
  // Emit error event
658
761
  this.events.onError?.(new Error(chunk.error.message || 'An error occurred'))
659
762
  }
660
763
 
661
764
  /**
662
- * Handle STEP_FINISHED event (for thinking/reasoning content)
765
+ * Handle STEP_FINISHED event (for thinking/reasoning content).
766
+ *
767
+ * Accumulates delta into thinkingContent and updates a single ThinkingPart
768
+ * in the UIMessage (replaced in-place, not appended).
769
+ *
770
+ * @see docs/chat-architecture.md#thinkingreasoning-content — Thinking flow
663
771
  */
664
772
  private handleStepFinishedEvent(
665
773
  chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
666
774
  ): void {
667
- const previous = this.thinkingContent
668
- let nextThinking = previous
669
-
670
- // Prefer delta over content
671
- if (chunk.delta && chunk.delta !== '') {
672
- nextThinking = previous + chunk.delta
673
- } else if (chunk.content && chunk.content !== '') {
674
- if (chunk.content.startsWith(previous)) {
675
- nextThinking = chunk.content
676
- } else if (previous.startsWith(chunk.content)) {
677
- nextThinking = previous
678
- } else {
679
- nextThinking = previous + chunk.content
680
- }
681
- }
775
+ this.ensureAssistantMessage()
682
776
 
683
- this.thinkingContent = nextThinking
777
+ this.thinkingContent += chunk.delta
684
778
 
685
779
  // Update UIMessage
686
780
  if (this.currentAssistantMessageId) {
@@ -700,9 +794,14 @@ export class StreamProcessor {
700
794
  }
701
795
 
702
796
  /**
703
- * Handle CUSTOM event
704
- * Handles special custom events like 'tool-input-available' for client-side tool execution
705
- * and 'approval-requested' for tool approval flows
797
+ * Handle CUSTOM event.
798
+ *
799
+ * Handles special custom events emitted by the TextEngine (not adapters):
800
+ * - 'tool-input-available': Client tool needs execution. Fires onToolCall.
801
+ * - 'approval-requested': Tool needs user approval. Updates tool-call part
802
+ * state and fires onApprovalRequest.
803
+ *
804
+ * @see docs/chat-architecture.md#client-tools-and-approval-flows — Full flow details
706
805
  */
707
806
  private handleCustomEvent(
708
807
  chunk: Extract<StreamChunk, { type: 'CUSTOM' }>,
@@ -754,29 +853,13 @@ export class StreamProcessor {
754
853
  }
755
854
 
756
855
  /**
757
- * Detect if an incoming content chunk represents a NEW text segment
758
- */
759
- private isNewTextSegment(
760
- chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
761
- previous: string,
762
- ): boolean {
763
- // Check if content is present (delta is always defined but may be empty string)
764
- if (chunk.content !== undefined) {
765
- if (chunk.content.length < previous.length) {
766
- return true
767
- }
768
- if (
769
- !chunk.content.startsWith(previous) &&
770
- !previous.startsWith(chunk.content)
771
- ) {
772
- return true
773
- }
774
- }
775
- return false
776
- }
777
-
778
- /**
779
- * Complete all tool calls
856
+ * Complete all tool calls — safety net for stream termination.
857
+ *
858
+ * Called by RUN_FINISHED and finalizeStream(). Force-transitions any tool call
859
+ * not yet in input-complete state. Handles cases where TOOL_CALL_END was
860
+ * missed (adapter bug, network error, aborted stream).
861
+ *
862
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Safety net behavior
780
863
  */
781
864
  private completeAllToolCalls(): void {
782
865
  this.toolCalls.forEach((toolCall, id) => {
@@ -824,7 +907,13 @@ export class StreamProcessor {
824
907
  }
825
908
 
826
909
  /**
827
- * Emit pending text update
910
+ * Emit pending text update.
911
+ *
912
+ * Calls updateTextPart() which has critical append-vs-replace logic:
913
+ * - If last UIMessage part is TextPart → replaces its content (same segment).
914
+ * - If last part is anything else → pushes new TextPart (new segment after tools).
915
+ *
916
+ * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
828
917
  */
829
918
  private emitTextUpdate(): void {
830
919
  this.lastEmittedText = this.currentSegmentText
@@ -854,10 +943,16 @@ export class StreamProcessor {
854
943
  }
855
944
 
856
945
  /**
857
- * Finalize the stream - complete all pending operations
946
+ * Finalize the stream — complete all pending operations.
947
+ *
948
+ * Called when the async iterable ends (stream closed). Acts as the final
949
+ * safety net: completes any remaining tool calls, flushes un-emitted text,
950
+ * and fires onStreamEnd.
951
+ *
952
+ * @see docs/chat-architecture.md#single-shot-text-response — Finalization step
858
953
  */
859
954
  finalizeStream(): void {
860
- // Complete any remaining tool calls
955
+ // Safety net: complete any remaining tool calls (e.g. on network errors / aborted streams)
861
956
  this.completeAllToolCalls()
862
957
 
863
958
  // Emit any pending text if not already emitted
@@ -865,7 +960,25 @@ export class StreamProcessor {
865
960
  this.emitTextUpdate()
866
961
  }
867
962
 
868
- // Emit stream end event
963
+ // Remove the assistant message if it only contains whitespace text
964
+ // (no tool calls, no meaningful content). This handles models like Gemini
965
+ // that sometimes return just "\n" during auto-continuation.
966
+ // Preserve the message on errors so the UI can show error state.
967
+ if (this.currentAssistantMessageId && !this.hasError) {
968
+ const assistantMessage = this.messages.find(
969
+ (m) => m.id === this.currentAssistantMessageId,
970
+ )
971
+ if (assistantMessage && this.isWhitespaceOnlyMessage(assistantMessage)) {
972
+ this.messages = this.messages.filter(
973
+ (m) => m.id !== this.currentAssistantMessageId,
974
+ )
975
+ this.emitMessagesChange()
976
+ this.currentAssistantMessageId = null
977
+ return
978
+ }
979
+ }
980
+
981
+ // Emit stream end event (only if a message was actually created)
869
982
  if (this.currentAssistantMessageId) {
870
983
  const assistantMessage = this.messages.find(
871
984
  (m) => m.id === this.currentAssistantMessageId,
@@ -950,8 +1063,8 @@ export class StreamProcessor {
950
1063
  this.toolCalls.clear()
951
1064
  this.toolCallOrder = []
952
1065
  this.finishReason = null
1066
+ this.hasError = false
953
1067
  this.isDone = false
954
- this.hasToolCallsSinceTextStart = false
955
1068
  this.chunkStrategy.reset?.()
956
1069
  }
957
1070
 
@@ -964,6 +1077,17 @@ export class StreamProcessor {
964
1077
  this.currentAssistantMessageId = null
965
1078
  }
966
1079
 
1080
+ /**
1081
+ * Check if a message contains only whitespace text and no other meaningful parts
1082
+ * (no tool calls, tool results, thinking, etc.)
1083
+ */
1084
+ private isWhitespaceOnlyMessage(message: UIMessage): boolean {
1085
+ if (message.parts.length === 0) return false
1086
+ return message.parts.every(
1087
+ (part) => part.type === 'text' && part.content.trim() === '',
1088
+ )
1089
+ }
1090
+
967
1091
  /**
968
1092
  * Replay a recording through the processor
969
1093
  */
@@ -295,10 +295,7 @@ export async function executeToolCalls(
295
295
 
296
296
  // Parse arguments, throwing error if invalid JSON
297
297
  let input: unknown = {}
298
- let argsStr = toolCall.function.arguments.trim() || '{}'
299
- // Normalize "null" to "{}" — can occur when the model streams a tool_use
300
- // block with no input_json_delta events (Anthropic adapter edge case)
301
- if (argsStr === 'null') argsStr = '{}'
298
+ const argsStr = toolCall.function.arguments.trim() || '{}'
302
299
  if (argsStr) {
303
300
  try {
304
301
  input = JSON.parse(argsStr)
package/src/types.ts CHANGED
@@ -789,9 +789,9 @@ export interface TextMessageContentEvent extends BaseAGUIEvent {
789
789
  type: 'TEXT_MESSAGE_CONTENT'
790
790
  /** Message identifier */
791
791
  messageId: string
792
- /** The incremental content token (may be undefined if only content is provided) */
793
- delta?: string
794
- /** Full accumulated content so far */
792
+ /** The incremental content token */
793
+ delta: string
794
+ /** Full accumulated content so far (optional, for debugging) */
795
795
  content?: string
796
796
  }
797
797
 
@@ -864,8 +864,8 @@ export interface StepFinishedEvent extends BaseAGUIEvent {
864
864
  /** Step identifier */
865
865
  stepId: string
866
866
  /** Incremental thinking content */
867
- delta?: string
868
- /** Full accumulated thinking content */
867
+ delta: string
868
+ /** Full accumulated thinking content (optional, for debugging) */
869
869
  content?: string
870
870
  }
871
871