Skip to content

symfonic.capabilities.tools.catalog

catalog

The tool catalogue โ€” one reading of the registry, shared by everyone.

Before T3.1.2 the same facts were derived in four places: the engine built manifest lines from tool objects, the registry held policy metadata, the react node rebuilt name tuples for rewrite gating, and the prompt builders re-read descriptions. Four derivations of one fact drift independently, and the failure mode is silent โ€” a tool the model is told it can call but which policy will never bind.

The catalogue is that reading, done once. It is immutable: build a new one when the registry changes (it is frozen at compile() anyway).

ToolCatalog

ToolCatalog(descriptors: Sequence[ToolDescriptor])

An ordered, immutable set of :class:ToolDescriptor readings.

Source code in src/symfonic/capabilities/tools/catalog.py
def __init__(self, descriptors: Sequence[ToolDescriptor]) -> None:
    by_name: dict[str, ToolDescriptor] = {}
    for descriptor in descriptors:
        if descriptor.name in by_name:
            raise ValueError(
                f"duplicate tool name {descriptor.name!r} in the catalogue; "
                "two tools cannot share a name (rename one with "
                "@symfonic_tool(name=...))"
            )
        by_name[descriptor.name] = descriptor
    self._descriptors: tuple[ToolDescriptor, ...] = tuple(descriptors)
    self._by_name = by_name

from_registry classmethod

from_registry(registry: ToolRegistryLike) -> ToolCatalog

Read a registry, lifting each tool's registration metadata.

Source code in src/symfonic/capabilities/tools/catalog.py
@classmethod
def from_registry(cls, registry: ToolRegistryLike) -> ToolCatalog:
    """Read a registry, lifting each tool's registration metadata."""
    descriptors: list[ToolDescriptor] = []
    for tool in registry.all_tools():
        name = getattr(tool, "name", None)
        registration = (
            registry.get_registration(name) if isinstance(name, str) else None
        )
        descriptors.append(
            ToolDescriptor.from_tool(tool, registration=registration)
        )
    return cls(descriptors)

from_tools classmethod

from_tools(tools: Iterable[Any]) -> ToolCatalog

Read a bare sequence of tool objects (no registry metadata).

Source code in src/symfonic/capabilities/tools/catalog.py
@classmethod
def from_tools(cls, tools: Iterable[Any]) -> ToolCatalog:
    """Read a bare sequence of tool objects (no registry metadata)."""
    return cls([ToolDescriptor.from_tool(tool) for tool in tools])

manifest_entries

manifest_entries() -> tuple[str, ...]

The agent-facing manifest: name: summary per visible tool.

This is the single source the system prompt renders. A tool hidden by any of the three visibility signals never appears, so a deliberately-hidden tool cannot leak into a rendered prompt.

Source code in src/symfonic/capabilities/tools/catalog.py
def manifest_entries(self) -> tuple[str, ...]:
    """The agent-facing manifest: ``name: summary`` per visible tool.

    This is the single source the system prompt renders. A tool
    hidden by any of the three visibility signals never appears, so a
    deliberately-hidden tool cannot leak into a rendered prompt.
    """
    return tuple(d.manifest_entry for d in self.visible_descriptors())

resolve

resolve(names: Iterable[str]) -> tuple[Any, ...]

Return the live tools for names, in catalogue order.

Unknown names are dropped rather than raising: the caller is usually a router repeating a name from somewhere less trusted than the registry.

Source code in src/symfonic/capabilities/tools/catalog.py
def resolve(self, names: Iterable[str]) -> tuple[Any, ...]:
    """Return the live tools for ``names``, in *catalogue* order.

    Unknown names are dropped rather than raising: the caller is
    usually a router repeating a name from somewhere less trusted
    than the registry.
    """
    wanted = {name for name in names}
    return tuple(d.tool for d in self._descriptors if d.name in wanted)

subset

subset(names: Iterable[str]) -> ToolCatalog

A catalogue holding only names, in catalogue order.

Source code in src/symfonic/capabilities/tools/catalog.py
def subset(self, names: Iterable[str]) -> ToolCatalog:
    """A catalogue holding only ``names``, in catalogue order."""
    wanted = {name for name in names}
    return ToolCatalog([d for d in self._descriptors if d.name in wanted])

ToolRegistryLike

Bases: Protocol

The narrow slice of a tool registry the catalogue reads.

Structural on purpose: the capability layer may not import the legacy symfonic.core.tools.registry module, and a registry that answers these two questions is a valid source whatever else it is.