Steer blocked HTML link drift
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
28de433e6abc247ac57d05f93c0291212a5989c1- Parents
-
c6e1cc1 - Tree
ba57c22
28de433
28de433e6abc247ac57d05f93c0291212a5989c1c6e1cc1
ba57c22| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/tool_batches.py
|
64 | 0 |
| M |
tests/test_tool_batches.py
|
50 | 0 |
src/loader/runtime/tool_batches.pymodified@@ -285,6 +285,10 @@ class ToolBatchRunner: | ||
| 285 | 285 | outcome.event_content, |
| 286 | 286 | dod=dod, |
| 287 | 287 | ) |
| 288 | + self._queue_blocked_html_declared_target_nudge( | |
| 289 | + tool_call, | |
| 290 | + outcome.event_content, | |
| 291 | + ) | |
| 288 | 292 | self._queue_blocked_active_repair_nudge(outcome.event_content) |
| 289 | 293 | self._queue_blocked_active_repair_mutation_nudge(outcome.event_content) |
| 290 | 294 | self._queue_blocked_completed_artifact_scope_nudge( |
@@ -685,6 +689,56 @@ class ToolBatchRunner: | ||
| 685 | 689 | "Do not reopen unrelated reference materials while this concrete repair target is unresolved." |
| 686 | 690 | ) |
| 687 | 691 | |
| 692 | + def _queue_blocked_html_declared_target_nudge( | |
| 693 | + self, | |
| 694 | + tool_call: ToolCall, | |
| 695 | + event_content: str, | |
| 696 | + ) -> None: | |
| 697 | + """Steer blocked HTML graph edits back to the root-declared local targets.""" | |
| 698 | + | |
| 699 | + if tool_call.name not in {"write", "edit", "patch"}: | |
| 700 | + return | |
| 701 | + if "HTML page introduces new local targets outside the current declared artifact set" not in event_content: | |
| 702 | + return | |
| 703 | + | |
| 704 | + target = str( | |
| 705 | + tool_call.arguments.get("file_path") | |
| 706 | + or tool_call.arguments.get("path") | |
| 707 | + or "" | |
| 708 | + ).strip() | |
| 709 | + if not target: | |
| 710 | + return | |
| 711 | + | |
| 712 | + closest_targets = _extract_blocked_html_target_list( | |
| 713 | + event_content, | |
| 714 | + "Closest declared local targets include:", | |
| 715 | + ) | |
| 716 | + declared_targets = _extract_blocked_html_target_list( | |
| 717 | + event_content, | |
| 718 | + "Already-declared local targets include:", | |
| 719 | + ) | |
| 720 | + | |
| 721 | + guidance = ( | |
| 722 | + "That HTML mutation introduced sibling targets outside the current declared local-link set. " | |
| 723 | + f"Stay on `{target}`." | |
| 724 | + ) | |
| 725 | + if closest_targets: | |
| 726 | + guidance += ( | |
| 727 | + " Replace the invented hrefs with the closest declared target(s): " | |
| 728 | + + ", ".join(f"`{candidate}`" for candidate in closest_targets[:3]) | |
| 729 | + + "." | |
| 730 | + ) | |
| 731 | + elif declared_targets: | |
| 732 | + guidance += ( | |
| 733 | + " Keep local links within the declared target set, for example: " | |
| 734 | + + ", ".join(f"`{candidate}`" for candidate in declared_targets[:3]) | |
| 735 | + + "." | |
| 736 | + ) | |
| 737 | + guidance += ( | |
| 738 | + " Resend one concrete mutation for that same file now instead of rereading the reference guide." | |
| 739 | + ) | |
| 740 | + self.context.queue_steering_message(guidance) | |
| 741 | + | |
| 688 | 742 | def _queue_blocked_invalid_mutation_nudge( |
| 689 | 743 | self, |
| 690 | 744 | tool_call: ToolCall, |
@@ -1587,6 +1641,16 @@ def _invalid_mutation_call_shape(tool_name: str) -> str: | ||
| 1587 | 1641 | return f"`{tool_name}(...)`" |
| 1588 | 1642 | |
| 1589 | 1643 | |
| 1644 | +def _extract_blocked_html_target_list(event_content: str, marker: str) -> list[str]: | |
| 1645 | + if marker not in event_content: | |
| 1646 | + return [] | |
| 1647 | + tail = event_content.split(marker, 1)[1].strip() | |
| 1648 | + target_text = tail.split(". ", 1)[0].strip() | |
| 1649 | + if not target_text: | |
| 1650 | + return [] | |
| 1651 | + return [item.strip() for item in target_text.split(",") if item.strip()] | |
| 1652 | + | |
| 1653 | + | |
| 1590 | 1654 | def _resume_suffix_for_target( |
| 1591 | 1655 | target: Path, |
| 1592 | 1656 | *, |
tests/test_tool_batches.pymodified@@ -4602,6 +4602,56 @@ def test_tool_batch_runner_blocked_completed_artifact_scope_nudge_prefers_verifi | ||
| 4602 | 4602 | assert "Do not reopen earlier reference materials." in queued[0] |
| 4603 | 4603 | |
| 4604 | 4604 | |
| 4605 | +def test_tool_batch_runner_blocked_html_declared_target_nudge_uses_closest_declared_target( | |
| 4606 | + temp_dir: Path, | |
| 4607 | +) -> None: | |
| 4608 | + async def assess_confidence( | |
| 4609 | + tool_name: str, | |
| 4610 | + tool_args: dict, | |
| 4611 | + context: str, | |
| 4612 | + ) -> ConfidenceAssessment: | |
| 4613 | + raise AssertionError("Confidence scoring should be disabled in this scenario") | |
| 4614 | + | |
| 4615 | + async def verify_action( | |
| 4616 | + tool_name: str, | |
| 4617 | + tool_args: dict, | |
| 4618 | + result: str, | |
| 4619 | + expected: str = "", | |
| 4620 | + ) -> ActionVerification: | |
| 4621 | + raise AssertionError("Verification should not run in this scenario") | |
| 4622 | + | |
| 4623 | + context = build_context( | |
| 4624 | + temp_dir=temp_dir, | |
| 4625 | + messages=[], | |
| 4626 | + safeguards=FakeSafeguards(), | |
| 4627 | + assess_confidence=assess_confidence, | |
| 4628 | + verify_action=verify_action, | |
| 4629 | + ) | |
| 4630 | + queued: list[str] = [] | |
| 4631 | + context.queue_steering_message_callback = queued.append | |
| 4632 | + runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir)) | |
| 4633 | + | |
| 4634 | + runner._queue_blocked_html_declared_target_nudge( | |
| 4635 | + ToolCall( | |
| 4636 | + id="write-ch1", | |
| 4637 | + name="write", | |
| 4638 | + arguments={"file_path": str(temp_dir / "guide" / "chapters" / "01-introduction.html")}, | |
| 4639 | + ), | |
| 4640 | + ( | |
| 4641 | + "[Blocked - HTML page introduces new local targets outside the current declared artifact set] " | |
| 4642 | + "Suggestion: Keep non-root HTML pages within the root-declared local-link set and avoid " | |
| 4643 | + "introducing new sibling targets that the guide root does not declare, for example fix: 02-setup.html. " | |
| 4644 | + "Already-declared local targets include: chapters/01-introduction.html, chapters/02-installation.html, " | |
| 4645 | + "chapters/03-configuration.html. Closest declared local targets include: chapters/02-installation.html" | |
| 4646 | + ), | |
| 4647 | + ) | |
| 4648 | + | |
| 4649 | + assert queued | |
| 4650 | + assert str(temp_dir / "guide" / "chapters" / "01-introduction.html") in queued[0] | |
| 4651 | + assert "`chapters/02-installation.html`" in queued[0] | |
| 4652 | + assert "same file now" in queued[0] | |
| 4653 | + | |
| 4654 | + | |
| 4605 | 4655 | @pytest.mark.asyncio |
| 4606 | 4656 | async def test_tool_batch_runner_blocked_empty_file_path_nudges_concrete_next_artifact( |
| 4607 | 4657 | temp_dir: Path, |