"""Bidirectional streaming model interface. Defines the abstract interface for models that support real-time bidirectional communication with persistent connections. Unlike traditional request-response models, bidirectional models maintain an open connection for streaming audio, text, and tool interactions. Features: - Persistent connection management with connect/close lifecycle - Real-time bidirectional communication (send and receive simultaneously) - Provider-agnostic event normalization - Support for audio, text, image, and tool result streaming """ import abc import logging from collections.abc import AsyncIterable from typing import Any, NoReturn, Protocol, cast, runtime_checkable from ....models.model import Model from ....types.content import Messages from ....types.tools import ToolResultBlock, ToolSpec from ..types.content import BidiContentBlock, BidiContentDelta from ..types.events import BidiOutputEvent from .configs import AudioConfig, ConnectionConfig logger = logging.getLogger(__name__) @runtime_checkable class Restartable(Protocol): """A bidirectional model that can replace its active connection while preserving context.""" async def restart( self, system_prompt: str | None = None, tools: list[ToolSpec] | None = None, messages: Messages | None = None, **restart_kwargs: Any, ) -> None: """Replace the active connection while preserving conversation context. Args: system_prompt: System instructions for the new connection. tools: Tool specifications for the new connection. messages: Conversation history to replay when required by the provider. **restart_kwargs: Provider-specific restart options. """ ... class BidiModel(Model, abc.ABC): """Abstract base class for bidirectional streaming models. This interface defines the contract for models that support persistent streaming connections with real-time audio and text communication. Implementations handle provider-specific protocols while exposing a standardized event-based API. Attributes: model_id: Provider model identifier. usage_is_cumulative: Whether the provider reports cumulative connection token totals (True) rather than per-response deltas (False, the default when absent). Providers reporting deltas may omit it. """ usage_is_cumulative: bool @property def model_id(self) -> str: """Get the configured model identifier.""" return cast(str, self.get_config()["model_id"]) def get_connection_config(self) -> ConnectionConfig: """Get the configured reconnect timing, or an empty config if unspecified.""" return cast(ConnectionConfig, self.get_config().get("connection", {})) def structured_output(self, *args: Any, **kwargs: Any) -> NoReturn: """Raise because bidirectional models do not support structured output.""" raise NotImplementedError("structured output is not supported by bidirectional models") def stream(self, *args: Any, **kwargs: Any) -> NoReturn: """Raise because bidirectional models use their persistent streaming API.""" raise NotImplementedError("regular streaming is not supported by bidirectional models") @abc.abstractmethod # pragma: no cover async def start( self, system_prompt: str | None = None, tools: list[ToolSpec] | None = None, messages: Messages | None = None, **kwargs: Any, ) -> None: """Establish a persistent streaming connection with the model. Opens a bidirectional connection that remains active for real-time communication. The connection supports concurrent sending and receiving of events until explicitly closed. Must be called before any send() or receive() operations. Args: system_prompt: System instructions to configure model behavior. tools: Tool specifications that the model can invoke during the conversation. messages: Initial conversation history to provide context. **kwargs: Provider-specific configuration options. """ pass @abc.abstractmethod # pragma: no cover async def stop(self) -> None: """Close the streaming connection and release resources. Terminates the active bidirectional connection and cleans up any associated resources such as network connections, buffers, or background tasks. After calling close(), the model instance cannot be used until start() is called again. """ pass @abc.abstractmethod # pragma: no cover def receive(self) -> AsyncIterable[BidiOutputEvent]: """Receive streaming events from the model. Each transcript has start and stop events, with zero or more deltas between them, sharing a content_id unique within the connection. Transcript streams may interleave, and user transcripts may arrive outside response boundaries. The stream continues until the connection is closed or an error occurs. Yields: BidiOutputEvent: Standardized event objects containing audio output, transcripts, tool calls, or control signals. """ pass @abc.abstractmethod # pragma: no cover async def send( self, content: BidiContentBlock | BidiContentDelta | ToolResultBlock, ) -> None: """Send content to the model over the active connection. Transmits user input or tool results to the model during an active streaming session. Supports multiple content types including text, audio, images, and tool execution results. Can be called multiple times during a conversation. Args: content: A TextBlock, AudioDelta, ImageBlock, or ToolResultBlock. Example: ``` from strands.experimental.bidi.types import AudioDelta from strands.types.content import TextBlock from strands.types.media import ImageBlock from strands.types.tools import ToolResultBlock await model.send(TextBlock("Hello")) await model.send(AudioDelta(format="pcm", source={"bytes": audio_bytes})) await model.send(ImageBlock(format="jpeg", source={"bytes": image_bytes})) await model.send(ToolResultBlock(tool_use_id="call-1", status="success", content=[{"text": "Done"}])) ``` """ pass class ConnectionTimeoutError(Exception): """Persistent model connection timeout. Bidirectional models are often configured with a connection time limit. Bedrock Nova Sonic, for example, keeps the connection open for 8 minutes max. Upon receiving a timeout, the agent loop is configured to restart the model connection so as to create a seamless, uninterrupted experience for the user. """ def __init__(self, message: str, **restart_config: Any) -> None: """Initialize error. Args: message: Timeout message from model. **restart_config: Configure restart specific behaviors in the call to model start. """ super().__init__(message) self.restart_config = restart_config @runtime_checkable class AudioCapable(Protocol): """Protocol for models that support audio input and output.""" def get_audio_config(self) -> AudioConfig: """Get the resolved audio configuration.""" ...