One selected-root streamed turn over the shared conversation authority.
Both selected streaming routes (/stream and /stream/typed) open their
turn here, so ownership, attachment family, capability and budget refusals,
session issuance, publication and empty-session discard are one code path.
Each route only chooses how the admitted raw events are framed.
open_shared_turn refuses before the model is called. SharedTurn.events
yields the admitted kernel events, publishes a usable final answer when the
done event arrives (before the route can frame its terminal), drops the
error event (the run's exception follows it) and discards an issued session
nothing was published to. TurnResponse discards on every response exit,
including a response that was never iterated.
SharedTurn
SharedTurn(conversation: Any, principal: Any, invocation: Any, scope: Any = None)
One streamed turn: issue before headers, publish before the terminal.
Source code in src/symfonic/agent/fastapi/shared_turn.py
| def __init__(self, conversation: Any, principal: Any, invocation: Any, scope: Any = None):
self._conversation = conversation
self._principal = principal
self._invocation = invocation
self.scope = scope
self._prepared: Any = None
self.issued: str | None = None
self._settled = False
|
revision
property
The prepared revision, when the authority checks publication by one.
release
async
Discard an issued session no turn was published to; idempotent.
Source code in src/symfonic/agent/fastapi/shared_turn.py
| async def release(self) -> None:
"""Discard an issued session no turn was published to; idempotent."""
if self.issued is None or self._settled:
return
self._settled = True
# A client disconnect cancels this task; only the cleanup is
# shielded, so the empty session is still removed.
with anyio.CancelScope(shield=True):
try:
await self._conversation.discard_empty(self._principal, self.issued)
except Exception:
logging.getLogger(__name__).warning(
"could not discard empty conversation after unsuccessful first turn",
exc_info=True,
)
|
settle
Keep an issued session that a recorded pause now refers to.
Source code in src/symfonic/agent/fastapi/shared_turn.py
| def settle(self) -> None:
"""Keep an issued session that a recorded pause now refers to."""
self._settled = True
|
TurnResponse
TurnResponse(turn: SharedTurn, content: Any, **kwargs: Any)
Bases: EventSourceResponse
Release the turn when the response ends, iterated or not.
Source code in src/symfonic/agent/fastapi/shared_turn.py
| def __init__(self, turn: SharedTurn, content: Any, **kwargs: Any) -> None:
super().__init__(content, **kwargs)
self._turn = turn
|
open_shared_turn
async
open_shared_turn(host: Any, conversation: Any, request: Any, raw_request: Any, scope: Any, *, admit: Any = admit_native_stream) -> tuple[SharedTurn, Any, Any]
Admit one turn before the model; every refusal is mapped for streams.
Source code in src/symfonic/agent/fastapi/shared_turn.py
| async def open_shared_turn(
host: Any,
conversation: Any,
request: Any,
raw_request: Any,
scope: Any,
*,
admit: Any = admit_native_stream,
) -> tuple[SharedTurn, Any, Any]:
"""Admit one turn before the model; every refusal is mapped for streams."""
await check_budget_for_tenant(raw_request, str(scope.tenant_id))
invocation = _CODEC.decode_invocation(
request,
scope=scope,
is_admin=request_is_admin(raw_request),
principal_id=request_user_id(raw_request),
)
try:
principal = principal_of(raw_request, scope)
if invocation.attachments:
from symfonic.platform.conversation_authority import ConversationFamilyRefused
raise ConversationFamilyRefused(
"shared local conversation history admits text-only chat"
)
prepared = None
if invocation.session_id:
prepared = await conversation.prepare(
principal, invocation.session_id, invocation.query
)
agent = await host.agent_for(scope)
admission = admit(agent, str(scope.tenant_id), invocation.query)
turn = SharedTurn(conversation, principal, invocation, scope)
await turn.open(prepared)
except Exception as exc:
from symfonic.platform.host import HostClosed
if isinstance(exc, HostClosed):
exc = SymfonicAgentError(
"the host is shutting down and is composing no further agents",
code="native_stream_unavailable",
)
raise raise_mapped(exc, STREAM_POLICY) from exc
return turn, agent, admission
|