From 673795bb1e538b2c2da7fa024da8540f3e10b320 Mon Sep 17 00:00:00 2001 From: parth Date: Mon, 9 Mar 2026 23:39:09 +0530 Subject: [PATCH] fixes Migrate fast/ module (fast_plan_tasks) to AgentField native .harness() #28 --- swe_af/fast/planner.py | 35 ++++++++++------------ tests/conftest.py | 16 ++++++++++ tests/fast/conftest.py | 6 ++++ tests/fast/test_planner.py | 50 +++++++++++++++---------------- tests/test_malformed_responses.py | 16 +++++----- 5 files changed, 71 insertions(+), 52 deletions(-) diff --git a/swe_af/fast/planner.py b/swe_af/fast/planner.py index 824883e..9cb87c3 100644 --- a/swe_af/fast/planner.py +++ b/swe_af/fast/planner.py @@ -9,7 +9,6 @@ import logging -from swe_af.agent_ai import AgentAI, AgentAIConfig from swe_af.fast import fast_router from swe_af.fast.prompts import FAST_PLANNER_SYSTEM_PROMPT, fast_planner_task_prompt from swe_af.fast.schemas import FastPlanResult, FastTask @@ -20,8 +19,9 @@ def _note(msg: str, tags: list[str] | None = None) -> None: """Log a message via fast_router.note() when attached, else fall back to logger.""" try: + # AgentRouter may raise RuntimeError on attribute access if not attached. fast_router.note(msg, tags=tags or []) - except RuntimeError: + except (RuntimeError, AttributeError): logger.debug("[fast_planner] %s (tags=%s)", msg, tags) @@ -96,38 +96,35 @@ async def fast_plan_tasks( additional_context=additional_context, ) - ai = AgentAI( - AgentAIConfig( - provider=ai_provider, + # Map 'claude' to 'claude-code' for AgentField router compatibility + provider = "claude-code" if ai_provider == "claude" else ai_provider + try: + res = await fast_router.harness( + prompt=task_prompt, + schema=FastPlanResult, + provider=provider, model=pm_model, - cwd=repo_path, max_turns=3, permission_mode=permission_mode or None, - ) - ) - - try: - response = await ai.run( - task_prompt, system_prompt=FAST_PLANNER_SYSTEM_PROMPT, - output_schema=FastPlanResult, + cwd=repo_path, ) - except Exception: - logger.exception("fast_plan_tasks: AgentAI.run() raised an exception; using fallback") + plan = res.parsed + except Exception as e: + logger.exception("fast_plan_tasks: fast_router.harness() raised an exception; using fallback") _note( - "fast_plan_tasks: LLM call failed; returning fallback plan", - tags=["fast_planner", "fallback"], + f"fast_plan_tasks: LLM call failed ({e}); returning fallback plan", + tags=["fast_planner", "fallback", "error"], ) return _fallback_plan(goal).model_dump() - if response.parsed is None: + if plan is None: _note( "fast_plan_tasks: parsed response is None; returning fallback plan", tags=["fast_planner", "fallback"], ) return _fallback_plan(goal).model_dump() - plan: FastPlanResult = response.parsed # Truncate to max_tasks using model_copy to avoid class-identity issues if len(plan.tasks) > max_tasks: plan = plan.model_copy(update={"tasks": plan.tasks[:max_tasks]}) diff --git a/tests/conftest.py b/tests/conftest.py index 8d5c7d3..2df263c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,6 +97,22 @@ def agentfield_server_guard() -> None: ) +@pytest.fixture(scope="session", autouse=True) +def attach_fast_router() -> None: + """Explicitly 'attach' fast_router to a mock agent to avoid RuntimeError in tests. + + AgentRouter raised RuntimeError on any attribute access if not attached. + This session fixture ensures all tests can safely interact with or patch + fast_router without triggering that check. + """ + from unittest.mock import MagicMock + + from swe_af.fast import fast_router + # Set the private _agent attribute to satisfy AgentRouter's attachment check. + # We use object.__setattr__ to avoid any potential __setattr__ guards. + object.__setattr__(fast_router, "_agent", MagicMock()) + + # --------------------------------------------------------------------------- # mock_agent_ai fixture # --------------------------------------------------------------------------- diff --git a/tests/fast/conftest.py b/tests/fast/conftest.py index 861f916..e0eadc9 100644 --- a/tests/fast/conftest.py +++ b/tests/fast/conftest.py @@ -46,6 +46,12 @@ def _reset_fast_router() -> None: # type: ignore[return] # Re-import swe_af.fast — this recreates fast_router and re-registers # all the @fast_router.reasoner() wrappers with original func references. importlib.import_module("swe_af.fast") + + # Explicitly 'attach' the fresh fast_router to a mock agent to avoid RuntimeError. + from unittest.mock import MagicMock + from swe_af.fast import fast_router + object.__setattr__(fast_router, "_agent", MagicMock()) + # Re-import sub-modules that register reasoners on the fresh fast_router. for mod in ( "swe_af.fast.executor", diff --git a/tests/fast/test_planner.py b/tests/fast/test_planner.py index a30e5fe..a4bddeb 100644 --- a/tests/fast/test_planner.py +++ b/tests/fast/test_planner.py @@ -141,10 +141,10 @@ def test_valid_llm_response_produces_fast_plan_result(self) -> None: ) mock_response = _make_mock_response(plan) - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run(fast_plan_tasks( goal="Build a REST API", @@ -164,10 +164,10 @@ def test_llm_parsed_none_triggers_fallback(self) -> None: mock_response = _make_mock_response(None) - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run(fast_plan_tasks( goal="Build something", @@ -184,11 +184,11 @@ def test_llm_parsed_none_triggers_fallback(self) -> None: def test_llm_exception_triggers_fallback(self) -> None: """When AgentAI.run() raises, the fallback plan is returned.""" from swe_af.fast.planner import fast_plan_tasks - - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(side_effect=RuntimeError("LLM connection error")) - MockAgentAI.return_value = instance + + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(side_effect=RuntimeError("LLM connection error")) + mock_router.note = MagicMock() result = _run(fast_plan_tasks( goal="Build something", @@ -205,10 +205,10 @@ def test_fallback_contains_at_least_one_task(self) -> None: mock_response = _make_mock_response(None) - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run(fast_plan_tasks(goal="Any goal", repo_path="/repo")) @@ -229,10 +229,10 @@ def test_max_tasks_one_truncates_result(self) -> None: plan = FastPlanResult(tasks=many_tasks, rationale="Many tasks.") mock_response = _make_mock_response(plan) - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run(fast_plan_tasks( goal="Build a thing", @@ -250,10 +250,10 @@ def test_max_tasks_respected_when_llm_returns_exact_count(self) -> None: plan = FastPlanResult(tasks=tasks, rationale="Exactly 3 tasks.") mock_response = _make_mock_response(plan) - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run(fast_plan_tasks( goal="Build a thing", diff --git a/tests/test_malformed_responses.py b/tests/test_malformed_responses.py index 2c4acf0..3daf343 100644 --- a/tests/test_malformed_responses.py +++ b/tests/test_malformed_responses.py @@ -51,10 +51,10 @@ def test_fast_plan_tasks_missing_tasks_field_triggers_fallback() -> None: mock_response = MagicMock() mock_response.parsed = None - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock(return_value=mock_response) - MockAgentAI.return_value = instance + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock(return_value=mock_response) + mock_router.note = MagicMock() result = _run( fast_plan_tasks( @@ -79,12 +79,12 @@ def test_fast_plan_tasks_exception_in_run_triggers_fallback() -> None: """When AgentAI.run raises an exception, the planner falls back gracefully.""" from swe_af.fast.planner import fast_plan_tasks - with patch("swe_af.fast.planner.AgentAI") as MockAgentAI: - instance = MagicMock() - instance.run = AsyncMock( + with patch("swe_af.fast.planner._note"), \ + patch("swe_af.fast.planner.fast_router") as mock_router: + mock_router.harness = AsyncMock( side_effect=ValueError("Response missing required 'tasks' field") ) - MockAgentAI.return_value = instance + mock_router.note = MagicMock() result = _run( fast_plan_tasks(