tenseleyflow/loader / c0bdb3d

Browse files

Shrink quality repair retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c0bdb3d39a10fdd0ca821585bff5e9337d7208a5
Parents
3942db8
Tree
22bd9c4

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 104 0
M tests/test_repair.py 55 0
src/loader/runtime/repair.pymodified
@@ -20,6 +20,7 @@ from .dod import (
2020
 from .parsing import parse_tool_calls
2121
 from .path_display import display_runtime_path
2222
 from .recovery import detect_missing_mutation_payload
23
+from .repair_focus import ActiveRepairContext, extract_active_repair_context
2324
 from .workflow import (
2425
     infer_output_outline_label,
2526
     infer_pending_todo_output_target,
@@ -267,6 +268,12 @@ class ResponseRepairer:
267268
         max_empty_retries: int,
268269
     ) -> str:
269270
         if dod is not None:
271
+            quality_repair_message = self._build_quality_repair_empty_retry_message(
272
+                retry_number=retry_number,
273
+                max_empty_retries=max_empty_retries,
274
+            )
275
+            if quality_repair_message is not None:
276
+                return quality_repair_message
270277
             minimal_retry_message = self._build_early_concrete_write_retry_message(
271278
                 dod,
272279
                 retry_number=retry_number,
@@ -661,6 +668,8 @@ class ResponseRepairer:
661668
     ) -> int:
662669
         if dod is None:
663670
             return base_max_empty_retries
671
+        if self._active_html_quality_repair_context() is not None:
672
+            return base_max_empty_retries + _LATE_STAGE_EMPTY_RETRY_EXTRA
664673
         completed_artifacts, missing_artifacts = self._planned_artifact_counts(dod)
665674
         if completed_artifacts >= 3 and missing_artifacts > 0:
666675
             return base_max_empty_retries + _LATE_STAGE_EMPTY_RETRY_EXTRA
@@ -802,6 +811,87 @@ class ResponseRepairer:
802811
             )
803812
         )
804813
 
814
+    def _build_quality_repair_empty_retry_message(
815
+        self,
816
+        *,
817
+        retry_number: int,
818
+        max_empty_retries: int,
819
+    ) -> str | None:
820
+        repair = self._active_html_quality_repair_context()
821
+        if repair is None:
822
+            return None
823
+
824
+        target = repair.artifact_path or (
825
+            repair.allowed_paths[0] if repair.allowed_paths else ""
826
+        )
827
+        if not target:
828
+            return None
829
+
830
+        issue_line = next(
831
+            (
832
+                line
833
+                for line in repair.repair_lines
834
+                if target in line and _repair_line_is_html_quality(line)
835
+            ),
836
+            next(
837
+                (
838
+                    line
839
+                    for line in repair.repair_lines
840
+                    if _repair_line_is_html_quality(line)
841
+                ),
842
+                "",
843
+            ),
844
+        )
845
+        remaining_targets = [
846
+            path
847
+            for path in repair.allowed_paths
848
+            if path and path != target
849
+        ]
850
+        remaining_line = ""
851
+        if remaining_targets:
852
+            preview = ", ".join(f"`{path}`" for path in remaining_targets[:3])
853
+            if len(remaining_targets) > 3:
854
+                preview += ", ..."
855
+            remaining_line = (
856
+                f"- After this file has a successful mutation, continue the other "
857
+                f"quality target(s): {preview}."
858
+            )
859
+
860
+        lines = [
861
+            "[EMPTY ASSISTANT RESPONSE]",
862
+            (
863
+                "Your last response was empty "
864
+                f"(retry {retry_number}/{max_empty_retries}) while repairing HTML "
865
+                "content quality. Shrink the next step."
866
+            ),
867
+            f"- Active quality repair target: `{target}`.",
868
+        ]
869
+        if issue_line:
870
+            lines.append(f"- Current verifier issue: {issue_line[2:] if issue_line.startswith('- ') else issue_line}")
871
+        lines.extend(
872
+            [
873
+                "- Use one bounded `edit`, `patch`, or `write` call for that same "
874
+                "file now. Append or replace a body section with 4-6 substantive "
875
+                "sections, lists, commands, or examples; do not attempt a giant "
876
+                "full-page rewrite from memory.",
877
+                "- Do not add table-of-contents entries, do not retarget links, and "
878
+                "do not reopen unrelated reference files for this retry.",
879
+                "- No narration, no TodoWrite, no final summary, and no empty "
880
+                "response; emit the mutation tool call now.",
881
+            ]
882
+        )
883
+        if remaining_line:
884
+            lines.append(remaining_line)
885
+        return "\n".join(lines)
886
+
887
+    def _active_html_quality_repair_context(self) -> ActiveRepairContext | None:
888
+        repair = extract_active_repair_context(self.context.session.messages)
889
+        if repair is None:
890
+            return None
891
+        if any(_repair_line_is_html_quality(line) for line in repair.repair_lines):
892
+            return repair
893
+        return None
894
+
805895
     def _planned_artifact_progress_lines(self, dod: DefinitionOfDone) -> list[str]:
