Skip to content

symfonic.cli.raw_reply

raw_reply

Observe the model's raw final reply for one symfonic chat turn.

Agent strips MEMORY_EXTRACT / GRAPH_OPERATIONS blocks in the backend, before any caller sees the text, and leaves the whitespace around a mid-text block that the legacy engine removed ("before <X/> after" -> "before after" there, "beforeafter" in legacy). Nothing downstream of Agent can recover what was cut, so the raw reply has to be read at the provider boundary.

This is an observer, not a filter: the chat model the provider returns is wrapped so that a completed ainvoke / astream records its text, and every message, tool call, usage record and exception passes through untouched. The provider object keeps its type, so family detection, cache dialect and capability probes see exactly what they saw before. Scope is one CLI turn: the patch is instance-level and undone on exit, and it is not safe to share one provider across concurrent turns.

RawReplyTap

RawReplyTap(provider: Any)

Context manager recording the last completed model reply's raw text.

Source code in src/symfonic/cli/raw_reply.py
def __init__(self, provider: Any) -> None:
    self._provider = provider
    self._previous: Any = _MISSING
    #: ``None`` until a model call completes; the legacy projection of the
    #: reply content (text blocks only) afterwards.
    self.raw: str | None = None

RawTextAgent

RawTextAgent(agent: Any, tap: RawReplyTap)

Hands PresentedAgent the raw reply in place of the kernel-cut one.

PresentedAgent applies the public legacy presentation to result.text. Feeding it the tap's raw text (when a reply was observed) is what lets it reproduce legacy whitespace around a mid-text block.

Source code in src/symfonic/cli/raw_reply.py
def __init__(self, agent: Any, tap: RawReplyTap) -> None:
    self._agent = agent
    self._tap = tap