tenseleyflow/loader / fdbe57a

Browse files

Shorten final empty retry

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
fdbe57a3697a246d955437e372d5564528e2500a
Parents
d14cf85
Tree
15b6295

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 62 13
M tests/test_repair.py 54 1
src/loader/runtime/repair.pymodified
@@ -426,6 +426,14 @@ class ResponseRepairer:
426426
             project_root=self.context.project_root,
427427
             todo_label=next_pending or "",
428428
         )
429
+        urgent_retry_message = self._urgent_empty_write_retry_message(
430
+            concrete_target,
431
+            outline_label=outline_label,
432
+            retry_number=retry_number,
433
+            max_empty_retries=max_empty_retries,
434
+        )
435
+        if urgent_retry_message is not None:
436
+            return urgent_retry_message
429437
         if (
430438
             next_pending
431439
             and _todo_is_mutation_step(next_pending)
@@ -1608,23 +1616,64 @@ class ResponseRepairer:
16081616
             if outline_label and outline_label.strip()
16091617
             else self._fallback_html_label(target)
16101618
         )
1619
+        return (
1620
+            "If blanking continues, use this minimal starter payload shape inside the `write` call now: "
1621
+            f"`{self._minimal_html_payload(target, label)}` and refine it later."
1622
+        )
1623
+
1624
+    def _urgent_empty_write_retry_message(
1625
+        self,
1626
+        target: Path,
1627
+        *,
1628
+        outline_label: str | None,
1629
+        retry_number: int,
1630
+        max_empty_retries: int,
1631
+    ) -> str | None:
1632
+        if retry_number < max_empty_retries:
1633
+            return None
1634
+        if target.suffix.lower() not in {".html", ".htm"}:
1635
+            return None
1636
+        label = (
1637
+            outline_label.strip()
1638
+            if outline_label and outline_label.strip()
1639
+            else self._fallback_html_label(target)
1640
+        )
1641
+        payload = self._minimal_html_payload(target, label)
1642
+        return "\n".join(
1643
+            [
1644
+                "[EMPTY ASSISTANT RESPONSE]",
1645
+                (
1646
+                    "Your last response was empty "
1647
+                    f"(retry {retry_number}/{max_empty_retries}). Emit one tool call only:"
1648
+                ),
1649
+                (
1650
+                    "write(file_path="
1651
+                    f"{json.dumps(display_runtime_path(target))}, "
1652
+                    f"content={json.dumps(payload)})"
1653
+                ),
1654
+                "No prose, no TodoWrite, no reads.",
1655
+            ]
1656
+        )
1657
+
1658
+    def _minimal_html_payload(self, target: Path, label: str) -> str:
16111659
         if self._is_index_html_target(target):
16121660
             return (
1613
-                "If blanking continues, use this minimal starter payload shape inside the `write` call now: "
1614
-                f"`<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
1615
-                f"content=\"width=device-width, initial-scale=1.0\"><title>{label}</title></head><body>"
1616
-                f"<div class=\"container\"><h1>{label}</h1><p>...</p><nav><ul>"
1617
-                "<li><a href=\"chapters/01-...html\">Chapter 1: ...</a></li>"
1618
-                "<li><a href=\"chapters/02-...html\">Chapter 2: ...</a></li>"
1619
-                "</ul></nav></div></body></html>` and refine it later."
1661
+                '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
1662
+                '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
1663
+                f"<title>{label}</title></head><body><div class=\"container\"><h1>{label}</h1>"
1664
+                "<p>Starter overview for this guide.</p><nav><ul>"
1665
+                '<li><a href="chapters/01-introduction.html">Chapter 1: Introduction</a></li>'
1666
+                '<li><a href="chapters/02-installation.html">Chapter 2: Installation</a></li>'
1667
+                "</ul></nav></div></body></html>"
16201668
             )