806896
         targets = collect_planned_artifact_targets(
807897
             dod,
@@ -1856,6 +1946,20 @@ def _is_summary_artifact_path(path: Path) -> bool:
18561946
     return path.name.lower() in _SUMMARY_ARTIFACT_NAMES
18571947
 
18581948
 
1949
+def _repair_line_is_html_quality(line: str) -> bool:
1950
+    lowered = line.lower()
1951
+    return any(
1952
+        phrase in lowered
1953
+        for phrase in (
1954
+            "thin content",
1955
+            "insufficient structured content",
1956
+            "content-quality",
1957
+            "quality target",
1958
+            "html guide content quality",
1959
+        )
1960
+    )
1961
+
1962
+
18591963
 def _should_encourage_initial_version(
18601964
     *,
18611965
     target: Path,
tests/test_repair.pymodified
@@ -266,6 +266,61 @@ def test_empty_response_retry_message_surfaces_missing_planned_artifacts_and_wor
266266
     assert "Continue from the confirmed progress below instead of restarting." in decision.retry_message
267267
 
268268
 
269
+def test_empty_response_retry_during_html_quality_repair_shrinks_mutation(
270
+    temp_dir: Path,
271
+) -> None:
272
+    context = build_context(
273
+        temp_dir=temp_dir,
274
+        use_react=False,
275
+    )
276
+    repairer = ResponseRepairer(context)
277
+    guide = temp_dir / "guides" / "nginx"
278
+    chapters = guide / "chapters"
279
+    chapters.mkdir(parents=True)
280
+    first_chapter = chapters / "01-introduction.html"
281
+    second_chapter = chapters / "02-installation.html"
282
+    first_chapter.write_text("<html><body><h1>Intro</h1></body></html>\n")
283
+    second_chapter.write_text("<html><body><h1>Install</h1></body></html>\n")
284
+    context.session.append(
285
+        Message(
286
+            role=Role.USER,
287
+            content=(
288
+                "Repair focus:\n"
289
+                f"- Improve `{first_chapter}`: insufficient structured content "
290
+                "(13 blocks, expected at least 18).\n"
291
+                f"- Improve `{second_chapter}`: thin content "
292
+                "(514 text chars, expected at least 1758).\n"
293
+                f"- Immediate next step: edit `{first_chapter}` with a substantial "
294
+                "expansion or replacement that satisfies its listed quality issue.\n"
295
+            ),
296
+        )
297
+    )
298
+    dod = create_definition_of_done("Create an equally thorough HTML guide.")
299
+    dod.touched_files = [str(first_chapter), str(second_chapter)]
300
+    dod.pending_items.append("Create chapter 1: Introduction to Nginx")
301
+
302
+    decision = repairer.handle_empty_response(
303
+        task="Create an equally thorough HTML guide.",
304
+        original_task=None,
305
+        empty_retry_count=1,
306
+        max_empty_retries=2,
307
+        dod=dod,
308
+    )
309
+
310
+    assert decision.should_continue is True
311
+    assert decision.retry_message is not None
312
+    assert "while repairing HTML content quality" in decision.retry_message
313
+    assert (
314
+        f"Active quality repair target: `{first_chapter.resolve(strict=False)}`"
315
+        in decision.retry_message
316
+    )
317
+    assert "Shrink the next step" in decision.retry_message
318
+    assert "4-6 substantive sections" in decision.retry_message
319
+    assert "do not attempt a giant full-page rewrite from memory" in decision.retry_message
320
+    assert "No narration, no TodoWrite, no final summary" in decision.retry_message
321
+    assert f"`{second_chapter.resolve(strict=False)}`" in decision.retry_message
322
+
323
+
269324
 def test_empty_response_retry_mentions_write_can_create_missing_parent_directories(
270325
     temp_dir: Path,
271326
 ) -> None: