Skip to content

symfonic.capabilities.tools.results

results

Result-policy capability — compaction, offload, recall (T3.1.3).

What happens to a tool result after the tool returns used to be three unrelated shapes: a hundred-line rewriter inside the react node, a pure pricing function in core/, and a framework tool in tools/. They implement one policy — a result is kept, compacted into a deterministic stub, or offloaded once offload provably saves money, and whatever left the wire must be reachable again on the model's initiative.

Three objects state that policy:

  • :class:ResultPolicy decides, and every decision carries its reason;
  • :class:ResultPolicyService applies decisions to a transcript under the id-preservation, immutability and determinism invariants;
  • :class:RecallService brings a payload back without ever raising into the tool-invocation path.

The capability imports nothing but its own package: the provider message type and the pricing gate are both injected.

RecallService

RecallService(ledger: Any)

Fetch a recorded tool result back from a ledger, defensively.

Source code in src/symfonic/capabilities/tools/results/recall.py
def __init__(self, ledger: Any) -> None:
    self._ledger = ledger

ResultDecision dataclass

ResultDecision(disposition: ResultDisposition, reason: str)

The disposition and the one-word reason behind it.

The reason is the deliverable. "Why is this 200KB payload still on the wire?" used to require reading a hundred-line rewriter with six early returns; it is now a string.

ResultDisposition

Bases: StrEnum

What happens to one settled tool result.

ResultPolicy

ResultPolicy(settings: ResultPolicySettings, *, net_saving_gate: NetSavingGate | None = None)

Decide what happens to one settled tool result.

Source code in src/symfonic/capabilities/tools/results/policy.py
def __init__(
    self,
    settings: ResultPolicySettings,
    *,
    net_saving_gate: NetSavingGate | None = None,
) -> None:
    self._settings = settings
    self._gate = net_saving_gate

for_settings

for_settings(settings: ResultPolicySettings) -> ResultPolicy

A policy over the same gate with different settings.

Source code in src/symfonic/capabilities/tools/results/policy.py
def for_settings(self, settings: ResultPolicySettings) -> ResultPolicy:
    """A policy over the same gate with different settings."""
    return ResultPolicy(settings, net_saving_gate=self._gate)

ResultPolicyService

ResultPolicyService(policy: ResultPolicy, *, ledger: Any, transcript: Transcript)

Rewrite the wire view of a transcript according to the policy.

Source code in src/symfonic/capabilities/tools/results/service.py
def __init__(
    self, policy: ResultPolicy, *, ledger: Any, transcript: Transcript,
) -> None:
    self._policy = policy
    self._ledger = ledger
    self._transcript = transcript
    self._last: tuple[ResultRecord, ...] = ()

last_decisions property

last_decisions: tuple[ResultRecord, ...]

What the most recent :meth:apply decided, per result.

apply async

apply(messages: Sequence[Any]) -> list[Any]

Return the wire view of messages.

Returns the argument itself (identity, not a copy) when nothing can change — which is every iteration of every deployment that leaves compaction off.

Source code in src/symfonic/capabilities/tools/results/service.py
async def apply(self, messages: Sequence[Any]) -> list[Any]:
    """Return the wire view of ``messages``.

    Returns the argument itself (identity, not a copy) when nothing
    can change — which is every iteration of every deployment that
    leaves compaction off.
    """
    self._last = ()
    settings = self._policy.settings
    if not settings.enabled:
        return list(messages) if not isinstance(messages, list) else messages

    transcript = self._transcript
    result_indices = [
        i for i, m in enumerate(messages) if transcript.is_tool_result(m)
    ]
    if not result_indices:
        return list(messages) if not isinstance(messages, list) else messages

    policy = self._policy
    if settings.offload_enabled and settings.turn_boundary_index is None:
        policy = policy.for_settings(
            settings.with_turn_boundary(self._derive_boundary(messages))
        )

    protected = set(
        result_indices[-settings.keep_last_n :] if settings.keep_last_n > 0 else []
    )
    call_meta = self._call_metadata(messages)

    rewritten: list[Any] = []
    records: list[ResultRecord] = []
    for index, message in enumerate(messages):
        if not transcript.is_tool_result(message):
            rewritten.append(message)
            continue
        content = transcript.content_text(message)
        decision = policy.decide(
            index=index,
            size_chars=len(content),
            protected=index in protected,
            viewable=transcript.is_viewable(message),
        )
        call_id = transcript.call_id(message)
        # The result message's own ``name`` wins when LangChain
        # populated it; otherwise the name comes from the call that
        # asked. Neither present means the pair is unattributable,
        # and the placeholder says so rather than reading as unnamed.
        name = (
            transcript.tool_name(message)
            or call_meta.get(call_id, ("<unknown_tool>", None))[0]
        )
        if not decision.rewrites or not call_id:
            rewritten.append(message)
            records.append(
                ResultRecord(call_id, name, len(content), decision, applied=False)
            )
            continue
        stub = await self._stub_for(call_id, name, call_meta, content)
        if stub is None:
            rewritten.append(message)
            records.append(
                ResultRecord(call_id, name, len(content), decision, applied=False)
            )
            continue
        rewritten.append(transcript.restub(message, stub, name))
        records.append(
            ResultRecord(call_id, name, len(content), decision, applied=True)
        )
    self._last = tuple(records)
    return rewritten

ResultPolicySettings dataclass

ResultPolicySettings(enabled: bool = False, size_chars: int = DEFAULT_SIZE_THRESHOLD_CHARS, keep_last_n: int = 1, offload_enabled: bool = False, large_offload_threshold_chars: int = DEFAULT_LARGE_OFFLOAD_THRESHOLD_CHARS, turn_boundary_index: int | None = None, model_name: str = '', expected_conversation_depth: int = 5, expected_recall_probability: float = 0.5)

Everything the policy needs to decide, read once.

enabled defaults to False. v8.7.1 (C1): the pre-T3.1.3 fallback used to default it True, so a dropped config key silently force-enabled compaction and pointed recall stubs at an unregistered recall tool — the payload was simply gone.

with_turn_boundary

with_turn_boundary(index: int | None) -> ResultPolicySettings

A copy carrying a derived boundary, when the caller had none.

Source code in src/symfonic/capabilities/tools/results/values.py
def with_turn_boundary(self, index: int | None) -> ResultPolicySettings:
    """A copy carrying a derived boundary, when the caller had none."""
    if self.turn_boundary_index is not None or index is None:
        return self
    return ResultPolicySettings(
        enabled=self.enabled,
        size_chars=self.size_chars,
        keep_last_n=self.keep_last_n,
        offload_enabled=self.offload_enabled,
        large_offload_threshold_chars=self.large_offload_threshold_chars,
        turn_boundary_index=index,
        model_name=self.model_name,
        expected_conversation_depth=self.expected_conversation_depth,
        expected_recall_probability=self.expected_recall_probability,
    )

ResultRecord dataclass

ResultRecord(call_id: str, tool_name: str, size_chars: int, decision: ResultDecision, applied: bool = False)

One decision, attributed to the call it was made about.

Transcript

Bases: Protocol

The narrow slice of a message transcript the service reads.