"""Tests for no-tool text completion orchestration.""" from __future__ import annotations from pathlib import Path import pytest from loader.agent.loop import Agent, AgentConfig from loader.llm.base import Message, Role from loader.runtime.conversation import ConversationRuntime from loader.runtime.dod import VerificationEvidence from loader.runtime.phases import TurnPhase from loader.runtime.turn_completion import TurnCompletionAction from loader.runtime.verification_observations import VerificationObservationStatus from tests.helpers.runtime_harness import ScriptedBackend def non_streaming_config() -> AgentConfig: """Shared config for direct turn-completion tests.""" return AgentConfig(auto_context=False, stream=False, max_iterations=8) @pytest.mark.asyncio async def test_turn_completion_requests_continuation_for_premature_text_response( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Fix the README heading.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) decision = await runtime.turn_completion.handle_text_response( content="I looked into it.", response_content="I looked into it.", task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 1 assert prepared.summary.completion_decision_code == "premature_completion_nudge" assert prepared.summary.completion_decision_summary == ( "requested one continuation because the non-mutating response looked incomplete" ) assert agent.session.last_completion_decision_code == "premature_completion_nudge" assert [ entry.decision_code for entry in prepared.summary.completion_trace ] == ["premature_completion_nudge"] assert prepared.summary.completion_trace[0].stage == "continuation_check" assert [entry.kind for entry in prepared.summary.workflow_timeline[-1:]] == [ "completion_continue" ] assert prepared.summary.workflow_timeline[-1].policy_stage == "continuation_check" assert prepared.summary.workflow_timeline[-1].policy_outcome == "continue" assert agent.session.messages[-1].role.value == "user" assert "concrete evidence" in agent.session.messages[-1].content assert "Carry out the requested change or command now" in agent.session.messages[-1].content assert any(event.type == "completion_check" for event in events) @pytest.mark.asyncio async def test_turn_completion_marks_non_mutating_response_done( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Explain Loader's clarify loop.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) decision = await runtime.turn_completion.handle_text_response( content="Loader uses a bounded clarify loop before execution.", response_content="Loader uses a bounded clarify loop before execution.", task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.COMPLETE assert prepared.summary.final_response == ( "Loader uses a bounded clarify loop before execution." ) assert prepared.summary.completion_decision_code == "non_mutating_response_accepted" assert prepared.summary.completion_decision_summary == ( "accepted the response because no mutating work required verification" ) assert agent.session.last_completion_decision_code == ( "non_mutating_response_accepted" ) assert [ entry.decision_code for entry in prepared.summary.completion_trace ] == [ "completion_response_accepted", "non_mutating_response_accepted", ] policy_entries = [ entry for entry in prepared.summary.workflow_timeline if entry.kind.startswith("completion_") ] assert [entry.kind for entry in policy_entries] == [ "completion_check", "completion_complete", ] assert policy_entries[0].policy_stage == "continuation_check" assert policy_entries[-1].policy_stage == "definition_of_done" assert [item.summary for item in prepared.summary.completion_trace[-1].evidence_provenance] == [ "verification was skipped because no mutating work required checks" ] assert [ item.status for item in prepared.summary.completion_trace[-1].verification_observations ] == [VerificationObservationStatus.SKIPPED.value] assert [ item.summary for item in prepared.summary.completion_trace[-1].verification_observations ] == ["verification was skipped because no mutating work required checks"] assert [item.status for item in policy_entries[-1].verification_observations] == [ VerificationObservationStatus.SKIPPED.value ] assert prepared.definition_of_done.status == "done" assert prepared.definition_of_done.last_verification_result == "skipped" assert any(event.type == "response" for event in events) assert any( event.type == "dod_status" and event.dod_status == "done" for event in events ) @pytest.mark.asyncio async def test_turn_completion_blocks_false_completion_without_preserving_it( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" "1. Create main index.html file:\n" " - `index.html`\n\n" "2. Create chapter files:\n" " - `chapters/01-getting-started.html`\n" " - `chapters/06-troubleshooting.html`\n" ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() (chapters_dir / "01-getting-started.html").write_text("

Getting Started

\n") (temp_dir / "index.html").write_text("

NGINX Guide

\n") prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.mutating_actions.append("write") prepared.definition_of_done.touched_files.extend( [ str(temp_dir / "index.html"), str(chapters_dir / "01-getting-started.html"), ] ) queued_messages: list[str] = [] runtime.context.queue_steering_message_callback = queued_messages.append completion_claim = ( "I've successfully completed the NGINX guide with all planned files " "and verified everything is done." ) decision = await runtime.turn_completion.handle_text_response( content=completion_claim, response_content=completion_claim, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert prepared.summary.assistant_messages == [] assert not any( message.role.value == "assistant" and message.content == completion_claim for message in agent.session.messages ) assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith( "[PLANNED ARTIFACTS STILL MISSING]" ) assert "`06-troubleshooting.html`" in agent.session.messages[-1].content assert queued_messages assert "06-troubleshooting.html" in queued_messages[-1] assert "Do not summarize, mark completion, or write bookkeeping notes yet" in queued_messages[-1] assert not any(event.type == "response" for event in events) @pytest.mark.asyncio async def test_turn_completion_interrupts_progress_intent_once_output_files_exist( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" "1. Create main index.html file:\n" f" - `{temp_dir / 'index.html'}`\n\n" "2. Create chapter files:\n" f" - `{temp_dir / 'chapters' / '01-introduction.html'}`\n" f" - `{temp_dir / 'chapters' / '02-installation.html'}`\n" ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() (temp_dir / "index.html").write_text("

NGINX Guide

\n") (chapters_dir / "01-introduction.html").write_text("

Intro

\n") prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.mutating_actions.append("write") prepared.definition_of_done.touched_files.extend( [ str(temp_dir / "index.html"), str(chapters_dir / "01-introduction.html"), ] ) prepared.definition_of_done.pending_items.append("Create chapter files for nginx guide") content = "Now I'll create the second chapter file for the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 1 assert prepared.summary.completion_decision_code == "in_progress_transition_continue" assert prepared.summary.assistant_messages[-1].content == content assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "02-installation.html" in agent.session.messages[-1].content assert not any( message.role.value == "user" and message.content.startswith("[PLANNED ARTIFACTS STILL MISSING]") for message in agent.session.messages ) @pytest.mark.asyncio async def test_turn_completion_uses_quality_repair_prompt_for_rewrite_narration( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Create an equally thorough HTML guide.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) chapter = temp_dir / "guides" / "nginx" / "chapters" / "01-introduction.html" chapter.parent.mkdir(parents=True) chapter.write_text("

Intro

\n") prepared.definition_of_done.touched_files.append(str(chapter)) prepared.definition_of_done.mutating_actions.append("write") agent.session.append( Message( role=Role.USER, content=( "Repair focus:\n" f"- Improve `{chapter}`: insufficient structured content " "(12 blocks, expected at least 18).\n" f"- Immediate next step: edit `{chapter}` with a substantial " "expansion or replacement that satisfies its listed quality issue.\n" ), ) ) content = ( "Let me try a different approach by rewriting the entire file with more " "comprehensive content:" ) decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert prepared.summary.completion_decision_code == "in_progress_transition_continue" assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE QUALITY REPAIR]") assert str(chapter.resolve(strict=False)) in agent.session.messages[-1].content assert ( "one concrete `patch`, `edit`, or `write` tool call" in agent.session.messages[-1].content ) assert "Do not rewrite the whole file from memory" in agent.session.messages[-1].content @pytest.mark.asyncio async def test_turn_completion_uses_exact_anchor_after_stale_quality_repair_context( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Create an equally thorough HTML guide.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) chapter = temp_dir / "guides" / "nginx" / "chapters" / "05-load-balancing.html" chapter.parent.mkdir(parents=True) chapter.write_text("

Load Balancing

\n") prepared.definition_of_done.touched_files.append(str(chapter)) prepared.definition_of_done.mutating_actions.append("edit") agent.session.append( Message( role=Role.USER, content=( "Repair focus:\n" f"- Improve `{chapter}`: thin content " "(846 text chars, expected at least 1758).\n" f"- Immediate next step: edit `{chapter}`.\n" ), ) ) agent.session.append( Message( role=Role.TOOL, content=( "Observation [edit]: Error: Failed to complete the operation after " f"2 attempts for {chapter}. old_string not found in file." ), ) ) content = "I'll rewrite the load balancing chapter with comprehensive content." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE message = agent.session.messages[-1].content assert message.startswith("[CONTINUE QUALITY REPAIR]") assert "exactly one `edit(file_path=..., old_string=..., new_string=...)`" in message assert "Use this exact `old_string` value from the current file" in message assert "```html\n\n```" in message assert "Do not call `read`, `patch`, `write`, `TodoWrite`, or summarize." in message @pytest.mark.asyncio async def test_turn_completion_forces_write_for_structural_html_repair( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Create an equally thorough HTML guide.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) chapter = temp_dir / "guides" / "nginx" / "chapters" / "08-troubleshooting.html" chapter.parent.mkdir(parents=True) chapter.write_text( "

Troubleshooting

\n" "

Trailing content.

\n" ) prepared.definition_of_done.touched_files.append(str(chapter)) agent.session.append( Message( role=Role.USER, content=( "Repair focus:\n" f"- Improve `{chapter}`: expected exactly one closing tag (found 2).\n" f"- Immediate next step: replace `{chapter}` with one complete valid HTML document.\n" ), ) ) content = "I will fix the malformed troubleshooting HTML structure." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE message = agent.session.messages[-1].content assert message.startswith("[CONTINUE QUALITY REPAIR]") assert "malformed HTML document structure" in message assert "expected exactly one closing " in message assert "exactly one closing `` tag" in message assert "exactly one `write(file_path=..., content=...)`" in message @pytest.mark.asyncio async def test_turn_completion_continues_queued_quality_repair_after_summary( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Repair generated HTML guide quality.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) first = temp_dir / "guides" / "nginx" / "chapters" / "01-introduction.html" second = temp_dir / "guides" / "nginx" / "chapters" / "02-installation.html" second.parent.mkdir(parents=True) first.write_text("

Intro

\n") second.write_text("

Install

\n") prepared.definition_of_done.touched_files.extend( [ str(first), str(second), ] ) prepared.definition_of_done.mutating_actions.append("edit") agent.session.append( Message( role=Role.USER, content=( "The active HTML content-quality repair target was updated. " f"Continue directly with the next listed quality target `{second}` " "using one substantial write/edit/patch anchored to current content.\n\n" "Repair focus:\n" f"- Improve `{second}`: thin content (513 text chars, expected at least 1758).\n" f"- Immediate next step: edit `{second}`.\n" "- Continue with one concrete `edit`, `patch`, or `write` call that actually changes the current generated file." ), ) ) content = ( "I've expanded the introduction chapter, so it should now meet the " "minimum quality threshold." ) decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert prepared.summary.completion_decision_code == "pending_quality_repair_continue" assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE QUALITY REPAIR]") assert str(second.resolve(strict=False)) in agent.session.messages[-1].content assert "one concrete `patch`, `edit`, or `write` tool call" in agent.session.messages[-1].content @pytest.mark.asyncio async def test_turn_completion_allows_first_progress_narration_before_any_output_exists( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" f"- `{temp_dir / 'index.html'}`\n" f"- `{temp_dir / 'chapters' / '01-introduction.html'}`\n" ) prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.pending_items.append( "Develop the main index.html file for nginx guide" ) content = "Now I'll create the main index.html file for the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 1 assert prepared.summary.assistant_messages[-1].content == content assert agent.session.messages[-1].role.value == "assistant" @pytest.mark.asyncio async def test_turn_completion_interrupts_repeated_concrete_progress_narration( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" "1. Create main index.html file:\n" f" - `{temp_dir / 'index.html'}`\n\n" "2. Create chapter files:\n" f" - `{temp_dir / 'chapters' / '01-introduction.html'}`\n" f" - `{temp_dir / 'chapters' / '02-installation.html'}`\n" ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() (temp_dir / "index.html").write_text("

NGINX Guide

\n") (chapters_dir / "01-introduction.html").write_text("

Intro

\n") prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.mutating_actions.append("write") prepared.definition_of_done.touched_files.extend( [ str(temp_dir / "index.html"), str(chapters_dir / "01-introduction.html"), ] ) prepared.definition_of_done.pending_items.append("Create chapter files for nginx guide") content = "Now I'll create the second chapter file for the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=1, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 2 assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "02-installation.html" in agent.session.messages[-1].content @pytest.mark.asyncio async def test_turn_completion_prioritizes_missing_artifact_continuation_over_text_loop( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" "1. Create main index.html file:\n" f" - `{temp_dir / 'index.html'}`\n\n" "2. Create chapter files:\n" f" - `{temp_dir / 'chapters' / '01-introduction.html'}`\n" f" - `{temp_dir / 'chapters' / '02-installation.html'}`\n" ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() (temp_dir / "index.html").write_text("

NGINX Guide

\n") (chapters_dir / "01-introduction.html").write_text("

Intro

\n") prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.mutating_actions.append("write") prepared.definition_of_done.touched_files.extend( [ str(temp_dir / "index.html"), str(chapters_dir / "01-introduction.html"), ] ) prepared.definition_of_done.pending_items.append("Create chapter files for nginx guide") content = "Let me continue creating the remaining chapter files for the nginx guide:" runtime.context.safeguards.record_response(content) runtime.context.safeguards.record_response(content) decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=2, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert prepared.summary.completion_decision_code == "in_progress_transition_continue" assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "02-installation.html" in agent.session.messages[-1].content assert not prepared.summary.final_response assert not any(event.type == "error" and "Text loop detected" in event.content for event in events) @pytest.mark.asyncio async def test_turn_completion_interrupts_first_narration_after_concrete_target_prompt( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" f"- `{temp_dir / 'index.html'}`\n" f"- `{temp_dir / 'chapters' / '01-introduction.html'}`\n" ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.pending_items.append( "Develop the main index.html file for nginx guide" ) agent.session.append( Message( role=Role.USER, content=( "[USER INTERRUPTION]: Directory setup is complete. Continue with the next pending item: " "`Develop the main index.html file for nginx guide`. Resume by creating `index.html` now. " f"Prefer one `write` call for `{(temp_dir / 'index.html').resolve(strict=False)}` instead of more rereads. " "Make your next response the concrete mutation tool call itself, not another bookkeeping-only turn." ), ) ) content = "Now I'll create the main index.html file for the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 1 assert prepared.summary.assistant_messages[-1].content == content assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "index.html" in agent.session.messages[-1].content @pytest.mark.asyncio async def test_turn_completion_first_chapter_continuation_allows_compact_initial_version( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) chapters_dir = temp_dir / "chapters" chapters_dir.mkdir() index_path = temp_dir / "index.html" index_path.write_text("\n") implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" f"- `{index_path}`\n" f"- `{chapters_dir / '01-introduction.html'}`\n" ) prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.touched_files.append(str(index_path)) prepared.definition_of_done.pending_items.append("Create chapter files for nginx guide") content = "Now I'll create the first chapter of the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=1, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 2 assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "01-introduction.html" in agent.session.messages[-1].content assert "write a compact but real initial version of that file now" in agent.session.messages[-1].content.lower() @pytest.mark.asyncio async def test_turn_completion_interrupts_first_chapter_narration_from_declared_index_graph( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task=( "Create a multi-file nginx guide under ~/Loader/guides/nginx " "with an index and chapter files." ), emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) guide_root = temp_dir / "Loader" / "guides" / "nginx" chapters_dir = guide_root / "chapters" chapters_dir.mkdir(parents=True) index_path = guide_root / "index.html" index_path.write_text( "\n".join( [ "", 'Chapter 1: Introduction to Nginx', 'Chapter 2: Installation and Setup', "", ] ) ) implementation_plan = temp_dir / "implementation.md" implementation_plan.write_text( "# Implementation Plan\n\n" "## File Changes\n\n" f"- `{index_path}`\n" f"- `{chapters_dir}/`\n" ) prepared.definition_of_done.implementation_plan = str(implementation_plan) prepared.definition_of_done.touched_files.append(str(index_path)) prepared.definition_of_done.mutating_actions.append("write") prepared.definition_of_done.pending_items.append( "Develop the nginx guide content following the same structure and cadence as the fortran guide" ) content = "Now I'll create the first chapter of the nginx guide." decision = await runtime.turn_completion.handle_text_response( content=content, response_content=content, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.CONTINUE assert decision.continuation_count == 1 assert prepared.summary.completion_decision_code == "in_progress_transition_continue" assert agent.session.messages[-1].role.value == "user" assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") assert "01-introduction.html" in agent.session.messages[-1].content @pytest.mark.asyncio async def test_turn_completion_handles_fake_tool_narration_without_reroute( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Summarize the current test status.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) narrated = "Used bash tool with command `pytest -q` and everything passed." decision = await runtime.turn_completion.handle_text_response( content=narrated, response_content=narrated, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.COMPLETE assert prepared.summary.final_response == narrated assert prepared.summary.completion_decision_code == "non_mutating_response_accepted" assert prepared.summary.completion_trace[-1].decision_code == ( "non_mutating_response_accepted" ) assert not any( "PRETENDING to use tools" in message.content for message in agent.session.messages ) assert any(event.type == "response" and event.content == narrated for event in events) @pytest.mark.asyncio async def test_turn_completion_handles_deflection_text_without_repair_prompt( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="What should I verify next?", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) deflection = "You can run pytest -q to verify the current state." decision = await runtime.turn_completion.handle_text_response( content=deflection, response_content=deflection, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.COMPLETE assert prepared.summary.final_response == deflection assert prepared.summary.completion_decision_code == "non_mutating_response_accepted" assert prepared.summary.completion_trace[-1].decision_code == ( "non_mutating_response_accepted" ) assert not any( "Please use your tools to execute the task" in message.content for message in agent.session.messages ) assert any(event.type == "response" and event.content == deflection for event in events) @pytest.mark.asyncio async def test_turn_completion_skips_self_critique_reroute( temp_dir: Path, ) -> None: backend = ScriptedBackend() config = non_streaming_config() config.reasoning.completion_check = False config.reasoning.self_critique = True agent = Agent( backend=backend, config=config, project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Explain Loader's clarify loop.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) detailed = ( "Loader might begin with a bounded clarify pass, perhaps asking follow-up " "questions when the task leaves touchpoints or decision boundaries unclear. " "It then shifts into execution once the workflow policy is satisfied." ) decision = await runtime.turn_completion.handle_text_response( content=detailed, response_content=detailed, task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=0, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.COMPLETE assert prepared.summary.final_response == detailed assert prepared.summary.completion_decision_code == "non_mutating_response_accepted" assert prepared.summary.completion_trace[-1].decision_code == ( "non_mutating_response_accepted" ) assert not any("[SELF-CRITIQUE]" in message.content for message in agent.session.messages) assert not any(event.type == "critique" for event in events) @pytest.mark.asyncio async def test_turn_completion_finalizes_when_follow_through_budget_is_exhausted( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Fix the README heading.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) decision = await runtime.turn_completion.handle_text_response( content="I looked into it.", response_content="I looked into it.", task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=agent.config.reasoning.max_continuation_prompts, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.FINALIZE assert decision.finalize_reason_code == "continuation_budget_exhausted" assert prepared.summary.final_response.startswith( "I stopped because I still could not show enough evidence" ) assert prepared.summary.completion_decision_code == "continuation_budget_exhausted" assert prepared.summary.failures == [ "missing follow-through evidence after continuation budget exhaustion" ] assert prepared.summary.completion_trace[-1].outcome == "finalize" assert prepared.summary.completion_trace[-1].decision_code == ( "continuation_budget_exhausted" ) assert prepared.summary.completion_trace[-1].evidence_summary == [ "showing the requested work was actually carried out" ] assert [item.status for item in prepared.summary.completion_trace[-1].evidence_provenance] == [ "missing" ] assert prepared.summary.workflow_timeline[-1].kind == "completion_finalize" assert prepared.summary.workflow_timeline[-1].evidence_summary == [ "showing the requested work was actually carried out" ] assert [event.type for event in events[-3:]] == [ "completion_check", "error", "response", ] @pytest.mark.asyncio async def test_turn_completion_uses_observed_verification_for_budget_exhaustion( temp_dir: Path, ) -> None: backend = ScriptedBackend() agent = Agent( backend=backend, config=non_streaming_config(), project_root=temp_dir, ) runtime = ConversationRuntime(agent) events = [] async def capture(event) -> None: events.append(event) prepared = await runtime.turn_preparation.prepare( task="Run pytest -q and make sure it works.", emit=capture, requested_mode="execute", original_task=None, on_user_question=None, ) prepared.definition_of_done.verification_commands = ["pytest -q"] prepared.definition_of_done.evidence = [ VerificationEvidence( command="pytest -q", passed=False, stderr="1 failed", kind="test", ) ] prepared.definition_of_done.last_verification_result = "failed" await runtime.phase_tracker.enter( TurnPhase.ASSISTANT, capture, detail="Requesting assistant response", reason_code="request_assistant_response", ) decision = await runtime.turn_completion.handle_text_response( content="The tests are done.", response_content="The tests are done.", task=prepared.task, effective_task=prepared.effective_task, iterations=1, max_iterations=agent.config.max_iterations, actions_taken=[], continuation_count=agent.config.reasoning.max_continuation_prompts, dod=prepared.definition_of_done, emit=capture, summary=prepared.summary, executor=prepared.executor, rollback_plan=prepared.rollback_plan, ) assert decision.action == TurnCompletionAction.FINALIZE assert decision.finalize_reason_code == "continuation_budget_exhausted" assert prepared.summary.final_response == ( "I stopped because the continuation budget was exhausted and observed " "verification still showed: verification failed for `pytest -q` [1 failed]." ) assert prepared.summary.completion_trace[-1].decision_code == ( "continuation_budget_exhausted" ) assert [ item.status for item in prepared.summary.completion_trace[-1].verification_observations ] == [VerificationObservationStatus.FAILED.value] assert [ item.summary for item in prepared.summary.completion_trace[-1].verification_observations ] == ["verification failed for `pytest -q`"] assert [ item.status for item in prepared.summary.workflow_timeline[-1].verification_observations ] == [VerificationObservationStatus.FAILED.value]