Skip to content

symfonic.core.tools.registry

registry

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.

SafetyLevel

Bases: str, Enum

Safety classification for tools.

ToolCategory

Bases: str, Enum

Categories for tool classification.

ToolRegistration dataclass

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.

ToolRegistry

ToolRegistry()

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

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

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

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

freeze() -> None

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

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

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

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

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

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