diff --git a/agent/analyzer.py b/agent/analyzer.py index cffbe762..26d281e2 100644 --- a/agent/analyzer.py +++ b/agent/analyzer.py @@ -46,8 +46,8 @@ from .server import ( from .tools.read_finding_outcomes import read_finding_outcomes from .tools.save_review_style import save_review_style_prompt from .utils.analyzer_skills import SKILLS_ROUTE, skill_path_for_mode -from .utils.github_app import get_github_app_installation_token from .utils.deferred_model import make_model_or_defer +from .utils.github_app import get_github_app_installation_token from .utils.model import DEFAULT_LLM_REASONING, provider_model_kwargs from .utils.sandbox_paths import aresolve_sandbox_work_dir from .utils.sandbox_state import unwrap_sandbox_backend diff --git a/agent/middleware/__init__.py b/agent/middleware/__init__.py index fd40fc52..50a4b709 100644 --- a/agent/middleware/__init__.py +++ b/agent/middleware/__init__.py @@ -11,6 +11,7 @@ _MIDDLEWARE_MODULES = { "PlanModeMiddleware": ".plan_mode", "refresh_github_proxy_before_model": ".refresh_github_proxy", "SlackAssistantStatusMiddleware": ".refresh_slack_status", + "RepairOrphanedToolCallsMiddleware": ".repair_orphaned_tool_calls", "SandboxCircuitBreakerMiddleware": ".sandbox_circuit_breaker", "SanitizeThinkingBlocksMiddleware": ".sanitize_thinking_blocks", "SanitizeToolInputsMiddleware": ".sanitize_tool_inputs", @@ -37,6 +38,7 @@ __all__ = [ "WorkflowPushGuardMiddleware", "SandboxCircuitBreakerMiddleware", "SlackAssistantStatusMiddleware", + "RepairOrphanedToolCallsMiddleware", "check_message_queue_before_model", "ensure_no_empty_msg", "notify_step_limit_reached", @@ -55,6 +57,7 @@ if TYPE_CHECKING: from .plan_mode import PlanModeMiddleware from .refresh_github_proxy import refresh_github_proxy_before_model from .refresh_slack_status import SlackAssistantStatusMiddleware + from .repair_orphaned_tool_calls import RepairOrphanedToolCallsMiddleware from .sandbox_circuit_breaker import SandboxCircuitBreakerMiddleware from .sanitize_thinking_blocks import SanitizeThinkingBlocksMiddleware from .sanitize_tool_inputs import SanitizeToolInputsMiddleware diff --git a/agent/reviewer.py b/agent/reviewer.py index a413386b..ae94602d 100644 --- a/agent/reviewer.py +++ b/agent/reviewer.py @@ -83,9 +83,9 @@ from .tools import ( ) from .utils.agents_md import fetch_agents_md from .utils.api_standards_skill import fetch_api_standards_skill +from .utils.deferred_model import make_model_or_defer from .utils.github_app import get_github_app_installation_token_with_expiry from .utils.github_token import cache_github_token_for_thread -from .utils.deferred_model import make_model_or_defer from .utils.model import DEFAULT_LLM_REASONING, provider_model_kwargs from .utils.repo_prep import materialize_trusted_skills, prepare_review_repo from .utils.sandbox_paths import aresolve_sandbox_work_dir diff --git a/tests/e2e/patches.py b/tests/e2e/patches.py index a9fb0115..edfb16ee 100644 --- a/tests/e2e/patches.py +++ b/tests/e2e/patches.py @@ -29,7 +29,6 @@ def apply() -> None: import importlib - from agent import server from agent.utils import auth, authorship from agent.utils import slack as slack_utils @@ -52,7 +51,11 @@ def apply() -> None: def _fake_make_model(model_id: str, **kwargs: object): # noqa: ARG001 return FakeScriptedChatModel(script=build_script()) - server.make_model = _fake_make_model + # ``make_model_or_defer`` (used by server/reviewer/analyzer) resolves + # ``make_model`` via the module global at call time, so patch it there. + import agent.utils.deferred_model as deferred_model + + deferred_model.make_model = _fake_make_model async def _dummy_install_token_with_expiry() -> tuple[str, str | None]: return "dummy-installation-token", None diff --git a/tests/test_agent_assembly_context.py b/tests/test_agent_assembly_context.py index 43668bf5..b6e0f36c 100644 --- a/tests/test_agent_assembly_context.py +++ b/tests/test_agent_assembly_context.py @@ -65,7 +65,7 @@ async def _capture_create_deep_agent_kwargs() -> dict[str, object]: ), patch("agent.server.load_profile", new_callable=AsyncMock, return_value=None), patch("agent.server.fallback_model_id_for", return_value=None), - patch("agent.server.make_model", side_effect=[MagicMock(), MagicMock()]), + patch("agent.utils.deferred_model.make_model", side_effect=[MagicMock(), MagicMock()]), patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent), ): diff --git a/tests/test_agent_subagent_models.py b/tests/test_agent_subagent_models.py index 7d317f88..a3f5573c 100644 --- a/tests/test_agent_subagent_models.py +++ b/tests/test_agent_subagent_models.py @@ -66,7 +66,9 @@ async def test_agent_uses_profile_subagent_model_override() -> None: }, ), patch("agent.server.fallback_model_id_for", return_value=None), - patch("agent.server.make_model", side_effect=[main_model, subagent_model]) as make_model, + patch( + "agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model] + ) as make_model, patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent), ): @@ -142,7 +144,9 @@ async def test_agent_subagent_inherits_profile_model_override_without_explicit_p }, ), patch("agent.server.fallback_model_id_for", return_value=None), - patch("agent.server.make_model", side_effect=[main_model, subagent_model]) as make_model, + patch( + "agent.utils.deferred_model.make_model", side_effect=[main_model, subagent_model] + ) as make_model, patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", side_effect=fake_create_deep_agent), ): diff --git a/tests/test_corridor_mcp.py b/tests/test_corridor_mcp.py index bc248e9e..57cdfad3 100644 --- a/tests/test_corridor_mcp.py +++ b/tests/test_corridor_mcp.py @@ -154,7 +154,7 @@ async def test_get_agent_passes_corridor_prompt_state() -> None: return_value=(("openai:gpt-5.5", "medium"), ("openai:gpt-5.5", "low")), ), patch.object(server, "fallback_model_id_for", return_value=None), - patch.object(server, "make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch.object( server, "_load_observability_tools", new_callable=AsyncMock, return_value=[] ), diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index 5ba69522..cdf0e65f 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -105,7 +105,9 @@ async def test_create_durable_run_no_webhook_when_none(monkeypatch: pytest.Monke @pytest.mark.asyncio -async def test_dispatch_agent_run_preserves_multitask_strategy(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_dispatch_agent_run_preserves_multitask_strategy( + monkeypatch: pytest.MonkeyPatch, +) -> None: client = _FakeClient() monkeypatch.setattr(dispatch, "COMPLETION_WEBHOOK_URL", None) diff --git a/tests/test_proxy_auth.py b/tests/test_proxy_auth.py index 15ff5766..69ced5e6 100644 --- a/tests/test_proxy_auth.py +++ b/tests/test_proxy_auth.py @@ -333,7 +333,7 @@ class TestRefreshProxyOnSandboxReuse: new_callable=AsyncMock, return_value=mock_sandbox, ), - patch("agent.server.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", return_value=_DummyAgent()), patch.dict( @@ -386,7 +386,7 @@ class TestRefreshProxyOnSandboxReuse: new_callable=AsyncMock, return_value="/workspace", ), - patch("agent.server.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.server.construct_system_prompt", return_value="prompt"), patch("agent.server.create_deep_agent", return_value=_DummyAgent()), patch.dict("agent.server.SANDBOX_BACKENDS", {}, clear=True), diff --git a/tests/test_reviewer.py b/tests/test_reviewer.py index b332502f..7f4c9264 100644 --- a/tests/test_reviewer.py +++ b/tests/test_reviewer.py @@ -204,7 +204,7 @@ async def test_reviewer_resolves_app_installation_token_at_run_start() -> None: new_callable=AsyncMock, return_value="/workspace", ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", return_value=dummy_agent) as create_agent, ): await reviewer.get_reviewer_agent(config) @@ -260,7 +260,7 @@ async def test_reviewer_reuses_app_token_for_sandbox_proxy() -> None: new_callable=AsyncMock, ), patch("agent.reviewer.fetch_agents_md", new_callable=AsyncMock, return_value=None), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", return_value=_DummyAgent()), ): await reviewer.get_reviewer_agent(config) @@ -335,7 +335,7 @@ async def test_reviewer_applies_eval_model_and_effort_overrides() -> None: new_callable=AsyncMock, return_value="/workspace", ), - patch("agent.reviewer.make_model", return_value=MagicMock()) as make_model, + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()) as make_model, patch("agent.reviewer.create_deep_agent", return_value=dummy_agent), patch( "agent.reviewer.fetch_agents_md", @@ -383,7 +383,7 @@ async def test_reviewer_subagent_inherits_eval_model_without_explicit_override() new_callable=AsyncMock, return_value="/workspace", ), - patch("agent.reviewer.make_model", return_value=MagicMock()) as make_model, + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()) as make_model, patch("agent.reviewer.create_deep_agent", return_value=dummy_agent), patch( "agent.reviewer.fetch_agents_md", @@ -441,7 +441,7 @@ async def test_reviewer_injects_repo_style_during_eval() -> None: new_callable=AsyncMock, return_value="Flag table rerender regressions.", ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), patch( "agent.reviewer.fetch_agents_md", @@ -507,7 +507,7 @@ async def test_reviewer_inlines_org_guidelines_into_system_prompt() -> None: new_callable=AsyncMock, return_value=None, ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -572,7 +572,7 @@ async def test_reviewer_inlines_agents_md_into_system_prompt() -> None: new_callable=AsyncMock, return_value="Always use the design system IconButton.", ) as mock_fetch_agents_md, - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -624,7 +624,7 @@ async def test_reviewer_inlines_claude_md_when_agents_md_absent() -> None: new_callable=AsyncMock, return_value="# CLAUDE.md\nUse semantic tokens only.", ) as mock_fetch_agents_md, - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -989,7 +989,7 @@ async def test_reviewer_injects_pr_review_threads_into_first_review_context() -> new_callable=AsyncMock, return_value=fake_threads, ) as mock_fetch_threads, - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1065,7 +1065,7 @@ async def test_reviewer_injects_pr_review_threads_into_re_review_context() -> No new_callable=AsyncMock, return_value=[], ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1123,7 +1123,7 @@ async def test_reviewer_omits_threads_block_when_fetch_returns_empty() -> None: new_callable=AsyncMock, return_value=[], ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1182,7 +1182,7 @@ async def test_reviewer_continues_when_thread_fetch_raises() -> None: new_callable=AsyncMock, side_effect=RuntimeError("network down"), ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1254,7 +1254,7 @@ async def test_reviewer_populates_diff_line_set_from_github_api() -> None: new_callable=AsyncMock, return_value=pr_diff, ) as mock_fetch_diff, - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1319,7 +1319,7 @@ async def test_reviewer_leaves_validation_disabled_when_diff_fetch_fails() -> No new_callable=AsyncMock, return_value=None, ), - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config) @@ -1385,7 +1385,7 @@ async def test_reviewer_injects_pr_title_and_body_into_context() -> None: new_callable=AsyncMock, return_value=("Add retry logic for uploads", "Retries flaky uploads up to 3 times."), ) as mock_fetch_metadata, - patch("agent.reviewer.make_model", return_value=MagicMock()), + patch("agent.utils.deferred_model.make_model", return_value=MagicMock()), patch("agent.reviewer.create_deep_agent", side_effect=fake_create_deep_agent), ): await reviewer.get_reviewer_agent(config)