- harness/es.py: append-only event store + проекции (образец — кооп-портал) - harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу - harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы - harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе (глобальный asyncio-рантайм Strands не переживает 2+ create_harness) - cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом) - 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
194 lines
7.4 KiB
Python
194 lines
7.4 KiB
Python
"""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."""
|
|
...
|