tenseleyflow/loader / edd45ab

Browse files

Interrupt narrated next steps

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
edd45ab9df650772d886e27c9bc9f24a89c9c4c4
Parents
77e1893
Tree
fcf10db

2 changed files

StatusFile+-
M src/loader/runtime/turn_completion.py 36 1
M tests/test_turn_completion.py 88 0
src/loader/runtime/turn_completion.pymodified
@@ -272,7 +272,14 @@ class TurnCompletionController:
272272
             assistant_message = Message(role=Role.ASSISTANT, content=response_content)
273273
             self.context.session.append(assistant_message)
274274
             summary.assistant_messages.append(assistant_message)
275
-            if progress_intent.target is not None and continuation_count == 0:
275
+            if (
276
+                progress_intent.target is not None
277
+                and continuation_count == 0
278
+                and not _recent_concrete_target_prompt(
279
+                    progress_messages,
280
+                    target=progress_intent.target,
281
+                )
282
+            ):
276283
                 self._append_completion_trace_entry(
277284
                     summary=summary,
278285
                     stage="continuation_check",
@@ -524,3 +531,31 @@ def _preferred_progress_target(
524531
     if next_output_file is not None:
525532
         return next_output_file
526533
     return None
534
+
535
+
536
+def _recent_concrete_target_prompt(
537
+    messages: list[object],
538
+    *,
539
+    target: Path,
540
+) -> bool:
541
+    target = target.expanduser().resolve(strict=False)
542
+    target_text = str(target)
543
+    target_name = target.name
544
+    for message in reversed(messages[-6:]):
545
+        role = getattr(message, "role", None)
546
+        if getattr(role, "value", role) != "user":
547
+            continue
548
+        content = str(getattr(message, "content", "") or "")
549
+        if not content:
550
+            continue
551
+        if "[CONTINUE CURRENT STEP]" not in content and "[USER INTERRUPTION]" not in content and "[EMPTY ASSISTANT RESPONSE]" not in content:
552
+            continue
553
+        if target_text not in content and target_name not in content:
554
+            continue
555
+        if (
556
+            "concrete mutation tool call" in content
557
+            or "Resume by creating" in content
558
+            or "Emit this tool shape now" in content
559
+        ):
560
+            return True
561
+    return False
tests/test_turn_completion.pymodified
@@ -7,6 +7,7 @@ from pathlib import Path
77
 import pytest
88
 
99
 from loader.agent.loop import Agent, AgentConfig
10
+from loader.llm.base import Message, Role
1011
 from loader.runtime.conversation import ConversationRuntime
1112
 from loader.runtime.dod import VerificationEvidence
1213
 from loader.runtime.phases import TurnPhase
@@ -454,6 +455,93 @@ async def test_turn_completion_interrupts_repeated_concrete_progress_narration(
454455
     assert "02-installation.html" in agent.session.messages[-1].content
455456
 
456457
 
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
+
457545
 @pytest.mark.asyncio
458546
 async def test_turn_completion_handles_fake_tool_narration_without_reroute(
459547
     temp_dir: Path,