tenseleyflow/loader / 4a5feaf

Browse files

Interrupt later progress narration

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4a5feafd03a4ae867234c0d96c738f93ab6442b6
Parents
edd45ab
Tree
143fc1b

2 changed files

StatusFile+-
M src/loader/runtime/turn_completion.py 27 0
M tests/test_turn_completion.py 75 2
src/loader/runtime/turn_completion.pymodified
@@ -275,6 +275,11 @@ class TurnCompletionController:
275275
             if (
276276
                 progress_intent.target is not None
277277
                 and continuation_count == 0
278
+                and _confirmed_output_file_count(
279
+                    dod,
280
+                    project_root=self.context.project_root,
281
+                )
282
+                == 0
278283
                 and not _recent_concrete_target_prompt(
279284
                     progress_messages,
280285
                     target=progress_intent.target,
@@ -533,6 +538,28 @@ def _preferred_progress_target(
533538
     return None
534539
 
535540
 
541
+def _confirmed_output_file_count(
542
+    dod: DefinitionOfDone,
543
+    *,
544
+    project_root: Path,
545
+) -> int:
546
+    return sum(
547
+        1
548
+        for target, expect_directory in collect_planned_artifact_targets(
549
+            dod,
550
+            project_root=project_root,
551
+            max_paths=12,
552
+        )
553
+        if not expect_directory
554
+        and planned_artifact_target_satisfied(
555
+            dod,
556
+            target=target,
557
+            expect_directory=False,
558
+            project_root=project_root,
559
+        )
560
+    )
561
+
562
+
536563
 def _recent_concrete_target_prompt(
537564
     messages: list[object],
538565
     *,
tests/test_turn_completion.pymodified
@@ -285,7 +285,7 @@ async def test_turn_completion_blocks_false_completion_without_preserving_it(
285285
 
286286
 
287287
 @pytest.mark.asyncio
288
-async def test_turn_completion_continues_progress_intent_without_dod_gate_spam(
288
+async def test_turn_completion_interrupts_progress_intent_once_output_files_exist(
289289
     temp_dir: Path,
290290
 ) -> None:
291291
     backend = ScriptedBackend()
@@ -363,7 +363,9 @@ async def test_turn_completion_continues_progress_intent_without_dod_gate_spam(
363363
     assert decision.continuation_count == 1
364364
     assert prepared.summary.completion_decision_code == "in_progress_transition_continue"
365365
     assert prepared.summary.assistant_messages[-1].content == content
366
-    assert agent.session.messages[-1].role.value == "assistant"
366
+    assert agent.session.messages[-1].role.value == "user"
367
+    assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]")
368
+    assert "02-installation.html" in agent.session.messages[-1].content
367369
     assert not any(
368370
         message.role.value == "user"
369371
         and message.content.startswith("[PLANNED ARTIFACTS STILL MISSING]")
@@ -371,6 +373,77 @@ async def test_turn_completion_continues_progress_intent_without_dod_gate_spam(
371373
     )
372374
 
373375
 
376
+@pytest.mark.asyncio
377
+async def test_turn_completion_allows_first_progress_narration_before_any_output_exists(
378
+    temp_dir: Path,
379
+) -> None:
380
+    backend = ScriptedBackend()
381
+    config = non_streaming_config()
382
+    config.reasoning.completion_check = False
383
+    agent = Agent(
384
+        backend=backend,
385
+        config=config,
386
+        project_root=temp_dir,
387
+    )
388
+    runtime = ConversationRuntime(agent)
389
+    events = []
390
+
391
+    async def capture(event) -> None:
392
+        events.append(event)
393
+
394
+    prepared = await runtime.turn_preparation.prepare(
395
+        task=(
396
+            "Create a multi-file nginx guide under ~/Loader/guides/nginx "
397
+            "with an index and chapter files."
398
+        ),
399
+        emit=capture,
400
+        requested_mode="execute",
401
+        original_task=None,
402
+        on_user_question=None,
403
+    )
404
+    await runtime.phase_tracker.enter(
405
+        TurnPhase.ASSISTANT,
406
+        capture,
407
+        detail="Requesting assistant response",
408
+        reason_code="request_assistant_response",
409
+    )
410
+
411
+    implementation_plan = temp_dir / "implementation.md"
412
+    implementation_plan.write_text(
413
+        "# Implementation Plan\n\n"
414
+        "## File Changes\n\n"
415
+        f"- `{temp_dir / 'index.html'}`\n"
416
+        f"- `{temp_dir / 'chapters' / '01-introduction.html'}`\n"
417
+    )
418
+
419
+    prepared.definition_of_done.implementation_plan = str(implementation_plan)
420
+    prepared.definition_of_done.pending_items.append(
421
+        "Develop the main index.html file for nginx guide"
422
+    )
423
+
424
+    content = "Now I'll create the main index.html file for the nginx guide."
425
+    decision = await runtime.turn_completion.handle_text_response(
426
+        content=content,
427
+        response_content=content,
428
+        task=prepared.task,
429
+        effective_task=prepared.effective_task,
430
+        iterations=1,
431
+        max_iterations=agent.config.max_iterations,
432
+        actions_taken=[],
433
+        continuation_count=0,
434
+        dod=prepared.definition_of_done,
435
+        emit=capture,
436
+        summary=prepared.summary,
437
+        executor=prepared.executor,
438
+        rollback_plan=prepared.rollback_plan,
439
+    )
440
+
441
+    assert decision.action == TurnCompletionAction.CONTINUE
442
+    assert decision.continuation_count == 1
443
+    assert prepared.summary.assistant_messages[-1].content == content
444
+    assert agent.session.messages[-1].role.value == "assistant"
445
+
446
+
374447
 @pytest.mark.asyncio
375448
 async def test_turn_completion_interrupts_repeated_concrete_progress_narration(
376449
     temp_dir: Path,