fix(agent): run the outbound payload sanitizers on the summary request
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.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user