@@ -781,6 +781,96 @@ async def test_turn_completion_first_chapter_continuation_allows_compact_initial |
| 781 | 781 | assert "write a compact but real initial version of that file now" in agent.session.messages[-1].content.lower() |
| 782 | 782 | |
| 783 | 783 | |
| 784 | +@pytest.mark.asyncio |
| 785 | +async def test_turn_completion_interrupts_first_chapter_narration_from_declared_index_graph( |
| 786 | + temp_dir: Path, |
| 787 | +) -> None: |
| 788 | + backend = ScriptedBackend() |
| 789 | + config = non_streaming_config() |
| 790 | + config.reasoning.completion_check = False |
| 791 | + agent = Agent( |
| 792 | + backend=backend, |
| 793 | + config=config, |
| 794 | + project_root=temp_dir, |
| 795 | + ) |
| 796 | + runtime = ConversationRuntime(agent) |
| 797 | + events = [] |
| 798 | + |
| 799 | + async def capture(event) -> None: |
| 800 | + events.append(event) |
| 801 | + |
| 802 | + prepared = await runtime.turn_preparation.prepare( |
| 803 | + task=( |
| 804 | + "Create a multi-file nginx guide under ~/Loader/guides/nginx " |
| 805 | + "with an index and chapter files." |
| 806 | + ), |
| 807 | + emit=capture, |
| 808 | + requested_mode="execute", |
| 809 | + original_task=None, |
| 810 | + on_user_question=None, |
| 811 | + ) |
| 812 | + await runtime.phase_tracker.enter( |
| 813 | + TurnPhase.ASSISTANT, |
| 814 | + capture, |
| 815 | + detail="Requesting assistant response", |
| 816 | + reason_code="request_assistant_response", |
| 817 | + ) |
| 818 | + |
| 819 | + guide_root = temp_dir / "Loader" / "guides" / "nginx" |
| 820 | + chapters_dir = guide_root / "chapters" |
| 821 | + chapters_dir.mkdir(parents=True) |
| 822 | + index_path = guide_root / "index.html" |
| 823 | + index_path.write_text( |
| 824 | + "\n".join( |
| 825 | + [ |
| 826 | + "<!DOCTYPE html>", |
| 827 | + '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>', |
| 828 | + '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>', |
| 829 | + "", |
| 830 | + ] |
| 831 | + ) |
| 832 | + ) |
| 833 | + |
| 834 | + implementation_plan = temp_dir / "implementation.md" |
| 835 | + implementation_plan.write_text( |
| 836 | + "# Implementation Plan\n\n" |
| 837 | + "## File Changes\n\n" |
| 838 | + f"- `{index_path}`\n" |
| 839 | + f"- `{chapters_dir}/`\n" |
| 840 | + ) |
| 841 | + |
| 842 | + prepared.definition_of_done.implementation_plan = str(implementation_plan) |
| 843 | + prepared.definition_of_done.touched_files.append(str(index_path)) |
| 844 | + prepared.definition_of_done.mutating_actions.append("write") |
| 845 | + prepared.definition_of_done.pending_items.append( |
| 846 | + "Develop the nginx guide content following the same structure and cadence as the fortran guide" |
| 847 | + ) |
| 848 | + |
| 849 | + content = "Now I'll create the first chapter of the nginx guide." |
| 850 | + decision = await runtime.turn_completion.handle_text_response( |
| 851 | + content=content, |
| 852 | + response_content=content, |
| 853 | + task=prepared.task, |
| 854 | + effective_task=prepared.effective_task, |
| 855 | + iterations=1, |
| 856 | + max_iterations=agent.config.max_iterations, |
| 857 | + actions_taken=[], |
| 858 | + continuation_count=0, |
| 859 | + dod=prepared.definition_of_done, |
| 860 | + emit=capture, |
| 861 | + summary=prepared.summary, |
| 862 | + executor=prepared.executor, |
| 863 | + rollback_plan=prepared.rollback_plan, |
| 864 | + ) |
| 865 | + |
| 866 | + assert decision.action == TurnCompletionAction.CONTINUE |
| 867 | + assert decision.continuation_count == 1 |
| 868 | + assert prepared.summary.completion_decision_code == "in_progress_transition_continue" |
| 869 | + assert agent.session.messages[-1].role.value == "user" |
| 870 | + assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") |
| 871 | + assert "01-introduction.html" in agent.session.messages[-1].content |
| 872 | + |
| 873 | + |
| 784 | 874 | @pytest.mark.asyncio |
| 785 | 875 | async def test_turn_completion_handles_fake_tool_narration_without_reroute( |
| 786 | 876 | temp_dir: Path, |