Skip to content

symfonic.capabilities.tools.values

values

Value types for the tool catalogue and selection capability (T3.1.2).

Everything here is frozen. A descriptor is a reading of a tool, not a handle on it: two readings of the same tool compare equal, which is what lets the manifest-parity suite assert equality instead of re-deriving.

SelectionResult dataclass

SelectionResult(tools: tuple[Any, ...] = (), forced_choice: str | None = None, trace: tuple[StageRecord, ...] = ())

The tools to bind, the choice to force, and why.

StageOutcome dataclass

StageOutcome(tools: tuple[Any, ...] | None = None, forced_choice: str | None = None, sets_forced_choice: bool = False, degraded: bool = False, detail: str = '')

One stage's answer.

tools is None means abstain โ€” "no policy here, leave the set alone" โ€” and is distinct from an empty tuple, which means "policy applied and nothing survived". Collapsing the two is how a resolver that could not decide ends up handing the model zero tools.

force classmethod

force(choice: str | None, detail: str = '', *, degraded: bool = False) -> StageOutcome

Record a forced tool choice without touching the candidate set.

Source code in src/symfonic/capabilities/tools/values.py
@classmethod
def force(
    cls, choice: str | None, detail: str = "", *, degraded: bool = False,
) -> StageOutcome:
    """Record a forced tool choice without touching the candidate set."""
    return cls(
        tools=None,
        forced_choice=choice,
        sets_forced_choice=True,
        degraded=degraded,
        detail=detail,
    )

StageRecord dataclass

StageRecord(stage: str, action: str, kept: tuple[str, ...] = (), dropped: tuple[str, ...] = (), added: tuple[str, ...] = (), reinstated: tuple[str, ...] = (), forced_choice: str | None = None, detail: str = '')

What one stage did, in names, for the decision trace.

ToolDescriptor dataclass

ToolDescriptor(name: str, tool: Any = None, description: str = '', hidden: bool = False, category: Any = None, safety_level: Any = None, cost_usd: float | None = None, requires: type | None = None, max_calls_per_invocation: int | None = None, scope: str | None = None, routing_mode: str | None = None)

Everything the framework knows about one tool, read once.

tool is the live object so a selection stage can hand the same instance back to bind_tools; every other field is derived data the rest of the framework used to re-derive for itself.

manifest_entry property

manifest_entry: str

The rendered name: summary manifest line.

summary property

summary: str

The first non-empty description line.

A LangChain tool's description is its whole docstring; framework tools run to several hundred characters of Args/Returns prose, enough to blow a JIT manifest budget on one entry.

from_tool classmethod

from_tool(tool: Any, *, registration: Any = None) -> ToolDescriptor

Read a tool (and optionally its registry registration).

Hidden-ness has three independent sources and any one of them is decisive: @symfonic_tool(visibility="hidden"), the lifted metadata["visible_to_agents"] is False a vanilla LangChain tool can carry, and ToolRegistration.visible_to_agents. The pre-T3.1.2 manifest derivation saw only the first two because it never held the registration.

Source code in src/symfonic/capabilities/tools/values.py
@classmethod
def from_tool(cls, tool: Any, *, registration: Any = None) -> ToolDescriptor:
    """Read a tool (and optionally its registry registration).

    Hidden-ness has three independent sources and any one of them is
    decisive: ``@symfonic_tool(visibility="hidden")``, the lifted
    ``metadata["visible_to_agents"] is False`` a vanilla LangChain
    tool can carry, and ``ToolRegistration.visible_to_agents``. The
    pre-T3.1.2 manifest derivation saw only the first two because it
    never held the registration.
    """
    raw_name = getattr(tool, "name", None)
    name = raw_name if isinstance(raw_name, str) and raw_name else type(tool).__name__

    sym_meta = getattr(tool, "_symfonic_metadata", None)
    visibility = getattr(sym_meta, "visibility", None)
    lifted = _lifted_metadata(tool)

    hidden = (
        visibility == "hidden"
        or lifted.get("visible_to_agents") is False
        or getattr(registration, "visible_to_agents", True) is False
    )

    return cls(
        name=name,
        tool=tool,
        description=getattr(tool, "description", "") or "",
        hidden=hidden,
        category=getattr(registration, "category", None),
        safety_level=getattr(registration, "safety_level", None),
        cost_usd=getattr(registration, "cost_usd", None),
        requires=getattr(registration, "requires", None),
        max_calls_per_invocation=getattr(
            registration, "max_calls_per_invocation", None,
        ),
        scope=getattr(sym_meta, "scope", None),
        routing_mode=getattr(sym_meta, "routing_mode", None),
    )

tool_name

tool_name(tool: Any) -> str | None

Best-effort structural name read.

Returns None for an object with no usable name. Callers treat None as "cannot be gated by name" and keep the tool, because dropping it would make a naming bug look like a policy decision.

Source code in src/symfonic/capabilities/tools/values.py
def tool_name(tool: Any) -> str | None:
    """Best-effort structural name read.

    Returns ``None`` for an object with no usable ``name``. Callers treat
    ``None`` as "cannot be gated by name" and keep the tool, because
    dropping it would make a naming bug look like a policy decision.
    """
    name = getattr(tool, "name", None)
    if isinstance(name, str) and name:
        return name
    return None