Source code for axio.stream

"""AgentStream: async iterator wrapper over the agent event generator."""

from __future__ import annotations

import logging
from collections.abc import AsyncGenerator

from .events import Error, Refusal, SessionEndEvent, StreamEvent, TextDelta
from .exceptions import StreamError
from .types import INCOMPLETE

logger = logging.getLogger(__name__)


[docs] class AgentStream: def __init__(self, generator: AsyncGenerator[StreamEvent, None]) -> None: self._generator = generator self._closed = False def __aiter__(self) -> AgentStream: return self async def __anext__(self) -> StreamEvent: if self._closed: raise StopAsyncIteration try: return await self._generator.__anext__() except StopAsyncIteration: self._closed = True raise
[docs] async def aclose(self) -> None: if not self._closed: self._closed = True await self._generator.aclose()
[docs] async def get_final_text(self) -> str: """Everything the turn said, and nothing about whether it finished saying it. A run ending on one of :data:`~axio.types.INCOMPLETE` returns a truncated answer that reads exactly like a whole one, because a ``str`` has nowhere to put the reason. It is logged as a warning here, and :meth:`get_session_end` carries it for a caller that needs to branch on it. ``Error`` still raises, so a broken turn is never returned as an answer. """ parts: list[str] = [] try: async for event in self: if isinstance(event, SessionEndEvent) and event.stop_reason in INCOMPLETE: logger.warning( "Returning an answer the model did not finish: the run ended on %s", event.stop_reason, ) if isinstance(event, Error): raise StreamError(str(event.exception)) from event.exception if isinstance(event, TextDelta): parts.append(event.delta) if isinstance(event, Refusal): # A refusal arrives instead of the answer, never beside it, so it is what the # turn said. Collected nowhere, run() returned an empty string for a turn that # had text the caller needed to see. parts.append(event.text) finally: await self.aclose() return "".join(parts)
[docs] async def get_session_end(self) -> SessionEndEvent: result: SessionEndEvent | None = None try: async for event in self: if isinstance(event, Error): raise StreamError(str(event.exception)) from event.exception if isinstance(event, SessionEndEvent): result = event finally: await self.aclose() if result is None: raise StreamError("Stream ended without SessionEndEvent") return result