symfonic.agent.cutover.messages¶
messages ¶
Kernel messages in and out of the legacy AgentResponse.messages shape.
The outbound dump, :func:as_legacy_messages, belongs to
:mod:symfonic.agent.response_messages (CORE-414): the native compatibility
HTTP routes call it without loading cutover, and this module re-exports the
same function for the delegate. Its module docstring carries the translation
contract and the two named losses (no message id, no
response_metadata), pinned by tests/agent/cutover/
test_facade_parity.py::test_the_projection_does_not_carry_provider_message_metadata.
What stays here is the inbound direction the delegate alone needs: trimming
and converting caller history into typed kernel messages
(:func:as_kernel_history), the :class:ReplayedMessage that keeps what
Message has no field for, and the history cap.
ReplayedMessage ¶
Bases: Message
A replayed history message that keeps what Message has no field for.
Message.content is a str (RES-2), so a history message whose
LangChain content is a list of typed blocks -- an image, a document, a
provider reasoning block -- has nowhere to land. Flattening it to its
text blocks and stopping there is a silent loss at the wire: legacy
replays the caller's message verbatim, so dropping blocks hides the image
from the model. blocks keeps the list beside the flattened text, and
:func:symfonic.agent.backend.messages.to_langchain rebuilds the wire
message from it, while content stays the str readers expect.
tool_name is the same argument for a replayed ToolMessage.name:
legacy sends and reports the caller's value verbatim, None included.
Every replayed tool message is a ReplayedMessage, so its name is never
re-derived from an earlier call (CORE-20); only a tool message the kernel
produced itself is named that way, by :mod:.tool_names.
as_kernel_history ¶
Trim and convert caller history into the kernel's typed messages.
The inbound direction, and it is deliberately not the mirror image of
:func:as_legacy_messages. Three things happen, in the order
_legacy_run_impl does them:
- Trim, on the LangChain messages, with the shared function. The slice
is :func:
symfonic.agent.history_trim.pair_aware_history_slice, which the engine's compatibility wrapper delegates to — so the two routes cannot disagree about which messages survive. A second copy of Anthropic's tool-pairing rule would fail as an HTTP 400 on the next provider call rather than as a visible parity difference, which is the worst available failure mode. - Convert each survivor into the typed
:class:
~symfonic.agent.facade_types.Messagethe kernel carries. - Nothing else. No reordering and no system-message filtering: the
replayed history reaches
build_requestexactly as legacy replays it intoinvocation_state["messages"].
cap is config.agent.max_conversation_messages, threaded from the
engine rather than written down here. CORE-177 admits plain integer caps;
fresh history still uses the shared slice. Authenticated checkpoint history
is converted without trimming: its initial turn already applied the cap.
The named loss, and its exact boundary. Message has no message
id and no response_metadata, so a replayed message's two do not
survive — the same two :func:as_legacy_messages cannot invent in the
other direction. A replayed AI message's usage_metadata does survive,
on Message.usage (CORE-21), so it is reported back as legacy reports it.
None of the three reaches the model: to_langchain rebuilds the role,
content and tool calls, which is what the provider is handed. Pinned in
tests/agent/cutover/test_history_parity.py.
Block content is a different case and must not be filed under the same
claim. Message.content is a str, so a block list has no field to
land in — but it does reach the model on the legacy route, which replays
the caller's BaseMessage verbatim. Flattening it here would drop a
replayed image at the wire while the A/B taken at the wire showed it. So
the blocks are carried on :class:ReplayedMessage instead of dropped, and
content keeps the flattened text for readers that want a str.
Telemetry. :func:symfonic.agent.history_trim.log_history_trim is the
shared implementation behind the legacy body's DEBUG record, so a trim
that drops the oldest turns is as
diagnosable on this route as on that one.
Source code in src/symfonic/agent/cutover/messages.py
as_legacy_messages ¶
Dump kernel messages in the shape the legacy engine returns, tools named.
Source code in src/symfonic/agent/response_messages.py
history_cap ¶
The cap :func:as_kernel_history trims at, given what was stated.
Beside the trim it feeds rather than on the delegate that passes it: the
delegate is translation, and "what does an unstated cap mean?" is a question
about this module's own function. bool is excluded explicitly because it
is an int and max_conversation_messages=True would otherwise trim
every replayed conversation to one message.