Skip to content

symfonic.capabilities.tools.execution.termination

termination

Loop termination — when the tool loop stops (T3.1.3).

Three independent reasons a react loop ends, previously three unlabelled return "finish" statements in one routing function:

  • the model stopped asking for tools — the healthy ending;
  • the same call, arguments included, keeps repeating — a stuck model;
  • the same tool keeps repeating with different arguments — a paginator that will never stop, invisible to the signature cut because no signature ever repeats.

The third is opt-in, because a tool walking pages is usually making progress and cutting it would be a regression, not a rescue.

The verdict carries its reason. "The agent stopped early" with no stated cause is the hardest report an adopter can send.

LoopTerminationPolicy dataclass

LoopTerminationPolicy(window: int = 10, signature_threshold: int = 3, name_only_threshold: int | None = None)

Decide whether the tool loop should run another hop.

from_mapping classmethod

from_mapping(mapping: Mapping[str, Any] | None) -> LoopTerminationPolicy

Read settings structurally, so a state dict and a typed config reach the policy through one call site.

Source code in src/symfonic/capabilities/tools/execution/termination.py
@classmethod
def from_mapping(cls, mapping: Mapping[str, Any] | None) -> LoopTerminationPolicy:
    """Read settings structurally, so a state dict and a typed config
    reach the policy through one call site."""
    source = mapping if isinstance(mapping, Mapping) else {}
    defaults = cls()
    name_only = source.get("name_only_threshold")
    return cls(
        window=_int_or(source.get("window"), defaults.window),
        signature_threshold=_int_or(
            source.get("signature_threshold"), defaults.signature_threshold,
        ),
        name_only_threshold=name_only if isinstance(name_only, int) else None,
    )

TerminationVerdict dataclass

TerminationVerdict(terminate: bool, reason: str, detail: str = '')

Stop or continue, and why.