From 973e8a8754265594c7516e602bd68912e65095ba Mon Sep 17 00:00:00 2001 From: ran Date: Thu, 26 Mar 2026 15:41:19 +0100 Subject: [PATCH 1/2] fix: inject synthetic tool results for pending frontend tool calls --- .../_message_adapters.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py index 5e4fced97c..d343602240 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py @@ -194,6 +194,26 @@ def _sanitize_tool_history(messages: list[Message]) -> list[Message]: pending_tool_call_ids = None pending_confirm_changes_id = None + # If the conversation ends with pending tool calls (e.g. a declaration-only + # frontend tool was called with no user message following), inject synthetic + # results so the next OpenAI call doesn't get a 400 for unmatched tool_call_ids. + if pending_tool_call_ids: + logger.info( + f"Messages ended 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}") + sanitized.append(Message( + role="tool", + contents=[ + Content.from_function_result( + call_id=pending_call_id, + result="Tool execution skipped - frontend tool result not yet received", + ) + ], + )) + return sanitized From e007ec189705ce64c5c73a3ea1cf1178f6301d61 Mon Sep 17 00:00:00 2001 From: ran Date: Fri, 27 Mar 2026 11:48:25 +0100 Subject: [PATCH 2/2] test: add unit test for end-of-history pending tool call injection --- .../ag-ui/tests/ag_ui/test_message_adapters.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py index 9508b53085..0477547dc7 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py +++ b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py @@ -984,6 +984,23 @@ def test_sanitize_json_confirm_changes_response(): assert len(result) >= 1 +def test_sanitize_pending_tool_at_end_of_history(): + """Messages ending with an assistant tool call and no following message inject a synthetic result.""" + from agent_framework_ag_ui._message_adapters import _sanitize_tool_history + + assistant_msg = Message( + role="assistant", + contents=[Content.from_function_call(call_id="c1", name="pieChart", arguments="{}")], + ) + + result = _sanitize_tool_history([assistant_msg]) + + tool_results = [m for m in result if m.role == "tool"] + assert len(tool_results) == 1 + assert tool_results[0].contents[0].call_id == "c1" + assert "skipped" in str(tool_results[0].contents[0].result).lower() + + # ── Deduplication edge cases ──