@@ -316,43 +316,66 @@ def sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
316316 )
317317 continue
318318
319- if role_value == "user" and pending_confirm_changes_id :
320- # Check if this is a confirm_changes response (JSON with "accepted" field)
321- user_text = ""
322- for content in msg .contents or []:
323- if isinstance (content , TextContent ):
324- user_text = content .text
325- break
319+ if role_value == "user" :
320+ # Check if this user message is a confirm_changes response (JSON with "accepted" field)
321+ # This must be checked BEFORE injecting synthetic results for pending tool calls
322+ if pending_confirm_changes_id :
323+ user_text = ""
324+ for content in msg .contents or []:
325+ if isinstance (content , TextContent ):
326+ user_text = content .text
327+ break
326328
327- try :
328- parsed = json .loads (user_text )
329- if "accepted" in parsed :
330- # This is a confirm_changes response - inject synthetic tool result
331- logger .info (
332- f"Injecting synthetic tool result for confirm_changes call_id={ pending_confirm_changes_id } "
333- )
329+ try :
330+ parsed = json .loads (user_text )
331+ if "accepted" in parsed :
332+ # This is a confirm_changes response - inject synthetic tool result
333+ logger .info (
334+ f"Injecting synthetic tool result for confirm_changes call_id={ pending_confirm_changes_id } "
335+ )
336+ synthetic_result = ChatMessage (
337+ role = "tool" ,
338+ contents = [
339+ FunctionResultContent (
340+ call_id = pending_confirm_changes_id ,
341+ result = "Confirmed" if parsed .get ("accepted" ) else "Rejected" ,
342+ )
343+ ],
344+ )
345+ sanitized .append (synthetic_result )
346+ if pending_tool_call_ids :
347+ pending_tool_call_ids .discard (pending_confirm_changes_id )
348+ pending_confirm_changes_id = None
349+ # Don't add the user message to sanitized - it's been converted to tool result
350+ continue
351+ except (json .JSONDecodeError , KeyError ) as e :
352+ # Failed to parse user message as confirm_changes response; continue normal processing
353+ logger .debug (f"Could not parse user message as confirm_changes response: { e } " )
354+
355+ # Before processing user message, check if there are pending tool calls without results
356+ # This happens when assistant made multiple tool calls but only some got results
357+ # This is checked AFTER confirm_changes special handling above
358+ if pending_tool_call_ids :
359+ logger .info (
360+ f"User message arrived with { len (pending_tool_call_ids )} pending tool calls - injecting synthetic results"
361+ )
362+ for pending_call_id in pending_tool_call_ids :
363+ logger .info (f"Injecting synthetic tool result for pending call_id={ pending_call_id } " )
334364 synthetic_result = ChatMessage (
335365 role = "tool" ,
336366 contents = [
337367 FunctionResultContent (
338- call_id = pending_confirm_changes_id ,
339- result = "Confirmed" if parsed . get ( "accepted" ) else "Rejected " ,
368+ call_id = pending_call_id ,
369+ result = "Tool execution skipped - user provided follow-up message " ,
340370 )
341371 ],
342372 )
343373 sanitized .append (synthetic_result )
344- if pending_tool_call_ids :
345- pending_tool_call_ids .discard (pending_confirm_changes_id )
346- pending_confirm_changes_id = None
347- # Don't add the user message to sanitized - it's been converted to tool result
348- continue
349- except (json .JSONDecodeError , KeyError ) as e :
350- # Failed to parse user message as confirm_changes response; continue normal processing
351- logger .debug (f"Could not parse user message as confirm_changes response: { e } " )
352-
353- # Not a confirm_changes response, continue normal processing
374+ pending_tool_call_ids = None
375+ pending_confirm_changes_id = None
376+
377+ # Normal user message processing
354378 sanitized .append (msg )
355- pending_tool_call_ids = None
356379 pending_confirm_changes_id = None
357380 continue
358381
@@ -365,6 +388,14 @@ def sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
365388 call_id = str (content .call_id )
366389 if call_id in pending_tool_call_ids :
367390 keep = True
391+ # Note: We do NOT remove call_id from pending here.
392+ # This allows duplicate tool results to pass through sanitization
393+ # so the deduplicator can choose the best one (prefer non-empty results).
394+ # We only clear pending_tool_call_ids when a user message arrives.
395+ if call_id == pending_confirm_changes_id :
396+ # For confirm_changes specifically, we do want to clear it
397+ # since we only expect one response
398+ pending_confirm_changes_id = None
368399 break
369400 if keep :
370401 sanitized .append (msg )
0 commit comments