Skip to content

Commit 6f92d11

Browse files
OBennerclaude
andcommitted
fix(agents): drive inline agent sessions via run_agent_session, not the phantom create_agent_session
ClaudeSDKClient (claude_agent_sdk) has no `create_agent_session` method, so every inline call site raised `'ClaudeSDKClient' object has no attribute 'create_agent_session'` at runtime. The shared generator boilerplate (agents/_generator_base) was fixed in #339; this covers the remaining inline call sites it called out as a follow-up: - documentation_generator.run_documentation_generator_session - migration_assistant.run_migration_assistant - test_generator.run_test_generator_session (writer + coverage-improvement) - agent_subprocess.run_agent_session (isolated-subprocess wrapper) Each now drives the session through run_agent_session() (agents/session.py) inside `async with client` — the same API the planner/coder/qa phases use — and treats a returned status == "error" as failure. The agent role prompt is delivered as the leading message since the SDK client only carries the generic base system prompt. agent_subprocess imports the helper under an alias so it does not shadow its own run_agent_session() wrapper. Add tests/test_inline_generator_sessions.py (mirrors tests/test_fixture_generator_session.py: a fake async-context-manager client with no create_agent_session attribute, agents.session.run_agent_session patched as an AsyncMock, asserting it is awaited and the client is entered). Update the existing migration/test-generation suites that mocked the phantom method to drive the shared session helper instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
1 parent dd5673e commit 6f92d11

9 files changed

Lines changed: 680 additions & 87 deletions

