Python · 90562 bytes Raw Blame History
1 """Tests for response-repair helpers on RuntimeContext."""
2
3 from __future__ import annotations
4
5 import json
6 from pathlib import Path
7 from types import SimpleNamespace
8
9 import pytest
10
11 from loader.llm.base import Message, Role, ToolCall
12 from loader.runtime.context import RuntimeContext
13 from loader.runtime.dod import create_definition_of_done
14 from loader.runtime.path_display import display_runtime_path
15 from loader.runtime.permissions import (
16 PermissionMode,
17 build_permission_policy,
18 load_permission_rules,
19 )
20 from loader.runtime.recovery import RecoveryContext
21 from loader.runtime.repair import ResponseRepairer
22 from loader.tools.base import create_default_registry
23 from tests.helpers.runtime_harness import ScriptedBackend
24
25
26 class FakeSession:
27 def __init__(self) -> None:
28 self.messages = []
29
30 def append(self, message) -> None:
31 self.messages.append(message)
32
33
34 class FakeCodeFilter:
35 def reset(self) -> None:
36 return None
37
38
39 class FakeSafeguards:
40 def __init__(self) -> None:
41 self.action_tracker = object()
42 self.validator = object()
43 self.code_filter = FakeCodeFilter()
44
45 def filter_stream_chunk(self, content: str) -> str:
46 return content
47
48 def filter_complete_content(self, content: str) -> str:
49 return content
50
51 def should_steer(self) -> bool:
52 return False
53
54 def get_steering_message(self) -> str | None:
55 return None
56
57 def record_response(self, content: str) -> None:
58 return None
59
60 def detect_text_loop(self, content: str) -> tuple[bool, str]:
61 return False, ""
62
63 def detect_loop(self) -> tuple[bool, str]:
64 return False, ""
65
66
67 def build_context(
68 *,
69 temp_dir: Path,
70 use_react: bool,
71 ) -> RuntimeContext:
72 registry = create_default_registry(temp_dir)
73 registry.configure_workspace_root(temp_dir)
74 rule_status = load_permission_rules(temp_dir)
75 policy = build_permission_policy(
76 active_mode=PermissionMode.WORKSPACE_WRITE,
77 workspace_root=temp_dir,
78 tool_requirements=registry.get_tool_requirements(),
79 rules=rule_status.rules,
80 )
81 session = FakeSession()
82 return RuntimeContext(
83 project_root=temp_dir,
84 backend=ScriptedBackend(),
85 registry=registry,
86 session=session, # type: ignore[arg-type]
87 config=SimpleNamespace(force_react=use_react),
88 capability_profile=SimpleNamespace(supports_native_tools=not use_react), # type: ignore[arg-type]
89 project_context=None,
90 permission_policy=policy,
91 permission_config_status=rule_status,
92 workflow_mode="execute",
93 safeguards=FakeSafeguards(),
94 )
95
96
97 def test_response_repairer_uses_runtime_parser_for_bracket_tool_fallback(
98 temp_dir: Path,
99 ) -> None:
100 context = build_context(
101 temp_dir=temp_dir,
102 use_react=False,
103 )
104 repairer = ResponseRepairer(context)
105
106 analysis = repairer.analyze_response(
107 content="I need clarification.",
108 response_content='[calls askuserquestion tool with: question="Which path?"]',
109 tool_calls=[],
110 extracted_iterations=0,
111 max_extracted_iterations=3,
112 )
113
114 assert analysis.tool_calls == [
115 ToolCall(
116 id="call_0",
117 name="AskUserQuestion",
118 arguments={"question": "Which path?"},
119 )
120 ]
121 assert analysis.tool_source == "raw_text"
122 assert analysis.clear_stream is True
123
124
125 def test_response_repairer_recovers_todowrite_from_runtime_registry(
126 temp_dir: Path,
127 ) -> None:
128 context = build_context(
129 temp_dir=temp_dir,
130 use_react=False,
131 )
132 repairer = ResponseRepairer(context)
133
134 analysis = repairer.analyze_response(
135 content="I'll track the work first.",
136 response_content=json.dumps(
137 {
138 "name": "TodoWrite",
139 "arguments": {
140 "todos": [
141 {
142 "content": "Run tests",
143 "active_form": "Running tests",
144 "status": "in_progress",
145 }
146 ]
147 },
148 }
149 ),
150 tool_calls=[],
151 extracted_iterations=0,
152 max_extracted_iterations=3,
153 )
154
155 assert analysis.tool_source == "raw_text"
156 assert analysis.clear_stream is True
157 assert analysis.tool_calls == [
158 ToolCall(
159 id="call_0",
160 name="TodoWrite",
161 arguments={
162 "todos": [
163 {
164 "content": "Run tests",
165 "active_form": "Running tests",
166 "status": "in_progress",
167 }
168 ]
169 },
170 )
171 ]
172
173
174 def test_response_repairer_fails_honestly_when_raw_tool_budget_is_exhausted(
175 temp_dir: Path,
176 ) -> None:
177 context = build_context(
178 temp_dir=temp_dir,
179 use_react=False,
180 )
181 repairer = ResponseRepairer(context)
182
183 analysis = repairer.analyze_response(
184 content=json.dumps(
185 {
186 "name": "read",
187 "arguments": {"file_path": "README.md"},
188 }
189 ),
190 response_content=json.dumps(
191 {
192 "name": "read",
193 "arguments": {"file_path": "README.md"},
194 }
195 ),
196 tool_calls=[],
197 extracted_iterations=3,
198 max_extracted_iterations=3,
199 )
200
201 assert analysis.should_stop is True
202 assert analysis.final_response == (
203 "I couldn't safely continue because the model kept emitting raw-text "
204 "tool calls instead of proper tool invocations. Please try again or "
205 "switch to a different backend/model."
206 )
207 assert analysis.failure == "raw-text tool recovery budget exhausted"
208 assert "Let me know if you'd like me to continue" not in analysis.final_response
209
210
211 def test_empty_response_retry_message_surfaces_missing_planned_artifacts_and_working_note(
212 temp_dir: Path,
213 ) -> None:
214 context = build_context(
215 temp_dir=temp_dir,
216 use_react=False,
217 )
218 repairer = ResponseRepairer(context)
219 implementation_plan = temp_dir / "implementation.md"
220 implementation_plan.write_text(
221 "\n".join(
222 [
223 "# Implementation Plan",
224 "",
225 "## File Changes",
226 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
227 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
228 "",
229 ]
230 )
231 )
232 first_artifact = temp_dir / "guides" / "nginx" / "index.html"
233 first_artifact.parent.mkdir(parents=True)
234 first_artifact.write_text("<html></html>\n")
235
236 dod = create_definition_of_done("Create a multi-file nginx guide.")
237 dod.implementation_plan = str(implementation_plan)
238 dod.touched_files.append(str(first_artifact))
239 dod.completed_items.append("Create the main index.html file")
240 dod.pending_items.append("Create each chapter file in sequence")
241
242 context.session.append(
243 SimpleNamespace(
244 role="tool",
245 content=(
246 "Observation [notepad_write_working]: Result: "
247 "- [2026-04-21T19:17:34Z] Creating fifth chapter file: Advanced configurations"
248 ),
249 )
250 )
251
252 decision = repairer.handle_empty_response(
253 task="Create a multi-file nginx guide.",
254 original_task=None,
255 empty_retry_count=1,
256 max_empty_retries=2,
257 dod=dod,
258 )
259
260 assert decision.should_continue is True
261 assert decision.retry_message is not None
262 assert "Latest working note: Creating fifth chapter file: Advanced configurations" in decision.retry_message
263 assert "Confirmed touched files: `index.html`" in decision.retry_message
264 assert "Confirmed completed work: Create the main index.html file" in decision.retry_message
265 assert "Next pending item: Create each chapter file in sequence" in decision.retry_message
266 assert "Continue from the confirmed progress below instead of restarting." in decision.retry_message
267
268
269 def test_empty_response_retry_mentions_write_can_create_missing_parent_directories(
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
278 guide_root = temp_dir / "guides" / "nginx"
279 index_path = guide_root / "index.html"
280
281 implementation_plan = temp_dir / "implementation.md"
282 implementation_plan.write_text(
283 "\n".join(
284 [
285 "# Implementation Plan",
286 "",
287 "## File Changes",
288 f"- `{index_path}`",
289 "",
290 ]
291 )
292 )
293
294 dod = create_definition_of_done("Create a multi-file nginx guide.")
295 dod.implementation_plan = str(implementation_plan)
296 dod.pending_items.extend(
297 [
298 "Create nginx guide directory structure",
299 "Write main index.html for nginx guide",
300 ]
301 )
302
303 decision = repairer.handle_empty_response(
304 task="Create a multi-file nginx guide.",
305 original_task=None,
306 empty_retry_count=1,
307 max_empty_retries=2,
308 dod=dod,
309 )
310
311 assert decision.should_continue is True
312 assert decision.retry_message is not None
313 assert (
314 "Resume with this exact next step: create `index.html`."
315 in decision.retry_message
316 )
317 assert (
318 "Prefer one `write(content=...)` call for "
319 f"`{display_runtime_path(index_path)}` before more research."
320 in decision.retry_message
321 )
322 assert (
323 "The `write` tool can create that file's parent directories automatically, so do the write in one step instead of stopping for a separate mkdir."
324 in decision.retry_message
325 )
326 assert (
327 f'Emit this tool shape now: `write(file_path="{display_runtime_path(index_path)}", content="...")`.'
328 in decision.retry_message
329 )
330 assert (
331 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
332 in decision.retry_message
333 )
334 assert (
335 "No narration, no TodoWrite, no rereads, and no empty response; emit the mutation tool call now."
336 in decision.retry_message
337 )
338
339
340 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
341 temp_dir: Path,
342 ) -> None:
343 context = build_context(
344 temp_dir=temp_dir,
345 use_react=False,
346 )
347 repairer = ResponseRepairer(context)
348
349 guide_root = temp_dir / "guides" / "nginx"
350 chapters_path = guide_root / "chapters"
351 index_path = guide_root / "index.html"
352
353 implementation_plan = temp_dir / "implementation.md"
354 implementation_plan.write_text(
355 "\n".join(
356 [
357 "# Implementation Plan",
358 "",
359 "## File Changes",
360 f"- `{chapters_path}/`",
361 f"- `{index_path}`",
362 "",
363 ]
364 )
365 )
366
367 dod = create_definition_of_done("Create a multi-file nginx guide.")
368 dod.implementation_plan = str(implementation_plan)
369 dod.pending_items.extend(
370 [
371 "Create the nginx directory structure",
372 "Create the main index.html file for nginx guide",
373 ]
374 )
375
376 decision = repairer.handle_empty_response(
377 task="Create a multi-file nginx guide.",
378 original_task=None,
379 empty_retry_count=1,
380 max_empty_retries=2,
381 dod=dod,
382 )
383
384 assert decision.should_continue is True
385 assert decision.retry_message is not None
386 assert (
387 "Resume with this exact next step: continue `Create the nginx directory structure` "
388 "by creating `chapters/`."
389 in decision.retry_message
390 )
391 assert (
392 "Prefer one concrete directory-creation step for "
393 f"`{display_runtime_path(chapters_path)}` before more research."
394 in decision.retry_message
395 )
396 expected_command = f"mkdir -p {display_runtime_path(chapters_path)}"
397 assert (
398 f'Emit this tool shape now: `bash(command="{expected_command}")`.'
399 in decision.retry_message
400 )
401 assert f'write(file_path="{display_runtime_path(chapters_path)}"' not in decision.retry_message
402
403
404 def test_empty_response_retry_uses_home_relative_path_for_home_artifacts(
405 temp_dir: Path,
406 monkeypatch: pytest.MonkeyPatch,
407 ) -> None:
408 monkeypatch.setenv("HOME", str(temp_dir.resolve(strict=False)))
409 context = build_context(
410 temp_dir=temp_dir,
411 use_react=False,
412 )
413 repairer = ResponseRepairer(context)
414
415 guide_root = temp_dir / "Loader" / "guides" / "nginx"
416 index_path = guide_root / "index.html"
417
418 implementation_plan = temp_dir / "implementation.md"
419 implementation_plan.write_text(
420 "\n".join(
421 [
422 "# Implementation Plan",
423 "",
424 "## File Changes",
425 f"- `{index_path}`",
426 "",
427 ]
428 )
429 )
430
431 dod = create_definition_of_done("Create a multi-file nginx guide.")
432 dod.implementation_plan = str(implementation_plan)
433 dod.pending_items.extend(
434 [
435 "Create nginx guide directory structure",
436 "Write main index.html for nginx guide",
437 ]
438 )
439
440 decision = repairer.handle_empty_response(
441 task="Create a multi-file nginx guide.",
442 original_task=None,
443 empty_retry_count=1,
444 max_empty_retries=2,
445 dod=dod,
446 )
447
448 assert decision.should_continue is True
449 assert decision.retry_message is not None
450 assert "`~/Loader/guides/nginx/index.html`" in decision.retry_message
451 assert (
452 'Emit this tool shape now: `write(file_path="~/Loader/guides/nginx/index.html", content="...")`.'
453 in decision.retry_message
454 )
455 assert (
456 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
457 in decision.retry_message
458 )
459
460
461 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
462 temp_dir: Path,
463 ) -> None:
464 context = build_context(
465 temp_dir=temp_dir,
466 use_react=False,
467 )
468 repairer = ResponseRepairer(context)
469
470 guide_root = temp_dir / "guides" / "nginx"
471 chapters = guide_root / "chapters"
472 chapters.mkdir(parents=True)
473 index_path = guide_root / "index.html"
474 first_chapter = chapters / "01-introduction.html"
475 second_chapter = chapters / "02-installation.html"
476 index_path.write_text("<html></html>\n")
477 first_chapter.write_text("<h1>Intro</h1>\n")
478
479 implementation_plan = temp_dir / "implementation.md"
480 implementation_plan.write_text(
481 "\n".join(
482 [
483 "# Implementation Plan",
484 "",
485 "## File Changes",
486 f"- `{index_path}`",
487 f"- `{first_chapter}`",
488 f"- `{second_chapter}`",
489 "",
490 ]
491 )
492 )
493
494 dod = create_definition_of_done("Create a multi-file nginx guide.")
495 dod.implementation_plan = str(implementation_plan)
496 dod.touched_files.extend([str(index_path), str(first_chapter)])
497 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
498
499 context.recovery_context = RecoveryContext(
500 original_tool="write",
501 original_args={"file_path": "", "content": "<html></html>\n"},
502 )
503 context.recovery_context.add_attempt(
504 "write",
505 {"file_path": "", "content": "<html></html>\n"},
506 "Empty file path",
507 )
508
509 decision = repairer.handle_empty_response(
510 task="Create a multi-file nginx guide.",
511 original_task=None,
512 empty_retry_count=1,
513 max_empty_retries=2,
514 dod=dod,
515 )
516
517 assert decision.should_continue is True
518 assert decision.retry_message is not None
519 assert (
520 "Last tool failure: resend `write` for "
521 f"`{display_runtime_path(second_chapter)}` with a valid `file_path` and real `content`."
522 in decision.retry_message
523 )
524 assert "Do not leave `file_path` empty" in decision.retry_message
525 assert (
526 f'Emit this tool shape now: `write(file_path="{display_runtime_path(second_chapter)}", content="...")`.'
527 in decision.retry_message
528 )
529
530
531 def test_empty_response_retry_respects_discovery_first_pending_step(
532 temp_dir: Path,
533 ) -> None:
534 context = build_context(
535 temp_dir=temp_dir,
536 use_react=False,
537 )
538 repairer = ResponseRepairer(context)
539
540 implementation_plan = temp_dir / "implementation.md"
541 implementation_plan.write_text(
542 "\n".join(
543 [
544 "# Implementation Plan",
545 "",
546 "## File Changes",
547 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
548 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
549 "",
550 ]
551 )
552 )
553
554 dod = create_definition_of_done("Create a multi-file nginx guide.")
555 dod.implementation_plan = str(implementation_plan)
556 dod.pending_items.extend(
557 [
558 "First, examine the existing fortran guide structure and content to understand the format",
559 "Create the nginx directory structure",
560 "Develop the main index.html file for the nginx guide",
561 ]
562 )
563
564 context.session.append(
565 SimpleNamespace(
566 role="tool",
567 content=(
568 "Observation [notepad_write_working]: Result: "
569 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
570 ),
571 )
572 )
573
574 decision = repairer.handle_empty_response(
575 task="Create a multi-file nginx guide.",
576 original_task=None,
577 empty_retry_count=1,
578 max_empty_retries=2,
579 dod=dod,
580 )
581
582 assert decision.should_continue is True
583 assert decision.retry_message is not None
584 assert (
585 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
586 in decision.retry_message
587 )
588 assert "one concrete evidence-gathering tool call" in decision.retry_message
589 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
590
591
592 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
593 temp_dir: Path,
594 ) -> None:
595 context = build_context(
596 temp_dir=temp_dir,
597 use_react=False,
598 )
599 repairer = ResponseRepairer(context)
600
601 guide_root = temp_dir / "guides" / "nginx"
602 chapters = guide_root / "chapters"
603 chapters.mkdir(parents=True)
604 index_path = guide_root / "index.html"
605 chapter_one = chapters / "01-getting-started.html"
606 chapter_two = chapters / "02-installation.html"
607 chapter_three = chapters / "03-first-website.html"
608 chapter_four = chapters / "04-configuration-basics.html"
609 index_path.write_text("<html></html>\n")
610 chapter_one.write_text("<h1>One</h1>\n")
611 chapter_two.write_text("<h1>Two</h1>\n")
612 chapter_three.write_text("<h1>Three</h1>\n")
613
614 implementation_plan = temp_dir / "implementation.md"
615 implementation_plan.write_text(
616 "\n".join(
617 [
618 "# Implementation Plan",
619 "",
620 "## File Changes",
621 f"- `{guide_root}/`",
622 f"- `{chapters}/`",
623 f"- `{index_path}`",
624 f"- `{chapter_one}`",
625 f"- `{chapter_two}`",
626 f"- `{chapter_three}`",
627 f"- `{chapter_four}`",
628 "",
629 ]
630 )
631 )
632
633 dod = create_definition_of_done("Create a multi-file nginx guide.")
634 dod.implementation_plan = str(implementation_plan)
635 dod.touched_files.extend(
636 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
637 )
638 dod.completed_items.extend(
639 [
640 "Create the directory structure for the new nginx guide",
641 "Create the main index.html file with proper structure",
642 ]
643 )
644 dod.pending_items.append("Create each chapter file in sequence")
645
646 decision = repairer.handle_empty_response(
647 task="Create a multi-file nginx guide.",
648 original_task=None,
649 empty_retry_count=3,
650 max_empty_retries=2,
651 dod=dod,
652 )
653
654 assert decision.should_continue is True
655 assert decision.retry_message is not None
656 assert "retry 3/4" in decision.retry_message
657 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
658
659
660 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
661 temp_dir: Path,
662 ) -> None:
663 context = build_context(
664 temp_dir=temp_dir,
665 use_react=False,
666 )
667 repairer = ResponseRepairer(context)
668
669 implementation_plan = temp_dir / "implementation.md"
670 implementation_plan.write_text(
671 "\n".join(
672 [
673 "# Implementation Plan",
674 "",
675 "## File Changes",
676 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
677 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
678 "",
679 ]
680 )
681 )
682
683 dod = create_definition_of_done("Create a multi-file nginx guide.")
684 dod.implementation_plan = str(implementation_plan)
685 dod.pending_items.append("Develop the main index.html file for the nginx guide")
686
687 decision = repairer.handle_empty_response(
688 task="Create a multi-file nginx guide.",
689 original_task=None,
690 empty_retry_count=3,
691 max_empty_retries=2,
692 dod=dod,
693 )
694
695 assert decision.should_continue is True
696 assert decision.retry_message is not None
697 assert "retry 3/4" in decision.retry_message
698 assert "Next missing planned artifact: `index.html`" in decision.retry_message
699 assert (
700 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
701 "by creating `index.html`."
702 in decision.retry_message
703 )
704
705
706 def test_empty_response_retry_budget_extends_after_setup_directories_exist(
707 temp_dir: Path,
708 ) -> None:
709 context = build_context(
710 temp_dir=temp_dir,
711 use_react=False,
712 )
713 repairer = ResponseRepairer(context)
714
715 guide_root = temp_dir / "guides" / "nginx"
716 chapters = guide_root / "chapters"
717 chapters.mkdir(parents=True)
718 index_path = guide_root / "index.html"
719
720 implementation_plan = temp_dir / "implementation.md"
721 implementation_plan.write_text(
722 "\n".join(
723 [
724 "# Implementation Plan",
725 "",
726 "## File Changes",
727 f"- `{guide_root}/`",
728 f"- `{index_path}`",
729 f"- `{chapters}/`",
730 "",
731 ]
732 )
733 )
734
735 dod = create_definition_of_done("Create a multi-file nginx guide.")
736 dod.implementation_plan = str(implementation_plan)
737 dod.completed_items.extend(
738 [
739 "First, examine the existing Fortran guide structure to understand the format and depth",
740 "Create the new nginx guide directory structure",
741 ]
742 )
743 dod.pending_items.append("Develop the main index.html file for the nginx guide")
744
745 decision = repairer.handle_empty_response(
746 task="Create a multi-file nginx guide.",
747 original_task=None,
748 empty_retry_count=5,
749 max_empty_retries=2,
750 dod=dod,
751 )
752
753 assert decision.should_continue is True
754 assert decision.retry_message is not None
755 assert "retry 5/6" in decision.retry_message
756 assert "Next missing planned artifact: `index.html`" in decision.retry_message
757 assert (
758 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
759 "by creating `index.html`."
760 in decision.retry_message
761 )
762
763
764 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
765 temp_dir: Path,
766 ) -> None:
767 context = build_context(
768 temp_dir=temp_dir,
769 use_react=False,
770 )
771 repairer = ResponseRepairer(context)
772
773 guide_root = temp_dir / "guides" / "nginx"
774 chapters = guide_root / "chapters"
775 guide_root.mkdir(parents=True)
776 chapters.mkdir()
777 index_path = guide_root / "index.html"
778 index_path.write_text("<html></html>\n")
779
780 implementation_plan = temp_dir / "implementation.md"
781 implementation_plan.write_text(
782 "\n".join(
783 [
784 "# Implementation Plan",
785 "",
786 "## File Changes",
787 f"- `{chapters}/`",
788 f"- `{index_path}`",
789 "",
790 ]
791 )
792 )
793
794 dod = create_definition_of_done("Create a multi-file nginx guide.")
795 dod.implementation_plan = str(implementation_plan)
796 dod.touched_files.append(str(index_path))
797 dod.completed_items.extend(
798 [
799 "Create the new nginx guide directory structure",
800 "Develop the main index.html file with proper structure",
801 ]
802 )
803 dod.pending_items.append("Create 01-introduction.html")
804
805 decision = repairer.handle_empty_response(
806 task="Create a multi-file nginx guide.",
807 original_task=None,
808 empty_retry_count=5,
809 max_empty_retries=2,
810 dod=dod,
811 )
812
813 assert decision.should_continue is True
814 assert decision.retry_message is not None
815 assert "retry 5/6" in decision.retry_message
816 assert (
817 "Resume with this exact next step: continue `Create 01-introduction.html` "
818 "by creating `01-introduction.html`."
819 in decision.retry_message
820 )
821 assert 'Emit this tool shape now: `write(file_path="' in decision.retry_message
822 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
823
824
825 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
826 temp_dir: Path,
827 ) -> None:
828 context = build_context(
829 temp_dir=temp_dir,
830 use_react=False,
831 )
832 context.session.messages.append(
833 SimpleNamespace(
834 content=(
835 "Observation [notepad_write_working]: Result: "
836 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
837 )
838 )
839 )
840 repairer = ResponseRepairer(context)
841
842 guide_root = temp_dir / "guides" / "nginx"
843 chapters = guide_root / "chapters"
844 chapters.mkdir(parents=True)
845 index_path = guide_root / "index.html"
846 chapter_one = chapters / "01-getting-started.html"
847 chapter_two = chapters / "02-installation.html"
848 chapter_three = chapters / "03-first-website.html"
849 chapter_four = chapters / "04-configuration-basics.html"
850 chapter_five = chapters / "05-advanced-features.html"
851 index_path.write_text("<html></html>\n")
852 chapter_one.write_text("<h1>One</h1>\n")
853 chapter_two.write_text("<h1>Two</h1>\n")
854 chapter_three.write_text("<h1>Three</h1>\n")
855 chapter_four.write_text("<h1>Four</h1>\n")
856
857 implementation_plan = temp_dir / "implementation.md"
858 implementation_plan.write_text(
859 "\n".join(
860 [
861 "# Implementation Plan",
862 "",
863 "## File Changes",
864 f"- `{guide_root}/`",
865 f"- `{chapters}/`",
866 f"- `{index_path}`",
867 f"- `{chapter_one}`",
868 f"- `{chapter_two}`",
869 f"- `{chapter_three}`",
870 f"- `{chapter_four}`",
871 f"- `{chapter_five}`",
872 "",
873 ]
874 )
875 )
876
877 dod = create_definition_of_done("Create a multi-file nginx guide.")
878 dod.implementation_plan = str(implementation_plan)
879 dod.touched_files.extend(
880 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
881 )
882 dod.completed_items.extend(
883 [
884 "Create the directory structure for the new nginx guide",
885 "Create the main index.html file with proper structure",
886 ]
887 )
888 dod.pending_items.append("Create each chapter file in sequence")
889
890 decision = repairer.handle_empty_response(
891 task="Create a multi-file nginx guide.",
892 original_task=None,
893 empty_retry_count=3,
894 max_empty_retries=2,
895 dod=dod,
896 )
897
898 assert decision.should_continue is True
899 assert decision.retry_message is not None
900 assert "Continue from the exact next step below." in decision.retry_message
901 assert "Latest working note:" not in decision.retry_message
902 assert "Confirmed completed work:" not in decision.retry_message
903 assert "Next pending item:" not in decision.retry_message
904
905
906 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
907 temp_dir: Path,
908 ) -> None:
909 context = build_context(
910 temp_dir=temp_dir,
911 use_react=False,
912 )
913 repairer = ResponseRepairer(context)
914
915 guide_root = temp_dir / "guides" / "nginx"
916 chapters = guide_root / "chapters"
917 chapters.mkdir(parents=True)
918 index_path = guide_root / "index.html"
919 index_path.write_text("<html></html>\n")
920
921 implementation_plan = temp_dir / "implementation.md"
922 implementation_plan.write_text(
923 "\n".join(
924 [
925 "# Implementation Plan",
926 "",
927 "## File Changes",
928 f"- `{guide_root}/`",
929 f"- `{chapters}/`",
930 f"- `{index_path}`",
931 f"- `{chapters / '02-installation.html'}`",
932 "",
933 ]
934 )
935 )
936
937 dod = create_definition_of_done("Create a multi-file nginx guide.")
938 dod.implementation_plan = str(implementation_plan)
939 dod.touched_files.append(str(index_path))
940 dod.pending_items.append("Write the introduction chapter")
941
942 decision = repairer.handle_empty_response(
943 task="Create a multi-file nginx guide.",
944 original_task=None,
945 empty_retry_count=1,
946 max_empty_retries=2,
947 dod=dod,
948 )
949
950 assert decision.should_continue is True
951 assert decision.retry_message is not None
952 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
953 assert (
954 "Resume with this exact next step: continue `Write the introduction chapter` "
955 "by creating the next output file under `chapters/`."
956 in decision.retry_message
957 )
958 assert (
959 "Prefer one concrete `write` call for a file inside "
960 f"`{display_runtime_path(chapters)}` before more research."
961 in decision.retry_message
962 )
963
964
965 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
966 temp_dir: Path,
967 ) -> None:
968 context = build_context(
969 temp_dir=temp_dir,
970 use_react=False,
971 )
972 repairer = ResponseRepairer(context)
973
974 guide_root = temp_dir / "guides" / "nginx"
975 chapters = guide_root / "chapters"
976 guide_root.mkdir(parents=True)
977 chapters.mkdir()
978 chapter_one = chapters / "01-introduction.html"
979 index_path = guide_root / "index.html"
980
981 implementation_plan = temp_dir / "implementation.md"
982 implementation_plan.write_text(
983 "\n".join(
984 [
985 "# Implementation Plan",
986 "",
987 "## File Changes",
988 f"- `{guide_root}/`",
989 f"- `{index_path}`",
990 f"- `{chapters}/`",
991 f"- `{chapter_one}`",
992 "",
993 ]
994 )
995 )
996
997 dod = create_definition_of_done("Create a multi-file nginx guide.")
998 dod.implementation_plan = str(implementation_plan)
999 dod.completed_items.extend(
1000 [
1001 "First, examine the existing Fortran guide structure to understand the format and depth",
1002 "Create the new nginx guide directory structure",
1003 ]
1004 )
1005 dod.pending_items.append("Develop the main index.html file with proper structure")
1006
1007 decision = repairer.handle_empty_response(
1008 task="Create a multi-file nginx guide.",
1009 original_task=None,
1010 empty_retry_count=2,
1011 max_empty_retries=2,
1012 dod=dod,
1013 )
1014
1015 assert decision.should_continue is True
1016 assert decision.retry_message is not None
1017 assert (
1018 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
1019 in decision.retry_message
1020 )
1021 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1022 assert "Prefer one `write(content=...)` call" in decision.retry_message
1023 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
1024
1025
1026 def test_empty_response_retry_adds_root_html_starter_shape_for_first_index_write(
1027 temp_dir: Path,
1028 ) -> None:
1029 context = build_context(
1030 temp_dir=temp_dir,
1031 use_react=False,
1032 )
1033 repairer = ResponseRepairer(context)
1034
1035 guide_root = temp_dir / "guides" / "nginx"
1036 chapters = guide_root / "chapters"
1037 guide_root.mkdir(parents=True)
1038 chapters.mkdir()
1039 index_path = guide_root / "index.html"
1040 chapter_one = chapters / "01-introduction.html"
1041 index_path.write_text(
1042 "\n".join(
1043 [
1044 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1045 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1046 "",
1047 ]
1048 )
1049 )
1050
1051 implementation_plan = temp_dir / "implementation.md"
1052 implementation_plan.write_text(
1053 "\n".join(
1054 [
1055 "# Implementation Plan",
1056 "",
1057 "## File Changes",
1058 f"- `{guide_root}/`",
1059 f"- `{index_path}`",
1060 f"- `{chapters}/`",
1061 f"- `{chapter_one}`",
1062 "",
1063 ]
1064 )
1065 )
1066
1067 dod = create_definition_of_done("Create a multi-file nginx guide.")
1068 dod.implementation_plan = str(implementation_plan)
1069 dod.completed_items.extend(
1070 [
1071 "First, examine the existing Fortran guide structure to understand the format and depth",
1072 "Create the new nginx guide directory structure",
1073 ]
1074 )
1075 dod.pending_items.append("Develop the main index.html file with proper structure")
1076
1077 decision = repairer.handle_empty_response(
1078 task="Create a multi-file nginx guide.",
1079 original_task=None,
1080 empty_retry_count=2,
1081 max_empty_retries=2,
1082 dod=dod,
1083 )
1084
1085 assert decision.should_continue is True
1086 assert decision.retry_message is not None
1087 assert (
1088 "If you get stuck, start with `<!DOCTYPE html>`, `<title>Nginx Guide</title>`, "
1089 "`<h1>Nginx Guide</h1>`, a short intro paragraph, and a linked chapter list that "
1090 "points at the guide pages you will create under `chapters/`."
1091 in decision.retry_message
1092 )
1093 assert "../index.html" not in decision.retry_message
1094
1095
1096 def test_repeated_first_index_retry_includes_root_html_payload_shape(
1097 temp_dir: Path,
1098 ) -> None:
1099 context = build_context(
1100 temp_dir=temp_dir,
1101 use_react=False,
1102 )
1103 repairer = ResponseRepairer(context)
1104
1105 guide_root = temp_dir / "guides" / "nginx"
1106 chapters = guide_root / "chapters"
1107 guide_root.mkdir(parents=True)
1108 chapters.mkdir()
1109 index_path = guide_root / "index.html"
1110 chapter_one = chapters / "01-introduction.html"
1111
1112 implementation_plan = temp_dir / "implementation.md"
1113 implementation_plan.write_text(
1114 "\n".join(
1115 [
1116 "# Implementation Plan",
1117 "",
1118 "## File Changes",
1119 f"- `{guide_root}/`",
1120 f"- `{index_path}`",
1121 f"- `{chapters}/`",
1122 f"- `{chapter_one}`",
1123 "",
1124 ]
1125 )
1126 )
1127
1128 dod = create_definition_of_done("Create a multi-file nginx guide.")
1129 dod.implementation_plan = str(implementation_plan)
1130 dod.completed_items.extend(
1131 [
1132 "First, examine the existing Fortran guide structure to understand the format and depth",
1133 "Create the new nginx guide directory structure",
1134 ]
1135 )
1136 dod.pending_items.append("Develop the main index.html file with proper structure")
1137
1138 decision = repairer.handle_empty_response(
1139 task="Create a multi-file nginx guide.",
1140 original_task=None,
1141 empty_retry_count=5,
1142 max_empty_retries=6,
1143 dod=dod,
1144 )
1145
1146 assert decision.should_continue is True
1147 assert decision.retry_message is not None
1148 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1149 assert "<title>Nginx Guide</title>" in decision.retry_message
1150 assert 'href="chapters/01-introduction.html"' in decision.retry_message
1151 assert "../index.html" not in decision.retry_message
1152
1153
1154 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
1155 temp_dir: Path,
1156 ) -> None:
1157 context = build_context(
1158 temp_dir=temp_dir,
1159 use_react=False,
1160 )
1161 repairer = ResponseRepairer(context)
1162
1163 guide_root = temp_dir / "guides" / "nginx"
1164 chapters = guide_root / "chapters"
1165 guide_root.mkdir(parents=True)
1166 chapters.mkdir()
1167 index_path = guide_root / "index.html"
1168 chapter_one = chapters / "01-introduction.html"
1169
1170 implementation_plan = temp_dir / "implementation.md"
1171 implementation_plan.write_text(
1172 "\n".join(
1173 [
1174 "# Implementation Plan",
1175 "",
1176 "## File Changes",
1177 f"- `{guide_root}/`",
1178 f"- `{chapters}/`",
1179 f"- `{index_path}`",
1180 f"- `{chapter_one}`",
1181 "",
1182 ]
1183 )
1184 )
1185
1186 dod = create_definition_of_done("Create a multi-file nginx guide.")
1187 dod.implementation_plan = str(implementation_plan)
1188 dod.completed_items.extend(
1189 [
1190 "First, examine the existing Fortran guide structure to understand the format and depth",
1191 "Create the new nginx guide directory structure",
1192 ]
1193 )
1194 dod.pending_items.append("Develop the main index.html file with proper structure")
1195
1196 decision = repairer.handle_empty_response(
1197 task="Create a multi-file nginx guide.",
1198 original_task=None,
1199 empty_retry_count=4,
1200 max_empty_retries=4,
1201 dod=dod,
1202 )
1203
1204 assert decision.should_continue is True
1205 assert decision.retry_message is not None
1206 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1207 assert (
1208 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
1209 "by creating `index.html`."
1210 in decision.retry_message
1211 )
1212 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
1213 assert "Remaining planned artifacts:" not in decision.retry_message
1214 assert (
1215 "Next observed output pattern under `chapters/`: `01-introduction.html`"
1216 not in decision.retry_message
1217 )
1218
1219
1220 def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_step(
1221 temp_dir: Path,
1222 ) -> None:
1223 context = build_context(
1224 temp_dir=temp_dir,
1225 use_react=False,
1226 )
1227 repairer = ResponseRepairer(context)
1228
1229 guide_root = temp_dir / "guides" / "nginx"
1230 chapters = guide_root / "chapters"
1231 chapters.mkdir(parents=True)
1232 index_path = guide_root / "index.html"
1233 index_path.write_text(
1234 "\n".join(
1235 [
1236 "<html>",
1237 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1238 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1239 "</html>",
1240 ]
1241 )
1242 + "\n"
1243 )
1244
1245 implementation_plan = temp_dir / "implementation.md"
1246 implementation_plan.write_text(
1247 "\n".join(
1248 [
1249 "# Implementation Plan",
1250 "",
1251 "## File Changes",
1252 f"- `{guide_root}/`",
1253 f"- `{chapters}/`",
1254 f"- `{index_path}`",
1255 "",
1256 ]
1257 )
1258 )
1259
1260 dod = create_definition_of_done("Create a multi-file nginx guide.")
1261 dod.implementation_plan = str(implementation_plan)
1262 dod.touched_files.append(str(index_path))
1263 dod.completed_items.append("Develop the main index.html file with proper structure")
1264 dod.pending_items.append("Create chapter files with content and structure")
1265
1266 decision = repairer.handle_empty_response(
1267 task="Create a multi-file nginx guide.",
1268 original_task=None,
1269 empty_retry_count=3,
1270 max_empty_retries=4,
1271 dod=dod,
1272 )
1273
1274 assert decision.should_continue is True
1275 assert decision.retry_message is not None
1276 assert "Next missing planned artifact:" not in decision.retry_message
1277 assert (
1278 "Resume with this exact next step: continue `Create chapter files with content and structure` "
1279 "by creating `01-introduction.html`."
1280 in decision.retry_message
1281 )
1282 assert (
1283 'Emit this tool shape now: `write(file_path="'
1284 in decision.retry_message
1285 )
1286 assert (
1287 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
1288 not in decision.retry_message
1289 )
1290 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
1291 assert "Follow the same full-payload one-file-at-a-time write pattern" not in decision.retry_message
1292 assert "Remaining planned artifacts:" not in decision.retry_message
1293 assert "Next pending item:" not in decision.retry_message
1294
1295
1296 def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapter_step(
1297 temp_dir: Path,
1298 ) -> None:
1299 context = build_context(
1300 temp_dir=temp_dir,
1301 use_react=False,
1302 )
1303 repairer = ResponseRepairer(context)
1304
1305 guide_root = temp_dir / "guides" / "nginx"
1306 chapters = guide_root / "chapters"
1307 chapters.mkdir(parents=True)
1308 index_path = guide_root / "index.html"
1309 chapter_one = chapters / "01-introduction.html"
1310 chapter_two = chapters / "02-installation.html"
1311 index_path.write_text(
1312 "\n".join(
1313 [
1314 "<html>",
1315 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1316 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1317 "</html>",
1318 ]
1319 )
1320 + "\n"
1321 )
1322 chapter_one.write_text("<h1>Introduction</h1>\n")
1323
1324 implementation_plan = temp_dir / "implementation.md"
1325 implementation_plan.write_text(
1326 "\n".join(
1327 [
1328 "# Implementation Plan",
1329 "",
1330 "## File Changes",
1331 f"- `{guide_root}/`",
1332 f"- `{chapters}/`",
1333 f"- `{index_path}`",
1334 "",
1335 ]
1336 )
1337 )
1338
1339 dod = create_definition_of_done("Create a multi-file nginx guide.")
1340 dod.implementation_plan = str(implementation_plan)
1341 dod.touched_files.extend([str(index_path), str(chapter_one)])
1342 dod.completed_items.extend(
1343 [
1344 "Develop the main index.html file with proper structure",
1345 "Create first chapter file (01-introduction.html)",
1346 ]
1347 )
1348 dod.pending_items.append("Create chapter files following the established pattern")
1349
1350 decision = repairer.handle_empty_response(
1351 task="Create a multi-file nginx guide.",
1352 original_task=None,
1353 empty_retry_count=1,
1354 max_empty_retries=2,
1355 dod=dod,
1356 )
1357
1358 assert decision.should_continue is True
1359 assert decision.retry_message is not None
1360 assert "Next pending item:" not in decision.retry_message
1361 assert (
1362 "Resume with this exact next step: continue `Create chapter files following the established pattern` "
1363 "by creating `02-installation.html`."
1364 in decision.retry_message
1365 )
1366 assert (
1367 "It is the next concrete output needed to continue `Create chapter files following the established pattern`."
1368 in decision.retry_message
1369 )
1370 assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message
1371 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
1372
1373
1374 def test_empty_response_retry_keeps_first_substantive_retry_lean(
1375 temp_dir: Path,
1376 ) -> None:
1377 context = build_context(
1378 temp_dir=temp_dir,
1379 use_react=False,
1380 )
1381 repairer = ResponseRepairer(context)
1382
1383 guide_root = temp_dir / "guides" / "nginx"
1384 chapters = guide_root / "chapters"
1385 chapters.mkdir(parents=True)
1386 index_path = guide_root / "index.html"
1387 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1388 reference_chapter.parent.mkdir(parents=True)
1389 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1390 index_path.write_text(
1391 "\n".join(
1392 [
1393 "<html>",
1394 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1395 "</html>",
1396 ]
1397 )
1398 + "\n"
1399 )
1400 context.session.append(
1401 Message(
1402 role=Role.ASSISTANT,
1403 content="",
1404 tool_calls=[
1405 ToolCall(
1406 id="call_ref",
1407 name="read",
1408 arguments={"file_path": str(reference_chapter)},
1409 )
1410 ],
1411 )
1412 )
1413
1414 implementation_plan = temp_dir / "implementation.md"
1415 implementation_plan.write_text(
1416 "\n".join(
1417 [
1418 "# Implementation Plan",
1419 "",
1420 "## File Changes",
1421 f"- `{guide_root}/`",
1422 f"- `{chapters}/`",
1423 f"- `{index_path}`",
1424 "",
1425 ]
1426 )
1427 )
1428
1429 dod = create_definition_of_done("Create a multi-file nginx guide.")
1430 dod.implementation_plan = str(implementation_plan)
1431 dod.touched_files.append(str(index_path))
1432 dod.completed_items.append("Develop the main index.html file with proper structure")
1433 dod.pending_items.append("Create chapter files following the established pattern")
1434
1435 decision = repairer.handle_empty_response(
1436 task="Create a multi-file nginx guide.",
1437 original_task=None,
1438 empty_retry_count=1,
1439 max_empty_retries=2,
1440 dod=dod,
1441 )
1442
1443 assert decision.should_continue is True
1444 assert decision.retry_message is not None
1445 assert (
1446 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1447 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1448 "and chapter body content."
1449 in decision.retry_message
1450 )
1451 assert (
1452 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1453 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1454 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1455 in decision.retry_message
1456 )
1457 assert display_runtime_path(reference_chapter) not in decision.retry_message
1458 assert "Reference cues from" not in decision.retry_message
1459 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1460 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1461
1462
1463 def test_late_first_substantive_retry_stays_lean(
1464 temp_dir: Path,
1465 ) -> None:
1466 context = build_context(
1467 temp_dir=temp_dir,
1468 use_react=False,
1469 )
1470 repairer = ResponseRepairer(context)
1471
1472 guide_root = temp_dir / "guides" / "nginx"
1473 chapters = guide_root / "chapters"
1474 chapters.mkdir(parents=True)
1475 index_path = guide_root / "index.html"
1476 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1477 reference_chapter.parent.mkdir(parents=True)
1478 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1479 index_path.write_text(
1480 "\n".join(
1481 [
1482 "<html>",
1483 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1484 "</html>",
1485 ]
1486 )
1487 + "\n"
1488 )
1489 context.session.append(
1490 Message(
1491 role=Role.ASSISTANT,
1492 content="",
1493 tool_calls=[
1494 ToolCall(
1495 id="call_ref",
1496 name="read",
1497 arguments={"file_path": str(reference_chapter)},
1498 )
1499 ],
1500 )
1501 )
1502
1503 implementation_plan = temp_dir / "implementation.md"
1504 implementation_plan.write_text(
1505 "\n".join(
1506 [
1507 "# Implementation Plan",
1508 "",
1509 "## File Changes",
1510 f"- `{guide_root}/`",
1511 f"- `{chapters}/`",
1512 f"- `{index_path}`",
1513 "",
1514 ]
1515 )
1516 )
1517
1518 dod = create_definition_of_done("Create a multi-file nginx guide.")
1519 dod.implementation_plan = str(implementation_plan)
1520 dod.touched_files.append(str(index_path))
1521 dod.completed_items.append("Develop the main index.html file with proper structure")
1522 dod.pending_items.append("Create chapter files following the established pattern")
1523
1524 decision = repairer.handle_empty_response(
1525 task="Create a multi-file nginx guide.",
1526 original_task=None,
1527 empty_retry_count=3,
1528 max_empty_retries=4,
1529 dod=dod,
1530 )
1531
1532 assert decision.should_continue is True
1533 assert decision.retry_message is not None
1534 assert (
1535 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1536 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1537 "and chapter body content."
1538 in decision.retry_message
1539 )
1540 assert (
1541 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1542 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1543 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1544 in decision.retry_message
1545 )
1546 assert display_runtime_path(reference_chapter) not in decision.retry_message
1547 assert "Reference cues from" not in decision.retry_message
1548 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1549 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1550
1551
1552 def test_repeated_first_substantive_retry_includes_minimal_payload_shape(
1553 temp_dir: Path,
1554 ) -> None:
1555 context = build_context(
1556 temp_dir=temp_dir,
1557 use_react=False,
1558 )
1559 repairer = ResponseRepairer(context)
1560
1561 guide_root = temp_dir / "guides" / "nginx"
1562 chapters = guide_root / "chapters"
1563 chapters.mkdir(parents=True)
1564 index_path = guide_root / "index.html"
1565 index_path.write_text(
1566 "\n".join(
1567 [
1568 "<html>",
1569 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1570 "</html>",
1571 ]
1572 )
1573 + "\n"
1574 )
1575
1576 implementation_plan = temp_dir / "implementation.md"
1577 implementation_plan.write_text(
1578 "\n".join(
1579 [
1580 "# Implementation Plan",
1581 "",
1582 "## File Changes",
1583 f"- `{guide_root}/`",
1584 f"- `{chapters}/`",
1585 f"- `{index_path}`",
1586 "",
1587 ]
1588 )
1589 )
1590
1591 dod = create_definition_of_done("Create a multi-file nginx guide.")
1592 dod.implementation_plan = str(implementation_plan)
1593 dod.touched_files.append(str(index_path))
1594 dod.completed_items.append("Develop the main index.html file with proper structure")
1595 dod.pending_items.append("Create chapter files following the established pattern")
1596
1597 decision = repairer.handle_empty_response(
1598 task="Create a multi-file nginx guide.",
1599 original_task=None,
1600 empty_retry_count=5,
1601 max_empty_retries=6,
1602 dod=dod,
1603 )
1604
1605 assert decision.should_continue is True
1606 assert decision.retry_message is not None
1607 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1608 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1609 assert "../index.html" in decision.retry_message
1610
1611
1612 def test_empty_response_retry_surfaces_minimal_child_html_payload_earlier_after_progress(
1613 temp_dir: Path,
1614 ) -> None:
1615 context = build_context(
1616 temp_dir=temp_dir,
1617 use_react=False,
1618 )
1619 repairer = ResponseRepairer(context)
1620
1621 guide_root = temp_dir / "guides" / "nginx"
1622 chapters = guide_root / "chapters"
1623 guide_root.mkdir(parents=True)
1624 chapters.mkdir()
1625 index_path = guide_root / "index.html"
1626 chapter_one = chapters / "01-introduction.html"
1627 index_path.write_text(
1628 "\n".join(
1629 [
1630 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1631 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1632 "",
1633 ]
1634 )
1635 )
1636
1637 implementation_plan = temp_dir / "implementation.md"
1638 implementation_plan.write_text(
1639 "\n".join(
1640 [
1641 "# Implementation Plan",
1642 "",
1643 "## File Changes",
1644 f"- `{guide_root}/`",
1645 f"- `{index_path}`",
1646 f"- `{chapters}/`",
1647 f"- `{chapter_one}`",
1648 "",
1649 ]
1650 )
1651 )
1652
1653 dod = create_definition_of_done("Create a multi-file nginx guide.")
1654 dod.implementation_plan = str(implementation_plan)
1655 dod.touched_files.append(str(index_path))
1656 dod.completed_items.append("Develop the main index.html file with proper structure")
1657 dod.pending_items.append("Create first chapter file (01-introduction.html)")
1658
1659 decision = repairer.handle_empty_response(
1660 task="Create a multi-file nginx guide.",
1661 original_task=None,
1662 empty_retry_count=3,
1663 max_empty_retries=6,
1664 dod=dod,
1665 )
1666
1667 assert decision.should_continue is True
1668 assert decision.retry_message is not None
1669 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1670 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1671
1672
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
1726 def test_first_substantive_retry_activates_on_first_empty_turn(
1727 temp_dir: Path,
1728 ) -> None:
1729 context = build_context(
1730 temp_dir=temp_dir,
1731 use_react=False,
1732 )
1733 repairer = ResponseRepairer(context)
1734
1735 guide_root = temp_dir / "guides" / "nginx"
1736 chapters = guide_root / "chapters"
1737 chapters.mkdir(parents=True)
1738 index_path = guide_root / "index.html"
1739 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1740 reference_chapter.parent.mkdir(parents=True)
1741 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1742 index_path.write_text(
1743 "\n".join(
1744 [
1745 "<html>",
1746 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1747 "</html>",
1748 ]
1749 )
1750 + "\n"
1751 )
1752 context.session.append(
1753 Message(
1754 role=Role.ASSISTANT,
1755 content="",
1756 tool_calls=[
1757 ToolCall(
1758 id="call_ref",
1759 name="read",
1760 arguments={"file_path": str(reference_chapter)},
1761 )
1762 ],
1763 )
1764 )
1765
1766 implementation_plan = temp_dir / "implementation.md"
1767 implementation_plan.write_text(
1768 "\n".join(
1769 [
1770 "# Implementation Plan",
1771 "",
1772 "## File Changes",
1773 f"- `{guide_root}/`",
1774 f"- `{chapters}/`",
1775 f"- `{index_path}`",
1776 "",
1777 ]
1778 )
1779 )
1780
1781 dod = create_definition_of_done("Create a multi-file nginx guide.")
1782 dod.implementation_plan = str(implementation_plan)
1783 dod.touched_files.append(str(index_path))
1784 dod.completed_items.append("Develop the main index.html file with proper structure")
1785 dod.pending_items.append("Create the nginx chapters content")
1786
1787 decision = repairer.handle_empty_response(
1788 task="Create a multi-file nginx guide.",
1789 original_task=None,
1790 empty_retry_count=1,
1791 max_empty_retries=4,
1792 dod=dod,
1793 )
1794
1795 assert decision.should_continue is True
1796 assert decision.retry_message is not None
1797 assert "Emit this tool shape now" in decision.retry_message
1798 assert "01-introduction.html" in decision.retry_message
1799 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1800
1801
1802 def test_late_first_substantive_retry_trims_context_to_core_write_cues(
1803 temp_dir: Path,
1804 ) -> None:
1805 context = build_context(
1806 temp_dir=temp_dir,
1807 use_react=False,
1808 )
1809 repairer = ResponseRepairer(context)
1810
1811 guide_root = temp_dir / "guides" / "nginx"
1812 chapters = guide_root / "chapters"
1813 chapters.mkdir(parents=True)
1814 index_path = guide_root / "index.html"
1815 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1816 reference_chapter.parent.mkdir(parents=True)
1817 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1818 index_path.write_text(
1819 "\n".join(
1820 [
1821 "<html>",
1822 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1823 "</html>",
1824 ]
1825 )
1826 + "\n"
1827 )
1828 context.session.append(
1829 Message(
1830 role=Role.ASSISTANT,
1831 content="",
1832 tool_calls=[
1833 ToolCall(
1834 id="call_ref",
1835 name="read",
1836 arguments={"file_path": str(reference_chapter)},
1837 )
1838 ],
1839 )
1840 )
1841
1842 implementation_plan = temp_dir / "implementation.md"
1843 implementation_plan.write_text(
1844 "\n".join(
1845 [
1846 "# Implementation Plan",
1847 "",
1848 "## File Changes",
1849 f"- `{guide_root}/`",
1850 f"- `{chapters}/`",
1851 f"- `{index_path}`",
1852 "",
1853 ]
1854 )
1855 )
1856
1857 dod = create_definition_of_done("Create a multi-file nginx guide.")
1858 dod.implementation_plan = str(implementation_plan)
1859 dod.touched_files.append(str(index_path))
1860 dod.completed_items.append("Develop the main index.html file with proper structure")
1861 dod.pending_items.append("Create chapter files following the established pattern")
1862
1863 decision = repairer.handle_empty_response(
1864 task="Create a multi-file nginx guide.",
1865 original_task=None,
1866 empty_retry_count=6,
1867 max_empty_retries=6,
1868 dod=dod,
1869 )
1870
1871 assert decision.should_continue is True
1872 assert decision.retry_message is not None
1873 assert (
1874 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1875 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1876 "and chapter body content."
1877 in decision.retry_message
1878 )
1879 assert (
1880 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1881 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1882 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1883 in decision.retry_message
1884 )
1885 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1886 assert (
1887 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1888 "as the starting pattern for this new file, then adapt the content to the current target."
1889 not in decision.retry_message
1890 )
1891 assert "Reference cues from" not in decision.retry_message
1892 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1893 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1894
1895
1896 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
1897 temp_dir: Path,
1898 ) -> None:
1899 context = build_context(
1900 temp_dir=temp_dir,
1901 use_react=False,
1902 )
1903 repairer = ResponseRepairer(context)
1904
1905 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
1906 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
1907 nginx_root.mkdir(parents=True)
1908 fortran_root.mkdir(parents=True)
1909 reference_index = fortran_root / "index.html"
1910 reference_index.write_text("<html>fortran</html>\n")
1911 output_index = nginx_root / "index.html"
1912
1913 implementation_plan = temp_dir / "implementation.md"
1914 implementation_plan.write_text(
1915 "\n".join(
1916 [
1917 "# Implementation Plan",
1918 "",
1919 "## File Changes",
1920 f"- `{output_index}`",
1921 f"- `{nginx_root / 'chapters'}/`",
1922 f"- `{reference_index}`",
1923 "",
1924 ]
1925 )
1926 )
1927
1928 dod = create_definition_of_done("Create a multi-file nginx guide.")
1929 dod.implementation_plan = str(implementation_plan)
1930 dod.touched_files.append(str(reference_index))
1931 dod.completed_items.append(
1932 "First, examine the existing Fortran guide structure and content"
1933 )
1934 dod.pending_items.append("Develop the nginx index.html file")
1935
1936 decision = repairer.handle_empty_response(
1937 task="Create a multi-file nginx guide.",
1938 original_task=None,
1939 empty_retry_count=2,
1940 max_empty_retries=2,
1941 dod=dod,
1942 )
1943
1944 assert decision.should_continue is True
1945 assert decision.retry_message is not None
1946 assert (
1947 "Resume with this exact next step: continue `Develop the nginx index.html file` "
1948 f"by creating `{output_index.name}`."
1949 in decision.retry_message
1950 )
1951 assert (
1952 f'Emit this tool shape now: `write(file_path="{display_runtime_path(output_index)}", content="...")`.'
1953 in decision.retry_message
1954 )
1955 assert str(reference_index) not in decision.retry_message
1956
1957
1958 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
1959 temp_dir: Path,
1960 ) -> None:
1961 context = build_context(
1962 temp_dir=temp_dir,
1963 use_react=False,
1964 )
1965 repairer = ResponseRepairer(context)
1966
1967 guide_root = temp_dir / "guides" / "nginx"
1968 chapters = guide_root / "chapters"
1969 chapters.mkdir(parents=True)
1970 index_path = guide_root / "index.html"
1971 index_path.write_text(
1972 "\n".join(
1973 [
1974 "<html>",
1975 '<a href="chapters/introduction.html">Introduction</a>',
1976 '<a href="chapters/installation.html">Installation</a>',
1977 "</html>",
1978 ]
1979 )
1980 + "\n"
1981 )
1982
1983 implementation_plan = temp_dir / "implementation.md"
1984 implementation_plan.write_text(
1985 "\n".join(
1986 [
1987 "# Implementation Plan",
1988 "",
1989 "## File Changes",
1990 f"- `{guide_root}/`",
1991 f"- `{chapters}/`",
1992 f"- `{index_path}`",
1993 f"- `{chapters / '02-installation.html'}`",
1994 "",
1995 ]
1996 )
1997 )
1998
1999 dod = create_definition_of_done("Create a multi-file nginx guide.")
2000 dod.implementation_plan = str(implementation_plan)
2001 dod.touched_files.append(str(index_path))
2002 dod.pending_items.append("Write the introduction chapter")
2003
2004 decision = repairer.handle_empty_response(
2005 task="Create a multi-file nginx guide.",
2006 original_task=None,
2007 empty_retry_count=1,
2008 max_empty_retries=2,
2009 dod=dod,
2010 )
2011
2012 assert decision.should_continue is True
2013 assert decision.retry_message is not None
2014 assert (
2015 "Resume with this exact next step: continue `Write the introduction chapter` "
2016 "by creating `introduction.html`."
2017 in decision.retry_message
2018 )
2019 assert "Next declared output under `chapters/`" not in decision.retry_message
2020 assert (
2021 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "introduction.html")}", content="...")`.'
2022 in decision.retry_message
2023 )
2024
2025
2026 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
2027 temp_dir: Path,
2028 ) -> None:
2029 context = build_context(
2030 temp_dir=temp_dir,
2031 use_react=False,
2032 )
2033 repairer = ResponseRepairer(context)
2034
2035 guide_root = temp_dir / "guides" / "nginx"
2036 chapters = guide_root / "chapters"
2037 chapters.mkdir(parents=True)
2038 index_path = guide_root / "index.html"
2039 chapter_one = chapters / "01-introduction.html"
2040 index_path.write_text("<html></html>\n")
2041 chapter_one.write_text("<html></html>\n")
2042
2043 implementation_plan = temp_dir / "implementation.md"
2044 implementation_plan.write_text(
2045 "\n".join(
2046 [
2047 "# Implementation Plan",
2048 "",
2049 "## File Changes",
2050 f"- `{guide_root}/`",
2051 f"- `{chapters}/`",
2052 f"- `{index_path}`",
2053 f"- `{chapters / '02-installation.html'}`",
2054 "",
2055 ]
2056 )
2057 )
2058
2059 dod = create_definition_of_done("Create a multi-file nginx guide.")
2060 dod.implementation_plan = str(implementation_plan)
2061 dod.touched_files.extend([str(index_path), str(chapter_one)])
2062 dod.completed_items.extend(
2063 [
2064 "Create index.html for nginx guide",
2065 "Create first chapter file (01-introduction.html)",
2066 ]
2067 )
2068 dod.pending_items.append("Create second chapter file (02-installation.html)")
2069
2070 decision = repairer.handle_empty_response(
2071 task="Create a multi-file nginx guide.",
2072 original_task=None,
2073 empty_retry_count=2,
2074 max_empty_retries=2,
2075 dod=dod,
2076 )
2077
2078 assert decision.should_continue is True
2079 assert decision.retry_message is not None
2080 assert (
2081 "Resume with this exact next step: continue `Create second chapter file "
2082 "(02-installation.html)` by creating `02-installation.html`."
2083 in decision.retry_message
2084 )
2085 assert (
2086 "Prefer one `write(content=...)` call for "
2087 f"`{display_runtime_path(chapters / '02-installation.html')}` "
2088 "before more research."
2089 in decision.retry_message
2090 )
2091 assert "Do not return another working note or empty response" in decision.retry_message
2092
2093
2094 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
2095 temp_dir: Path,
2096 ) -> None:
2097 context = build_context(
2098 temp_dir=temp_dir,
2099 use_react=False,
2100 )
2101 repairer = ResponseRepairer(context)
2102
2103 guide_root = temp_dir / "guides" / "nginx"
2104 chapters = guide_root / "chapters"
2105 chapters.mkdir(parents=True)
2106 index_path = guide_root / "index.html"
2107 chapter_one = chapters / "01-introduction.html"
2108 index_path.write_text(
2109 "\n".join(
2110 [
2111 "<html>",
2112 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
2113 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
2114 "</html>",
2115 ]
2116 )
2117 + "\n"
2118 )
2119 chapter_one.write_text("<html></html>\n")
2120
2121 implementation_plan = temp_dir / "implementation.md"
2122 implementation_plan.write_text(
2123 "\n".join(
2124 [
2125 "# Implementation Plan",
2126 "",
2127 "## File Changes",
2128 f"- `{guide_root}/`",
2129 f"- `{chapters}/`",
2130 f"- `{index_path}`",
2131 f"- `{chapters / '02-installation.html'}`",
2132 "",
2133 ]
2134 )
2135 )
2136
2137 dod = create_definition_of_done("Create a multi-file nginx guide.")
2138 dod.implementation_plan = str(implementation_plan)
2139 dod.touched_files.extend([str(index_path), str(chapter_one)])
2140 dod.completed_items.extend(
2141 [
2142 "Create index.html for nginx guide",
2143 "Create Chapter 1: Introduction to NGINX Tool",
2144 ]
2145 )
2146 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
2147
2148 decision = repairer.handle_empty_response(
2149 task="Create a multi-file nginx guide.",
2150 original_task=None,
2151 empty_retry_count=2,
2152 max_empty_retries=2,
2153 dod=dod,
2154 )
2155
2156 assert decision.should_continue is True
2157 assert decision.retry_message is not None
2158 assert (
2159 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
2160 "by creating `02-installation.html`."
2161 in decision.retry_message
2162 )
2163 assert (
2164 "Prefer one `write(content=...)` call for "
2165 f"`{display_runtime_path(chapters / '02-installation.html')}` "
2166 "before more research."
2167 in decision.retry_message
2168 )
2169 assert (
2170 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "02-installation.html")}", content="...")`.'
2171 in decision.retry_message
2172 )
2173 assert (
2174 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
2175 "so it matches the current guide structure."
2176 in decision.retry_message
2177 )
2178
2179
2180 def test_late_chapter_retry_reuses_existing_sibling_html_structure(
2181 temp_dir: Path,
2182 ) -> None:
2183 context = build_context(
2184 temp_dir=temp_dir,
2185 use_react=False,
2186 )
2187 repairer = ResponseRepairer(context)
2188
2189 guide_root = temp_dir / "guides" / "nginx"
2190 chapters = guide_root / "chapters"
2191 chapters.mkdir(parents=True)
2192 index_path = guide_root / "index.html"
2193 chapter_one = chapters / "01-introduction.html"
2194 chapter_two = chapters / "02-installation.html"
2195 chapter_three = chapters / "03-basic-configuration.html"
2196 index_path.write_text(
2197 "\n".join(
2198 [
2199 "<html>",
2200 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
2201 '<a href="chapters/02-installation.html">Chapter 2: Installation</a>',
2202 '<a href="chapters/03-basic-configuration.html">Chapter 3: Basic Configuration</a>',
2203 "</html>",
2204 ]
2205 )
2206 + "\n"
2207 )
2208 chapter_one.write_text("<html><body><h1>Chapter 1</h1></body></html>\n")
2209 chapter_two.write_text("<html><body><h1>Chapter 2</h1></body></html>\n")
2210
2211 implementation_plan = temp_dir / "implementation.md"
2212 implementation_plan.write_text(
2213 "\n".join(
2214 [
2215 "# Implementation Plan",
2216 "",
2217 "## File Changes",
2218 f"- `{guide_root}/`",
2219 f"- `{chapters}/`",
2220 f"- `{index_path}`",
2221 f"- `{chapter_one}`",
2222 f"- `{chapter_two}`",
2223 f"- `{chapter_three}`",
2224 "",
2225 ]
2226 )
2227 )
2228
2229 dod = create_definition_of_done("Create a multi-file nginx guide.")
2230 dod.implementation_plan = str(implementation_plan)
2231 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2232 dod.completed_items.extend(
2233 [
2234 "Create index.html for nginx guide",
2235 "Create first chapter file (01-introduction.html)",
2236 "Create second chapter file (02-installation.html)",
2237 ]
2238 )
2239 dod.pending_items.append("Create third chapter file (03-basic-configuration.html)")
2240
2241 decision = repairer.handle_empty_response(
2242 task="Create a multi-file nginx guide.",
2243 original_task=None,
2244 empty_retry_count=4,
2245 max_empty_retries=2,
2246 dod=dod,
2247 )
2248
2249 assert decision.should_continue is True
2250 assert decision.retry_message is not None
2251 assert (
2252 "Reuse the overall structure and navigation pattern from "
2253 f"`{display_runtime_path(chapter_two)}` as the starting pattern for "
2254 "`Chapter 3: Basic Configuration`; adapt the title, heading, and body content "
2255 "to the new chapter."
2256 in decision.retry_message
2257 )
2258 assert (
2259 "If you get stuck, start with `<title>Chapter 3: Basic Configuration</title>`, "
2260 "`<h1>Chapter 3: Basic Configuration</h1>`, one introductory paragraph, a couple "
2261 "of `<h2>` sections with short body text, and a back link to `../index.html`."
2262 in decision.retry_message
2263 )
2264
2265
2266 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
2267 temp_dir: Path,
2268 ) -> None:
2269 context = build_context(
2270 temp_dir=temp_dir,
2271 use_react=False,
2272 )
2273 repairer = ResponseRepairer(context)
2274
2275 guide_root = temp_dir / "guides" / "nginx"
2276 chapters = guide_root / "chapters"
2277 chapters.mkdir(parents=True)
2278 chapter_one = chapters / "01-introduction.html"
2279 chapter_one.write_text("<html></html>\n")
2280
2281 implementation_plan = temp_dir / "implementation.md"
2282 implementation_plan.write_text(
2283 "\n".join(
2284 [
2285 "# Implementation Plan",
2286 "",
2287 "## File Changes",
2288 f"- `{guide_root}/`",
2289 f"- `{chapters}/`",
2290 f"- `{guide_root / 'index.html'}`",
2291 f"- `{chapters / '01-introduction.html'}`",
2292 "",
2293 ]
2294 )
2295 )
2296
2297 dod = create_definition_of_done("Create a multi-file nginx guide.")
2298 dod.implementation_plan = str(implementation_plan)
2299 dod.touched_files.append(str(chapter_one))
2300 dod.completed_items.append("Create first chapter file (01-introduction.html)")
2301 dod.pending_items.append("Develop the main index.html file for the nginx guide")
2302
2303 recovery_context = RecoveryContext(
2304 original_tool="write",
2305 original_args={
2306 "file_path": "~/Loader/guides/nginx/index.html",
2307 "content_chars": 1354,
2308 "content_lines": 30,
2309 },
2310 )
2311 recovery_context.add_attempt(
2312 "write",
2313 {
2314 "file_path": "~/Loader/guides/nginx/index.html",
2315 "content_chars": 1354,
2316 "content_lines": 30,
2317 },
2318 "WriteTool.execute() missing 1 required positional argument: 'content'",
2319 )
2320 context.recovery_context = recovery_context
2321
2322 decision = repairer.handle_empty_response(
2323 task="Create a multi-file nginx guide.",
2324 original_task=None,
2325 empty_retry_count=2,
2326 max_empty_retries=2,
2327 dod=dod,
2328 )
2329
2330 assert decision.should_continue is True
2331 assert decision.retry_message is not None
2332 assert "resend `write`" in decision.retry_message
2333 assert "content_chars" in decision.retry_message
2334 assert "index.html" in decision.retry_message
2335
2336
2337 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
2338 temp_dir: Path,
2339 ) -> None:
2340 context = build_context(
2341 temp_dir=temp_dir,
2342 use_react=False,
2343 )
2344 repairer = ResponseRepairer(context)
2345
2346 guide_root = temp_dir / "guides" / "nginx"
2347 chapters = guide_root / "chapters"
2348 chapters.mkdir(parents=True)
2349 index_path = guide_root / "index.html"
2350 chapter_one = chapters / "01-introduction.html"
2351 index_path.write_text(
2352 "\n".join(
2353 [
2354 "<html>",
2355 '<a href="chapters/01-introduction.html">Introduction</a>',
2356 '<a href="chapters/02-installation.html">Installation</a>',
2357 "</html>",
2358 ]
2359 )
2360 + "\n"
2361 )
2362 chapter_one.write_text("<html></html>\n")
2363
2364 implementation_plan = temp_dir / "implementation.md"
2365 implementation_plan.write_text(
2366 "\n".join(
2367 [
2368 "# Implementation Plan",
2369 "",
2370 "## File Changes",
2371 f"- `{guide_root}/`",
2372 f"- `{chapters}/`",
2373 f"- `{index_path}`",
2374 f"- `{chapters / '02-installation.html'}`",
2375 "",
2376 ]
2377 )
2378 )
2379
2380 dod = create_definition_of_done("Create a multi-file nginx guide.")
2381 dod.implementation_plan = str(implementation_plan)
2382 dod.touched_files.extend([str(index_path), str(chapter_one)])
2383 dod.completed_items.extend(
2384 [
2385 "Create index.html for nginx guide",
2386 "Create first chapter file (01-introduction.html)",
2387 ]
2388 )
2389 dod.pending_items.append("Create second chapter file (02-installation.html)")
2390
2391 decision = repairer.handle_empty_response(
2392 task="Create a multi-file nginx guide.",
2393 original_task=None,
2394 empty_retry_count=1,
2395 max_empty_retries=2,
2396 dod=dod,
2397 )
2398
2399 assert decision.should_continue is True
2400 assert decision.retry_message is not None
2401 assert "Continue from the exact next step below." in decision.retry_message
2402 assert "Confirmed completed work:" not in decision.retry_message
2403 assert "Next pending item:" not in decision.retry_message
2404 assert (
2405 "Resume with this exact next step: continue `Create second chapter file "
2406 "(02-installation.html)` by creating `02-installation.html`."
2407 in decision.retry_message
2408 )
2409
2410
2411 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
2412 temp_dir: Path,
2413 ) -> None:
2414 context = build_context(
2415 temp_dir=temp_dir,
2416 use_react=False,
2417 )
2418 repairer = ResponseRepairer(context)
2419
2420 guide_root = temp_dir / "guides" / "nginx"
2421 chapters = guide_root / "chapters"
2422 chapters.mkdir(parents=True)
2423 index_path = guide_root / "index.html"
2424 chapter_one = chapters / "01-introduction.html"
2425 index_path.write_text("<html></html>\n")
2426 chapter_one.write_text("<html></html>\n")
2427
2428 implementation_plan = temp_dir / "implementation.md"
2429 implementation_plan.write_text(
2430 "\n".join(
2431 [
2432 "# Implementation Plan",
2433 "",
2434 "## File Changes",
2435 f"- `{guide_root}/`",
2436 f"- `{chapters}/`",
2437 f"- `{index_path}`",
2438 f"- `{chapter_one}`",
2439 f"- `{chapters / '02-installation.html'}`",
2440 "",
2441 ]
2442 )
2443 )
2444
2445 dod = create_definition_of_done("Create a multi-file nginx guide.")
2446 dod.implementation_plan = str(implementation_plan)
2447 dod.touched_files.extend([str(index_path), str(chapter_one)])
2448 dod.completed_items.extend(
2449 [
2450 "Develop the main index.html file for the nginx guide",
2451 "Create first chapter file (01-introduction.html)",
2452 ]
2453 )
2454 dod.pending_items.extend(
2455 [
2456 "Create the nginx directory structure",
2457 "Create second chapter file (02-installation.html)",
2458 ]
2459 )
2460
2461 decision = repairer.handle_empty_response(
2462 task="Create a multi-file nginx guide.",
2463 original_task=None,
2464 empty_retry_count=1,
2465 max_empty_retries=2,
2466 dod=dod,
2467 )
2468
2469 assert decision.should_continue is True
2470 assert decision.retry_message is not None
2471 assert "Create the nginx directory structure" not in decision.retry_message
2472 assert "02-installation.html" in decision.retry_message
2473
2474
2475 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
2476 temp_dir: Path,
2477 ) -> None:
2478 context = build_context(
2479 temp_dir=temp_dir,
2480 use_react=False,
2481 )
2482 repairer = ResponseRepairer(context)
2483
2484 guide_root = temp_dir / "guides" / "nginx"
2485 chapters = guide_root / "chapters"
2486 chapters.mkdir(parents=True)
2487 index_path = guide_root / "index.html"
2488 chapter_one = chapters / "01-getting-started.html"
2489 chapter_two = chapters / "02-installation.html"
2490 chapter_three = chapters / "03-first-website.html"
2491 chapter_four = chapters / "04-configuration-basics.html"
2492 index_path.write_text("<html></html>\n")
2493 chapter_one.write_text("<h1>One</h1>\n")
2494 chapter_two.write_text("<h1>Two</h1>\n")
2495 chapter_three.write_text("<h1>Three</h1>\n")
2496
2497 implementation_plan = temp_dir / "implementation.md"
2498 implementation_plan.write_text(
2499 "\n".join(
2500 [
2501 "# Implementation Plan",
2502 "",
2503 "## File Changes",
2504 f"- `{guide_root}/`",
2505 f"- `{chapters}/`",
2506 f"- `{index_path}`",
2507 f"- `{chapter_one}`",
2508 f"- `{chapter_two}`",
2509 f"- `{chapter_three}`",
2510 f"- `{chapter_four}`",
2511 "",
2512 ]
2513 )
2514 )
2515
2516 dod = create_definition_of_done("Create a multi-file nginx guide.")
2517 dod.implementation_plan = str(implementation_plan)
2518 dod.touched_files.extend(
2519 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
2520 )
2521 dod.completed_items.extend(
2522 [
2523 "Create the directory structure for the new nginx guide",
2524 "Create the main index.html file with proper structure",
2525 ]
2526 )
2527 dod.pending_items.append("Create each chapter file in sequence")
2528
2529 decision = repairer.handle_empty_response(
2530 task="Create a multi-file nginx guide.",
2531 original_task=None,
2532 empty_retry_count=5,
2533 max_empty_retries=2,
2534 dod=dod,
2535 )
2536
2537 assert decision.should_continue is False
2538 assert decision.final_response is not None
2539 assert "retrying 4 times" in decision.final_response
2540
2541
2542 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
2543 temp_dir: Path,
2544 ) -> None:
2545 context = build_context(
2546 temp_dir=temp_dir,
2547 use_react=False,
2548 )
2549 repairer = ResponseRepairer(context)
2550
2551 guide_root = temp_dir / "guides" / "nginx"
2552 chapters = guide_root / "chapters"
2553 chapters.mkdir(parents=True)
2554 implementation_plan = temp_dir / "implementation.md"
2555 implementation_plan.write_text(
2556 "\n".join(
2557 [
2558 "# Implementation Plan",
2559 "",
2560 "## File Changes",
2561 f"- `{guide_root / 'index.html'}`",
2562 f"- `{chapters / '01-getting-started.html'}`",
2563 f"- `{chapters / '02-installation.html'}`",
2564 "",
2565 ]
2566 )
2567 )
2568
2569 dod = create_definition_of_done("Create a multi-file nginx guide.")
2570 dod.implementation_plan = str(implementation_plan)
2571 dod.touched_files.extend(
2572 [
2573 str(guide_root / "index.html"),
2574 str(chapters / "01-getting-started.html"),
2575 ]
2576 )
2577 dod.completed_items.extend(
2578 [
2579 "Create the directory structure for the new nginx guide",
2580 "Create the main index.html file with proper structure",
2581 ]
2582 )
2583 dod.pending_items.append("Create each chapter file in sequence")
2584
2585 decision = repairer.handle_empty_response(
2586 task="Create a multi-file nginx guide.",
2587 original_task=None,
2588 empty_retry_count=1,
2589 max_empty_retries=2,
2590 dod=dod,
2591 )
2592
2593 assert decision.retry_message is not None
2594 assert "Continue from the exact next step below." in decision.retry_message
2595 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
2596
2597
2598 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
2599 temp_dir: Path,
2600 ) -> None:
2601 context = build_context(
2602 temp_dir=temp_dir,
2603 use_react=False,
2604 )
2605 repairer = ResponseRepairer(context)
2606
2607 guide_root = temp_dir / "guides" / "nginx"
2608 chapters = guide_root / "chapters"
2609 chapters.mkdir(parents=True)
2610 index_path = guide_root / "index.html"
2611 chapter_one = chapters / "01-getting-started.html"
2612 chapter_two = chapters / "02-installation.html"
2613 chapter_three = chapters / "03-first-website.html"
2614 index_path.write_text("<html></html>\n")
2615 chapter_one.write_text("<h1>One</h1>\n")
2616 chapter_two.write_text("<h1>Two</h1>\n")
2617
2618 implementation_plan = temp_dir / "implementation.md"
2619 implementation_plan.write_text(
2620 "\n".join(
2621 [
2622 "# Implementation Plan",
2623 "",
2624 "## File Changes",
2625 f"- `{guide_root}/`",
2626 f"- `{chapters}/`",
2627 f"- `{index_path}`",
2628 f"- `{chapter_one}`",
2629 f"- `{chapter_two}`",
2630 f"- `{chapter_three}`",
2631 "",
2632 ]
2633 )
2634 )
2635
2636 dod = create_definition_of_done("Create a multi-file nginx guide.")
2637 dod.implementation_plan = str(implementation_plan)
2638 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2639 dod.completed_items.extend(
2640 [
2641 "Create the main index.html file with proper structure",
2642 "Link all chapters together properly",
2643 ]
2644 )
2645 dod.pending_items.append("Create each chapter file in sequence")
2646
2647 decision = repairer.handle_empty_response(
2648 task="Create a multi-file nginx guide.",
2649 original_task=None,
2650 empty_retry_count=1,
2651 max_empty_retries=2,
2652 dod=dod,
2653 )
2654
2655 assert decision.retry_message is not None
2656 assert "Link all chapters together properly" not in decision.retry_message
2657 assert "Continue from the exact next step below." in decision.retry_message
2658 assert "Resume with this exact next step:" in decision.retry_message
2659
2660
2661 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
2662 temp_dir: Path,
2663 ) -> None:
2664 context = build_context(
2665 temp_dir=temp_dir,
2666 use_react=False,
2667 )
2668 repairer = ResponseRepairer(context)
2669
2670 reference_chapters = temp_dir / "fortran" / "chapters"
2671 reference_chapters.mkdir(parents=True)
2672 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
2673
2674 guide_root = temp_dir / "guides" / "nginx"
2675 chapters = guide_root / "chapters"
2676 chapters.mkdir(parents=True)
2677 index_path = guide_root / "index.html"
2678 index_path.write_text("<html></html>\n")
2679
2680 implementation_plan = temp_dir / "implementation.md"
2681 implementation_plan.write_text(
2682 "\n".join(
2683 [
2684 "# Implementation Plan",
2685 "",
2686 "## File Changes",
2687 f"- `{guide_root}/`",
2688 f"- `{chapters}/`",
2689 f"- `{index_path}`",
2690 "",
2691 ]
2692 )
2693 )
2694
2695 dod = create_definition_of_done("Create a multi-file nginx guide.")
2696 dod.implementation_plan = str(implementation_plan)
2697 dod.touched_files.append(str(index_path))
2698 dod.pending_items.append("Write the introduction chapter")
2699 context.session.append(
2700 Message(
2701 role=Role.ASSISTANT,
2702 content="",
2703 tool_calls=[
2704 ToolCall(
2705 id="read-ref-1",
2706 name="read",
2707 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
2708 )
2709 ],
2710 )
2711 )
2712
2713 decision = repairer.handle_empty_response(
2714 task="Create a multi-file nginx guide.",
2715 original_task=None,
2716 empty_retry_count=1,
2717 max_empty_retries=2,
2718 dod=dod,
2719 )
2720
2721 assert decision.should_continue is True
2722 assert decision.retry_message is not None
2723 assert (
2724 "Resume with this exact next step: continue `Write the introduction chapter` "
2725 "by creating `01-introduction.html`."
2726 in decision.retry_message
2727 )
2728 assert (
2729 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "01-introduction.html")}", content="...")`.'
2730 in decision.retry_message
2731 )
2732 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
2733 assert display_runtime_path(reference_chapters / "01-introduction.html") not in decision.retry_message