Skip to content

symfonic.capabilities.tools.execution.executor

executor

The tool executor — the lifecycle nothing else owns (T3.1.3).

Individual phases can each answer one question. Only the executor can answer these:

  • order — preconditions, then steering, then dispatch, stated as a method you can read rather than as the order three modules happen to be called in;
  • the calls-in-equals-results-out invariant — every call yields exactly one :class:ToolOutcome, whichever phase produced it, because an unanswered tool_use block permanently bricks a provider thread;
  • retry accounting — attempts belong to the call, not to the tool;
  • containment — a raising tool becomes an error outcome the model can read and recover from, while a cancellation propagates, because a cancelled run must not be kept alive one more hop by a ToolMessage.

Dispatch is injected as a callable. The executor therefore needs no graph, no provider and no tool object, and the suite drives the whole lifecycle with lambdas.

ToolExecutor

ToolExecutor(*, preconditions: PreconditionSet | None = None, steering: SteeringGate | None = None, retries: RetrySchedule | None = None, progress: ProgressSink | None = None)

Run a batch of tool calls through the lifecycle, in order.

Source code in src/symfonic/capabilities/tools/execution/executor.py
def __init__(
    self,
    *,
    preconditions: PreconditionSet | None = None,
    steering: SteeringGate | None = None,
    retries: RetrySchedule | None = None,
    progress: ProgressSink | None = None,
) -> None:
    self._preconditions = preconditions or PreconditionSet([])
    self._steering = steering or SteeringGate(None)
    self._retries = retries or RetrySchedule()
    self._progress = progress or NullProgressSink()