diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 1d1de4ab..6834e879 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -190,6 +190,7 @@ class AgentLoop: context_window_tokens: int | None = None, context_block_limit: int | None = None, max_tool_result_chars: int | None = None, + fail_on_tool_error: bool | None = None, provider_retry_mode: str = "standard", tool_hint_max_length: int | None = None, cron_service: CronService | None = None, @@ -287,6 +288,7 @@ class AgentLoop: disabled_skills=disabled_skills, max_iterations=self.max_iterations, max_concurrent_subagents=max_concurrent_subagents, + fail_on_tool_error=fail_on_tool_error, llm_wall_timeout_for_session=lambda sk: runner_wall_llm_timeout_s(self.sessions, sk), ) self._unified_session = unified_session @@ -377,6 +379,7 @@ class AgentLoop: context_window_tokens=context_window_tokens, context_block_limit=defaults.context_block_limit, max_tool_result_chars=defaults.max_tool_result_chars, + fail_on_tool_error=defaults.fail_on_tool_error, provider_retry_mode=defaults.provider_retry_mode, tool_hint_max_length=defaults.tool_hint_max_length, restrict_to_workspace=config.tools.restrict_to_workspace, diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index d93f8419..85dbce66 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -86,6 +86,7 @@ class SubagentManager: disabled_skills: list[str] | None = None, max_iterations: int | None = None, max_concurrent_subagents: int | None = None, + fail_on_tool_error: bool | None = None, llm_wall_timeout_for_session: Callable[[str | None], float | None] | None = None, ): defaults = AgentDefaults() @@ -107,6 +108,11 @@ class SubagentManager: if max_concurrent_subagents is not None else defaults.max_concurrent_subagents ) + self.fail_on_tool_error = ( + fail_on_tool_error + if fail_on_tool_error is not None + else defaults.fail_on_tool_error + ) self.runner = AgentRunner(provider) self._llm_wall_timeout_for_session = llm_wall_timeout_for_session self._running_tasks: dict[str, asyncio.Task[None]] = {} @@ -251,7 +257,7 @@ class SubagentManager: max_iterations_message="Task completed but no final response was generated.", finalize_on_max_iterations=False, error_message=None, - fail_on_tool_error=True, + fail_on_tool_error=self.fail_on_tool_error, checkpoint_callback=_on_checkpoint, session_key=sess_key, workspace=root, diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 22f2acfa..feb2d62e 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -132,6 +132,7 @@ class AgentDefaults(Base): fallback_models: list[FallbackCandidate] = Field(default_factory=list) max_tool_iterations: int = 200 max_concurrent_subagents: int = Field(default=1, ge=1) + fail_on_tool_error: bool = True max_tool_result_chars: int = 16_000 provider_retry_mode: Literal["standard", "persistent"] = "standard" tool_hint_max_length: int = Field( diff --git a/tests/agent/test_subagent.py b/tests/agent/test_subagent.py index 32b5aa6a..529c436b 100644 --- a/tests/agent/test_subagent.py +++ b/tests/agent/test_subagent.py @@ -1,11 +1,12 @@ """Tests for SubagentManager.""" from pathlib import Path -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest -from nanobot.agent.subagent import SubagentManager +from nanobot.agent.runner import AgentRunResult +from nanobot.agent.subagent import SubagentManager, SubagentStatus from nanobot.agent.tools.filesystem import FileToolsConfig from nanobot.bus.queue import MessageBus from nanobot.config.schema import ToolsConfig @@ -79,3 +80,33 @@ def test_subagent_respects_file_tool_toggle(tmp_path): "write_file", } assert file_tools.isdisjoint(tools.tool_names) + + +@pytest.mark.asyncio +async def test_subagent_forwards_fail_on_tool_error_to_runner(tmp_path): + provider = MagicMock(spec=LLMProvider) + provider.get_default_model.return_value = "test" + sm = SubagentManager( + provider=provider, + workspace=tmp_path, + bus=MessageBus(), + model="test", + max_tool_result_chars=16_000, + fail_on_tool_error=False, + ) + sm.runner.run = AsyncMock( + return_value=AgentRunResult(final_content="ok", messages=[], stop_reason="completed") + ) + sm._announce_result = AsyncMock() + + status = SubagentStatus( + task_id="t1", + label="label", + task_description="task", + started_at=0.0, + ) + + await sm._run_subagent("t1", "task", "label", {"channel": "cli", "chat_id": "direct"}, status) + + spec = sm.runner.run.call_args.args[0] + assert spec.fail_on_tool_error is False