fix(runner): narrow BaseException catch to Exception in tool execution

The tool execution path caught BaseException, which includes
KeyboardInterrupt, SystemExit, MemoryError, and GeneratorExit.
These should never be caught and converted into conversational
error messages. CancelledError is already handled separately.

Change except BaseException to except Exception so fatal signals
propagate instead of being swallowed.

Adds parametrized regression test for KeyboardInterrupt and
SystemExit propagation.

Fixes #4788
This commit is contained in:
axelray-dev
2026-07-21 13:46:05 +08:00
committed by Xubin Ren
parent b6156fdd79
commit fde55d06e2
2 changed files with 35 additions and 1 deletions
+1 -1
View File
@@ -1174,7 +1174,7 @@ class AgentRunner:
result = await spec.tools.execute(tool_call.name, params)
except asyncio.CancelledError:
raise
except BaseException as exc:
except Exception as exc:
await hook.on_execute_tool_error(context, tool_call, tool, params, exc)
event = {
"name": tool_call.name,
+34
View File
@@ -3,6 +3,7 @@ session message isolation, and tool result preservation."""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -45,6 +46,39 @@ async def test_runner_returns_structured_tool_error():
]
@pytest.mark.asyncio
@pytest.mark.parametrize("control_error", [KeyboardInterrupt, SystemExit])
async def test_runner_propagates_tool_control_flow_exceptions(control_error: type[BaseException]):
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
async def execute(_name, _args):
raise control_error("stop")
tools = SimpleNamespace(
get_definitions=lambda: [],
execute=execute,
)
runner = AgentRunner()
spec = make_run_spec(
provider,
initial_messages=[],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
)
with pytest.raises(control_error):
await runner._run_tool(
spec,
ToolCallRequest(id="call_1", name="list_dir", arguments={}),
external_lookup_counts={},
workspace_violation_counts={},
)
@pytest.mark.asyncio
async def test_llm_error_not_appended_to_session_messages():
"""When LLM returns finish_reason='error', the error content must NOT be