@@ -51,28 +51,43 @@ export type ToolCallStreamEvent = ApiStreamToolCallStartChunk | ApiStreamToolCal
5151 * provider-level raw chunks into start/delta/end events.
5252 */
5353export class NativeToolCallParser {
54+ private static readonly defaultScope = { }
55+
5456 // Streaming state management for argument accumulation (keyed by tool call id)
5557 // Note: name is string to accommodate dynamic MCP tools (mcp--serverName--toolName)
56- private static streamingToolCalls = new Map <
57- string ,
58- {
59- id : string
60- name : string
61- argumentsAccumulator : string
62- }
58+ private static streamingToolCallsByScope = new WeakMap <
59+ object ,
60+ Map < string , { id : string ; name : string ; argumentsAccumulator : string } >
6361 > ( )
6462
65- // Raw chunk tracking state (keyed by index from API stream)
66- private static rawChunkTracker = new Map <
67- number ,
68- {
69- id : string
70- name : string
71- hasStarted : boolean
72- deltaBuffer : string [ ]
73- }
63+ // Raw chunk tracking state (keyed by index from one API stream)
64+ private static rawChunkTrackersByScope = new WeakMap <
65+ object ,
66+ Map < number , { id : string ; name : string ; hasStarted : boolean ; deltaBuffer : string [ ] } >
7467 > ( )
7568
69+ public static createScope ( ) : object {
70+ return { }
71+ }
72+
73+ private static getStreamingToolCalls ( scope = this . defaultScope ) {
74+ let streamingToolCalls = this . streamingToolCallsByScope . get ( scope )
75+ if ( ! streamingToolCalls ) {
76+ streamingToolCalls = new Map ( )
77+ this . streamingToolCallsByScope . set ( scope , streamingToolCalls )
78+ }
79+ return streamingToolCalls
80+ }
81+
82+ private static getRawChunkTracker ( scope = this . defaultScope ) {
83+ let rawChunkTracker = this . rawChunkTrackersByScope . get ( scope )
84+ if ( ! rawChunkTracker ) {
85+ rawChunkTracker = new Map ( )
86+ this . rawChunkTrackersByScope . set ( scope , rawChunkTracker )
87+ }
88+ return rawChunkTracker
89+ }
90+
7691 private static coerceOptionalBoolean ( value : unknown ) : boolean | undefined {
7792 if ( typeof value === "boolean" ) {
7893 return value
@@ -96,16 +111,20 @@ export class NativeToolCallParser {
96111 * This is the entry point for providers that emit tool_call_partial chunks.
97112 * Returns an array of events to be processed by the consumer.
98113 */
99- public static processRawChunk ( chunk : {
100- index : number
101- id ?: string
102- name ?: string
103- arguments ?: string
104- } ) : ToolCallStreamEvent [ ] {
114+ public static processRawChunk (
115+ chunk : {
116+ index : number
117+ id ?: string
118+ name ?: string
119+ arguments ?: string
120+ } ,
121+ scope = this . defaultScope ,
122+ ) : ToolCallStreamEvent [ ] {
105123 const events : ToolCallStreamEvent [ ] = [ ]
106124 const { index, id, name, arguments : args } = chunk
125+ const rawChunkTracker = this . getRawChunkTracker ( scope )
107126
108- let tracked = this . rawChunkTracker . get ( index )
127+ let tracked = rawChunkTracker . get ( index )
109128
110129 // Initialize new tool call tracking when we receive an id
111130 if ( id && ! tracked ) {
@@ -115,7 +134,7 @@ export class NativeToolCallParser {
115134 hasStarted : false ,
116135 deltaBuffer : [ ] ,
117136 }
118- this . rawChunkTracker . set ( index , tracked )
137+ rawChunkTracker . set ( index , tracked )
119138 }
120139
121140 if ( ! tracked ) {
@@ -167,11 +186,15 @@ export class NativeToolCallParser {
167186 * Process stream finish reason.
168187 * Emits end events when finish_reason is 'tool_calls'.
169188 */
170- public static processFinishReason ( finishReason : string | null | undefined ) : ToolCallStreamEvent [ ] {
189+ public static processFinishReason (
190+ finishReason : string | null | undefined ,
191+ scope = this . defaultScope ,
192+ ) : ToolCallStreamEvent [ ] {
171193 const events : ToolCallStreamEvent [ ] = [ ]
194+ const rawChunkTracker = this . rawChunkTrackersByScope . get ( scope )
172195
173- if ( finishReason === "tool_calls" && this . rawChunkTracker . size > 0 ) {
174- for ( const [ , tracked ] of this . rawChunkTracker . entries ( ) ) {
196+ if ( finishReason === "tool_calls" && rawChunkTracker && rawChunkTracker . size > 0 ) {
197+ for ( const [ , tracked ] of rawChunkTracker . entries ( ) ) {
175198 events . push ( {
176199 type : "tool_call_end" ,
177200 id : tracked . id ,
@@ -186,20 +209,21 @@ export class NativeToolCallParser {
186209 * Finalize any remaining tool calls that weren't explicitly ended.
187210 * Should be called at the end of stream processing.
188211 */
189- public static finalizeRawChunks ( ) : ToolCallStreamEvent [ ] {
212+ public static finalizeRawChunks ( scope = this . defaultScope ) : ToolCallStreamEvent [ ] {
190213 const events : ToolCallStreamEvent [ ] = [ ]
214+ const rawChunkTracker = this . rawChunkTrackersByScope . get ( scope )
191215
192- if ( this . rawChunkTracker . size > 0 ) {
193- for ( const [ , tracked ] of this . rawChunkTracker . entries ( ) ) {
216+ if ( rawChunkTracker && rawChunkTracker . size > 0 ) {
217+ for ( const [ , tracked ] of rawChunkTracker . entries ( ) ) {
194218 if ( tracked . hasStarted ) {
195219 events . push ( {
196220 type : "tool_call_end" ,
197221 id : tracked . id ,
198222 } )
199223 }
200224 }
201- this . rawChunkTracker . clear ( )
202225 }
226+ this . rawChunkTrackersByScope . delete ( scope )
203227
204228 return events
205229 }
@@ -208,17 +232,17 @@ export class NativeToolCallParser {
208232 * Clear all raw chunk tracking state.
209233 * Should be called when a new API request starts.
210234 */
211- public static clearRawChunkState ( ) : void {
212- this . rawChunkTracker . clear ( )
235+ public static clearRawChunkState ( scope = this . defaultScope ) : void {
236+ this . rawChunkTrackersByScope . delete ( scope )
213237 }
214238
215239 /**
216240 * Start streaming a new tool call.
217241 * Initializes tracking for incremental argument parsing.
218242 * Accepts string to support both ToolName and dynamic MCP tools (mcp--serverName--toolName).
219243 */
220- public static startStreamingToolCall ( id : string , name : string ) : void {
221- this . streamingToolCalls . set ( id , {
244+ public static startStreamingToolCall ( id : string , name : string , scope = this . defaultScope ) : void {
245+ this . getStreamingToolCalls ( scope ) . set ( id , {
222246 id,
223247 name,
224248 argumentsAccumulator : "" ,
@@ -230,25 +254,25 @@ export class NativeToolCallParser {
230254 * Should be called when a new API request starts to prevent memory leaks
231255 * from interrupted streams.
232256 */
233- public static clearAllStreamingToolCalls ( ) : void {
234- this . streamingToolCalls . clear ( )
257+ public static clearAllStreamingToolCalls ( scope = this . defaultScope ) : void {
258+ this . streamingToolCallsByScope . delete ( scope )
235259 }
236260
237261 /**
238262 * Check if there are any active streaming tool calls.
239263 * Useful for debugging and testing.
240264 */
241- public static hasActiveStreamingToolCalls ( ) : boolean {
242- return this . streamingToolCalls . size > 0
265+ public static hasActiveStreamingToolCalls ( scope = this . defaultScope ) : boolean {
266+ return ( this . streamingToolCallsByScope . get ( scope ) ?. size ?? 0 ) > 0
243267 }
244268
245269 /**
246270 * Process a chunk of JSON arguments for a streaming tool call.
247271 * Uses partial-json-parser to extract values from incomplete JSON immediately.
248272 * Returns a partial ToolUse with currently parsed parameters.
249273 */
250- public static processStreamingChunk ( id : string , chunk : string ) : ToolUse | null {
251- const toolCall = this . streamingToolCalls . get ( id )
274+ public static processStreamingChunk ( id : string , chunk : string , scope = this . defaultScope ) : ToolUse | null {
275+ const toolCall = this . streamingToolCallsByScope . get ( scope ) ? .get ( id )
252276 if ( ! toolCall ) {
253277 return null
254278 }
@@ -291,8 +315,9 @@ export class NativeToolCallParser {
291315 * Finalize a streaming tool call.
292316 * Parses the complete JSON and returns the final ToolUse or McpToolUse.
293317 */
294- public static finalizeStreamingToolCall ( id : string ) : ToolUse | McpToolUse | null {
295- const toolCall = this . streamingToolCalls . get ( id )
318+ public static finalizeStreamingToolCall ( id : string , scope = this . defaultScope ) : ToolUse | McpToolUse | null {
319+ const streamingToolCalls = this . streamingToolCallsByScope . get ( scope )
320+ const toolCall = streamingToolCalls ?. get ( id )
296321 if ( ! toolCall ) {
297322 return null
298323 }
@@ -306,7 +331,10 @@ export class NativeToolCallParser {
306331 } )
307332
308333 // Clean up streaming state
309- this . streamingToolCalls . delete ( id )
334+ streamingToolCalls ?. delete ( id )
335+ if ( streamingToolCalls ?. size === 0 ) {
336+ this . streamingToolCallsByScope . delete ( scope )
337+ }
310338
311339 return finalToolUse
312340 }
0 commit comments