diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 364af69085..6a87c7c1e0 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -2353,7 +2353,7 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str: for msg in messages: api_msg = msg.copy() agent._copy_reasoning_content_for_api(msg, api_msg) - for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"): + for internal_field in ("reasoning", "finish_reason"): api_msg.pop(internal_field, None) # Strict OpenAI-compatible gateways (Fireworks-backed OpenCode Go, # Mistral, Moonshot/Kimi) reject any message key outside the Chat @@ -2376,8 +2376,6 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str: # sidecar-carrying message and re-prefilling the whole transcript # at exactly the moment the context is largest. substitute_api_content(api_msg) - for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]: - api_msg.pop(internal_key, None) if _needs_sanitize: # In MoA mode, agent.model is the virtual preset name, # not the actual aggregator model. Resolve the real @@ -2411,8 +2409,18 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str: # Same safety net as the main loop: drop thinking-only assistant # turns so Anthropic-family providers don't 400 the summary call. + # _thinking_prefill must survive until here so the drop pass can + # recognize stubs after reasoning fields are stripped. api_messages = agent._drop_thinking_only_and_merge_users(api_messages) + # Strip all remaining underscore-prefixed scaffolding keys before the + # wire. The summary path calls chat.completions.create() directly, + # bypassing the transport's universal underscore-key sweeper. + for api_msg in api_messages: + if isinstance(api_msg, dict): + for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]: + api_msg.pop(internal_key, None) + summary_extra_body = {} try: from agent.auxiliary_client import _fixed_temperature_for_model, OMIT_TEMPERATURE as _OMIT_TEMP diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 95648b0698..1a236e6eac 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -1907,13 +1907,8 @@ def run_conversation( # Remove finish_reason - not accepted by strict APIs (e.g. Mistral) if "finish_reason" in api_msg: api_msg.pop("finish_reason") - # ``_thinking_prefill`` deliberately survives here. The reasoning - # fields it accompanies were just stripped above, which leaves - # ``_is_thinking_only_assistant`` no way to recognize the stub, so - # ``_drop_thinking_only_and_merge_users`` below could never fire on - # it. The transport strips every leading-underscore key before the - # wire (agent/transports/chat_completions.py), so keeping it costs - # nothing. + # _thinking_prefill survives here intentionally: the drop pass below + # needs it. The transport strips all underscore keys before the wire. # Strip length-continuation marks; not every transport drops underscore keys. api_msg.pop("_length_continuation_fragment", None) api_msg.pop("_length_continuation_nudge", None) diff --git a/run_agent.py b/run_agent.py index 275cca799c..64d3491a84 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4626,14 +4626,8 @@ class AIAgent: return False if msg.get("tool_calls"): return False - # A prefill stub is thinking-only by construction: the loop only sets - # ``_thinking_prefill`` on a turn that had reasoning and no visible - # text. It has to be checked ahead of the content inspection below, - # because both things this function would otherwise key on are gone by - # the time the drop pass runs. The reasoning fields are stripped from - # the API copy for providers that don't echo them back, and - # ``repair_empty_non_final_messages`` rewrites a non-final stub's empty - # content to a placeholder, which would read here as real output. + # Prefill stubs are thinking-only by construction; check before content + # inspection since repair_empty_non_final_messages may have healed content. if msg.get("_thinking_prefill"): return True # Does it have any actual output?