11import { logger } from "../../utils/logger.js" ;
22
33const TELEGRAM_MESSAGE_SAFE_LENGTH = 4000 ;
4+ const DEFAULT_STREAM_KEY = "default" ;
5+
6+ export type ToolStreamKey = "default" | "subagent" | "todo" ;
47
58interface ToolCallStreamerOptions {
69 throttleMs : number ;
@@ -15,6 +18,7 @@ interface StreamEntry {
1518}
1619
1720interface StreamState {
21+ key : ToolStreamKey ;
1822 sessionId : string ;
1923 entries : StreamEntry [ ] ;
2024 latestParts : string [ ] ;
@@ -114,26 +118,31 @@ export class ToolCallStreamer {
114118 this . deleteText = options . deleteText ;
115119 }
116120
117- append ( sessionId : string , text : string ) : void {
121+ append ( sessionId : string , text : string , streamKey : ToolStreamKey = DEFAULT_STREAM_KEY ) : void {
118122 const normalizedText = text . trim ( ) ;
119123 if ( ! sessionId || ! normalizedText ) {
120124 return ;
121125 }
122126
123- const state = this . getOrCreateState ( sessionId ) ;
127+ const state = this . getOrCreateState ( sessionId , streamKey ) ;
124128 state . entries . push ( { text : normalizedText } ) ;
125129 state . latestParts = buildParts ( state . entries ) ;
126130 this . ensureTimer ( state ) ;
127131 }
128132
129- replaceByPrefix ( sessionId : string , prefix : string , text : string ) : void {
133+ replaceByPrefix (
134+ sessionId : string ,
135+ prefix : string ,
136+ text : string ,
137+ streamKey : ToolStreamKey = DEFAULT_STREAM_KEY ,
138+ ) : void {
130139 const normalizedPrefix = prefix . trim ( ) ;
131140 const normalizedText = text . trim ( ) ;
132141 if ( ! sessionId || ! normalizedPrefix || ! normalizedText ) {
133142 return ;
134143 }
135144
136- const state = this . getOrCreateState ( sessionId ) ;
145+ const state = this . getOrCreateState ( sessionId , streamKey ) ;
137146 const existingEntry = state . entries . find ( ( entry ) => entry . prefix === normalizedPrefix ) ;
138147 if ( existingEntry ) {
139148 existingEntry . text = normalizedText ;
@@ -146,27 +155,24 @@ export class ToolCallStreamer {
146155 }
147156
148157 async flushSession ( sessionId : string , reason : string ) : Promise < void > {
149- const state = this . states . get ( sessionId ) ;
150- if ( ! state ) {
151- return ;
152- }
153-
154- this . clearTimer ( state ) ;
155- await this . enqueueTask ( state , ( ) => this . syncState ( state , reason ) ) ;
158+ const states = this . getStatesForSession ( sessionId ) ;
159+ await Promise . all (
160+ states . map ( async ( state ) => {
161+ this . clearTimer ( state ) ;
162+ await this . enqueueTask ( state , ( ) => this . syncState ( state , reason ) ) ;
163+ } ) ,
164+ ) ;
156165 }
157166
158167 async breakSession ( sessionId : string , reason : string ) : Promise < void > {
159- const state = this . states . get ( sessionId ) ;
160- if ( ! state ) {
161- return ;
168+ const states = this . getStatesForSession ( sessionId ) ;
169+ for ( const state of states ) {
170+ state . isBreaking = true ;
171+ this . clearTimer ( state ) ;
172+ await this . enqueueTask ( state , ( ) => this . syncState ( state , reason ) ) ;
173+ this . cancelState ( state ) ;
174+ this . removeState ( state ) ;
162175 }
163-
164- state . isBreaking = true ;
165- this . getOrCreateState ( sessionId ) ;
166- this . clearTimer ( state ) ;
167- await this . enqueueTask ( state , ( ) => this . syncState ( state , reason ) ) ;
168- this . cancelState ( state ) ;
169- this . removeState ( state ) ;
170176 logger . debug ( `[ToolCallStreamer] Broke session stream: session=${ sessionId } , reason=${ reason } ` ) ;
171177 }
172178
@@ -202,8 +208,20 @@ export class ToolCallStreamer {
202208 }
203209 }
204210
205- private getOrCreateState ( sessionId : string ) : StreamState {
206- const existing = this . states . get ( sessionId ) ;
211+ private getStateId ( sessionId : string , streamKey : ToolStreamKey ) : string {
212+ return `${ sessionId } :${ streamKey } ` ;
213+ }
214+
215+ private getStatesForSession ( sessionId : string ) : StreamState [ ] {
216+ return Array . from ( this . allStates ) . filter ( ( state ) => state . sessionId === sessionId ) ;
217+ }
218+
219+ private getOrCreateState (
220+ sessionId : string ,
221+ streamKey : ToolStreamKey = DEFAULT_STREAM_KEY ,
222+ ) : StreamState {
223+ const stateId = this . getStateId ( sessionId , streamKey ) ;
224+ const existing = this . states . get ( stateId ) ;
207225 if ( existing && ! existing . isBroken && ! existing . cancelled && ! existing . isBreaking ) {
208226 return existing ;
209227 }
@@ -214,6 +232,7 @@ export class ToolCallStreamer {
214232 }
215233
216234 const state : StreamState = {
235+ key : streamKey ,
217236 sessionId,
218237 entries : [ ] ,
219238 latestParts : [ ] ,
@@ -228,7 +247,7 @@ export class ToolCallStreamer {
228247 fatalErrorLogged : false ,
229248 } ;
230249
231- this . states . set ( sessionId , state ) ;
250+ this . states . set ( stateId , state ) ;
232251 this . allStates . add ( state ) ;
233252 return state ;
234253 }
@@ -279,8 +298,9 @@ export class ToolCallStreamer {
279298 }
280299
281300 private removeState ( state : StreamState ) : void {
282- if ( this . states . get ( state . sessionId ) === state ) {
283- this . states . delete ( state . sessionId ) ;
301+ const stateId = this . getStateId ( state . sessionId , state . key ) ;
302+ if ( this . states . get ( stateId ) === state ) {
303+ this . states . delete ( stateId ) ;
284304 }
285305
286306 this . allStates . delete ( state ) ;
0 commit comments