Skip to content

symfonic.capabilities.tools.execution.steering

steering

Steering — what the model is told about a call that did not run (T3.1.3).

A guard that vetoes a call is not an error path: because the graph routes the tool node back to the model, the synthesised answer is the steer. Getting it wrong in either direction is expensive — a veto with no answer leaves an unanswered tool_use block, which providers reject on every later turn, and an answer with no veto runs the call the guard forbade.

The gate therefore has one invariant: calls in == results out. Every call is either admitted or answered, never neither and never both.

SteeringGate

SteeringGate(consult: Consultant | None)

Consult a guard per call and partition into admitted vs answered.

The consultant is injected rather than imported: the legacy seam builds a BeforeToolCallEvent and calls a callback manager, a test passes a lambda, and the capability needs to know about neither.

The verdict contract is structural and deliberately narrow — a mapping with action == "skip", plus content and an optional is_error. Anything else admits the call, including a consultant that raises: a broken guard must not stall dispatch.

Source code in src/symfonic/capabilities/tools/execution/steering.py
def __init__(self, consult: Consultant | None) -> None:
    self._consult = consult