From 98cadadd84fb1354fa5e999ed0e2f0eb294dbeae Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:26:52 +0530 Subject: [PATCH] refactor: extract _build_partial_stream_stub helper Deduplicates the SimpleNamespace stub-response construction that was copy-pasted between the tool-call-drop guard and the text-only-drop guard (both in interruptible_streaming_api_call). The third site (error-handler at ~L3775) has a structurally different shape (different role/reasoning/model/usage sources + _content_filter_terminated tag) and is left inline with an existing comment. --- agent/chat_completion_helpers.py | 74 +++++++++++++++++--------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 140805df663b..6e583f2cae42 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -2197,6 +2197,36 @@ def cleanup_task_resources(agent, task_id: str) -> None: logger.warning(f"Failed to cleanup browser for task {task_id}: {e}") +def _build_partial_stream_stub( + role, full_content, full_reasoning, model_name, usage_obj, *, + dropped_tool_names=None, +): + """Build a partial-stream-stub response for mid-stream drop scenarios. + + Used when the SSE stream ends without a ``finish_reason`` after + delivering content (text-only drops, tool-call-arg drops). The stub + is tagged ``PARTIAL_STREAM_STUB_ID`` with ``FINISH_REASON_LENGTH`` so + the conversation loop enters its continuation/retry path instead of + silently accepting truncated output as a complete turn (#32086). + """ + mock_message = SimpleNamespace( + role=role, + content=full_content, + tool_calls=None, + reasoning_content=full_reasoning, + ) + mock_choice = SimpleNamespace( + index=0, + message=mock_message, + finish_reason=FINISH_REASON_LENGTH, + ) + return SimpleNamespace( + id=PARTIAL_STREAM_STUB_ID, + model=model_name, + choices=[mock_choice], + usage=usage_obj, + _dropped_tool_names=dropped_tool_names or None, + ) def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=None): @@ -2978,24 +3008,11 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta= "mid-tool-call stream drop, not an output-length truncation.", _dropped_names, ) - full_reasoning = "".join(reasoning_parts) or None - mock_message = SimpleNamespace( - role=role, - content=full_content, - tool_calls=None, - reasoning_content=full_reasoning, - ) - mock_choice = SimpleNamespace( - index=0, - message=mock_message, - finish_reason=FINISH_REASON_LENGTH, - ) - return SimpleNamespace( - id=PARTIAL_STREAM_STUB_ID, - model=model_name, - choices=[mock_choice], - usage=usage_obj, - _dropped_tool_names=_dropped_names or None, + return _build_partial_stream_stub( + role, full_content, + "".join(reasoning_parts) or None, + model_name, usage_obj, + dropped_tool_names=_dropped_names or None, ) # Text-only stream drop: the upstream closed the connection (or the @@ -3013,23 +3030,10 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta= "Stream ended with no finish_reason after delivering text " "with no tool calls; treating as a mid-stream drop." ) - full_reasoning = "".join(reasoning_parts) or None - mock_message = SimpleNamespace( - role=role, - content=full_content, - tool_calls=None, - reasoning_content=full_reasoning, - ) - mock_choice = SimpleNamespace( - index=0, - message=mock_message, - finish_reason=FINISH_REASON_LENGTH, - ) - return SimpleNamespace( - id=PARTIAL_STREAM_STUB_ID, - model=model_name, - choices=[mock_choice], - usage=usage_obj, + return _build_partial_stream_stub( + role, full_content, + "".join(reasoning_parts) or None, + model_name, usage_obj, ) effective_finish_reason = finish_reason or "stop"