Skip to content

symfonic.capabilities.tools.execution.binding

binding

Schema binding — the first phase of the tool lifecycle (T3.1.3).

Selection decides which tools are offered; binding turns that answer into a provider-shaped schema on the model. Three things can go wrong and each used to be an unnamed early return inside the react node:

  • the set is empty — nothing to bind;
  • the provider has no bind_tools — degrade to the bare model;
  • the provider has no tool_choice keyword — bind without the force.

Naming them is the point. "The model got no tools this turn" and "the model got its tools but the force was dropped" are different incidents, and before this task both looked identical from the outside: an unannotated model object.

BindingResult dataclass

BindingResult(model: Any, bound: bool, reason: str, forced_choice: str | None = None, degraded: bool = False, notes: tuple[str, ...] = ())

The bound model plus a stated reason for whatever happened.

SchemaBinder

SchemaBinder(*, annotate_cache: Callable[[Any, str], None] | None = None)

Bind a tool set onto a chat model, structurally.

The binder never imports a chat-model class: it calls bind_tools on whatever it is handed and reads the exceptions the provider contract documents. annotate_cache is injected because cache annotation is provider-wire-format work that belongs on the legacy side of the seam, not in a capability.

Source code in src/symfonic/capabilities/tools/execution/binding.py
def __init__(
    self, *, annotate_cache: Callable[[Any, str], None] | None = None,
) -> None:
    self._annotate_cache = annotate_cache