ToolRegistry — tool registration, metadata, filtering, freeze.
Per ADR-PFX-009 and TDD §2.6: Registry frozen at compile(),
tools filtered by capability, category, safety, and cost.
@symfonic_tool (Roadmap Item 6) is now GA: tools produced by the
decorator carry a _symfonic_metadata attribute, and register
lifts that metadata into the corresponding ToolRegistration fields.
The former experimental_tool_decorator gate has been retired — the
decorator ships stable and no flag is required. set_experimental_tool_decorator
survives as a deprecated no-op for backward compatibility.
Bases: str, Enum
Safety classification for tools.
Bases: str, Enum
Categories for tool classification.
ToolRegistration(
tool: BaseTool,
category: ToolCategory = ToolCategory.CUSTOM,
safety_level: SafetyLevel = SafetyLevel.RESTRICTED,
max_calls_per_invocation: int | None = None,
visible_to_agents: bool = True,
cost_usd: float | None = None,
requires: type | None = None,
)
Metadata for a registered tool.
Tool registration, filtering, cost estimation, and freeze.
Frozen automatically by AgentGraph.compile(). After freeze,
register() raises RuntimeError.
Source code in src/symfonic/core/tools/registry.py
| def __init__(self) -> None:
self._registrations: dict[str, ToolRegistration] = {}
self._frozen: bool = False
|
all_tools() -> list[BaseTool]
Return all registered tools.
Source code in src/symfonic/core/tools/registry.py
| def all_tools(self) -> list[BaseTool]:
"""Return all registered tools."""
return [r.tool for r in self._registrations.values()]
|
cost_usd_for_invocation(
tool_names: list[str],
) -> float | None
Calculate total cost for a set of tool invocations.
Returns None if any tool has unknown cost.
Source code in src/symfonic/core/tools/registry.py
| def cost_usd_for_invocation(self, tool_names: list[str]) -> float | None:
"""Calculate total cost for a set of tool invocations.
Returns None if any tool has unknown cost.
"""
total = 0.0
for name in tool_names:
reg = self._registrations.get(name)
if reg is None or reg.cost_usd is None:
return None
total += reg.cost_usd
return total
|
filter(
allowed_names: list[str] | None = None,
categories: list[ToolCategory] | None = None,
max_safety_level: SafetyLevel = SafetyLevel.DANGEROUS,
agent_visible_only: bool = False,
available_capabilities: set[type] | None = None,
debug: bool = False,
) -> list[BaseTool]
Filter tools by criteria.
When debug=True, each excluded tool emits a logger.debug() message.
Source code in src/symfonic/core/tools/registry.py
| def filter(
self,
allowed_names: list[str] | None = None,
categories: list[ToolCategory] | None = None,
max_safety_level: SafetyLevel = SafetyLevel.DANGEROUS,
agent_visible_only: bool = False,
available_capabilities: set[type] | None = None,
debug: bool = False,
) -> list[BaseTool]:
"""Filter tools by criteria.
When debug=True, each excluded tool emits a logger.debug() message.
"""
result: list[BaseTool] = []
max_order = _SAFETY_ORDER[max_safety_level]
for name, reg in self._registrations.items():
# Name filter
if allowed_names is not None and name not in allowed_names:
if debug:
logger.debug("Tool '%s' excluded — not in allowed_names", name)
continue
# Category filter
if categories is not None and reg.category not in categories:
if debug:
logger.debug(
"Tool '%s' excluded — category %s not in filter",
name, reg.category,
)
continue
# Safety filter
if _SAFETY_ORDER[reg.safety_level] > max_order:
if debug:
logger.debug(
"Tool '%s' excluded — safety level %s exceeds max",
name, reg.safety_level,
)
continue
# Agent visibility filter
if agent_visible_only and not reg.visible_to_agents:
if debug:
logger.debug("Tool '%s' excluded — not visible to agents", name)
continue
# Capability filter
if (
reg.requires is not None
and available_capabilities is not None
and reg.requires not in available_capabilities
):
if debug:
logger.debug(
"Tool '%s' excluded — missing capability %s",
name,
reg.requires.__name__,
)
continue
result.append(reg.tool)
return result
|
Freeze the registry. Called automatically by AgentGraph.compile().
Source code in src/symfonic/core/tools/registry.py
| def freeze(self) -> None:
"""Freeze the registry. Called automatically by AgentGraph.compile()."""
self._frozen = True
|
get_registration(tool_name: str) -> ToolRegistration | None
Get registration metadata for a tool by name.
Source code in src/symfonic/core/tools/registry.py
| def get_registration(self, tool_name: str) -> ToolRegistration | None:
"""Get registration metadata for a tool by name."""
return self._registrations.get(tool_name)
|
register(tool: BaseTool, **metadata: Any) -> ToolRegistry
Register a single tool with optional metadata. Returns self for chaining.
Raises SymfonicAgentError(code='bad_request') if tool was
produced by @symfonic_tool and the experimental flag is off.
Source code in src/symfonic/core/tools/registry.py
| def register(self, tool: BaseTool, **metadata: Any) -> ToolRegistry:
"""Register a single tool with optional metadata. Returns self for chaining.
Raises ``SymfonicAgentError(code='bad_request')`` if ``tool`` was
produced by ``@symfonic_tool`` and the experimental flag is off.
"""
self._assert_mutable()
lifted = self._apply_symfonic_metadata(tool, metadata)
reg = ToolRegistration(tool=tool, **lifted)
self._registrations[tool.name] = reg
return self
|
register_many(tools: list[BaseTool]) -> ToolRegistry
Register multiple tools with default metadata. Returns self for chaining.
Source code in src/symfonic/core/tools/registry.py
| def register_many(self, tools: list[BaseTool]) -> ToolRegistry:
"""Register multiple tools with default metadata. Returns self for chaining."""
for tool in tools:
self.register(tool)
return self
|
set_experimental_tool_decorator(enabled: bool) -> None
Deprecated no-op. @symfonic_tool is GA and needs no gate.
Retained so existing engine wiring and adopter code that toggled
the former experimental gate keep working. The enabled value
is ignored — decorator-produced tools always register.
Source code in src/symfonic/core/tools/registry.py
| def set_experimental_tool_decorator(self, enabled: bool) -> None:
"""Deprecated no-op. ``@symfonic_tool`` is GA and needs no gate.
Retained so existing engine wiring and adopter code that toggled
the former experimental gate keep working. The ``enabled`` value
is ignored — decorator-produced tools always register.
"""
return None
|
tools_within_budget(
budget_usd: float, include_unknown_cost: bool = True
) -> list[BaseTool]
Return tools whose cost is within budget.
If include_unknown_cost is True, tools with no cost info are included.
Source code in src/symfonic/core/tools/registry.py
| def tools_within_budget(
self, budget_usd: float, include_unknown_cost: bool = True
) -> list[BaseTool]:
"""Return tools whose cost is within budget.
If include_unknown_cost is True, tools with no cost info are included.
"""
result: list[BaseTool] = []
for reg in self._registrations.values():
if reg.cost_usd is None:
if include_unknown_cost:
result.append(reg.tool)
elif reg.cost_usd <= budget_usd:
result.append(reg.tool)
return result
|