tenseleyflow/loader / 3942db8

Browse files

Nudge missing asset retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3942db8529f158d59a4e8c7d4ecb963aefbfd4a9
Parents
35c0bcb
Tree
6653dd8

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 45 0
M tests/test_tool_batches.py 53 0
src/loader/runtime/tool_batches.pymodified
@@ -358,6 +358,10 @@ class ToolBatchRunner:
358358
                     outcome.event_content,
359359
                     dod=dod,
360360
                 )
361
+                self._queue_blocked_html_asset_nudge(
362
+                    tool_call,
363
+                    outcome.event_content,
364
+                )
361365
                 self._queue_blocked_active_repair_nudge(outcome.event_content)
362366
                 self._queue_blocked_active_repair_mutation_nudge(outcome.event_content)
363367
                 self._queue_blocked_completed_artifact_scope_nudge(
@@ -1348,6 +1352,47 @@ class ToolBatchRunner:
13481352
             + verification_suffix
13491353
         )
13501354
 
1355
+    def _queue_blocked_html_asset_nudge(
1356
+        self,
1357
+        tool_call: ToolCall,
1358
+        event_content: str,
1359
+    ) -> None:
1360
+        """Make missing local asset feedback sticky for the immediate retry."""
1361
+
1362
+        if tool_call.name not in {"write", "edit", "patch"}:
1363
+            return
1364
+        if "HTML local asset references do not exist" not in event_content:
1365
+            return
1366
+
1367
+        target = str(
1368
+            tool_call.arguments.get("file_path")
1369
+            or tool_call.arguments.get("path")
1370
+            or ""
1371
+        ).strip()
1372
+        if not target:
1373
+            return
1374
+
1375
+        missing_assets = _extract_blocked_html_target_list(
1376
+            event_content,
1377
+            "Missing local asset href(s):",
1378
+        )
1379
+        asset_preview = ""
1380
+        if missing_assets:
1381
+            asset_preview = (
1382
+                " Remove or replace "
1383
+                + ", ".join(f"`{asset}`" for asset in missing_assets[:3])
1384
+                + "."
1385
+            )
1386
+
1387
+        self.context.queue_steering_message(
1388
+            f"The last HTML mutation for `{target}` was blocked, so that file was "
1389
+            "not created or updated. Retry the same file with one concrete "
1390
+            "`write`, `edit`, or `patch` call that omits missing local asset hrefs."
1391
+            f"{asset_preview} Inline necessary styling/content, or create the referenced "
1392
+            "asset before linking it. Do not resend the same `<link>` tag and do "
1393
+            "not claim completion until the blocked file write succeeds."
1394
+        )
1395
+
13511396
     def _queue_blocked_invalid_mutation_nudge(
13521397
         self,
13531398
         tool_call: ToolCall,
tests/test_tool_batches.pymodified
@@ -7781,6 +7781,59 @@ def test_tool_batch_runner_blocked_html_missing_target_after_outputs_exist_prefe
77817781
     assert "Replace broken hrefs with existing local targets or remove the broken link." in queued[0]
77827782
 
77837783
 
7784
+def test_tool_batch_runner_blocked_html_asset_nudge_retries_same_file(
7785
+    temp_dir: Path,
7786
+) -> None:
7787
+    async def assess_confidence(
7788
+        tool_name: str,
7789
+        tool_args: dict,
7790
+        context: str,
7791
+    ) -> ConfidenceAssessment:
7792
+        raise AssertionError("Confidence scoring should not run in this scenario")
7793
+
7794
+    async def verify_action(
7795
+        tool_name: str,
7796
+        tool_args: dict,
7797
+        result: str,
7798
+        expected: str = "",
7799
+    ) -> ActionVerification:
7800
+        raise AssertionError("Verification should not run in this scenario")
7801
+
7802
+    context = build_context(
7803
+        temp_dir=temp_dir,
7804
+        messages=[],
7805
+        safeguards=FakeSafeguards(),
7806
+        assess_confidence=assess_confidence,
7807
+        verify_action=verify_action,
7808
+    )
7809
+    queued: list[str] = []
7810
+    context.queue_steering_message_callback = queued.append
7811
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
7812
+    target = temp_dir / "guide" / "chapters" / "03-configuration.html"
7813
+
7814
+    runner._queue_blocked_html_asset_nudge(
7815
+        ToolCall(
7816
+            id="write-config",
7817
+            name="write",
7818
+            arguments={"file_path": str(target)},
7819
+        ),
7820
+        (
7821
+            "[Blocked - HTML local asset references do not exist] Suggestion: "
7822
+            "Use only existing local assets for non-HTML href values. "
7823
+            "Missing local asset href(s): ../styles.css. Remove the asset link, "
7824
+            "create the referenced asset first, inline the styling/content, or point "
7825
+            "the href at an existing local file."
7826
+        ),
7827
+    )
7828
+
7829
+    assert queued
7830
+    assert str(target) in queued[0]
7831
+    assert "was not created or updated" in queued[0]
7832
+    assert "Remove or replace `../styles.css`." in queued[0]
7833
+    assert "Do not resend the same `<link>` tag" in queued[0]
7834
+    assert "do not claim completion" in queued[0]
7835
+
7836
+
77847837
 @pytest.mark.asyncio
77857838
 async def test_tool_batch_runner_blocked_empty_file_path_nudges_concrete_next_artifact(
77867839
     temp_dir: Path,