Skip to content

Commit 56c7ae9

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

File tree

2 files changed

+135
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)