Skip to content

symfonic.capabilities.tools.execution.preconditions

preconditions

Preconditions — may this call run at all? (T3.1.3)

Two facts the registry has recorded since v7 and nothing ever enforced: ToolRegistration.requires (the capability a tool needs) and max_calls_per_invocation (its per-invocation budget). T3.1.2 lifted both onto :class:~symfonic.capabilities.tools.values.ToolDescriptor and left them unused, naming this task as their home.

The composition rule is fail open: a check that raises admits the call. A predicate bug that silently turned into a deny-all would take every tool offline at once and look, from the transcript, exactly like a model that stopped calling tools.

CallBudget

CallBudget(descriptors: Mapping[str, ToolDescriptor])

Enforce max_calls_per_invocation, per tool, per invocation.

Counting is stateful by necessity — a budget is a fact about the invocation, not about the call — so the object is scoped to one invocation and :meth:reset starts the next one.

Source code in src/symfonic/capabilities/tools/execution/preconditions.py
def __init__(self, descriptors: Mapping[str, ToolDescriptor]) -> None:
    self._descriptors = dict(descriptors)
    self._counts: Counter[str] = Counter()

CapabilityRequirement

CapabilityRequirement(descriptors: Mapping[str, ToolDescriptor], *, available: frozenset[type] = frozenset())

Block a tool whose declared requires capability is not present.

A tool the catalogue has never heard of is admitted: "the catalogue does not know this tool" is a wiring bug and must stay distinguishable from "a precondition forbids this tool". That is T3.1.2's rule for the policy stage, applied at the execution seam.

Source code in src/symfonic/capabilities/tools/execution/preconditions.py
def __init__(
    self,
    descriptors: Mapping[str, ToolDescriptor],
    *,
    available: frozenset[type] = frozenset(),
) -> None:
    self._descriptors = dict(descriptors)
    self._available = available

Precondition

Bases: Protocol

A named check that may object to one call.

PreconditionSet

PreconditionSet(checks: Sequence[Precondition])

Run checks in order; the first objection wins.

Source code in src/symfonic/capabilities/tools/execution/preconditions.py
def __init__(self, checks: Sequence[Precondition]) -> None:
    self._checks = tuple(checks)

descriptors_by_name

descriptors_by_name(catalog: Any) -> dict[str, ToolDescriptor]

Read a :class:~symfonic.capabilities.tools.catalog.ToolCatalog (or anything iterable of descriptors) into the mapping the checks want.

Source code in src/symfonic/capabilities/tools/execution/preconditions.py
def descriptors_by_name(catalog: Any) -> dict[str, ToolDescriptor]:
    """Read a :class:`~symfonic.capabilities.tools.catalog.ToolCatalog`
    (or anything iterable of descriptors) into the mapping the checks want."""
    return {d.name: d for d in catalog}