symfonic.agent.cutover.turn_request¶
turn_request ¶
The caller's half of one invocation, built once for all three projections.
Its own module rather than a third function in messages: that module is at
its line budget, and "what this turn asks" is a different responsibility from
"how a LangChain message becomes a kernel message" -- it uses that conversion
rather than being part of it.
turn_request ¶
turn_request(query: str, scope: Any, history: Sequence[Any] | None, attachments: Sequence[Any] | None, *, cap: int, run_id: str = '', root_run_id: str = '', parent_run_id: str | None = None) -> TurnRequest
The caller's half of one invocation -- for every projection alike.
One function for the blocking, streaming and typed projections on purpose:
all three travel to the same PromptAssembly and are consumed by the same
build_request call, so a divergence between them would be a difference
in what the model is asked, invisible in the answer a scripted model
returns.
history is converted; attachments are not, and the asymmetry is a
fact about the types rather than an omission. The facade takes history
as list[BaseMessage] and the kernel carries typed
:class:~symfonic.agent.facade_types.Message values, so a converter has to
exist and :func:as_kernel_history is it -- it also owns the trim, because
the trim operates on the LangChain messages. The facade takes
attachments as Sequence[ContentPart] and ContentPart is exactly
what symfonic.agent._content_blocks._build_human_content accepts at the
far end of the kernel's chain, which is the same function the legacy body
calls. Converting them would mean re-encoding a value into the shape it
already has, and every such round trip is a place a provider-specific block
can lose a field.
Moved here from delegate when that module went over its line budget, for
the reason as_kernel_history already lives here: it is message
translation.
run_id is the caller's, and leaving it empty is not a neutral
default. runner.py builds RequestContext(plan, run_id=request.run_id
or None), so an empty field makes the kernel mint an id of its own -- and
the turn then has two: the one AgentResponse.run_id echoes back to the
caller, and an internal one that nothing else reports but that the
delegation lineage handed to children is built from. A child correlated to
an id its parent does not answer to is not correlated (issue #131).
Lineage is inherited from the ambient run when the caller names none
(issue #131, K2). A SymfonicAgent used as a sub-agent -- the composition
every example writes, SubAgent(agent=SymfonicAgent(...)) -- cannot be
told its ancestry: SymfonicAgent.run ends in a **state_overrides
catch-all, which accepts_run_lineage correctly refuses to send lineage
to because that catch-all raises RetiredArgumentError on unknown keys.
So the roster withholds it and the child arrives with nothing, a root run
correlated by no mechanism at all.
Reading it from the context the child is already running inside closes that
without widening a signature the TA8.18 retirement is narrowing. The cost is
named rather than hidden: this is context inheritance, so an agent an
adopter invokes from their own tool mid-turn also inherits the enclosing
run's lineage. A turn with no enclosing run stays a root -- identity is
None and both fields stay empty, which is what RunObservation's
request.root_run_id or context.run_id already expects.
An explicit argument always wins over the ambient one, so a caller that knows better is never overridden by the context it happens to run in.