@@ -20,6 +20,7 @@ from .dod import ( |
| 20 | 20 | from .parsing import parse_tool_calls |
| 21 | 21 | from .path_display import display_runtime_path |
| 22 | 22 | from .recovery import detect_missing_mutation_payload |
| 23 | +from .repair_focus import ActiveRepairContext, extract_active_repair_context |
| 23 | 24 | from .workflow import ( |
| 24 | 25 | infer_output_outline_label, |
| 25 | 26 | infer_pending_todo_output_target, |
@@ -267,6 +268,12 @@ class ResponseRepairer: |
| 267 | 268 | max_empty_retries: int, |
| 268 | 269 | ) -> str: |
| 269 | 270 | 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 |
| 270 | 277 | minimal_retry_message = self._build_early_concrete_write_retry_message( |
| 271 | 278 | dod, |
| 272 | 279 | retry_number=retry_number, |
@@ -661,6 +668,8 @@ class ResponseRepairer: |
| 661 | 668 | ) -> int: |
| 662 | 669 | if dod is None: |
| 663 | 670 | 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 |
| 664 | 673 | completed_artifacts, missing_artifacts = self._planned_artifact_counts(dod) |
| 665 | 674 | if completed_artifacts >= 3 and missing_artifacts > 0: |
| 666 | 675 | return base_max_empty_retries + _LATE_STAGE_EMPTY_RETRY_EXTRA |
@@ -802,6 +811,87 @@ class ResponseRepairer: |
| 802 | 811 | ) |
| 803 | 812 | ) |
| 804 | 813 | |
| 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 | + |
| 805 | 895 | def _planned_artifact_progress_lines(self, dod: DefinitionOfDone) -> list[str]: |
| 806 | 896 | targets = collect_planned_artifact_targets( |
| 807 | 897 | dod, |
@@ -1856,6 +1946,20 @@ def _is_summary_artifact_path(path: Path) -> bool: |
| 1856 | 1946 | return path.name.lower() in _SUMMARY_ARTIFACT_NAMES |
| 1857 | 1947 | |
| 1858 | 1948 | |
| 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 | + |
| 1859 | 1963 | def _should_encourage_initial_version( |
| 1860 | 1964 | *, |
| 1861 | 1965 | target: Path, |