@@ -7,6 +7,7 @@ from pathlib import Path |
| 7 | 7 | import pytest |
| 8 | 8 | |
| 9 | 9 | from loader.agent.loop import Agent, AgentConfig |
| 10 | +from loader.llm.base import Message, Role |
| 10 | 11 | from loader.runtime.conversation import ConversationRuntime |
| 11 | 12 | from loader.runtime.dod import VerificationEvidence |
| 12 | 13 | from loader.runtime.phases import TurnPhase |
@@ -454,6 +455,93 @@ async def test_turn_completion_interrupts_repeated_concrete_progress_narration( |
| 454 | 455 | assert "02-installation.html" in agent.session.messages[-1].content |
| 455 | 456 | |
| 456 | 457 | |
| 458 | +@pytest.mark.asyncio |
| 459 | +async def test_turn_completion_interrupts_first_narration_after_concrete_target_prompt( |
| 460 | + temp_dir: Path, |
| 461 | +) -> None: |
| 462 | + backend = ScriptedBackend() |
| 463 | + config = non_streaming_config() |
| 464 | + config.reasoning.completion_check = False |
| 465 | + agent = Agent( |
| 466 | + backend=backend, |
| 467 | + config=config, |
| 468 | + project_root=temp_dir, |
| 469 | + ) |
| 470 | + runtime = ConversationRuntime(agent) |
| 471 | + events = [] |
| 472 | + |
| 473 | + async def capture(event) -> None: |
| 474 | + events.append(event) |
| 475 | + |
| 476 | + prepared = await runtime.turn_preparation.prepare( |
| 477 | + task=( |
| 478 | + "Create a multi-file nginx guide under ~/Loader/guides/nginx " |
| 479 | + "with an index and chapter files." |
| 480 | + ), |
| 481 | + emit=capture, |
| 482 | + requested_mode="execute", |
| 483 | + original_task=None, |
| 484 | + on_user_question=None, |
| 485 | + ) |
| 486 | + await runtime.phase_tracker.enter( |
| 487 | + TurnPhase.ASSISTANT, |
| 488 | + capture, |
| 489 | + detail="Requesting assistant response", |
| 490 | + reason_code="request_assistant_response", |
| 491 | + ) |
| 492 | + |
| 493 | + implementation_plan = temp_dir / "implementation.md" |
| 494 | + implementation_plan.write_text( |
| 495 | + "# Implementation Plan\n\n" |
| 496 | + "## File Changes\n\n" |
| 497 | + f"- `{temp_dir / 'index.html'}`\n" |
| 498 | + f"- `{temp_dir / 'chapters' / '01-introduction.html'}`\n" |
| 499 | + ) |
| 500 | + chapters_dir = temp_dir / "chapters" |
| 501 | + chapters_dir.mkdir() |
| 502 | + |
| 503 | + prepared.definition_of_done.implementation_plan = str(implementation_plan) |
| 504 | + prepared.definition_of_done.pending_items.append( |
| 505 | + "Develop the main index.html file for nginx guide" |
| 506 | + ) |
| 507 | + |
| 508 | + agent.session.append( |
| 509 | + Message( |
| 510 | + role=Role.USER, |
| 511 | + content=( |
| 512 | + "[USER INTERRUPTION]: Directory setup is complete. Continue with the next pending item: " |
| 513 | + "`Develop the main index.html file for nginx guide`. Resume by creating `index.html` now. " |
| 514 | + f"Prefer one `write` call for `{(temp_dir / 'index.html').resolve(strict=False)}` instead of more rereads. " |
| 515 | + "Make your next response the concrete mutation tool call itself, not another bookkeeping-only turn." |
| 516 | + ), |
| 517 | + ) |
| 518 | + ) |
| 519 | + |
| 520 | + content = "Now I'll create the main index.html file for the nginx guide." |
| 521 | + decision = await runtime.turn_completion.handle_text_response( |
| 522 | + content=content, |
| 523 | + response_content=content, |
| 524 | + task=prepared.task, |
| 525 | + effective_task=prepared.effective_task, |
| 526 | + iterations=1, |
| 527 | + max_iterations=agent.config.max_iterations, |
| 528 | + actions_taken=[], |
| 529 | + continuation_count=0, |
| 530 | + dod=prepared.definition_of_done, |
| 531 | + emit=capture, |
| 532 | + summary=prepared.summary, |
| 533 | + executor=prepared.executor, |
| 534 | + rollback_plan=prepared.rollback_plan, |
| 535 | + ) |
| 536 | + |
| 537 | + assert decision.action == TurnCompletionAction.CONTINUE |
| 538 | + assert decision.continuation_count == 1 |
| 539 | + assert prepared.summary.assistant_messages[-1].content == content |
| 540 | + assert agent.session.messages[-1].role.value == "user" |
| 541 | + assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") |
| 542 | + assert "index.html" in agent.session.messages[-1].content |
| 543 | + |
| 544 | + |
| 457 | 545 | @pytest.mark.asyncio |
| 458 | 546 | async def test_turn_completion_handles_fake_tool_narration_without_reroute( |
| 459 | 547 | temp_dir: Path, |