Python · 72735 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` call for "
319 f"`{display_runtime_path(index_path)}` before any more reference reads."
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 "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message
335
336
337 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
338 temp_dir: Path,
339 ) -> None:
340 context = build_context(
341 temp_dir=temp_dir,
342 use_react=False,
343 )
344 repairer = ResponseRepairer(context)
345
346 guide_root = temp_dir / "guides" / "nginx"
347 chapters_path = guide_root / "chapters"
348 index_path = guide_root / "index.html"
349
350 implementation_plan = temp_dir / "implementation.md"
351 implementation_plan.write_text(
352 "\n".join(
353 [
354 "# Implementation Plan",
355 "",
356 "## File Changes",
357 f"- `{chapters_path}/`",
358 f"- `{index_path}`",
359 "",
360 ]
361 )
362 )
363
364 dod = create_definition_of_done("Create a multi-file nginx guide.")
365 dod.implementation_plan = str(implementation_plan)
366 dod.pending_items.extend(
367 [
368 "Create the nginx directory structure",
369 "Create the main index.html file for nginx guide",
370 ]
371 )
372
373 decision = repairer.handle_empty_response(
374 task="Create a multi-file nginx guide.",
375 original_task=None,
376 empty_retry_count=1,
377 max_empty_retries=2,
378 dod=dod,
379 )
380
381 assert decision.should_continue is True
382 assert decision.retry_message is not None
383 assert (
384 "Resume with this exact next step: continue `Create the nginx directory structure` "
385 "by creating `chapters/`."
386 in decision.retry_message
387 )
388 assert (
389 "Prefer one concrete directory-creation step for "
390 f"`{display_runtime_path(chapters_path)}` before more research."
391 in decision.retry_message
392 )
393 expected_command = f"mkdir -p {display_runtime_path(chapters_path)}"
394 assert (
395 f'Emit this tool shape now: `bash(command="{expected_command}")`.'
396 in decision.retry_message
397 )
398 assert f'write(file_path="{display_runtime_path(chapters_path)}"' not in decision.retry_message
399
400
401 def test_empty_response_retry_uses_home_relative_path_for_home_artifacts(
402 temp_dir: Path,
403 monkeypatch: pytest.MonkeyPatch,
404 ) -> None:
405 monkeypatch.setenv("HOME", str(temp_dir.resolve(strict=False)))
406 context = build_context(
407 temp_dir=temp_dir,
408 use_react=False,
409 )
410 repairer = ResponseRepairer(context)
411
412 guide_root = temp_dir / "Loader" / "guides" / "nginx"
413 index_path = guide_root / "index.html"
414
415 implementation_plan = temp_dir / "implementation.md"
416 implementation_plan.write_text(
417 "\n".join(
418 [
419 "# Implementation Plan",
420 "",
421 "## File Changes",
422 f"- `{index_path}`",
423 "",
424 ]
425 )
426 )
427
428 dod = create_definition_of_done("Create a multi-file nginx guide.")
429 dod.implementation_plan = str(implementation_plan)
430 dod.pending_items.extend(
431 [
432 "Create nginx guide directory structure",
433 "Write main index.html for nginx guide",
434 ]
435 )
436
437 decision = repairer.handle_empty_response(
438 task="Create a multi-file nginx guide.",
439 original_task=None,
440 empty_retry_count=1,
441 max_empty_retries=2,
442 dod=dod,
443 )
444
445 assert decision.should_continue is True
446 assert decision.retry_message is not None
447 assert "`~/Loader/guides/nginx/index.html`" in decision.retry_message
448 assert (
449 'Emit this tool shape now: `write(file_path="~/Loader/guides/nginx/index.html", content="...")`.'
450 in decision.retry_message
451 )
452 assert (
453 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
454 in decision.retry_message
455 )
456
457
458 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
459 temp_dir: Path,
460 ) -> None:
461 context = build_context(
462 temp_dir=temp_dir,
463 use_react=False,
464 )
465 repairer = ResponseRepairer(context)
466
467 guide_root = temp_dir / "guides" / "nginx"
468 chapters = guide_root / "chapters"
469 chapters.mkdir(parents=True)
470 index_path = guide_root / "index.html"
471 first_chapter = chapters / "01-introduction.html"
472 second_chapter = chapters / "02-installation.html"
473 index_path.write_text("<html></html>\n")
474 first_chapter.write_text("<h1>Intro</h1>\n")
475
476 implementation_plan = temp_dir / "implementation.md"
477 implementation_plan.write_text(
478 "\n".join(
479 [
480 "# Implementation Plan",
481 "",
482 "## File Changes",
483 f"- `{index_path}`",
484 f"- `{first_chapter}`",
485 f"- `{second_chapter}`",
486 "",
487 ]
488 )
489 )
490
491 dod = create_definition_of_done("Create a multi-file nginx guide.")
492 dod.implementation_plan = str(implementation_plan)
493 dod.touched_files.extend([str(index_path), str(first_chapter)])
494 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
495
496 context.recovery_context = RecoveryContext(
497 original_tool="write",
498 original_args={"file_path": "", "content": "<html></html>\n"},
499 )
500 context.recovery_context.add_attempt(
501 "write",
502 {"file_path": "", "content": "<html></html>\n"},
503 "Empty file path",
504 )
505
506 decision = repairer.handle_empty_response(
507 task="Create a multi-file nginx guide.",
508 original_task=None,
509 empty_retry_count=1,
510 max_empty_retries=2,
511 dod=dod,
512 )
513
514 assert decision.should_continue is True
515 assert decision.retry_message is not None
516 assert (
517 "Last tool failure: resend `write` for "
518 f"`{display_runtime_path(second_chapter)}` with a valid `file_path` and real `content`."
519 in decision.retry_message
520 )
521 assert "Do not leave `file_path` empty" in decision.retry_message
522 assert (
523 f'Emit this tool shape now: `write(file_path="{display_runtime_path(second_chapter)}", content="...")`.'
524 in decision.retry_message
525 )
526
527
528 def test_empty_response_retry_respects_discovery_first_pending_step(
529 temp_dir: Path,
530 ) -> None:
531 context = build_context(
532 temp_dir=temp_dir,
533 use_react=False,
534 )
535 repairer = ResponseRepairer(context)
536
537 implementation_plan = temp_dir / "implementation.md"
538 implementation_plan.write_text(
539 "\n".join(
540 [
541 "# Implementation Plan",
542 "",
543 "## File Changes",
544 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
545 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
546 "",
547 ]
548 )
549 )
550
551 dod = create_definition_of_done("Create a multi-file nginx guide.")
552 dod.implementation_plan = str(implementation_plan)
553 dod.pending_items.extend(
554 [
555 "First, examine the existing fortran guide structure and content to understand the format",
556 "Create the nginx directory structure",
557 "Develop the main index.html file for the nginx guide",
558 ]
559 )
560
561 context.session.append(
562 SimpleNamespace(
563 role="tool",
564 content=(
565 "Observation [notepad_write_working]: Result: "
566 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
567 ),
568 )
569 )
570
571 decision = repairer.handle_empty_response(
572 task="Create a multi-file nginx guide.",
573 original_task=None,
574 empty_retry_count=1,
575 max_empty_retries=2,
576 dod=dod,
577 )
578
579 assert decision.should_continue is True
580 assert decision.retry_message is not None
581 assert (
582 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
583 in decision.retry_message
584 )
585 assert "one concrete evidence-gathering tool call" in decision.retry_message
586 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
587
588
589 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
590 temp_dir: Path,
591 ) -> None:
592 context = build_context(
593 temp_dir=temp_dir,
594 use_react=False,
595 )
596 repairer = ResponseRepairer(context)
597
598 guide_root = temp_dir / "guides" / "nginx"
599 chapters = guide_root / "chapters"
600 chapters.mkdir(parents=True)
601 index_path = guide_root / "index.html"
602 chapter_one = chapters / "01-getting-started.html"
603 chapter_two = chapters / "02-installation.html"
604 chapter_three = chapters / "03-first-website.html"
605 chapter_four = chapters / "04-configuration-basics.html"
606 index_path.write_text("<html></html>\n")
607 chapter_one.write_text("<h1>One</h1>\n")
608 chapter_two.write_text("<h1>Two</h1>\n")
609 chapter_three.write_text("<h1>Three</h1>\n")
610
611 implementation_plan = temp_dir / "implementation.md"
612 implementation_plan.write_text(
613 "\n".join(
614 [
615 "# Implementation Plan",
616 "",
617 "## File Changes",
618 f"- `{guide_root}/`",
619 f"- `{chapters}/`",
620 f"- `{index_path}`",
621 f"- `{chapter_one}`",
622 f"- `{chapter_two}`",
623 f"- `{chapter_three}`",
624 f"- `{chapter_four}`",
625 "",
626 ]
627 )
628 )
629
630 dod = create_definition_of_done("Create a multi-file nginx guide.")
631 dod.implementation_plan = str(implementation_plan)
632 dod.touched_files.extend(
633 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
634 )
635 dod.completed_items.extend(
636 [
637 "Create the directory structure for the new nginx guide",
638 "Create the main index.html file with proper structure",
639 ]
640 )
641 dod.pending_items.append("Create each chapter file in sequence")
642
643 decision = repairer.handle_empty_response(
644 task="Create a multi-file nginx guide.",
645 original_task=None,
646 empty_retry_count=3,
647 max_empty_retries=2,
648 dod=dod,
649 )
650
651 assert decision.should_continue is True
652 assert decision.retry_message is not None
653 assert "retry 3/4" in decision.retry_message
654 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
655
656
657 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
658 temp_dir: Path,
659 ) -> None:
660 context = build_context(
661 temp_dir=temp_dir,
662 use_react=False,
663 )
664 repairer = ResponseRepairer(context)
665
666 implementation_plan = temp_dir / "implementation.md"
667 implementation_plan.write_text(
668 "\n".join(
669 [
670 "# Implementation Plan",
671 "",
672 "## File Changes",
673 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
674 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
675 "",
676 ]
677 )
678 )
679
680 dod = create_definition_of_done("Create a multi-file nginx guide.")
681 dod.implementation_plan = str(implementation_plan)
682 dod.pending_items.append("Develop the main index.html file for the nginx guide")
683
684 decision = repairer.handle_empty_response(
685 task="Create a multi-file nginx guide.",
686 original_task=None,
687 empty_retry_count=3,
688 max_empty_retries=2,
689 dod=dod,
690 )
691
692 assert decision.should_continue is True
693 assert decision.retry_message is not None
694 assert "retry 3/4" in decision.retry_message
695 assert "Next missing planned artifact: `index.html`" in decision.retry_message
696 assert (
697 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
698 "by creating `index.html`."
699 in decision.retry_message
700 )
701
702
703 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
704 temp_dir: Path,
705 ) -> None:
706 context = build_context(
707 temp_dir=temp_dir,
708 use_react=False,
709 )
710 repairer = ResponseRepairer(context)
711
712 guide_root = temp_dir / "guides" / "nginx"
713 chapters = guide_root / "chapters"
714 guide_root.mkdir(parents=True)
715 chapters.mkdir()
716 index_path = guide_root / "index.html"
717 index_path.write_text("<html></html>\n")
718
719 implementation_plan = temp_dir / "implementation.md"
720 implementation_plan.write_text(
721 "\n".join(
722 [
723 "# Implementation Plan",
724 "",
725 "## File Changes",
726 f"- `{chapters}/`",
727 f"- `{index_path}`",
728 "",
729 ]
730 )
731 )
732
733 dod = create_definition_of_done("Create a multi-file nginx guide.")
734 dod.implementation_plan = str(implementation_plan)
735 dod.touched_files.append(str(index_path))
736 dod.completed_items.extend(
737 [
738 "Create the new nginx guide directory structure",
739 "Develop the main index.html file with proper structure",
740 ]
741 )
742 dod.pending_items.append("Create 01-introduction.html")
743
744 decision = repairer.handle_empty_response(
745 task="Create a multi-file nginx guide.",
746 original_task=None,
747 empty_retry_count=5,
748 max_empty_retries=2,
749 dod=dod,
750 )
751
752 assert decision.should_continue is True
753 assert decision.retry_message is not None
754 assert "retry 5/6" in decision.retry_message
755 assert "Continue `Create 01-introduction.html` by creating `01-introduction.html`." in decision.retry_message
756 assert 'Emit this tool shape now: `write(file_path="' in decision.retry_message
757 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
758
759
760 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
761 temp_dir: Path,
762 ) -> None:
763 context = build_context(
764 temp_dir=temp_dir,
765 use_react=False,
766 )
767 context.session.messages.append(
768 SimpleNamespace(
769 content=(
770 "Observation [notepad_write_working]: Result: "
771 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
772 )
773 )
774 )
775 repairer = ResponseRepairer(context)
776
777 guide_root = temp_dir / "guides" / "nginx"
778 chapters = guide_root / "chapters"
779 chapters.mkdir(parents=True)
780 index_path = guide_root / "index.html"
781 chapter_one = chapters / "01-getting-started.html"
782 chapter_two = chapters / "02-installation.html"
783 chapter_three = chapters / "03-first-website.html"
784 chapter_four = chapters / "04-configuration-basics.html"
785 chapter_five = chapters / "05-advanced-features.html"
786 index_path.write_text("<html></html>\n")
787 chapter_one.write_text("<h1>One</h1>\n")
788 chapter_two.write_text("<h1>Two</h1>\n")
789 chapter_three.write_text("<h1>Three</h1>\n")
790 chapter_four.write_text("<h1>Four</h1>\n")
791
792 implementation_plan = temp_dir / "implementation.md"
793 implementation_plan.write_text(
794 "\n".join(
795 [
796 "# Implementation Plan",
797 "",
798 "## File Changes",
799 f"- `{guide_root}/`",
800 f"- `{chapters}/`",
801 f"- `{index_path}`",
802 f"- `{chapter_one}`",
803 f"- `{chapter_two}`",
804 f"- `{chapter_three}`",
805 f"- `{chapter_four}`",
806 f"- `{chapter_five}`",
807 "",
808 ]
809 )
810 )
811
812 dod = create_definition_of_done("Create a multi-file nginx guide.")
813 dod.implementation_plan = str(implementation_plan)
814 dod.touched_files.extend(
815 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
816 )
817 dod.completed_items.extend(
818 [
819 "Create the directory structure for the new nginx guide",
820 "Create the main index.html file with proper structure",
821 ]
822 )
823 dod.pending_items.append("Create each chapter file in sequence")
824
825 decision = repairer.handle_empty_response(
826 task="Create a multi-file nginx guide.",
827 original_task=None,
828 empty_retry_count=3,
829 max_empty_retries=2,
830 dod=dod,
831 )
832
833 assert decision.should_continue is True
834 assert decision.retry_message is not None
835 assert "Continue from the exact next step below." in decision.retry_message
836 assert "Latest working note:" not in decision.retry_message
837 assert "Confirmed completed work:" not in decision.retry_message
838 assert "Next pending item:" not in decision.retry_message
839
840
841 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
842 temp_dir: Path,
843 ) -> None:
844 context = build_context(
845 temp_dir=temp_dir,
846 use_react=False,
847 )
848 repairer = ResponseRepairer(context)
849
850 guide_root = temp_dir / "guides" / "nginx"
851 chapters = guide_root / "chapters"
852 chapters.mkdir(parents=True)
853 index_path = guide_root / "index.html"
854 index_path.write_text("<html></html>\n")
855
856 implementation_plan = temp_dir / "implementation.md"
857 implementation_plan.write_text(
858 "\n".join(
859 [
860 "# Implementation Plan",
861 "",
862 "## File Changes",
863 f"- `{guide_root}/`",
864 f"- `{chapters}/`",
865 f"- `{index_path}`",
866 f"- `{chapters / '02-installation.html'}`",
867 "",
868 ]
869 )
870 )
871
872 dod = create_definition_of_done("Create a multi-file nginx guide.")
873 dod.implementation_plan = str(implementation_plan)
874 dod.touched_files.append(str(index_path))
875 dod.pending_items.append("Write the introduction chapter")
876
877 decision = repairer.handle_empty_response(
878 task="Create a multi-file nginx guide.",
879 original_task=None,
880 empty_retry_count=1,
881 max_empty_retries=2,
882 dod=dod,
883 )
884
885 assert decision.should_continue is True
886 assert decision.retry_message is not None
887 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
888 assert (
889 "Resume with this exact next step: continue `Write the introduction chapter` "
890 "by creating the next output file under `chapters/`."
891 in decision.retry_message
892 )
893 assert (
894 "Prefer one concrete `write` call for a file inside "
895 f"`{display_runtime_path(chapters)}` before more research."
896 in decision.retry_message
897 )
898
899
900 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
901 temp_dir: Path,
902 ) -> None:
903 context = build_context(
904 temp_dir=temp_dir,
905 use_react=False,
906 )
907 repairer = ResponseRepairer(context)
908
909 guide_root = temp_dir / "guides" / "nginx"
910 chapters = guide_root / "chapters"
911 guide_root.mkdir(parents=True)
912 chapters.mkdir()
913 chapter_one = chapters / "01-introduction.html"
914 index_path = guide_root / "index.html"
915
916 implementation_plan = temp_dir / "implementation.md"
917 implementation_plan.write_text(
918 "\n".join(
919 [
920 "# Implementation Plan",
921 "",
922 "## File Changes",
923 f"- `{guide_root}/`",
924 f"- `{index_path}`",
925 f"- `{chapters}/`",
926 f"- `{chapter_one}`",
927 "",
928 ]
929 )
930 )
931
932 dod = create_definition_of_done("Create a multi-file nginx guide.")
933 dod.implementation_plan = str(implementation_plan)
934 dod.completed_items.extend(
935 [
936 "First, examine the existing Fortran guide structure to understand the format and depth",
937 "Create the new nginx guide directory structure",
938 ]
939 )
940 dod.pending_items.append("Develop the main index.html file with proper structure")
941
942 decision = repairer.handle_empty_response(
943 task="Create a multi-file nginx guide.",
944 original_task=None,
945 empty_retry_count=2,
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 (
953 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
954 in decision.retry_message
955 )
956 assert "Next missing planned artifact: `index.html`" in decision.retry_message
957 assert "Prefer one `write(content=...)` call" in decision.retry_message
958 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
959
960
961 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
962 temp_dir: Path,
963 ) -> None:
964 context = build_context(
965 temp_dir=temp_dir,
966 use_react=False,
967 )
968 repairer = ResponseRepairer(context)
969
970 guide_root = temp_dir / "guides" / "nginx"
971 chapters = guide_root / "chapters"
972 guide_root.mkdir(parents=True)
973 chapters.mkdir()
974 index_path = guide_root / "index.html"
975 chapter_one = chapters / "01-introduction.html"
976
977 implementation_plan = temp_dir / "implementation.md"
978 implementation_plan.write_text(
979 "\n".join(
980 [
981 "# Implementation Plan",
982 "",
983 "## File Changes",
984 f"- `{guide_root}/`",
985 f"- `{chapters}/`",
986 f"- `{index_path}`",
987 f"- `{chapter_one}`",
988 "",
989 ]
990 )
991 )
992
993 dod = create_definition_of_done("Create a multi-file nginx guide.")
994 dod.implementation_plan = str(implementation_plan)
995 dod.completed_items.extend(
996 [
997 "First, examine the existing Fortran guide structure to understand the format and depth",
998 "Create the new nginx guide directory structure",
999 ]
1000 )
1001 dod.pending_items.append("Develop the main index.html file with proper structure")
1002
1003 decision = repairer.handle_empty_response(
1004 task="Create a multi-file nginx guide.",
1005 original_task=None,
1006 empty_retry_count=4,
1007 max_empty_retries=4,
1008 dod=dod,
1009 )
1010
1011 assert decision.should_continue is True
1012 assert decision.retry_message is not None
1013 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1014 assert (
1015 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
1016 "by creating `index.html`."
1017 in decision.retry_message
1018 )
1019 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
1020 assert "Remaining planned artifacts:" not in decision.retry_message
1021 assert (
1022 "Next observed output pattern under `chapters/`: `01-introduction.html`"
1023 not in decision.retry_message
1024 )
1025
1026
1027 def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_step(
1028 temp_dir: Path,
1029 ) -> None:
1030 context = build_context(
1031 temp_dir=temp_dir,
1032 use_react=False,
1033 )
1034 repairer = ResponseRepairer(context)
1035
1036 guide_root = temp_dir / "guides" / "nginx"
1037 chapters = guide_root / "chapters"
1038 chapters.mkdir(parents=True)
1039 index_path = guide_root / "index.html"
1040 index_path.write_text(
1041 "\n".join(
1042 [
1043 "<html>",
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 "</html>",
1047 ]
1048 )
1049 + "\n"
1050 )
1051
1052 implementation_plan = temp_dir / "implementation.md"
1053 implementation_plan.write_text(
1054 "\n".join(
1055 [
1056 "# Implementation Plan",
1057 "",
1058 "## File Changes",
1059 f"- `{guide_root}/`",
1060 f"- `{chapters}/`",
1061 f"- `{index_path}`",
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.touched_files.append(str(index_path))
1070 dod.completed_items.append("Develop the main index.html file with proper structure")
1071 dod.pending_items.append("Create chapter files with content and structure")
1072
1073 decision = repairer.handle_empty_response(
1074 task="Create a multi-file nginx guide.",
1075 original_task=None,
1076 empty_retry_count=3,
1077 max_empty_retries=4,
1078 dod=dod,
1079 )
1080
1081 assert decision.should_continue is True
1082 assert decision.retry_message is not None
1083 assert "Next missing planned artifact:" not in decision.retry_message
1084 assert (
1085 "Continue `Create chapter files with content and structure` by creating `01-introduction.html`."
1086 in decision.retry_message
1087 )
1088 assert (
1089 'Emit this tool shape now: `write(file_path="'
1090 in decision.retry_message
1091 )
1092 assert (
1093 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
1094 in decision.retry_message
1095 )
1096 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
1097 assert "Follow the same full-payload one-file-at-a-time write pattern" not in decision.retry_message
1098 assert "Remaining planned artifacts:" not in decision.retry_message
1099 assert "Next pending item:" not in decision.retry_message
1100
1101
1102 def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapter_step(
1103 temp_dir: Path,
1104 ) -> None:
1105 context = build_context(
1106 temp_dir=temp_dir,
1107 use_react=False,
1108 )
1109 repairer = ResponseRepairer(context)
1110
1111 guide_root = temp_dir / "guides" / "nginx"
1112 chapters = guide_root / "chapters"
1113 chapters.mkdir(parents=True)
1114 index_path = guide_root / "index.html"
1115 chapter_one = chapters / "01-introduction.html"
1116 chapter_two = chapters / "02-installation.html"
1117 index_path.write_text(
1118 "\n".join(
1119 [
1120 "<html>",
1121 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1122 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1123 "</html>",
1124 ]
1125 )
1126 + "\n"
1127 )
1128 chapter_one.write_text("<h1>Introduction</h1>\n")
1129
1130 implementation_plan = temp_dir / "implementation.md"
1131 implementation_plan.write_text(
1132 "\n".join(
1133 [
1134 "# Implementation Plan",
1135 "",
1136 "## File Changes",
1137 f"- `{guide_root}/`",
1138 f"- `{chapters}/`",
1139 f"- `{index_path}`",
1140 "",
1141 ]
1142 )
1143 )
1144
1145 dod = create_definition_of_done("Create a multi-file nginx guide.")
1146 dod.implementation_plan = str(implementation_plan)
1147 dod.touched_files.extend([str(index_path), str(chapter_one)])
1148 dod.completed_items.extend(
1149 [
1150 "Develop the main index.html file with proper structure",
1151 "Create first chapter file (01-introduction.html)",
1152 ]
1153 )
1154 dod.pending_items.append("Create chapter files following the established pattern")
1155
1156 decision = repairer.handle_empty_response(
1157 task="Create a multi-file nginx guide.",
1158 original_task=None,
1159 empty_retry_count=1,
1160 max_empty_retries=2,
1161 dod=dod,
1162 )
1163
1164 assert decision.should_continue is True
1165 assert decision.retry_message is not None
1166 assert "Next pending item:" not in decision.retry_message
1167 assert (
1168 "Resume with this exact next step: continue `Create chapter files following the established pattern` "
1169 "by creating `02-installation.html`."
1170 in decision.retry_message
1171 )
1172 assert (
1173 "It is the next concrete output needed to continue `Create chapter files following the established pattern`."
1174 in decision.retry_message
1175 )
1176 assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message
1177 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
1178
1179
1180 def test_empty_response_retry_reuses_known_reference_structure_for_first_substantive_file(
1181 temp_dir: Path,
1182 ) -> None:
1183 context = build_context(
1184 temp_dir=temp_dir,
1185 use_react=False,
1186 )
1187 repairer = ResponseRepairer(context)
1188
1189 guide_root = temp_dir / "guides" / "nginx"
1190 chapters = guide_root / "chapters"
1191 chapters.mkdir(parents=True)
1192 index_path = guide_root / "index.html"
1193 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1194 reference_chapter.parent.mkdir(parents=True)
1195 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1196 index_path.write_text(
1197 "\n".join(
1198 [
1199 "<html>",
1200 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1201 "</html>",
1202 ]
1203 )
1204 + "\n"
1205 )
1206 context.session.append(
1207 Message(
1208 role=Role.ASSISTANT,
1209 content="",
1210 tool_calls=[
1211 ToolCall(
1212 id="call_ref",
1213 name="read",
1214 arguments={"file_path": str(reference_chapter)},
1215 )
1216 ],
1217 )
1218 )
1219
1220 implementation_plan = temp_dir / "implementation.md"
1221 implementation_plan.write_text(
1222 "\n".join(
1223 [
1224 "# Implementation Plan",
1225 "",
1226 "## File Changes",
1227 f"- `{guide_root}/`",
1228 f"- `{chapters}/`",
1229 f"- `{index_path}`",
1230 "",
1231 ]
1232 )
1233 )
1234
1235 dod = create_definition_of_done("Create a multi-file nginx guide.")
1236 dod.implementation_plan = str(implementation_plan)
1237 dod.touched_files.append(str(index_path))
1238 dod.completed_items.append("Develop the main index.html file with proper structure")
1239 dod.pending_items.append("Create chapter files following the established pattern")
1240
1241 decision = repairer.handle_empty_response(
1242 task="Create a multi-file nginx guide.",
1243 original_task=None,
1244 empty_retry_count=1,
1245 max_empty_retries=2,
1246 dod=dod,
1247 )
1248
1249 assert decision.should_continue is True
1250 assert decision.retry_message is not None
1251 assert (
1252 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1253 "as the starting pattern for this new file, then adapt the content to the current target."
1254 in decision.retry_message
1255 )
1256 assert (
1257 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1258 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1259 "and chapter body content."
1260 in decision.retry_message
1261 )
1262 assert (
1263 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1264 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1265 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1266 in decision.retry_message
1267 )
1268
1269
1270 def test_compact_first_substantive_retry_reuses_known_reference_structure(
1271 temp_dir: Path,
1272 ) -> None:
1273 context = build_context(
1274 temp_dir=temp_dir,
1275 use_react=False,
1276 )
1277 repairer = ResponseRepairer(context)
1278
1279 guide_root = temp_dir / "guides" / "nginx"
1280 chapters = guide_root / "chapters"
1281 chapters.mkdir(parents=True)
1282 index_path = guide_root / "index.html"
1283 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1284 reference_chapter.parent.mkdir(parents=True)
1285 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1286 index_path.write_text(
1287 "\n".join(
1288 [
1289 "<html>",
1290 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1291 "</html>",
1292 ]
1293 )
1294 + "\n"
1295 )
1296 context.session.append(
1297 Message(
1298 role=Role.ASSISTANT,
1299 content="",
1300 tool_calls=[
1301 ToolCall(
1302 id="call_ref",
1303 name="read",
1304 arguments={"file_path": str(reference_chapter)},
1305 )
1306 ],
1307 )
1308 )
1309
1310 implementation_plan = temp_dir / "implementation.md"
1311 implementation_plan.write_text(
1312 "\n".join(
1313 [
1314 "# Implementation Plan",
1315 "",
1316 "## File Changes",
1317 f"- `{guide_root}/`",
1318 f"- `{chapters}/`",
1319 f"- `{index_path}`",
1320 "",
1321 ]
1322 )
1323 )
1324
1325 dod = create_definition_of_done("Create a multi-file nginx guide.")
1326 dod.implementation_plan = str(implementation_plan)
1327 dod.touched_files.append(str(index_path))
1328 dod.completed_items.append("Develop the main index.html file with proper structure")
1329 dod.pending_items.append("Create chapter files following the established pattern")
1330
1331 decision = repairer.handle_empty_response(
1332 task="Create a multi-file nginx guide.",
1333 original_task=None,
1334 empty_retry_count=3,
1335 max_empty_retries=4,
1336 dod=dod,
1337 )
1338
1339 assert decision.should_continue is True
1340 assert decision.retry_message is not None
1341 assert (
1342 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1343 "as the starting pattern for this new file, then adapt the content to the current target."
1344 in decision.retry_message
1345 )
1346 assert (
1347 f"Reference cues from `{display_runtime_path(reference_chapter)}`: "
1348 "<h1>Chapter 1: Introduction to Fortran</h1>"
1349 in decision.retry_message
1350 )
1351 assert (
1352 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1353 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1354 "and chapter body content."
1355 in decision.retry_message
1356 )
1357 assert (
1358 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1359 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1360 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1361 in decision.retry_message
1362 )
1363
1364
1365 def test_late_first_substantive_retry_trims_context_to_core_write_cues(
1366 temp_dir: Path,
1367 ) -> None:
1368 context = build_context(
1369 temp_dir=temp_dir,
1370 use_react=False,
1371 )
1372 repairer = ResponseRepairer(context)
1373
1374 guide_root = temp_dir / "guides" / "nginx"
1375 chapters = guide_root / "chapters"
1376 chapters.mkdir(parents=True)
1377 index_path = guide_root / "index.html"
1378 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1379 reference_chapter.parent.mkdir(parents=True)
1380 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1381 index_path.write_text(
1382 "\n".join(
1383 [
1384 "<html>",
1385 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1386 "</html>",
1387 ]
1388 )
1389 + "\n"
1390 )
1391 context.session.append(
1392 Message(
1393 role=Role.ASSISTANT,
1394 content="",
1395 tool_calls=[
1396 ToolCall(
1397 id="call_ref",
1398 name="read",
1399 arguments={"file_path": str(reference_chapter)},
1400 )
1401 ],
1402 )
1403 )
1404
1405 implementation_plan = temp_dir / "implementation.md"
1406 implementation_plan.write_text(
1407 "\n".join(
1408 [
1409 "# Implementation Plan",
1410 "",
1411 "## File Changes",
1412 f"- `{guide_root}/`",
1413 f"- `{chapters}/`",
1414 f"- `{index_path}`",
1415 "",
1416 ]
1417 )
1418 )
1419
1420 dod = create_definition_of_done("Create a multi-file nginx guide.")
1421 dod.implementation_plan = str(implementation_plan)
1422 dod.touched_files.append(str(index_path))
1423 dod.completed_items.append("Develop the main index.html file with proper structure")
1424 dod.pending_items.append("Create chapter files following the established pattern")
1425
1426 decision = repairer.handle_empty_response(
1427 task="Create a multi-file nginx guide.",
1428 original_task=None,
1429 empty_retry_count=6,
1430 max_empty_retries=6,
1431 dod=dod,
1432 )
1433
1434 assert decision.should_continue is True
1435 assert decision.retry_message is not None
1436 assert (
1437 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1438 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1439 "and chapter body content."
1440 in decision.retry_message
1441 )
1442 assert (
1443 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1444 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1445 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1446 in decision.retry_message
1447 )
1448 assert (
1449 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1450 "as the starting pattern for this new file, then adapt the content to the current target."
1451 not in decision.retry_message
1452 )
1453 assert "Reference cues from" not in decision.retry_message
1454 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1455
1456
1457 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
1458 temp_dir: Path,
1459 ) -> None:
1460 context = build_context(
1461 temp_dir=temp_dir,
1462 use_react=False,
1463 )
1464 repairer = ResponseRepairer(context)
1465
1466 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
1467 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
1468 nginx_root.mkdir(parents=True)
1469 fortran_root.mkdir(parents=True)
1470 reference_index = fortran_root / "index.html"
1471 reference_index.write_text("<html>fortran</html>\n")
1472 output_index = nginx_root / "index.html"
1473
1474 implementation_plan = temp_dir / "implementation.md"
1475 implementation_plan.write_text(
1476 "\n".join(
1477 [
1478 "# Implementation Plan",
1479 "",
1480 "## File Changes",
1481 f"- `{output_index}`",
1482 f"- `{nginx_root / 'chapters'}/`",
1483 f"- `{reference_index}`",
1484 "",
1485 ]
1486 )
1487 )
1488
1489 dod = create_definition_of_done("Create a multi-file nginx guide.")
1490 dod.implementation_plan = str(implementation_plan)
1491 dod.touched_files.append(str(reference_index))
1492 dod.completed_items.append(
1493 "First, examine the existing Fortran guide structure and content"
1494 )
1495 dod.pending_items.append("Develop the nginx index.html file")
1496
1497 decision = repairer.handle_empty_response(
1498 task="Create a multi-file nginx guide.",
1499 original_task=None,
1500 empty_retry_count=2,
1501 max_empty_retries=2,
1502 dod=dod,
1503 )
1504
1505 assert decision.should_continue is True
1506 assert decision.retry_message is not None
1507 assert (
1508 "Prefer one `write(content=...)` call for "
1509 f"`{display_runtime_path(output_index)}` before more research."
1510 in decision.retry_message
1511 )
1512 assert str(reference_index) not in decision.retry_message
1513
1514
1515 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
1516 temp_dir: Path,
1517 ) -> None:
1518 context = build_context(
1519 temp_dir=temp_dir,
1520 use_react=False,
1521 )
1522 repairer = ResponseRepairer(context)
1523
1524 guide_root = temp_dir / "guides" / "nginx"
1525 chapters = guide_root / "chapters"
1526 chapters.mkdir(parents=True)
1527 index_path = guide_root / "index.html"
1528 index_path.write_text(
1529 "\n".join(
1530 [
1531 "<html>",
1532 '<a href="chapters/introduction.html">Introduction</a>',
1533 '<a href="chapters/installation.html">Installation</a>',
1534 "</html>",
1535 ]
1536 )
1537 + "\n"
1538 )
1539
1540 implementation_plan = temp_dir / "implementation.md"
1541 implementation_plan.write_text(
1542 "\n".join(
1543 [
1544 "# Implementation Plan",
1545 "",
1546 "## File Changes",
1547 f"- `{guide_root}/`",
1548 f"- `{chapters}/`",
1549 f"- `{index_path}`",
1550 f"- `{chapters / '02-installation.html'}`",
1551 "",
1552 ]
1553 )
1554 )
1555
1556 dod = create_definition_of_done("Create a multi-file nginx guide.")
1557 dod.implementation_plan = str(implementation_plan)
1558 dod.touched_files.append(str(index_path))
1559 dod.pending_items.append("Write the introduction chapter")
1560
1561 decision = repairer.handle_empty_response(
1562 task="Create a multi-file nginx guide.",
1563 original_task=None,
1564 empty_retry_count=1,
1565 max_empty_retries=2,
1566 dod=dod,
1567 )
1568
1569 assert decision.should_continue is True
1570 assert decision.retry_message is not None
1571 assert "Next missing planned artifact: `introduction.html`" in decision.retry_message
1572 assert (
1573 "Resume with this exact next step: continue `Write the introduction chapter` "
1574 "by creating `introduction.html`."
1575 in decision.retry_message
1576 )
1577 assert "Next declared output under `chapters/`" not in decision.retry_message
1578 assert (
1579 f"Prefer one `write(content=...)` call for `{(chapters / 'introduction.html').resolve(strict=False)}` "
1580 "before more research."
1581 in decision.retry_message
1582 )
1583
1584
1585 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
1586 temp_dir: Path,
1587 ) -> None:
1588 context = build_context(
1589 temp_dir=temp_dir,
1590 use_react=False,
1591 )
1592 repairer = ResponseRepairer(context)
1593
1594 guide_root = temp_dir / "guides" / "nginx"
1595 chapters = guide_root / "chapters"
1596 chapters.mkdir(parents=True)
1597 index_path = guide_root / "index.html"
1598 chapter_one = chapters / "01-introduction.html"
1599 index_path.write_text("<html></html>\n")
1600 chapter_one.write_text("<html></html>\n")
1601
1602 implementation_plan = temp_dir / "implementation.md"
1603 implementation_plan.write_text(
1604 "\n".join(
1605 [
1606 "# Implementation Plan",
1607 "",
1608 "## File Changes",
1609 f"- `{guide_root}/`",
1610 f"- `{chapters}/`",
1611 f"- `{index_path}`",
1612 f"- `{chapters / '02-installation.html'}`",
1613 "",
1614 ]
1615 )
1616 )
1617
1618 dod = create_definition_of_done("Create a multi-file nginx guide.")
1619 dod.implementation_plan = str(implementation_plan)
1620 dod.touched_files.extend([str(index_path), str(chapter_one)])
1621 dod.completed_items.extend(
1622 [
1623 "Create index.html for nginx guide",
1624 "Create first chapter file (01-introduction.html)",
1625 ]
1626 )
1627 dod.pending_items.append("Create second chapter file (02-installation.html)")
1628
1629 decision = repairer.handle_empty_response(
1630 task="Create a multi-file nginx guide.",
1631 original_task=None,
1632 empty_retry_count=2,
1633 max_empty_retries=2,
1634 dod=dod,
1635 )
1636
1637 assert decision.should_continue is True
1638 assert decision.retry_message is not None
1639 assert (
1640 "Resume with this exact next step: continue `Create second chapter file "
1641 "(02-installation.html)` by creating `02-installation.html`."
1642 in decision.retry_message
1643 )
1644 assert (
1645 "Prefer one `write(content=...)` call for "
1646 f"`{display_runtime_path(chapters / '02-installation.html')}` "
1647 "before more research."
1648 in decision.retry_message
1649 )
1650 assert "Do not return another working note or empty response" in decision.retry_message
1651
1652
1653 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
1654 temp_dir: Path,
1655 ) -> None:
1656 context = build_context(
1657 temp_dir=temp_dir,
1658 use_react=False,
1659 )
1660 repairer = ResponseRepairer(context)
1661
1662 guide_root = temp_dir / "guides" / "nginx"
1663 chapters = guide_root / "chapters"
1664 chapters.mkdir(parents=True)
1665 index_path = guide_root / "index.html"
1666 chapter_one = chapters / "01-introduction.html"
1667 index_path.write_text(
1668 "\n".join(
1669 [
1670 "<html>",
1671 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
1672 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1673 "</html>",
1674 ]
1675 )
1676 + "\n"
1677 )
1678 chapter_one.write_text("<html></html>\n")
1679
1680 implementation_plan = temp_dir / "implementation.md"
1681 implementation_plan.write_text(
1682 "\n".join(
1683 [
1684 "# Implementation Plan",
1685 "",
1686 "## File Changes",
1687 f"- `{guide_root}/`",
1688 f"- `{chapters}/`",
1689 f"- `{index_path}`",
1690 f"- `{chapters / '02-installation.html'}`",
1691 "",
1692 ]
1693 )
1694 )
1695
1696 dod = create_definition_of_done("Create a multi-file nginx guide.")
1697 dod.implementation_plan = str(implementation_plan)
1698 dod.touched_files.extend([str(index_path), str(chapter_one)])
1699 dod.completed_items.extend(
1700 [
1701 "Create index.html for nginx guide",
1702 "Create Chapter 1: Introduction to NGINX Tool",
1703 ]
1704 )
1705 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
1706
1707 decision = repairer.handle_empty_response(
1708 task="Create a multi-file nginx guide.",
1709 original_task=None,
1710 empty_retry_count=2,
1711 max_empty_retries=2,
1712 dod=dod,
1713 )
1714
1715 assert decision.should_continue is True
1716 assert decision.retry_message is not None
1717 assert (
1718 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
1719 "by creating `02-installation.html`."
1720 in decision.retry_message
1721 )
1722 assert (
1723 "Prefer one `write(content=...)` call for "
1724 f"`{display_runtime_path(chapters / '02-installation.html')}` "
1725 "before more research."
1726 in decision.retry_message
1727 )
1728 assert (
1729 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "02-installation.html")}", content="...")`.'
1730 in decision.retry_message
1731 )
1732 assert (
1733 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
1734 "so it matches the current guide structure."
1735 in decision.retry_message
1736 )
1737
1738
1739 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
1740 temp_dir: Path,
1741 ) -> None:
1742 context = build_context(
1743 temp_dir=temp_dir,
1744 use_react=False,
1745 )
1746 repairer = ResponseRepairer(context)
1747
1748 guide_root = temp_dir / "guides" / "nginx"
1749 chapters = guide_root / "chapters"
1750 chapters.mkdir(parents=True)
1751 chapter_one = chapters / "01-introduction.html"
1752 chapter_one.write_text("<html></html>\n")
1753
1754 implementation_plan = temp_dir / "implementation.md"
1755 implementation_plan.write_text(
1756 "\n".join(
1757 [
1758 "# Implementation Plan",
1759 "",
1760 "## File Changes",
1761 f"- `{guide_root}/`",
1762 f"- `{chapters}/`",
1763 f"- `{guide_root / 'index.html'}`",
1764 f"- `{chapters / '01-introduction.html'}`",
1765 "",
1766 ]
1767 )
1768 )
1769
1770 dod = create_definition_of_done("Create a multi-file nginx guide.")
1771 dod.implementation_plan = str(implementation_plan)
1772 dod.touched_files.append(str(chapter_one))
1773 dod.completed_items.append("Create first chapter file (01-introduction.html)")
1774 dod.pending_items.append("Develop the main index.html file for the nginx guide")
1775
1776 recovery_context = RecoveryContext(
1777 original_tool="write",
1778 original_args={
1779 "file_path": "~/Loader/guides/nginx/index.html",
1780 "content_chars": 1354,
1781 "content_lines": 30,
1782 },
1783 )
1784 recovery_context.add_attempt(
1785 "write",
1786 {
1787 "file_path": "~/Loader/guides/nginx/index.html",
1788 "content_chars": 1354,
1789 "content_lines": 30,
1790 },
1791 "WriteTool.execute() missing 1 required positional argument: 'content'",
1792 )
1793 context.recovery_context = recovery_context
1794
1795 decision = repairer.handle_empty_response(
1796 task="Create a multi-file nginx guide.",
1797 original_task=None,
1798 empty_retry_count=2,
1799 max_empty_retries=2,
1800 dod=dod,
1801 )
1802
1803 assert decision.should_continue is True
1804 assert decision.retry_message is not None
1805 assert "resend `write`" in decision.retry_message
1806 assert "content_chars" in decision.retry_message
1807 assert "index.html" in decision.retry_message
1808
1809
1810 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
1811 temp_dir: Path,
1812 ) -> None:
1813 context = build_context(
1814 temp_dir=temp_dir,
1815 use_react=False,
1816 )
1817 repairer = ResponseRepairer(context)
1818
1819 guide_root = temp_dir / "guides" / "nginx"
1820 chapters = guide_root / "chapters"
1821 chapters.mkdir(parents=True)
1822 index_path = guide_root / "index.html"
1823 chapter_one = chapters / "01-introduction.html"
1824 index_path.write_text(
1825 "\n".join(
1826 [
1827 "<html>",
1828 '<a href="chapters/01-introduction.html">Introduction</a>',
1829 '<a href="chapters/02-installation.html">Installation</a>',
1830 "</html>",
1831 ]
1832 )
1833 + "\n"
1834 )
1835 chapter_one.write_text("<html></html>\n")
1836
1837 implementation_plan = temp_dir / "implementation.md"
1838 implementation_plan.write_text(
1839 "\n".join(
1840 [
1841 "# Implementation Plan",
1842 "",
1843 "## File Changes",
1844 f"- `{guide_root}/`",
1845 f"- `{chapters}/`",
1846 f"- `{index_path}`",
1847 f"- `{chapters / '02-installation.html'}`",
1848 "",
1849 ]
1850 )
1851 )
1852
1853 dod = create_definition_of_done("Create a multi-file nginx guide.")
1854 dod.implementation_plan = str(implementation_plan)
1855 dod.touched_files.extend([str(index_path), str(chapter_one)])
1856 dod.completed_items.extend(
1857 [
1858 "Create index.html for nginx guide",
1859 "Create first chapter file (01-introduction.html)",
1860 ]
1861 )
1862 dod.pending_items.append("Create second chapter file (02-installation.html)")
1863
1864 decision = repairer.handle_empty_response(
1865 task="Create a multi-file nginx guide.",
1866 original_task=None,
1867 empty_retry_count=1,
1868 max_empty_retries=2,
1869 dod=dod,
1870 )
1871
1872 assert decision.should_continue is True
1873 assert decision.retry_message is not None
1874 assert "Continue from the exact next step below." in decision.retry_message
1875 assert "Confirmed completed work:" not in decision.retry_message
1876 assert "Next pending item:" not in decision.retry_message
1877 assert (
1878 "Resume with this exact next step: continue `Create second chapter file "
1879 "(02-installation.html)` by creating `02-installation.html`."
1880 in decision.retry_message
1881 )
1882
1883
1884 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
1885 temp_dir: Path,
1886 ) -> None:
1887 context = build_context(
1888 temp_dir=temp_dir,
1889 use_react=False,
1890 )
1891 repairer = ResponseRepairer(context)
1892
1893 guide_root = temp_dir / "guides" / "nginx"
1894 chapters = guide_root / "chapters"
1895 chapters.mkdir(parents=True)
1896 index_path = guide_root / "index.html"
1897 chapter_one = chapters / "01-introduction.html"
1898 index_path.write_text("<html></html>\n")
1899 chapter_one.write_text("<html></html>\n")
1900
1901 implementation_plan = temp_dir / "implementation.md"
1902 implementation_plan.write_text(
1903 "\n".join(
1904 [
1905 "# Implementation Plan",
1906 "",
1907 "## File Changes",
1908 f"- `{guide_root}/`",
1909 f"- `{chapters}/`",
1910 f"- `{index_path}`",
1911 f"- `{chapter_one}`",
1912 f"- `{chapters / '02-installation.html'}`",
1913 "",
1914 ]
1915 )
1916 )
1917
1918 dod = create_definition_of_done("Create a multi-file nginx guide.")
1919 dod.implementation_plan = str(implementation_plan)
1920 dod.touched_files.extend([str(index_path), str(chapter_one)])
1921 dod.completed_items.extend(
1922 [
1923 "Develop the main index.html file for the nginx guide",
1924 "Create first chapter file (01-introduction.html)",
1925 ]
1926 )
1927 dod.pending_items.extend(
1928 [
1929 "Create the nginx directory structure",
1930 "Create second chapter file (02-installation.html)",
1931 ]
1932 )
1933
1934 decision = repairer.handle_empty_response(
1935 task="Create a multi-file nginx guide.",
1936 original_task=None,
1937 empty_retry_count=1,
1938 max_empty_retries=2,
1939 dod=dod,
1940 )
1941
1942 assert decision.should_continue is True
1943 assert decision.retry_message is not None
1944 assert "Create the nginx directory structure" not in decision.retry_message
1945 assert "02-installation.html" in decision.retry_message
1946
1947
1948 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
1949 temp_dir: Path,
1950 ) -> None:
1951 context = build_context(
1952 temp_dir=temp_dir,
1953 use_react=False,
1954 )
1955 repairer = ResponseRepairer(context)
1956
1957 guide_root = temp_dir / "guides" / "nginx"
1958 chapters = guide_root / "chapters"
1959 chapters.mkdir(parents=True)
1960 index_path = guide_root / "index.html"
1961 chapter_one = chapters / "01-getting-started.html"
1962 chapter_two = chapters / "02-installation.html"
1963 chapter_three = chapters / "03-first-website.html"
1964 chapter_four = chapters / "04-configuration-basics.html"
1965 index_path.write_text("<html></html>\n")
1966 chapter_one.write_text("<h1>One</h1>\n")
1967 chapter_two.write_text("<h1>Two</h1>\n")
1968 chapter_three.write_text("<h1>Three</h1>\n")
1969
1970 implementation_plan = temp_dir / "implementation.md"
1971 implementation_plan.write_text(
1972 "\n".join(
1973 [
1974 "# Implementation Plan",
1975 "",
1976 "## File Changes",
1977 f"- `{guide_root}/`",
1978 f"- `{chapters}/`",
1979 f"- `{index_path}`",
1980 f"- `{chapter_one}`",
1981 f"- `{chapter_two}`",
1982 f"- `{chapter_three}`",
1983 f"- `{chapter_four}`",
1984 "",
1985 ]
1986 )
1987 )
1988
1989 dod = create_definition_of_done("Create a multi-file nginx guide.")
1990 dod.implementation_plan = str(implementation_plan)
1991 dod.touched_files.extend(
1992 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
1993 )
1994 dod.completed_items.extend(
1995 [
1996 "Create the directory structure for the new nginx guide",
1997 "Create the main index.html file with proper structure",
1998 ]
1999 )
2000 dod.pending_items.append("Create each chapter file in sequence")
2001
2002 decision = repairer.handle_empty_response(
2003 task="Create a multi-file nginx guide.",
2004 original_task=None,
2005 empty_retry_count=5,
2006 max_empty_retries=2,
2007 dod=dod,
2008 )
2009
2010 assert decision.should_continue is False
2011 assert decision.final_response is not None
2012 assert "retrying 4 times" in decision.final_response
2013
2014
2015 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
2016 temp_dir: Path,
2017 ) -> None:
2018 context = build_context(
2019 temp_dir=temp_dir,
2020 use_react=False,
2021 )
2022 repairer = ResponseRepairer(context)
2023
2024 guide_root = temp_dir / "guides" / "nginx"
2025 chapters = guide_root / "chapters"
2026 chapters.mkdir(parents=True)
2027 implementation_plan = temp_dir / "implementation.md"
2028 implementation_plan.write_text(
2029 "\n".join(
2030 [
2031 "# Implementation Plan",
2032 "",
2033 "## File Changes",
2034 f"- `{guide_root / 'index.html'}`",
2035 f"- `{chapters / '01-getting-started.html'}`",
2036 f"- `{chapters / '02-installation.html'}`",
2037 "",
2038 ]
2039 )
2040 )
2041
2042 dod = create_definition_of_done("Create a multi-file nginx guide.")
2043 dod.implementation_plan = str(implementation_plan)
2044 dod.touched_files.extend(
2045 [
2046 str(guide_root / "index.html"),
2047 str(chapters / "01-getting-started.html"),
2048 ]
2049 )
2050 dod.completed_items.extend(
2051 [
2052 "Create the directory structure for the new nginx guide",
2053 "Create the main index.html file with proper structure",
2054 ]
2055 )
2056 dod.pending_items.append("Create each chapter file in sequence")
2057
2058 decision = repairer.handle_empty_response(
2059 task="Create a multi-file nginx guide.",
2060 original_task=None,
2061 empty_retry_count=1,
2062 max_empty_retries=2,
2063 dod=dod,
2064 )
2065
2066 assert decision.retry_message is not None
2067 assert "Continue from the exact next step below." in decision.retry_message
2068 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
2069
2070
2071 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
2072 temp_dir: Path,
2073 ) -> None:
2074 context = build_context(
2075 temp_dir=temp_dir,
2076 use_react=False,
2077 )
2078 repairer = ResponseRepairer(context)
2079
2080 guide_root = temp_dir / "guides" / "nginx"
2081 chapters = guide_root / "chapters"
2082 chapters.mkdir(parents=True)
2083 index_path = guide_root / "index.html"
2084 chapter_one = chapters / "01-getting-started.html"
2085 chapter_two = chapters / "02-installation.html"
2086 chapter_three = chapters / "03-first-website.html"
2087 index_path.write_text("<html></html>\n")
2088 chapter_one.write_text("<h1>One</h1>\n")
2089 chapter_two.write_text("<h1>Two</h1>\n")
2090
2091 implementation_plan = temp_dir / "implementation.md"
2092 implementation_plan.write_text(
2093 "\n".join(
2094 [
2095 "# Implementation Plan",
2096 "",
2097 "## File Changes",
2098 f"- `{guide_root}/`",
2099 f"- `{chapters}/`",
2100 f"- `{index_path}`",
2101 f"- `{chapter_one}`",
2102 f"- `{chapter_two}`",
2103 f"- `{chapter_three}`",
2104 "",
2105 ]
2106 )
2107 )
2108
2109 dod = create_definition_of_done("Create a multi-file nginx guide.")
2110 dod.implementation_plan = str(implementation_plan)
2111 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2112 dod.completed_items.extend(
2113 [
2114 "Create the main index.html file with proper structure",
2115 "Link all chapters together properly",
2116 ]
2117 )
2118 dod.pending_items.append("Create each chapter file in sequence")
2119
2120 decision = repairer.handle_empty_response(
2121 task="Create a multi-file nginx guide.",
2122 original_task=None,
2123 empty_retry_count=1,
2124 max_empty_retries=2,
2125 dod=dod,
2126 )
2127
2128 assert decision.retry_message is not None
2129 assert "Link all chapters together properly" not in decision.retry_message
2130 assert "Continue from the exact next step below." in decision.retry_message
2131 assert "Resume with this exact next step:" in decision.retry_message
2132
2133
2134 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
2135 temp_dir: Path,
2136 ) -> None:
2137 context = build_context(
2138 temp_dir=temp_dir,
2139 use_react=False,
2140 )
2141 repairer = ResponseRepairer(context)
2142
2143 reference_chapters = temp_dir / "fortran" / "chapters"
2144 reference_chapters.mkdir(parents=True)
2145 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
2146
2147 guide_root = temp_dir / "guides" / "nginx"
2148 chapters = guide_root / "chapters"
2149 chapters.mkdir(parents=True)
2150 index_path = guide_root / "index.html"
2151 index_path.write_text("<html></html>\n")
2152
2153 implementation_plan = temp_dir / "implementation.md"
2154 implementation_plan.write_text(
2155 "\n".join(
2156 [
2157 "# Implementation Plan",
2158 "",
2159 "## File Changes",
2160 f"- `{guide_root}/`",
2161 f"- `{chapters}/`",
2162 f"- `{index_path}`",
2163 "",
2164 ]
2165 )
2166 )
2167
2168 dod = create_definition_of_done("Create a multi-file nginx guide.")
2169 dod.implementation_plan = str(implementation_plan)
2170 dod.touched_files.append(str(index_path))
2171 dod.pending_items.append("Write the introduction chapter")
2172 context.session.append(
2173 Message(
2174 role=Role.ASSISTANT,
2175 content="",
2176 tool_calls=[
2177 ToolCall(
2178 id="read-ref-1",
2179 name="read",
2180 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
2181 )
2182 ],
2183 )
2184 )
2185
2186 decision = repairer.handle_empty_response(
2187 task="Create a multi-file nginx guide.",
2188 original_task=None,
2189 empty_retry_count=1,
2190 max_empty_retries=2,
2191 dod=dod,
2192 )
2193
2194 assert decision.should_continue is True
2195 assert decision.retry_message is not None
2196 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
2197 assert (
2198 "Resume with this exact next step: continue `Write the introduction chapter` "
2199 "by creating `01-introduction.html`."
2200 in decision.retry_message
2201 )
2202 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
2203 assert (
2204 "It mirrors the observed filename pattern from another `chapters/` directory "
2205 "you already inspected."
2206 in decision.retry_message
2207 )