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, _checkpoint_history: _CheckpointHistory | 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.
Source code in src/symfonic/agent/cutover/turn_request.py
| def 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,
_checkpoint_history: _CheckpointHistory | 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.
"""
# Deferred for the import-footprint gate, for the reason
# ``DelegationTools.delegate`` gives: a delegation needs this, resolving
# the facade does not.
from symfonic.kernel.contracts.run_identity import current_run_identity
identity = current_run_identity()
if _checkpoint_history is not None and type(_checkpoint_history) is not _CheckpointHistory:
raise TypeError("continuation requires authenticated checkpoint history")
return TurnRequest(
prompt=query,
append_user_message=_checkpoint_history is None,
checkpoint_continuation=_checkpoint_history is not None,
scope=scope,
history=as_kernel_history(
_checkpoint_history if _checkpoint_history is not None else list(history or ()), cap=cap
),
attachments=tuple(attachments or ()),
run_id=run_id,
root_run_id=root_run_id or (identity.root_run_id if identity else ""),
parent_run_id=parent_run_id or (identity.run_id if identity else None),
)
|