@@ -17,6 +17,7 @@ import type * as AI from 'ai';
1717
1818import type { DecoderOutput , MessageAccumulator } from '../../core/codec/types.js' ;
1919import { stripUndefined } from '../../utils.js' ;
20+ import { toolBase , transitionToolPart } from './tool-transitions.js' ;
2021
2122// ---------------------------------------------------------------------------
2223// Internal types
@@ -38,15 +39,6 @@ interface ToolPartTracker {
3839 inputText : string ;
3940}
4041
41- /** Fields shared by all DynamicToolUIPart state variants. */
42- interface ToolBaseFields {
43- type : 'dynamic-tool' ;
44- toolName : string ;
45- toolCallId : string ;
46- title ?: string ;
47- providerExecuted ?: boolean ;
48- }
49-
5042/** Bundled per-message state for an in-progress message. */
5143interface ActiveMessageState {
5244 message : AI . UIMessage ;
@@ -56,34 +48,6 @@ interface ActiveMessageState {
5648 streamStatus : Map < string , StreamStatus > ;
5749}
5850
59- // ---------------------------------------------------------------------------
60- // Tool base helper
61- // ---------------------------------------------------------------------------
62-
63- /**
64- * Extract the state-independent base fields for a DynamicToolUIPart.
65- * Works with both chunks (tool-input-start, etc.) and existing parts.
66- * @param source - Any object containing the required tool identity fields.
67- * @param source.toolCallId - The tool call identifier.
68- * @param source.toolName - The tool name.
69- * @param source.title - Optional display title.
70- * @param source.providerExecuted - Whether the provider executed the tool.
71- * @returns Base fields shared across all DynamicToolUIPart state variants.
72- */
73- const toolBase = ( source : {
74- toolCallId : string ;
75- toolName : string ;
76- title ?: string ;
77- providerExecuted ?: boolean ;
78- } ) : ToolBaseFields =>
79- stripUndefined ( {
80- type : 'dynamic-tool' as const ,
81- toolCallId : source . toolCallId ,
82- toolName : source . toolName ,
83- title : source . title ,
84- providerExecuted : source . providerExecuted ,
85- } ) ;
86-
8751// ---------------------------------------------------------------------------
8852// DeltaStreamTracker — manages text or reasoning stream accumulation
8953// ---------------------------------------------------------------------------
@@ -172,23 +136,13 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
172136 }
173137 }
174138
175- seedMessages ( messages : { messageId : string ; message : AI . UIMessage } [ ] ) : void {
176- for ( const { messageId, message } of messages ) {
177- this . _seedOne ( messageId , message ) ;
178- }
179- }
180-
181- completeSeeded ( messageId : string ) : void {
182- this . _activeMessages . delete ( messageId ) ;
183- }
184-
185- private _seedOne ( messageId : string , message : AI . UIMessage ) : void {
139+ initMessage ( messageId : string , message : AI . UIMessage ) : void {
186140 const existing = this . _activeMessages . get ( messageId ) ;
187141
188142 if ( existing ) {
189- // Update: sync the active state with an externally updated message.
143+ // Already active — sync with the externally updated message.
190144 // Replace the message and rebuild tool trackers so the accumulator
191- // reflects updates (e.g. tool results published via cross-turn events )
145+ // reflects updates (e.g. cross-turn amendments applied to the tree )
192146 // that happened outside the streaming flow.
193147 const cloned = structuredClone ( message ) ;
194148 const listIdx = this . _messageList . indexOf ( existing . message ) ;
@@ -207,16 +161,15 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
207161 return ;
208162 }
209163
164+ // Not active — create tracking state from the existing message.
210165 const cloned = structuredClone ( message ) ;
211166 const toolTrackers : Record < string , ToolPartTracker > = { } ;
212167 const streamStatus = new Map < string , StreamStatus > ( ) ;
213168
214- // Reconstruct tool trackers from existing dynamic-tool parts
215169 for ( let i = 0 ; i < cloned . parts . length ; i ++ ) {
216170 const part = cloned . parts [ i ] ;
217171 if ( part ?. type === 'dynamic-tool' ) {
218172 toolTrackers [ part . toolCallId ] = { partIndex : i , inputText : '' } ;
219- // All existing tool parts have completed their input phase
220173 streamStatus . set ( part . toolCallId , 'finished' ) ;
221174 }
222175 }
@@ -232,8 +185,7 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
232185 this . _activeMessages . set ( messageId , state ) ;
233186
234187 // If this message is already in the list (completed previously),
235- // replace it in-place so updates apply to the same reference.
236- // Otherwise push as a new entry.
188+ // replace in-place. Otherwise push as a new entry.
237189 const existingIdx = this . _messageList . findIndex ( ( m ) => m . id === message . id ) ;
238190 if ( existingIdx === - 1 ) {
239191 this . _messageList . push ( state . message ) ;
@@ -242,6 +194,10 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
242194 }
243195 }
244196
197+ completeMessage ( messageId : string ) : void {
198+ this . _activeMessages . delete ( messageId ) ;
199+ }
200+
245201 // -------------------------------------------------------------------------
246202 // Shared helpers
247203 // -------------------------------------------------------------------------
@@ -573,48 +529,7 @@ class DefaultUIMessageAccumulator implements MessageAccumulator<AI.UIMessageChun
573529 const found = this . _getToolPart ( chunk . toolCallId , state ) ;
574530 if ( ! found ) return ;
575531
576- switch ( chunk . type ) {
577- case 'tool-output-available' : {
578- state . message . parts [ found . tracker . partIndex ] = stripUndefined ( {
579- ...toolBase ( found . part ) ,
580- state : 'output-available' as const ,
581- input : found . part . input ,
582- output : chunk . output ,
583- preliminary : chunk . preliminary ,
584- } ) ;
585- break ;
586- }
587-
588- case 'tool-output-error' : {
589- state . message . parts [ found . tracker . partIndex ] = {
590- ...toolBase ( found . part ) ,
591- state : 'output-error' ,
592- input : found . part . input ,
593- errorText : chunk . errorText ,
594- } ;
595- break ;
596- }
597-
598- case 'tool-output-denied' : {
599- state . message . parts [ found . tracker . partIndex ] = {
600- ...toolBase ( found . part ) ,
601- state : 'output-denied' ,
602- input : found . part . input ,
603- approval : { id : '' , approved : false } ,
604- } ;
605- break ;
606- }
607-
608- case 'tool-approval-request' : {
609- state . message . parts [ found . tracker . partIndex ] = {
610- ...toolBase ( found . part ) ,
611- state : 'approval-requested' ,
612- input : found . part . input ,
613- approval : { id : chunk . approvalId } ,
614- } ;
615- break ;
616- }
617- }
532+ state . message . parts [ found . tracker . partIndex ] = transitionToolPart ( found . part , chunk ) ;
618533 }
619534
620535 // -------------------------------------------------------------------------
0 commit comments