-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Python: fix: buffer out-of-order tool results in _sanitize_tool_history #4946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,11 @@ def _sanitize_tool_history(messages: list[Message]) -> list[Message]: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Normalize tool ordering and inject synthetic results for AG-UI edge cases.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sanitized: list[Message] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending_tool_call_ids: set[str] | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Buffer individual function_result Contents keyed by call_id for tool messages that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # arrive before their assistant message (out-of-order history). Buffering at the Content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # level (not the Message level) prevents a multi-result tool message from being | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # re-injected multiple times or leaking unrelated results into the wrong assistant turn. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| orphaned_tool_results: dict[str, Any] = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending_confirm_changes_id: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for msg in messages: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -77,6 +82,20 @@ def _sanitize_tool_history(messages: list[Message]) -> list[Message]: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending_confirm_changes_id = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| str(confirm_changes_call.call_id) if confirm_changes_call and confirm_changes_call.call_id else None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Re-inject any buffered tool results that belong to this assistant message. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Build a single synthetic Message containing only the matched Contents so that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # unrelated results from the same original message are not re-emitted, and the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # same result is never appended more than once. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matched_contents = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for call_id in list(tool_ids): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if call_id in orphaned_tool_results: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matched_contents.append(orphaned_tool_results.pop(call_id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pending_tool_call_ids: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pending_tool_call_ids.discard(call_id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if matched_contents: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sanitized.append(Message(role="tool", contents=matched_contents)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+98
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for call_id in list(tool_ids): | |
| if call_id in orphaned_tool_results: | |
| sanitized.append(orphaned_tool_results.pop(call_id)) | |
| if pending_tool_call_ids: | |
| pending_tool_call_ids.discard(call_id) | |
| if tool_ids: | |
| # Group buffered tool messages by underlying Message object so that: | |
| # - Each original tool message is re-emitted at most once. | |
| # - We can filter contents to only the function_result entries matching | |
| # the current assistant message's tool_ids. | |
| grouped_by_message: dict[int, dict[str, Any]] = {} | |
| for call_id in list(tool_ids): | |
| msg_for_call = orphaned_tool_results.get(call_id) | |
| if not msg_for_call: | |
| continue | |
| msg_key = id(msg_for_call) | |
| group = grouped_by_message.setdefault( | |
| msg_key, {"message": msg_for_call, "call_ids": set()} | |
| ) | |
| group["call_ids"].add(call_id) | |
| for group in grouped_by_message.values(): | |
| msg_for_group: Message = cast(Message, group["message"]) | |
| call_ids_for_msg: set[str] = cast(set[str], group["call_ids"]) | |
| # Only keep function_result contents whose call_id matches one of the | |
| # tool_ids for this assistant message. This avoids re-emitting unrelated | |
| # function_result entries that belong to other tool calls. | |
| filtered_contents = [ | |
| c | |
| for c in (msg_for_group.contents or []) | |
| if getattr(c, "type", None) == "function_result" | |
| and getattr(c, "call_id", None) is not None | |
| and str(c.call_id) in call_ids_for_msg | |
| ] | |
| if filtered_contents: | |
| sanitized.append( | |
| Message(role=msg_for_group.role, contents=filtered_contents) | |
| ) | |
| # Mark these call_ids as consumed from both orphaned_tool_results and | |
| # pending_tool_call_ids so they are not processed again. | |
| for consumed_call_id in call_ids_for_msg: | |
| orphaned_tool_results.pop(consumed_call_id, None) | |
| if pending_tool_call_ids: | |
| pending_tool_call_ids.discard(consumed_call_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
orphaned_tool_resultsis declared asdict[str, Any]but it only storesContentinstances (function_result contents). Tightening this todict[str, Content](andmatched_contents: list[Content]) will improve type safety and avoid leakingAnyintoMessage(contents=...)in this typed package.