Skip to content

Commit e92dcb3

Browse files
authored
Python: fix tool call id mismatch in ag-ui (microsoft#2166)
* Fix state for pending requests bug * Bump ver * Update changelog
1 parent 3c874d0 commit e92dcb3

3 files changed

Lines changed: 63 additions & 28 deletions

File tree

python/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- **agent-framework-azurefunctions**: Merge Azure Functions feature branch (#1916)
1515

16+
### Fixed
17+
18+
- **agent-framework-ag-ui**: fix tool call id mismatch in ag-ui ([#2166](https://github.com/microsoft/agent-framework/pull/2166))
19+
1620
## [1.0.0b251112] - 2025-11-12
1721

1822
### Added

python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

python/packages/ag-ui/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agent-framework-ag-ui"
3-
version = "1.0.0b251112"
3+
version = "1.0.0b251112.post1"
44
description = "AG-UI protocol integration for Agent Framework"
55
readme = "README.md"
66
license-files = ["LICENSE"]

0 commit comments

Comments
 (0)