Nudge active quality repairs
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
9097b6f61693c22a7d6e059da76ade978d7c743f- Parents
-
8f1b6b4 - Tree
bfe79a7
9097b6f
9097b6f61693c22a7d6e059da76ade978d7c743f8f1b6b4
bfe79a7| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/turn_completion.py
|
56 | 0 |
| M |
tests/test_turn_completion.py
|
82 | 0 |
src/loader/runtime/turn_completion.pymodified@@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable | ||
| 6 | 6 | from dataclasses import dataclass |
| 7 | 7 | from enum import StrEnum |
| 8 | 8 | from pathlib import Path |
| 9 | +from typing import cast | |
| 9 | 10 | |
| 10 | 11 | from ..llm.base import Message, Role |
| 11 | 12 | from .completion_policy import CompletionPolicy |
@@ -26,6 +27,7 @@ from .policy_timeline import ( | ||
| 26 | 27 | completion_timeline_kind, |
| 27 | 28 | ) |
| 28 | 29 | from .repair import ResponseRepairer |
| 30 | +from .repair_focus import extract_active_repair_context | |
| 29 | 31 | from .rollback import RollbackPlan |
| 30 | 32 | from .verification_observations import VerificationObservation |
| 31 | 33 | from .workflow import ( |
@@ -416,6 +418,10 @@ def _build_in_progress_continuation( | ||
| 416 | 418 | if not _looks_like_progress_intent(content): |
| 417 | 419 | return None |
| 418 | 420 | |
| 421 | + quality_repair = _build_html_quality_repair_continuation(messages) | |
| 422 | + if quality_repair is not None: | |
| 423 | + return quality_repair | |
| 424 | + | |
| 419 | 425 | missing_artifact = _next_missing_planned_artifact( |
| 420 | 426 | dod, |
| 421 | 427 | project_root=project_root, |
@@ -472,6 +478,42 @@ def _build_in_progress_continuation( | ||
| 472 | 478 | return None |
| 473 | 479 | |
| 474 | 480 | |
| 481 | +def _build_html_quality_repair_continuation( | |
| 482 | + messages: list[object], | |
| 483 | +) -> InProgressContinuation | None: | |
| 484 | + repair = extract_active_repair_context(cast(list[Message], messages)) | |
| 485 | + if repair is None: | |
| 486 | + return None | |
| 487 | + if not any(_repair_line_is_html_quality(line) for line in repair.repair_lines): | |
| 488 | + return None | |
| 489 | + | |
| 490 | + target_text = repair.artifact_path or ( | |
| 491 | + repair.allowed_paths[0] if repair.allowed_paths else "" | |
| 492 | + ) | |
| 493 | + if not target_text: | |
| 494 | + return None | |
| 495 | + | |
| 496 | + issue_line = next( | |
| 497 | + ( | |
| 498 | + line[2:] if line.startswith("- ") else line | |
| 499 | + for line in repair.repair_lines | |
| 500 | + if target_text in line and _repair_line_is_html_quality(line) | |
| 501 | + ), | |
| 502 | + "", | |
| 503 | + ) | |
| 504 | + issue_sentence = f" Current verifier issue: {issue_line}" if issue_line else "" | |
| 505 | + prompt = ( | |
| 506 | + "[CONTINUE QUALITY REPAIR]\n" | |
| 507 | + "You just described a content-quality repair, but did not execute it. " | |
| 508 | + f"Emit one concrete `patch`, `edit`, or `write` tool call for `{target_text}` now." | |
| 509 | + f"{issue_sentence} " | |
| 510 | + "Prefer a bounded append or body-section replacement before the existing " | |
| 511 | + "back link, footer, or closing body. Do not rewrite the whole file from " | |
| 512 | + "memory, do not reopen unrelated reference files, and do not summarize." | |
| 513 | + ) | |
| 514 | + return InProgressContinuation(prompt=prompt, target=None) | |
| 515 | + | |
| 516 | + | |
| 475 | 517 | def _looks_like_progress_intent(content: str) -> bool: |
| 476 | 518 | text = content.lower().strip() |
| 477 | 519 | if not text or "?" in text: |
@@ -481,6 +523,20 @@ def _looks_like_progress_intent(content: str) -> bool: | ||
| 481 | 523 | return any(marker in text for marker in _PROGRESS_INTENT_HINTS) |
| 482 | 524 | |
| 483 | 525 | |
| 526 | +def _repair_line_is_html_quality(line: str) -> bool: | |
| 527 | + lowered = line.lower() | |
| 528 | + return any( | |
| 529 | + phrase in lowered | |
| 530 | + for phrase in ( | |
| 531 | + "thin content", | |
| 532 | + "insufficient structured content", | |
| 533 | + "content-quality", | |
| 534 | + "quality target", | |
| 535 | + "html guide content quality", | |
| 536 | + ) | |
| 537 | + ) | |
| 538 | + | |
| 539 | + | |
| 484 | 540 | def _next_missing_planned_artifact( |
| 485 | 541 | dod: DefinitionOfDone, |
| 486 | 542 | *, |
tests/test_turn_completion.pymodified@@ -373,6 +373,88 @@ async def test_turn_completion_interrupts_progress_intent_once_output_files_exis | ||
| 373 | 373 | ) |
| 374 | 374 | |
| 375 | 375 | |
| 376 | +@pytest.mark.asyncio | |
| 377 | +async def test_turn_completion_uses_quality_repair_prompt_for_rewrite_narration( | |
| 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="Create an equally thorough HTML guide.", | |
| 396 | + emit=capture, | |
| 397 | + requested_mode="execute", | |
| 398 | + original_task=None, | |
| 399 | + on_user_question=None, | |
| 400 | + ) | |
| 401 | + await runtime.phase_tracker.enter( | |
| 402 | + TurnPhase.ASSISTANT, | |
| 403 | + capture, | |
| 404 | + detail="Requesting assistant response", | |
| 405 | + reason_code="request_assistant_response", | |
| 406 | + ) | |
| 407 | + | |
| 408 | + chapter = temp_dir / "guides" / "nginx" / "chapters" / "01-introduction.html" | |
| 409 | + chapter.parent.mkdir(parents=True) | |
| 410 | + chapter.write_text("<html><body><h1>Intro</h1></body></html>\n") | |
| 411 | + prepared.definition_of_done.touched_files.append(str(chapter)) | |
| 412 | + prepared.definition_of_done.mutating_actions.append("write") | |
| 413 | + agent.session.append( | |
| 414 | + Message( | |
| 415 | + role=Role.USER, | |
| 416 | + content=( | |
| 417 | + "Repair focus:\n" | |
| 418 | + f"- Improve `{chapter}`: insufficient structured content " | |
| 419 | + "(12 blocks, expected at least 18).\n" | |
| 420 | + f"- Immediate next step: edit `{chapter}` with a substantial " | |
| 421 | + "expansion or replacement that satisfies its listed quality issue.\n" | |
| 422 | + ), | |
| 423 | + ) | |
| 424 | + ) | |
| 425 | + | |
| 426 | + content = ( | |
| 427 | + "Let me try a different approach by rewriting the entire file with more " | |
| 428 | + "comprehensive content:" | |
| 429 | + ) | |
| 430 | + decision = await runtime.turn_completion.handle_text_response( | |
| 431 | + content=content, | |
| 432 | + response_content=content, | |
| 433 | + task=prepared.task, | |
| 434 | + effective_task=prepared.effective_task, | |
| 435 | + iterations=1, | |
| 436 | + max_iterations=agent.config.max_iterations, | |
| 437 | + actions_taken=[], | |
| 438 | + continuation_count=0, | |
| 439 | + dod=prepared.definition_of_done, | |
| 440 | + emit=capture, | |
| 441 | + summary=prepared.summary, | |
| 442 | + executor=prepared.executor, | |
| 443 | + rollback_plan=prepared.rollback_plan, | |
| 444 | + ) | |
| 445 | + | |
| 446 | + assert decision.action == TurnCompletionAction.CONTINUE | |
| 447 | + assert prepared.summary.completion_decision_code == "in_progress_transition_continue" | |
| 448 | + assert agent.session.messages[-1].role.value == "user" | |
| 449 | + assert agent.session.messages[-1].content.startswith("[CONTINUE QUALITY REPAIR]") | |
| 450 | + assert str(chapter.resolve(strict=False)) in agent.session.messages[-1].content | |
| 451 | + assert ( | |
| 452 | + "one concrete `patch`, `edit`, or `write` tool call" | |
| 453 | + in agent.session.messages[-1].content | |
| 454 | + ) | |
| 455 | + assert "Do not rewrite the whole file from memory" in agent.session.messages[-1].content | |
| 456 | + | |
| 457 | + | |
| 376 | 458 | @pytest.mark.asyncio |
| 377 | 459 | async def test_turn_completion_allows_first_progress_narration_before_any_output_exists( |
| 378 | 460 | temp_dir: Path, |