Skip to content

Commit 4beba2b

Browse files
committed
feat(tools): add replace method to ToolRegistry
1 parent 45dd597 commit 4beba2b

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/strands/tools/registry.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,34 @@ def register_tool(self, tool: AgentTool) -> None:
279279
list(self.dynamic_tools.keys()),
280280
)
281281

282+
def replace(self, tool_name: str, new_tool: AgentTool) -> None:
283+
"""Replace an existing tool with a new implementation.
284+
285+
This performs an atomic swap of the tool implementation in the registry.
286+
The replacement takes effect on the next agent invocation.
287+
288+
Args:
289+
tool_name: Name of the tool to replace.
290+
new_tool: New tool implementation.
291+
292+
Raises:
293+
ValueError: If the tool doesn't exist or if names don't match.
294+
"""
295+
if tool_name not in self.registry:
296+
raise ValueError(f"Cannot replace tool '{tool_name}' - tool does not exist")
297+
298+
if new_tool.tool_name != tool_name:
299+
raise ValueError(f"Tool names must match - expected '{tool_name}', got '{new_tool.tool_name}'")
300+
301+
# Atomic replacement in main registry
302+
self.registry[tool_name] = new_tool
303+
304+
# Update dynamic_tools to match new tool's dynamic status
305+
if new_tool.is_dynamic:
306+
self.dynamic_tools[tool_name] = new_tool
307+
elif tool_name in self.dynamic_tools:
308+
del self.dynamic_tools[tool_name]
309+
282310
def get_tools_dirs(self) -> List[Path]:
283311
"""Get all tool directory paths.
284312

tests/strands/tools/test_registry.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,3 +511,112 @@ def test_validate_tool_spec_with_ref_property():
511511
assert props["ref_field"] == {"$ref": "#/$defs/SomeType"}
512512
assert "type" not in props["ref_field"]
513513
assert "description" not in props["ref_field"]
514+
515+
516+
def test_tool_registry_replace_existing_tool():
517+
"""Test replacing an existing tool."""
518+
old_tool = MagicMock()
519+
old_tool.tool_name = "my_tool"
520+
old_tool.is_dynamic = False
521+
old_tool.supports_hot_reload = False
522+
523+
new_tool = MagicMock()
524+
new_tool.tool_name = "my_tool"
525+
new_tool.is_dynamic = False
526+
527+
registry = ToolRegistry()
528+
registry.register_tool(old_tool)
529+
registry.replace("my_tool", new_tool)
530+
531+
assert registry.registry["my_tool"] == new_tool
532+
533+
534+
def test_tool_registry_replace_nonexistent_tool():
535+
"""Test replacing a tool that doesn't exist raises ValueError."""
536+
new_tool = MagicMock()
537+
new_tool.tool_name = "my_tool"
538+
539+
registry = ToolRegistry()
540+
541+
with pytest.raises(ValueError, match="Cannot replace tool 'my_tool' - tool does not exist"):
542+
registry.replace("my_tool", new_tool)
543+
544+
545+
def test_tool_registry_replace_with_different_name():
546+
"""Test replacing with different name raises ValueError."""
547+
old_tool = MagicMock()
548+
old_tool.tool_name = "old_tool"
549+
old_tool.is_dynamic = False
550+
old_tool.supports_hot_reload = False
551+
552+
new_tool = MagicMock()
553+
new_tool.tool_name = "new_tool"
554+
555+
registry = ToolRegistry()
556+
registry.register_tool(old_tool)
557+
558+
with pytest.raises(ValueError, match="Tool names must match"):
559+
registry.replace("old_tool", new_tool)
560+
561+
562+
def test_tool_registry_replace_dynamic_tool():
563+
"""Test replacing a dynamic tool updates both registries."""
564+
old_tool = MagicMock()
565+
old_tool.tool_name = "dynamic_tool"
566+
old_tool.is_dynamic = True
567+
old_tool.supports_hot_reload = True
568+
569+
new_tool = MagicMock()
570+
new_tool.tool_name = "dynamic_tool"
571+
new_tool.is_dynamic = True
572+
573+
registry = ToolRegistry()
574+
registry.register_tool(old_tool)
575+
registry.replace("dynamic_tool", new_tool)
576+
577+
assert registry.registry["dynamic_tool"] == new_tool
578+
assert registry.dynamic_tools["dynamic_tool"] == new_tool
579+
580+
581+
def test_tool_registry_replace_dynamic_with_non_dynamic():
582+
"""Test replacing a dynamic tool with non-dynamic tool removes from dynamic_tools."""
583+
old_tool = MagicMock()
584+
old_tool.tool_name = "my_tool"
585+
old_tool.is_dynamic = True
586+
old_tool.supports_hot_reload = True
587+
588+
new_tool = MagicMock()
589+
new_tool.tool_name = "my_tool"
590+
new_tool.is_dynamic = False
591+
592+
registry = ToolRegistry()
593+
registry.register_tool(old_tool)
594+
595+
assert "my_tool" in registry.dynamic_tools
596+
597+
registry.replace("my_tool", new_tool)
598+
599+
assert registry.registry["my_tool"] == new_tool
600+
assert "my_tool" not in registry.dynamic_tools
601+
602+
603+
def test_tool_registry_replace_non_dynamic_with_dynamic():
604+
"""Test replacing a non-dynamic tool with dynamic tool adds to dynamic_tools."""
605+
old_tool = MagicMock()
606+
old_tool.tool_name = "my_tool"
607+
old_tool.is_dynamic = False
608+
old_tool.supports_hot_reload = False
609+
610+
new_tool = MagicMock()
611+
new_tool.tool_name = "my_tool"
612+
new_tool.is_dynamic = True
613+
614+
registry = ToolRegistry()
615+
registry.register_tool(old_tool)
616+
617+
assert "my_tool" not in registry.dynamic_tools
618+
619+
registry.replace("my_tool", new_tool)
620+
621+
assert registry.registry["my_tool"] == new_tool
622+
assert registry.dynamic_tools["my_tool"] == new_tool

0 commit comments

Comments
 (0)