16211669
         return (
1622
-            "If blanking continues, use this minimal starter payload shape inside the `write` call now: "
1623
-            f"`<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
1624
-            f"content=\"width=device-width, initial-scale=1.0\"><title>{label}</title></head><body>"
1625
-            f"<div class=\"container\"><h1>{label}</h1><p>...</p><h2>Overview</h2><p>...</p>"
1626
-            f"<h2>Key Steps</h2><p>...</p><p><a href=\"../index.html\">← Back to Main Guide Index</a></p>"
1627
-            "</div></body></html>` and refine it later."
1670
+            '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
1671
+            '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
1672
+            f"<title>{label}</title></head><body><div class=\"container\"><h1>{label}</h1>"
1673
+            "<p>Starter content for this chapter.</p><h2>Overview</h2><p>Key concepts go here.</p>"
1674
+            "<h2>Key Steps</h2><p>Practical steps go here.</p>"
1675
+            '<p><a href="../index.html">Back to Main Guide Index</a></p>'
1676
+            "</div></body></html>"
16281677
         )
16291678
 
16301679
     @staticmethod
tests/test_repair.pymodified
@@ -1147,7 +1147,7 @@ def test_repeated_first_index_retry_includes_root_html_payload_shape(
11471147
     assert decision.retry_message is not None
11481148
     assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
11491149
     assert "<title>Nginx Guide</title>" in decision.retry_message
1150
-    assert 'href="chapters/01-...html"' in decision.retry_message
1150
+    assert 'href="chapters/01-introduction.html"' in decision.retry_message
11511151
     assert "../index.html" not in decision.retry_message
11521152
 
11531153
 
@@ -1670,6 +1670,59 @@ def test_empty_response_retry_surfaces_minimal_child_html_payload_earlier_after_
16701670
     assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
16711671
 
16721672
 
1673
+def test_final_empty_response_retry_uses_short_exact_write_call(
1674
+    temp_dir: Path,
1675
+) -> None:
1676
+    context = build_context(
1677
+        temp_dir=temp_dir,
1678
+        use_react=False,
1679
+    )
1680
+    repairer = ResponseRepairer(context)
1681
+
1682
+    guide_root = temp_dir / "guides" / "nginx"
1683
+    chapters = guide_root / "chapters"
1684
+    index_path = guide_root / "index.html"
1685
+    chapter_one = chapters / "01-introduction.html"
1686
+    chapters.mkdir(parents=True)
1687
+    index_path.write_text("<h1>Nginx Guide</h1>\n")
1688
+    implementation_plan = temp_dir / "implementation.md"
1689
+    implementation_plan.write_text(
1690
+        "\n".join(
1691
+            [
1692
+                "# Implementation Plan",
1693
+                "",
1694
+                "## File Changes",
1695
+                f"- `{index_path}`",
1696
+                f"- `{chapter_one}`",
1697
+                "",
1698
+            ]
1699
+        )
1700
+    )
1701
+
1702
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1703
+    dod.implementation_plan = str(implementation_plan)
1704
+    dod.touched_files.append(str(index_path))
1705
+    dod.completed_items.append("Develop the main index.html file with proper structure")
1706
+    dod.pending_items.append("Create first chapter file (01-introduction.html)")
1707
+
1708
+    decision = repairer.handle_empty_response(
1709
+        task="Create a multi-file nginx guide.",
1710
+        original_task=None,
1711
+        empty_retry_count=10,
1712
+        max_empty_retries=6,
1713
+        dod=dod,
1714
+    )
1715
+
1716
+    assert decision.should_continue is True
1717
+    assert decision.retry_message is not None
1718
+    assert "Emit one tool call only" in decision.retry_message
1719
+    assert 'write(file_path="' in decision.retry_message
1720
+    assert "01-introduction.html" in decision.retry_message
1721
+    assert "No prose, no TodoWrite, no reads." in decision.retry_message
1722
+    assert "If blanking continues" not in decision.retry_message
1723
+    assert len(decision.retry_message) < 1000
1724
+
1725
+
16731726
 def test_first_substantive_retry_activates_on_first_empty_turn(
16741727
     temp_dir: Path,
16751728
 ) -> None: