Python: fix: inject synthetic tool results for pending frontend tool calls#4935
Python: fix: inject synthetic tool results for pending frontend tool calls#4935ranst91 wants to merge 1 commit intomicrosoft:mainfrom
Conversation
|
@ranst91 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
Pull request overview
Extends AG-UI message sanitization so that when a conversation ends with an assistant tool_calls message (common for declaration-only/frontend tools), synthetic tool results are injected to prevent OpenAI from rejecting the next request due to unmatched tool_call_ids.
Changes:
- Inject synthetic
role="tool"function_resultmessages when the message list ends with pending tool calls. - Add logging to indicate pending tool calls were resolved via synthetic results.
| # 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", | ||
| ) | ||
| ], | ||
| )) |
There was a problem hiding this comment.
Injecting a synthetic tool result at the end will cause any later real tool result for the same call_id (arriving in a subsequent request) to be treated as unmatched and dropped by _sanitize_tool_history (the synthetic result clears pending_tool_call_ids before the real tool message is processed). If frontend tools can legitimately return results asynchronously, consider limiting this end-of-history injection to tools known to be result-less (declaration-only), or adjust the sanitization logic to prefer/replace the synthetic placeholder when a real tool result is later received.
| # 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 |
There was a problem hiding this comment.
This adds new behavior for the "assistant tool_calls at end of history" case, but there isn't a regression test covering it. Please add a unit test that passes [assistant(tool_call)] with no following user/tool message and asserts a synthetic role="tool" function_result is appended for the call_id.
|
Could you let us know, please, which version of |
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
Description
When an agent calls a declaration-only (frontend/client-side) tool — such as
a generative UI component like
pieChart— and the conversation ends withouta user message following, the tool call remains "pending" in the message
history with no corresponding tool result.
On the next request, OpenAI rejects the conversation with a 400 error:
"An assistant message with 'tool_calls' must be followed by tool messages
responding to each 'tool_call_id'."
_sanitize_tool_history already handles this when a user message arrives while
calls are pending — it injects synthetic results before the user turn. This
fix extends that logic to also cover the end-of-messages case, so pending
tool calls are always resolved before the history is sent to OpenAI.
Contribution Checklist