From 4fe2132c95c5eeb57048f918521eed18c0d05fcc Mon Sep 17 00:00:00 2001 From: HiddenPuppy Date: Tue, 14 Apr 2026 13:43:40 +0800 Subject: [PATCH 1/2] Bugfix: avoid stale tool-turn fallback on empty responses --- run_agent.py | 36 +++++++++-------- tests/run_agent/test_run_agent.py | 66 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/run_agent.py b/run_agent.py index 5922534646c1..b63d89265a7f 100644 --- a/run_agent.py +++ b/run_agent.py @@ -9985,25 +9985,29 @@ def _stop_spinner(): assistant_msg = self._build_assistant_message(assistant_message, finish_reason) - # If this turn has both content AND tool_calls, capture the content - # as a fallback final response. Common pattern: model delivers its - # answer and calls memory/skill tools as a side-effect in the same - # turn. If the follow-up turn after tools is empty, we use this. + # Only carry forward earlier turn content as a fallback + # when EVERY tool call is post-response housekeeping + # (memory, todo, skill_manage, etc.). Using substantive + # tool turns as fallback content silently abandons + # unfinished multi-step tasks on the next empty response. + _HOUSEKEEPING_TOOLS = frozenset({ + "memory", "todo", "skill_manage", "session_search", + }) + _all_housekeeping = all( + tc.function.name in _HOUSEKEEPING_TOOLS + for tc in assistant_message.tool_calls + ) turn_content = assistant_message.content or "" if turn_content and self._has_content_after_think_block(turn_content): - self._last_content_with_tools = turn_content + if _all_housekeeping: + self._last_content_with_tools = turn_content + else: + self._last_content_with_tools = None # Only mute subsequent output when EVERY tool call in - # this turn is post-response housekeeping (memory, todo, - # skill_manage, etc.). If any substantive tool is present - # (search_files, read_file, write_file, terminal, ...), - # keep output visible so the user sees progress. - _HOUSEKEEPING_TOOLS = frozenset({ - "memory", "todo", "skill_manage", "session_search", - }) - _all_housekeeping = all( - tc.function.name in _HOUSEKEEPING_TOOLS - for tc in assistant_message.tool_calls - ) + # this turn is post-response housekeeping. If any + # substantive tool is present (search_files, read_file, + # write_file, terminal, ...), keep output visible so + # the user sees progress. if _all_housekeeping and self._has_stream_consumers(): self._mute_post_response = True elif self.quiet_mode: diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index d71e6a625542..23f15ca97856 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -2079,6 +2079,72 @@ def _fake_api_call(api_kwargs): assert result["final_response"] == "Fresh partial content from this turn" assert result["api_calls"] == 1 + def test_empty_after_substantive_tool_turn_retries_instead_of_using_stale_content(self, agent): + """Substantive tool turns must not become the final answer fallback.""" + self._setup_agent(agent) + tc = _mock_tool_call(name="web_search", arguments='{"q":"test"}', call_id="c1") + resp1 = _mock_response( + content="I found a lead and I'm checking one more source.", + finish_reason="tool_calls", + tool_calls=[tc], + ) + resp2 = _mock_response(content=None, finish_reason="stop") + resp3 = _mock_response( + content="Here is the completed answer after the follow-up retry.", + finish_reason="stop", + ) + agent.client.chat.completions.create.side_effect = [resp1, resp2, resp3] + + status_messages = [] + + with ( + patch("run_agent.handle_function_call", return_value="search result"), + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + patch.object(agent, "_emit_status", side_effect=status_messages.append), + ): + result = agent.run_conversation("search something") + + assert result["completed"] is True + assert result["final_response"] == "Here is the completed answer after the follow-up retry." + assert result["api_calls"] == 3 + assert not any( + "using earlier content as final answer" in msg.lower() + for msg in status_messages + ) + + def test_empty_after_housekeeping_tool_turn_uses_prior_content_fallback(self, agent_with_memory_tool): + """Housekeeping-only tool turns may still reuse prior visible content.""" + self._setup_agent(agent_with_memory_tool) + tc = _mock_tool_call(name="memory", arguments='{"content":"saved note"}', call_id="c1") + resp1 = _mock_response( + content="Saved that to memory.", + finish_reason="tool_calls", + tool_calls=[tc], + ) + resp2 = _mock_response(content=None, finish_reason="stop") + agent_with_memory_tool.client.chat.completions.create.side_effect = [resp1, resp2] + + status_messages = [] + + with ( + patch("run_agent.handle_function_call", return_value="saved"), + patch.object(agent_with_memory_tool, "_persist_session"), + patch.object(agent_with_memory_tool, "_save_trajectory"), + patch.object(agent_with_memory_tool, "_cleanup_task_resources"), + patch.object(agent_with_memory_tool, "_emit_status", side_effect=status_messages.append), + ): + result = agent_with_memory_tool.run_conversation("remember this") + + assert result["completed"] is True + assert result["final_response"] == "Saved that to memory." + assert result["api_calls"] == 2 + assert any( + "using earlier content as final answer" in msg.lower() + for msg in status_messages + ) + def test_nous_401_refreshes_after_remint_and_retries(self, agent): self._setup_agent(agent) agent.provider = "nous" From dbd90f6ddfa9c5fe0f38a45a0dda106198465e4a Mon Sep 17 00:00:00 2001 From: HiddenPuppy Date: Tue, 14 Apr 2026 14:30:30 +0800 Subject: [PATCH 2/2] Bugfix: allow GitHub noreply contributor emails --- .github/workflows/contributor-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributor-check.yml b/.github/workflows/contributor-check.yml index f8d65a3ea4b6..e4e0cb0e8a76 100644 --- a/.github/workflows/contributor-check.yml +++ b/.github/workflows/contributor-check.yml @@ -35,7 +35,7 @@ jobs: while IFS= read -r email; do # Skip teknium and bot emails case "$email" in - *teknium*|*noreply@github.com*|*dependabot*|*github-actions*|*anthropic.com*|*cursor.com*) + *teknium*|*noreply@github.com*|*@users.noreply.github.com|*dependabot*|*github-actions*|*anthropic.com*|*cursor.com*) continue ;; esac