apps/backend/agents/agent_subprocess.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,14 @@ async def run_agent_session(
149149
Returns:
150150
Dictionary with session results
151151
"""
152+
# ClaudeSDKClient has no `create_agent_session` method. Drive the session
153+
# with the shared run_agent_session() helper (agents/session.py) inside
154+
# `async with client` — the same API the planner/coder/qa phases use.
155+
# Imported under an alias so it does not shadow this module's own
156+
# run_agent_session() wrapper (defined above).
157+
from agents.session import run_agent_session as run_sdk_session
152158
from core.client import create_client
159+
from task_logger import LogPhase
153160

154161
_debug(f"Starting {agent_type} agent session in subprocess")
155162
_debug_verbose(f"Project: {project_dir}")
@@ -168,23 +175,44 @@ async def run_agent_session(
168175

169176
_debug(f"Client created, starting agent session: {session_name}")
170177

171-
# Run the agent session
172-
session_kwargs = {
173-
"name": session_name,
174-
"starting_message": starting_message,
175-
}
178+
# Drive the session through the shared helper. The role prompt
179+
# (system_prompt) leads the message when provided; the SDK client only
180+
# carries the generic base system prompt.
181+
session_message = (
182+
f"{system_prompt}\n\n{starting_message}"
183+
if system_prompt
184+
else starting_message
185+
)
186+
phase = (
187+
LogPhase.PLANNING
188+
if agent_type == "planner"
189+
else LogPhase.VALIDATION
190+
if agent_type in ("qa_reviewer", "qa_fixer")
191+
else LogPhase.CODING
192+
)
176193

177-
if system_prompt:
178-
session_kwargs["system_prompt"] = system_prompt
194+
async with client:
195+
status, response_text, _usage, _decisions = await run_sdk_session(
196+
client=client,
197+
message=session_message,
198+
spec_dir=spec_dir,
199+
phase=phase,
200+
)
179201

180-
response = await client.create_agent_session(**session_kwargs)
202+
if status == "error":
203+
_debug_error(f"Agent session failed: {response_text}")
204+
return {
205+
"success": False,
206+
"output": None,
207+
"error": response_text,
208+
}
181209

182210
_debug_success("Agent session completed successfully")
183211

184212
return {
185213
"success": True,
186214
"output": {
187-
"response": str(response) if response else None,
215+
"response": response_text or None,
188216
"agent_type": agent_type,
189217
"session_name": session_name,
190218
},

apps/backend/agents/documentation_generator.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ async def run_documentation_generator_session(
146146
f"{len(analysis_results.get('classes', []))} classes for documentation",
147147
)
148148

149-
# Validate the documentation generator prompt exists
149+
# Load the documentation generator prompt (delivered as the leading message)
150150
try:
151-
get_agent_prompt("documentation_generator")
151+
prompt = get_agent_prompt("documentation_generator")
152152
except Exception as e:
153153
error_msg = f"Failed to load documentation_generator prompt: {e}"
154154
logger.error(error_msg)
@@ -202,14 +202,34 @@ async def run_documentation_generator_session(
202202
task_logger.log_entry(LogEntryType.ERROR, error_msg)
203203
return {"generated_files": [], "success": False, "error": error_msg}
204204

205-
# Run the agent session
205+
# Run the agent session.
206+
#
207+
# ClaudeSDKClient has no `create_agent_session` method — agent turns are
208+
# driven by run_agent_session() (agents/session.py) inside `async with
209+
# client`, the same session API the planner/coder/qa phases use. The agent
210+
# role prompt leads the message since the SDK client only carries the
211+
# generic base system prompt.
212+
from .session import run_agent_session
213+
206214
try:
207215
print_status("Starting documentation generation session...", "progress")
208216

209-
await client.create_agent_session(
210-
name="documentation-generator-session",
211-
starting_message=starting_message,
212-
)
217+
session_message = f"{prompt}\n\n{starting_message}"
218+
async with client:
219+
status, response_text, _usage, _decisions = await run_agent_session(
220+
client=client,
221+
message=session_message,
222+
spec_dir=spec_dir,
223+
verbose=verbose,
224+
phase=LogPhase.CODING,
225+
)
226+
227+
if status == "error":
228+
error_msg = f"Documentation generation session failed: {response_text}"
229+
logger.error(error_msg)
230+
if task_logger:
231+
task_logger.log_entry(LogEntryType.ERROR, error_msg)
232+
return {"generated_files": [], "success": False, "error": error_msg}
213233

214234
if task_logger:
215235
task_logger.log_entry(

apps/backend/agents/migration_assistant.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,35 @@ async def run_migration_assistant(
272272
print_status("Running migration assistant...", "progress")
273273
print()
274274

275+
# ClaudeSDKClient has no `create_agent_session` method — drive the session
276+
# with run_agent_session() (agents/session.py) inside `async with client`,
277+
# the same session API the planner/coder/qa phases use. The agent role
278+
# prompt leads the message since the SDK client only carries the generic
279+
# base system prompt.
280+
from .session import run_agent_session
281+
275282
try:
283+
session_message = f"{_prompt}\n\n{starting_message}"
276284
async with client:
277-
await client.create_agent_session(
278-
name="migration-assistant-session",
279-
starting_message=starting_message,
285+
status, response_text, _usage, _decisions = await run_agent_session(
286+
client=client,
287+
message=session_message,
288+
spec_dir=spec_dir,
289+
verbose=verbose,
290+
phase=LogPhase.CODING,
280291
)
281292

293+
if status == "error":
294+
error_msg = f"Migration session failed: {response_text}"
295+
logger.error(error_msg)
296+
if task_logger:
297+
task_logger.log(error_msg, LogEntryType.ERROR)
298+
return {
299+
"checkpoints_created": 0,
300+
"success": False,
301+
"error": error_msg,
302+
}
303+
282304
# Log completion
283305
if task_logger:
284306
task_logger.end_phase(

apps/backend/agents/test_generator.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,14 +1595,32 @@ async def run_test_generator_session(
15951595
task_logger.log_error(error_msg)
15961596
return {"generated_files": [], "success": False, "error": error_msg}
15971597

1598-
# Run the agent session
1598+
# Run the agent session.
1599+
#
1600+
# ClaudeSDKClient has no `create_agent_session` method — agent turns are
1601+
# driven by run_agent_session() (agents/session.py) inside `async with
1602+
# client`. The agent role prompt leads the message since the SDK client
1603+
# only carries the generic base system prompt.
1604+
from .session import run_agent_session
1605+
15991606
print_status("Running Test Generator Agent...", "progress")
16001607
try:
1601-
response = await client.create_agent_session(
1602-
name="test-generator-session",
1603-
starting_message=starting_message,
1604-
system_prompt=prompt,
1605-
)
1608+
session_message = f"{prompt}\n\n{starting_message}"
1609+
async with client:
1610+
status, response, _usage, _decisions = await run_agent_session(
1611+
client=client,
1612+
message=session_message,
1613+
spec_dir=spec_dir,
1614+
verbose=verbose,
1615+
phase=LogPhase.CODING,
1616+
)
1617+
1618+
if status == "error":
1619+
error_msg = f"Test Generator Agent session failed: {response}"
1620+
logger.error(error_msg)
1621+
if task_logger:
1622+
task_logger.log_error(error_msg)
1623+
return {"generated_files": [], "success": False, "error": error_msg}
16061624

16071625
if verbose:
16081626
logger.info(f"Test Generator Agent response: {response}")
@@ -1714,16 +1732,28 @@ async def run_test_generator_session(
17141732
Generate additional test cases to improve coverage to at least {min_threshold:.0f}%.
17151733
"""
17161734

1717-
# Run improvement iteration
1735+
# Run improvement iteration. A fresh connected session (the prior
1736+
# `async with client` above already disconnected) — the
1737+
# improvement_message is self-contained and the agent re-reads the
1738+
# test files it wrote earlier from disk.
17181739
try:
17191740
print_status(
17201741
"Running Test Generator Agent improvement iteration...", "progress"
17211742
)
1722-
response = await client.create_agent_session(
1723-
name="test-generator-improvement",
1724-
starting_message=improvement_message,
1725-
system_prompt=prompt,
1726-
)
1743+
improvement_session_message = f"{prompt}\n\n{improvement_message}"
1744+
async with client:
1745+
status, response, _usage, _decisions = await run_agent_session(
1746+
client=client,
1747+
message=improvement_session_message,
1748+
spec_dir=spec_dir,
1749+
verbose=verbose,
1750+
phase=LogPhase.CODING,
1751+
)
1752+
1753+
if status == "error":
1754+
# Non-fatal: keep the tests already generated above.
1755+
logger.warning(f"Test improvement iteration failed: {response}")
1756+
print_status(f"Improvement iteration failed: {response}", "warning")
17271757

17281758
if verbose:
17291759
logger.info(f"Test Generator improvement response: {response}")

tests/e2e/test_qa_test_generation_flow.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,17 +329,15 @@ async def test_e2e_test_generation_flow(
329329
# Import after fixtures are set up
330330
from qa.loop import run_qa_validation_loop
331331

332-
# Mock the client creation and QA agent sessions to avoid actual API calls
332+
# Mock the client creation and QA agent sessions to avoid actual API calls.
333+
# The client is only ever used as an async context manager here — the test
334+
# generation and QA sessions are mocked at the qa.loop boundary
335+
# (run_test_generator_session / run_qa_agent_session below), so no real
336+
# session API is invoked on it.
333337
mock_client = AsyncMock()
334338
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
335339
mock_client.__aexit__ = AsyncMock(return_value=None)
336340

337-
# Mock create_agent_session to return a valid response
338-
async def mock_agent_session(*args, **kwargs):
339-
return {"test_files": ["test_utils.py", "test_auth.py"]}
340-
341-
mock_client.create_agent_session = mock_agent_session
342-
343341
with contextlib.ExitStack() as stack:
344342
stack.enter_context(patch("qa.loop.create_client", return_value=mock_client))
345343
stack.enter_context(

tests/integration/test_migration_workflow.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import subprocess
1616
import sys
1717
from pathlib import Path
18-
from unittest.mock import AsyncMock, patch
18+
from unittest.mock import AsyncMock, MagicMock, patch
1919

2020
import pytest
2121

@@ -353,6 +353,21 @@ def test_plan_includes_rollback_strategy(self, sample_project_files):
353353
class TestMigrationWorkflowEndToEnd:
354354
"""End-to-end tests for complete migration workflow."""
355355

356+
@pytest.fixture(autouse=True)
357+
def mock_session_runner(self):
358+
"""Patch the shared session driver used by run_migration_assistant.
359+
360+
``ClaudeSDKClient`` has no ``create_agent_session`` method; the inline
361+
migration session is driven by ``agents.session.run_agent_session``
362+
inside ``async with client``. Patch it to a success 4-tuple; tests that
363+
need a failure reconfigure ``self._mock_run`` (e.g. ``side_effect``).
364+
"""
365+
self._mock_run = AsyncMock(
366+
return_value=("complete", "migration done", None, MagicMock())
367+
)
368+
with patch("agents.session.run_agent_session", new=self._mock_run):
369+
yield self._mock_run
370+
356371
@pytest.mark.asyncio
357372
@patch("agents.migration_assistant.get_phase_thinking_budget")
358373
@patch("agents.migration_assistant.get_phase_model")
@@ -377,7 +392,6 @@ async def test_migration_assistant_initialization_with_context(
377392
mock_client = AsyncMock()
378393
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
379394
mock_client.__aexit__ = AsyncMock(return_value=None)
380-
mock_client.create_agent_session = AsyncMock(return_value={"status": "ok"})
381395
mock_create_client.return_value = mock_client
382396

383397
migration_context = {
@@ -400,10 +414,12 @@ async def test_migration_assistant_initialization_with_context(
400414
assert call_kwargs["project_dir"] == project_dir
401415
assert call_kwargs["spec_dir"] == spec_dir
402416

403-
# Verify session was created
404-
mock_client.create_agent_session.assert_called_once()
405-
session_call = mock_client.create_agent_session.call_args
406-
assert "migration-assistant-session" in str(session_call)
417+
# Verify the session was driven through the shared helper, with the
418+
# migration context carried in the message.
419+
self._mock_run.assert_awaited_once()
420+
message = self._mock_run.await_args.kwargs["message"]
421+
assert "React 16" in message
422+
assert "React 18" in message
407423

408424
@pytest.mark.asyncio
409425
@patch("agents.migration_assistant.get_phase_thinking_budget")
@@ -429,7 +445,6 @@ async def test_migration_creates_checkpoints(
429445
mock_client = AsyncMock()
430446
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
431447
mock_client.__aexit__ = AsyncMock(return_value=None)
432-
mock_client.create_agent_session = AsyncMock(return_value={"status": "ok"})
433448
mock_create_client.return_value = mock_client
434449

435450
# Pre-create checkpoint structure to simulate agent creating it
@@ -479,14 +494,12 @@ async def test_migration_handles_failures(
479494
mock_get_phase_model.return_value = "claude-sonnet-4-5-20250929"
480495
mock_get_phase_thinking_budget.return_value = 4096
481496

482-
# Mock client that raises an exception
497+
# Mock client; the shared session driver raises mid-session.
483498
mock_client = AsyncMock()
484499
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
485500
mock_client.__aexit__ = AsyncMock(return_value=None)
486-
mock_client.create_agent_session = AsyncMock(
487-
side_effect=Exception("Simulated failure")
488-
)
489501
mock_create_client.return_value = mock_client
502+
self._mock_run.side_effect = Exception("Simulated failure")
490503

491504
result = await run_migration_assistant(
492505
project_dir=project_dir, spec_dir=spec_dir

0 commit comments

Comments
 (0)