Skip to content

Commit 3ed4827

Browse files
authored
Merge pull request #31 from Parth-Vasave/migrate-fast-planner-harness
Migrate fast_plan_tasks to AgentField native .harness() (#28)
2 parents 8bc4cb6 + 673795b commit 3ed4827

5 files changed

Lines changed: 71 additions & 52 deletions

File tree

swe_af/fast/planner.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import logging
1111

12-
from swe_af.agent_ai import AgentAI, AgentAIConfig
1312
from swe_af.fast import fast_router
1413
from swe_af.fast.prompts import FAST_PLANNER_SYSTEM_PROMPT, fast_planner_task_prompt
1514
from swe_af.fast.schemas import FastPlanResult, FastTask
@@ -20,8 +19,9 @@
2019
def _note(msg: str, tags: list[str] | None = None) -> None:
2120
"""Log a message via fast_router.note() when attached, else fall back to logger."""
2221
try:
22+
# AgentRouter may raise RuntimeError on attribute access if not attached.
2323
fast_router.note(msg, tags=tags or [])
24-
except RuntimeError:
24+
except (RuntimeError, AttributeError):
2525
logger.debug("[fast_planner] %s (tags=%s)", msg, tags)
2626

2727

@@ -96,38 +96,35 @@ async def fast_plan_tasks(
9696
additional_context=additional_context,
9797
)
9898

99-
ai = AgentAI(
100-
AgentAIConfig(
101-
provider=ai_provider,
99+
# Map 'claude' to 'claude-code' for AgentField router compatibility
100+
provider = "claude-code" if ai_provider == "claude" else ai_provider
101+
try:
102+
res = await fast_router.harness(
103+
prompt=task_prompt,
104+
schema=FastPlanResult,
105+
provider=provider,
102106
model=pm_model,
103-
cwd=repo_path,
104107
max_turns=3,
105108
permission_mode=permission_mode or None,
106-
)
107-
)
108-
109-
try:
110-
response = await ai.run(
111-
task_prompt,
112109
system_prompt=FAST_PLANNER_SYSTEM_PROMPT,
113-
output_schema=FastPlanResult,
110+
cwd=repo_path,
114111
)
115-
except Exception:
116-
logger.exception("fast_plan_tasks: AgentAI.run() raised an exception; using fallback")
112+
plan = res.parsed
113+
except Exception as e:
114+
logger.exception("fast_plan_tasks: fast_router.harness() raised an exception; using fallback")
117115
_note(
118-
"fast_plan_tasks: LLM call failed; returning fallback plan",
119-
tags=["fast_planner", "fallback"],
116+
f"fast_plan_tasks: LLM call failed ({e}); returning fallback plan",
117+
tags=["fast_planner", "fallback", "error"],
120118
)
121119
return _fallback_plan(goal).model_dump()
122120

123-
if response.parsed is None:
121+
if plan is None:
124122
_note(
125123
"fast_plan_tasks: parsed response is None; returning fallback plan",
126124
tags=["fast_planner", "fallback"],
127125
)
128126
return _fallback_plan(goal).model_dump()
129127

130-
plan: FastPlanResult = response.parsed
131128
# Truncate to max_tasks using model_copy to avoid class-identity issues
132129
if len(plan.tasks) > max_tasks:
133130
plan = plan.model_copy(update={"tasks": plan.tasks[:max_tasks]})

tests/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ def agentfield_server_guard() -> None:
9797
)
9898

9999

100+
@pytest.fixture(scope="session", autouse=True)
101+
def attach_fast_router() -> None:
102+
"""Explicitly 'attach' fast_router to a mock agent to avoid RuntimeError in tests.
103+
104+
AgentRouter raised RuntimeError on any attribute access if not attached.
105+
This session fixture ensures all tests can safely interact with or patch
106+
fast_router without triggering that check.
107+
"""
108+
from unittest.mock import MagicMock
109+
110+
from swe_af.fast import fast_router
111+
# Set the private _agent attribute to satisfy AgentRouter's attachment check.
112+
# We use object.__setattr__ to avoid any potential __setattr__ guards.
113+
object.__setattr__(fast_router, "_agent", MagicMock())
114+
115+
100116
# ---------------------------------------------------------------------------
101117
# mock_agent_ai fixture
102118
# ---------------------------------------------------------------------------

tests/fast/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ def _reset_fast_router() -> None: # type: ignore[return]
4646
# Re-import swe_af.fast — this recreates fast_router and re-registers
4747
# all the @fast_router.reasoner() wrappers with original func references.
4848
importlib.import_module("swe_af.fast")
49+
50+
# Explicitly 'attach' the fresh fast_router to a mock agent to avoid RuntimeError.
51+
from unittest.mock import MagicMock
52+
from swe_af.fast import fast_router
53+
object.__setattr__(fast_router, "_agent", MagicMock())
54+
4955
# Re-import sub-modules that register reasoners on the fresh fast_router.
5056
for mod in (
5157
"swe_af.fast.executor",

tests/fast/test_planner.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ def test_valid_llm_response_produces_fast_plan_result(self) -> None:
141141
)
142142
mock_response = _make_mock_response(plan)
143143

144-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
145-
instance = MagicMock()
146-
instance.run = AsyncMock(return_value=mock_response)
147-
MockAgentAI.return_value = instance
144+
with patch("swe_af.fast.planner._note"), \
145+
patch("swe_af.fast.planner.fast_router") as mock_router:
146+
mock_router.harness = AsyncMock(return_value=mock_response)
147+
mock_router.note = MagicMock()
148148

149149
result = _run(fast_plan_tasks(
150150
goal="Build a REST API",
@@ -164,10 +164,10 @@ def test_llm_parsed_none_triggers_fallback(self) -> None:
164164

165165
mock_response = _make_mock_response(None)
166166

167-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
168-
instance = MagicMock()
169-
instance.run = AsyncMock(return_value=mock_response)
170-
MockAgentAI.return_value = instance
167+
with patch("swe_af.fast.planner._note"), \
168+
patch("swe_af.fast.planner.fast_router") as mock_router:
169+
mock_router.harness = AsyncMock(return_value=mock_response)
170+
mock_router.note = MagicMock()
171171

172172
result = _run(fast_plan_tasks(
173173
goal="Build something",
@@ -184,11 +184,11 @@ def test_llm_parsed_none_triggers_fallback(self) -> None:
184184
def test_llm_exception_triggers_fallback(self) -> None:
185185
"""When AgentAI.run() raises, the fallback plan is returned."""
186186
from swe_af.fast.planner import fast_plan_tasks
187-
188-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
189-
instance = MagicMock()
190-
instance.run = AsyncMock(side_effect=RuntimeError("LLM connection error"))
191-
MockAgentAI.return_value = instance
187+
188+
with patch("swe_af.fast.planner._note"), \
189+
patch("swe_af.fast.planner.fast_router") as mock_router:
190+
mock_router.harness = AsyncMock(side_effect=RuntimeError("LLM connection error"))
191+
mock_router.note = MagicMock()
192192

193193
result = _run(fast_plan_tasks(
194194
goal="Build something",
@@ -205,10 +205,10 @@ def test_fallback_contains_at_least_one_task(self) -> None:
205205

206206
mock_response = _make_mock_response(None)
207207

208-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
209-
instance = MagicMock()
210-
instance.run = AsyncMock(return_value=mock_response)
211-
MockAgentAI.return_value = instance
208+
with patch("swe_af.fast.planner._note"), \
209+
patch("swe_af.fast.planner.fast_router") as mock_router:
210+
mock_router.harness = AsyncMock(return_value=mock_response)
211+
mock_router.note = MagicMock()
212212

213213
result = _run(fast_plan_tasks(goal="Any goal", repo_path="/repo"))
214214

@@ -229,10 +229,10 @@ def test_max_tasks_one_truncates_result(self) -> None:
229229
plan = FastPlanResult(tasks=many_tasks, rationale="Many tasks.")
230230
mock_response = _make_mock_response(plan)
231231

232-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
233-
instance = MagicMock()
234-
instance.run = AsyncMock(return_value=mock_response)
235-
MockAgentAI.return_value = instance
232+
with patch("swe_af.fast.planner._note"), \
233+
patch("swe_af.fast.planner.fast_router") as mock_router:
234+
mock_router.harness = AsyncMock(return_value=mock_response)
235+
mock_router.note = MagicMock()
236236

237237
result = _run(fast_plan_tasks(
238238
goal="Build a thing",
@@ -250,10 +250,10 @@ def test_max_tasks_respected_when_llm_returns_exact_count(self) -> None:
250250
plan = FastPlanResult(tasks=tasks, rationale="Exactly 3 tasks.")
251251
mock_response = _make_mock_response(plan)
252252

253-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
254-
instance = MagicMock()
255-
instance.run = AsyncMock(return_value=mock_response)
256-
MockAgentAI.return_value = instance
253+
with patch("swe_af.fast.planner._note"), \
254+
patch("swe_af.fast.planner.fast_router") as mock_router:
255+
mock_router.harness = AsyncMock(return_value=mock_response)
256+
mock_router.note = MagicMock()
257257

258258
result = _run(fast_plan_tasks(
259259
goal="Build a thing",

tests/test_malformed_responses.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ def test_fast_plan_tasks_missing_tasks_field_triggers_fallback() -> None:
5151
mock_response = MagicMock()
5252
mock_response.parsed = None
5353

54-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
55-
instance = MagicMock()
56-
instance.run = AsyncMock(return_value=mock_response)
57-
MockAgentAI.return_value = instance
54+
with patch("swe_af.fast.planner._note"), \
55+
patch("swe_af.fast.planner.fast_router") as mock_router:
56+
mock_router.harness = AsyncMock(return_value=mock_response)
57+
mock_router.note = MagicMock()
5858

5959
result = _run(
6060
fast_plan_tasks(
@@ -79,12 +79,12 @@ def test_fast_plan_tasks_exception_in_run_triggers_fallback() -> None:
7979
"""When AgentAI.run raises an exception, the planner falls back gracefully."""
8080
from swe_af.fast.planner import fast_plan_tasks
8181

82-
with patch("swe_af.fast.planner.AgentAI") as MockAgentAI:
83-
instance = MagicMock()
84-
instance.run = AsyncMock(
82+
with patch("swe_af.fast.planner._note"), \
83+
patch("swe_af.fast.planner.fast_router") as mock_router:
84+
mock_router.harness = AsyncMock(
8585
side_effect=ValueError("Response missing required 'tasks' field")
8686
)
87-
MockAgentAI.return_value = instance
87+
mock_router.note = MagicMock()
8888

8989
result = _run(
9090
fast_plan_tasks(

0 commit comments

Comments
 (0)