tenseleyflow/loader / 4db4c36

Browse files

Preserve blocked asset retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4db4c36ee436137776091803c479fa8225cc28cc
Parents
74d4b92
Tree
d56f591

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 98 0
M tests/test_repair.py 65 0
src/loader/runtime/repair.pymodified
@@ -275,6 +275,22 @@ class ResponseRepairer:
275275
         retry_number: int,
276276
         max_empty_retries: int,
277277
     ) -> str:
278
+        blocked_asset_lines = self._blocked_html_asset_empty_retry_lines(dod)
279
+        if blocked_asset_lines:
280
+            return "\n".join(
281
+                [
282
+                    "[EMPTY ASSISTANT RESPONSE]",
283
+                    (
284
+                        "Your last response was empty "
285
+                        f"(retry {retry_number}/{max_empty_retries}) after a blocked "
286
+                        "HTML asset reference. Retry the blocked mutation directly."
287
+                    ),
288
+                    *[f"- {line}" for line in blocked_asset_lines],
289
+                    "",
290
+                    "Emit that corrected mutation tool call now. Do not return an empty response.",
291
+                ]
292
+            )
293
+
278294
         if dod is not None:
279295
             quality_repair_message = self._build_quality_repair_empty_retry_message(
280296
                 retry_number=retry_number,
@@ -392,6 +408,77 @@ class ResponseRepairer:
392408
             ]
393409
         )
394410
 
