Normalize edit content aliases
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
d328e1ce266baf94e4ac7d1000e0b0f1226131f9- Parents
-
07dbbb9 - Tree
c9f6279
d328e1c
d328e1ce266baf94e4ac7d1000e0b0f1226131f907dbbb9
c9f6279| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/executor.py
|
18 | 0 |
| M |
tests/test_permissions.py
|
32 | 0 |
src/loader/runtime/executor.pymodified@@ -78,6 +78,7 @@ class ToolExecutor: | ||
| 78 | 78 | ) -> ToolExecutionOutcome: |
| 79 | 79 | """Execute a tool call through one consistent runtime path.""" |
| 80 | 80 | |
| 81 | + tool_call = _normalize_tool_call_arguments(tool_call) | |
| 81 | 82 | self.tracer.record( |
| 82 | 83 | "tool.received", |
| 83 | 84 | tool_name=tool_call.name, |
@@ -536,3 +537,20 @@ class ToolExecutor: | ||
| 536 | 537 | if reason: |
| 537 | 538 | details.append(f"reason={reason}") |
| 538 | 539 | return "\n".join(details) |
| 540 | + | |
| 541 | + | |
| 542 | +def _normalize_tool_call_arguments(tool_call: ToolCall) -> ToolCall: | |
| 543 | + """Accept narrow, unambiguous argument aliases from local models.""" | |
| 544 | + | |
| 545 | + if tool_call.name != "edit": | |
| 546 | + return tool_call | |
| 547 | + | |
| 548 | + arguments = dict(tool_call.arguments) | |
| 549 | + if "new_string" not in arguments and "content" in arguments: | |
| 550 | + arguments["new_string"] = arguments["content"] | |
| 551 | + return ToolCall( | |
| 552 | + id=tool_call.id, | |
| 553 | + name=tool_call.name, | |
| 554 | + arguments=arguments, | |
| 555 | + ) | |
| 556 | + return tool_call | |
tests/test_permissions.pymodified@@ -207,6 +207,38 @@ async def test_allow_mode_executor_skips_prompt_for_destructive_write( | ||
| 207 | 207 | assert prompts == [] |
| 208 | 208 | |
| 209 | 209 | |
| 210 | +@pytest.mark.asyncio | |
| 211 | +async def test_executor_accepts_edit_content_alias_for_new_string( | |
| 212 | + temp_dir: Path, | |
| 213 | +) -> None: | |
| 214 | + registry = create_default_registry(temp_dir) | |
| 215 | + policy = build_permission_policy( | |
| 216 | + active_mode=PermissionMode.ALLOW, | |
| 217 | + workspace_root=temp_dir, | |
| 218 | + tool_requirements=registry.get_tool_requirements(), | |
| 219 | + ) | |
| 220 | + executor = ToolExecutor(registry, RuntimeTracer(), policy) | |
| 221 | + target = temp_dir / "guide.html" | |
| 222 | + target.write_text("<h1>Old</h1>\n") | |
| 223 | + | |
| 224 | + outcome = await executor.execute_tool_call( | |
| 225 | + ToolCall( | |
| 226 | + id="edit-1", | |
| 227 | + name="edit", | |
| 228 | + arguments={ | |
| 229 | + "file_path": str(target), | |
| 230 | + "old_string": "<h1>Old</h1>", | |
| 231 | + "content": "<h1>New</h1>", | |
| 232 | + }, | |
| 233 | + ), | |
| 234 | + source="native", | |
| 235 | + ) | |
| 236 | + | |
| 237 | + assert outcome.state == ToolExecutionState.EXECUTED | |
| 238 | + assert target.read_text() == "<h1>New</h1>\n" | |
| 239 | + assert outcome.tool_call.arguments["new_string"] == "<h1>New</h1>" | |
| 240 | + | |
| 241 | + | |
| 210 | 242 | @pytest.mark.asyncio |
| 211 | 243 | async def test_ask_rule_prompts_even_when_allow_mode(temp_dir: Path) -> None: |
| 212 | 244 | prompts: list[str] = [] |