Source code for litestar_mcp.agent.streaming

"""Typed SSE streaming frames and httpx2 EventSource consumer for agent execution."""

from typing import TYPE_CHECKING, Any, Literal

import httpx2
import msgspec
from litestar.response.sse import ServerSentEventMessage
from litestar.serialization import decode_json

from litestar_mcp.core.serialization import to_json

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

    from litestar.types import TypeEncodersMap

__all__ = ("TERMINAL_EVENTS", "AgentStreamFrame", "StreamEventType", "iter_agent_stream")

StreamEventType = Literal[
    "session",
    "thought",
    "delta",
    "tool_call",
    "tool_result",
    "agent_transfer",
    "ping",
    "complete",
    "error",
]
TERMINAL_EVENTS: frozenset[str] = frozenset({"complete", "error"})


[docs] class AgentStreamFrame(msgspec.Struct, kw_only=True, omit_defaults=True): """Typed SSE frame emitted during agent execution.""" event_type: StreamEventType turn_id: str seq: int = 0 agent_name: str | None = None text: str | None = None tool_name: str | None = None call_id: str | None = None payload: dict[str, Any] | None = None
[docs] def to_sse_message(self, type_encoders: "TypeEncodersMap | None" = None) -> ServerSentEventMessage: """Encode as an SSE message whose id is seq when non-zero and whose event is event_type.""" return ServerSentEventMessage( id=str(self.seq) if self.seq else None, event=self.event_type, data=to_json(self, type_encoders=type_encoders), )
[docs] @classmethod def from_sse(cls, event: httpx2.ServerSentEvent) -> "AgentStreamFrame": """Decode an SSE event, taking seq from the event id when the payload omits it.""" frame = decode_json(event.data, target_type=cls) if not frame.seq and event.id and event.id.isdigit(): return msgspec.structs.replace(frame, seq=int(event.id)) return frame
[docs] async def iter_agent_stream( response: httpx2.Response, *, max_event_size: int | None = 1048576, ) -> "AsyncIterator[AgentStreamFrame]": """Consume a streaming httpx2.Response using httpx2.EventSource and yield AgentStreamFrame objects.""" event_source = httpx2.EventSource(response, max_event_size=max_event_size) async for sse in event_source: if not sse.data: continue yield AgentStreamFrame.from_sse(sse)