From 53cfca976690eaddf0ad997dc419250802efe9cf Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:32:34 +0000 Subject: [PATCH] fix(agent): run the outbound payload sanitizers on the summary request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building the summary through `_build_api_kwargs` means it now carries `tools`, but it still skipped the `_sanitize_structure_surrogates` / `_force_ascii_payload` chokepoint the main loop applies after building (turn_api_request). On cache-planned routes the main loop scrubs a deep copy of the tools, so `agent.tools` can still hold a lone surrogate that providers reject with a non-retryable 400 (#50959 class) — a request the tool-less summary never used to make. Apply the same two passes here. --- agent/chat_completion_helpers.py | 9 ++++++++- tests/agent/test_run_agent.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index ed69474018..fb93a63493 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -38,7 +38,8 @@ from agent.model_metadata import is_local_endpoint from agent.message_content import flatten_message_text from agent.message_metadata import append_message, stamp_message_timestamp from agent.message_sanitization import ( - _sanitize_surrogates, _repair_tool_call_arguments, normalize_finish_reason as _normalize_finish_reason, + _sanitize_structure_non_ascii, _sanitize_structure_surrogates, _sanitize_surrogates, + _repair_tool_call_arguments, normalize_finish_reason as _normalize_finish_reason, ) from agent.reasoning_summaries import append_streamed_reasoning_detail, separate_glued_reasoning_blocks from agent.stream_single_writer import claim_stream_writer, stream_writer_is_current @@ -2055,6 +2056,12 @@ def _chat_summary_attempt(agent, api_messages: list, api_request_id: str): # tool_choice="none": SGLang removes tools from its rendered prompt in that # mode, which would invalidate the cached prefix this path is preserving. summary_kwargs = agent._build_api_kwargs(api_messages) + # Same outbound chokepoint as turn_api_request: the summary now carries ``tools``, and on + # cache-planned routes the main loop scrubbed a deep copy, so ``agent.tools`` may still hold + # the lone surrogates / non-ASCII bytes the provider 400s on (#50959 class). + _sanitize_structure_surrogates(summary_kwargs) + if agent._force_ascii_payload: + _sanitize_structure_non_ascii(summary_kwargs) def _attempt(retry_count: int) -> str: summary_client = agent._ensure_primary_openai_client(reason="iteration_limit_summary_retry" if retry_count else "iteration_limit_summary") diff --git a/tests/agent/test_run_agent.py b/tests/agent/test_run_agent.py index 0dd326dc9f..eceaf5f4ba 100644 --- a/tests/agent/test_run_agent.py +++ b/tests/agent/test_run_agent.py @@ -2808,6 +2808,23 @@ class TestHandleMaxIterations: assert all(summary["model"] == ordinary["model"] for summary in summaries) assert all(summary["messages"][0] == ordinary["messages"][0] for summary in summaries) + def test_summary_request_scrubs_surrogates_in_tool_schema(self, agent): + """The summary rides the same outbound surrogate chokepoint as the main loop (#50959 class).""" + agent.client.chat.completions.create.return_value = _mock_response(content="Summary") + agent._cached_system_prompt = "You are helpful." + agent.tools = [{"type": "function", "function": { + "name": "web_search", "description": "lone surrogate \ud83d here", + "parameters": {"type": "object", "properties": {}}, + }}] + + result = agent._handle_max_iterations([{"role": "user", "content": "do stuff"}], 60) + + assert result == "Summary" + sent = agent.client.chat.completions.create.call_args.kwargs + description = sent["tools"][0]["function"]["description"] + assert "\ud83d" not in description + description.encode("utf-8") # a provider serializes this; lone surrogates raise here + def test_summary_tool_call_only_response_retries_once(self, agent): """A tool-only summary is scrubbed to empty and receives one retry.""" agent.client.chat.completions.create.side_effect = [