411
+    def _blocked_html_asset_empty_retry_lines(
412
+        self,
413
+        dod: DefinitionOfDone | None,
414
+    ) -> list[str]:
415
+        messages = list(getattr(self.context.session, "messages", []) or [])
416
+        for message in reversed(messages[-8:]):
417
+            content = str(getattr(message, "content", "") or "")
418
+            if "HTML local asset references do not exist" not in content:
419
+                continue
420
+            missing_assets = _extract_missing_local_asset_hrefs(content)
421
+            if not missing_assets:
422
+                continue
423
+            tool_call = self._tool_call_for_tool_result(message, messages)
424
+            target = self._tool_call_file_target(tool_call)
425
+            if not target:
426
+                preferred_target = self._preferred_retry_target(dod)
427
+                target = str(preferred_target) if preferred_target is not None else ""
428
+            if not target:
429
+                continue
430
+
431
+            asset_preview = ", ".join(f"`{asset}`" for asset in missing_assets[:3])
432
+            return [
433
+                (
434
+                    f"Last blocked HTML mutation target: "
435
+                    f"`{display_runtime_path(target)}`."
436
+                ),
437
+                f"Missing local asset href(s): {asset_preview}.",
438
+                (
439
+                    "Retry the same file, but remove the entire stylesheet/image/script "
440
+                    "tag that points at those missing hrefs, or create the referenced "
441
+                    "asset first before linking it."
442
+                ),
443
+                (
444
+                    "Prefer removing the asset link and inlining necessary styling/content "
445
+                    "unless the user explicitly requested a shared asset file."
446
+                ),
447
+                (
448
+                    "Do not include those missing href values in the next `write`, "
449
+                    "`edit`, or `patch` content."
450
+                ),
451
+            ]
452
+        return []
453
+
454
+    def _tool_call_for_tool_result(
455
+        self,
456
+        tool_result_message: object,
457
+        messages: list[object],
458
+    ) -> ToolCall | None:
459
+        result_ids = {
460
+            str(getattr(result, "tool_call_id", "") or "")
461
+            for result in getattr(tool_result_message, "tool_results", []) or []
462
+            if str(getattr(result, "tool_call_id", "") or "")
463
+        }
464
+        if not result_ids:
465
+            return None
466
+        for message in reversed(messages):
467
+            for tool_call in getattr(message, "tool_calls", []) or []:
468
+                if str(getattr(tool_call, "id", "") or "") in result_ids:
469
+                    return tool_call
470
+        return None
471
+
472
+    @staticmethod
473
+    def _tool_call_file_target(tool_call: ToolCall | None) -> str:
474
+        if tool_call is None:
475
+            return ""
476
+        return str(
477
+            tool_call.arguments.get("file_path")
478
+            or tool_call.arguments.get("path")
479
+            or ""
480
+        ).strip()
481
+
395482
     def _build_early_concrete_write_retry_message(
396483
         self,
397484
         dod: DefinitionOfDone,
@@ -2010,6 +2097,17 @@ def _repair_line_is_html_quality(line: str) -> bool:
20102097
     return repair_line_is_html_quality(line)
20112098
 
20122099
 
2100
+def _extract_missing_local_asset_hrefs(content: str) -> list[str]:
2101
+    marker = "Missing local asset href(s):"
2102
+    if marker not in content:
2103
+        return []
2104
+    tail = content.split(marker, 1)[1].strip()
2105
+    target_text = tail.split(". ", 1)[0].strip()
2106
+    if not target_text:
2107
+        return []
2108
+    return [item.strip() for item in target_text.split(",") if item.strip()]
2109
+
2110
+
20132111
 def _should_encourage_initial_version(
20142112
     *,
20152113
     target: Path,
tests/test_repair.pymodified
@@ -489,6 +489,71 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori
489489
     )
490490
 
491491
 
492
+def test_empty_response_retry_preserves_blocked_html_asset_correction(
493
+    temp_dir: Path,
494
+) -> None:
495
+    context = build_context(
496
+        temp_dir=temp_dir,
497
+        use_react=False,
498
+    )
499
+    repairer = ResponseRepairer(context)
500
+
501
+    target = temp_dir / "guides" / "nginx" / "chapters" / "07-security.html"
502
+    context.session.append(
503
+        Message(
504
+            role=Role.ASSISTANT,
505
+            content="",
506
+            tool_calls=[
507
+                ToolCall(
508
+                    id="write-security",
509
+                    name="write",
510
+                    arguments={
511
+                        "file_path": str(target),
512
+                        "content": '<link rel="stylesheet" href="../styles.css">',
513
+                    },
514
+                )
515
+            ],
516
+        )
517
+    )
518
+    context.session.append(
519
+        Message.tool_result_message(
520
+            tool_call_id="write-security",
521
+            display_content=(
522
+                "[Blocked - HTML local asset references do not exist] Suggestion: "
523
+                "Use only existing local assets for non-HTML href values. "
524
+                "Missing local asset href(s): ../styles.css. Remove the asset link, "
525
+                "create the referenced asset first, inline the styling/content, or point "
526
+                "the href at an existing local file."
527
+            ),
528
+            result_content=(
529
+                "[Blocked - HTML local asset references do not exist] Suggestion: "
530
+                "Missing local asset href(s): ../styles.css."
531
+            ),
532
+            is_error=True,
533
+        )
534
+    )
535
+
536
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
537
+    dod.pending_items.append("Create individual chapter files")
538
+
539
+    decision = repairer.handle_empty_response(
540
+        task="Create a multi-file nginx guide.",
541
+        original_task=None,
542
+        empty_retry_count=1,
543
+        max_empty_retries=2,
544
+        dod=dod,
545
+    )
546
+
547
+    assert decision.should_continue is True
548
+    assert decision.retry_message is not None
549
+    assert "after a blocked HTML asset reference" in decision.retry_message
550
+    assert f"`{display_runtime_path(target)}`" in decision.retry_message
551
+    assert "Missing local asset href(s): `../styles.css`." in decision.retry_message
552
+    assert "remove the entire stylesheet/image/script tag" in decision.retry_message
553
+    assert "Do not include those missing href values" in decision.retry_message
554
+    assert "Create individual chapter files" not in decision.retry_message
555
+
556
+
492557
 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
493558
     temp_dir: Path,
494559
 ) -> None: