Skip to content

symfonic.agent.backend.conversation

conversation

The conversation and model ports, in LangChain's vocabulary.

This is the only module that speaks both languages: kernel values on one side, BaseMessage and provider chunks on the other. Keeping the translation in one place is what lets the kernel stay stdlib-pure and the provider details stay changeable.

ConversationAdapter

ConversationAdapter(provider: Any)

Builds the request and records each completed round (ConversationPort).

Source code in src/symfonic/agent/backend/conversation.py
def __init__(self, provider: Any) -> None:
    self._provider = provider

close_round

close_round(transcript: Transcript, turn: ModelTurn, requests: Sequence[ToolRequest], outcomes: Sequence[Any]) -> None

Append one round's assistant reply and any tool observations.

The final answer goes on the wire too, even though no further inference call reads it: the terminal extraction pass validates the final answer against the schema, and omitting it would make output a second, independent generation free to contradict text (RES-8).

Source code in src/symfonic/agent/backend/conversation.py
def close_round(
    self,
    transcript: Transcript,
    turn: ModelTurn,
    requests: Sequence[ToolRequest],
    outcomes: Sequence[Any],
) -> None:
    """Append one round's assistant reply and any tool observations.

    The final answer goes on the wire too, even though no further inference
    call reads it: the terminal extraction pass validates *the final answer*
    against the schema, and omitting it would make ``output`` a second,
    independent generation free to contradict ``text`` (RES-8).
    """
    transcript.typed.append(assistant_message(turn.text, tuple(outcomes)))
    if not outcomes:
        transcript.append_wire(turn.payload)
        return

    from symfonic.agent.backend.tools import remap_tool_call_ids

    call_ids = [request.call_id for request in requests]
    transcript.append_wire(remap_tool_call_ids(turn.payload, call_ids))
    for outcome in outcomes:
        observation = outcome.result if outcome.error is None else outcome.error
        transcript.typed.append(
            Message(role="tool", content=observation or "", tool_call_id=outcome.id)
        )
        transcript.wire.append(
            ToolMessage(content=observation or "", tool_call_id=outcome.id)
        )

ModelAdapter

ModelAdapter(bound: Any)

Executes the plan's model decision; never re-picks one (ModelPort).

Source code in src/symfonic/agent/backend/conversation.py
def __init__(self, bound: Any) -> None:
    self._bound = bound

StreamingRound

StreamingRound(bound: Any, transcript: Transcript)

One streaming round, aggregating chunks as it yields deltas.

Source code in src/symfonic/agent/backend/conversation.py
def __init__(self, bound: Any, transcript: Transcript) -> None:
    self._bound = bound
    self._transcript = transcript
    self._aggregate: Any = None
    self._text: list[str] = []

result

result() -> ModelTurn

The completed round, whose text is exactly the deltas that were sent.

Deriving the round's text from the emitted deltas rather than from the aggregate is what makes EVT-3's byte-for-byte equality hold in a multi-round tool run, not only in the single-round case.

Source code in src/symfonic/agent/backend/conversation.py
def result(self) -> ModelTurn:
    """The completed round, whose text is exactly the deltas that were sent.

    Deriving the round's text from the emitted deltas rather than from the
    aggregate is what makes EVT-3's byte-for-byte equality hold in a
    multi-round tool run, not only in the single-round case.
    """
    return turn_of(self._aggregate, "".join(self._text))

turn_of

turn_of(response: Any, text: str) -> ModelTurn

Project one provider message onto the kernel's ModelTurn.

Source code in src/symfonic/agent/backend/conversation.py
def turn_of(response: Any, text: str) -> ModelTurn:
    """Project one provider message onto the kernel's ``ModelTurn``."""
    requested = list(getattr(response, "tool_calls", None) or [])
    return ModelTurn(
        text=text,
        tool_requests=tuple(
            ToolRequest(
                call_id=call.get("id") or "",
                name=call.get("name", ""),
                arguments=dict(call.get("args") or {}),
            )
            for call in requested
        ),
        usage=_usage(response),
        stop_reason=normalize_stop_reason(response),
        payload=response,
    )