From e92dcb3d5d8eca580735d1b4b97607d4d1ebe2c6 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:57:59 +0900 Subject: [PATCH] Python: fix tool call id mismatch in ag-ui (#2166) * Fix state for pending requests bug * Bump ver * Update changelog --- python/CHANGELOG.md | 4 + .../agent_framework_ag_ui/_orchestrators.py | 83 +++++++++++++------ python/packages/ag-ui/pyproject.toml | 2 +- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 5b407ec16d..3c914b9fcb 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **agent-framework-azurefunctions**: Merge Azure Functions feature branch (#1916) +### Fixed + +- **agent-framework-ag-ui**: fix tool call id mismatch in ag-ui ([#2166](https://github.com/microsoft/agent-framework/pull/2166)) + ## [1.0.0b251112] - 2025-11-12 ### Added diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py b/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py index 3ebba6a66d..1f6a43d8c1 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py @@ -316,43 +316,66 @@ class DefaultOrchestrator(Orchestrator): ) continue - if role_value == "user" and pending_confirm_changes_id: - # Check if this is a confirm_changes response (JSON with "accepted" field) - user_text = "" - for content in msg.contents or []: - if isinstance(content, TextContent): - user_text = content.text - break + if role_value == "user": + # Check if this user message is a confirm_changes response (JSON with "accepted" field) + # This must be checked BEFORE injecting synthetic results for pending tool calls + if pending_confirm_changes_id: + user_text = "" + for content in msg.contents or []: + if isinstance(content, TextContent): + user_text = content.text + break - try: - parsed = json.loads(user_text) - if "accepted" in parsed: - # This is a confirm_changes response - inject synthetic tool result - logger.info( - f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}" - ) + try: + parsed = json.loads(user_text) + if "accepted" in parsed: + # This is a confirm_changes response - inject synthetic tool result + logger.info( + f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}" + ) + synthetic_result = ChatMessage( + role="tool", + contents=[ + FunctionResultContent( + call_id=pending_confirm_changes_id, + result="Confirmed" if parsed.get("accepted") else "Rejected", + ) + ], + ) + sanitized.append(synthetic_result) + if pending_tool_call_ids: + pending_tool_call_ids.discard(pending_confirm_changes_id) + pending_confirm_changes_id = None + # Don't add the user message to sanitized - it's been converted to tool result + continue + except (json.JSONDecodeError, KeyError) as e: + # Failed to parse user message as confirm_changes response; continue normal processing + logger.debug(f"Could not parse user message as confirm_changes response: {e}") + + # Before processing user message, check if there are pending tool calls without results + # This happens when assistant made multiple tool calls but only some got results + # This is checked AFTER confirm_changes special handling above + if pending_tool_call_ids: + logger.info( + f"User message arrived with {len(pending_tool_call_ids)} pending tool calls - injecting synthetic results" + ) + for pending_call_id in pending_tool_call_ids: + logger.info(f"Injecting synthetic tool result for pending call_id={pending_call_id}") synthetic_result = ChatMessage( role="tool", contents=[ FunctionResultContent( - call_id=pending_confirm_changes_id, - result="Confirmed" if parsed.get("accepted") else "Rejected", + call_id=pending_call_id, + result="Tool execution skipped - user provided follow-up message", ) ], ) sanitized.append(synthetic_result) - if pending_tool_call_ids: - pending_tool_call_ids.discard(pending_confirm_changes_id) - pending_confirm_changes_id = None - # Don't add the user message to sanitized - it's been converted to tool result - continue - except (json.JSONDecodeError, KeyError) as e: - # Failed to parse user message as confirm_changes response; continue normal processing - logger.debug(f"Could not parse user message as confirm_changes response: {e}") + pending_tool_call_ids = None + pending_confirm_changes_id = None - # Not a confirm_changes response, continue normal processing + # Normal user message processing sanitized.append(msg) - pending_tool_call_ids = None pending_confirm_changes_id = None continue @@ -365,6 +388,14 @@ class DefaultOrchestrator(Orchestrator): call_id = str(content.call_id) if call_id in pending_tool_call_ids: keep = True + # Note: We do NOT remove call_id from pending here. + # This allows duplicate tool results to pass through sanitization + # so the deduplicator can choose the best one (prefer non-empty results). + # We only clear pending_tool_call_ids when a user message arrives. + if call_id == pending_confirm_changes_id: + # For confirm_changes specifically, we do want to clear it + # since we only expect one response + pending_confirm_changes_id = None break if keep: sanitized.append(msg) diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index 63a4c60fdb..95b0f46bc3 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-framework-ag-ui" -version = "1.0.0b251112" +version = "1.0.0b251112.post1" description = "AG-UI protocol integration for Agent Framework" readme = "README.md" license-files = ["LICENSE"]