Protocols¶
Axio’s extensibility comes from a small set of runtime-checkable protocols and abstract base classes. Implement any of them to plug your own components into the framework. Subclassing the agent and monkey-patching are not needed.
classDiagram
direction TB
class CompletionTransport {
<<Protocol>>
+stream(messages, tools, system) AsyncIterator~StreamEvent~
}
class ContextStore {
<<ABC>>
+append(message)*
+get_history() list~Message~*
+session_id str
+fork() ContextStore
+clear()
+close()
+list_sessions() list~SessionInfo~
}
class PermissionGuard {
<<ABC>>
+check(tool, **kwargs) dict*
}
Agent --> CompletionTransport
Agent --> ContextStore
Tool --> PermissionGuard
CompletionTransport¶
The transport protocol has a single method:
from typing import Any, runtime_checkable, Protocol
from collections.abc import AsyncIterator
from axio.messages import Message
from axio import Tool, StreamEvent
@runtime_checkable
class CompletionTransport(Protocol):
def stream(
self,
messages: list[Message],
tools: list[Tool[Any]],
system: str,
) -> AsyncIterator[StreamEvent]: ...
The agent calls stream() on every iteration, passing the full conversation
history, the available tools, and the system prompt. The transport yields
StreamEvent values as they arrive from the LLM.
Available transports (each in its own installable package):
Transport |
Package |
Notes |
|---|---|---|
|
Anthropic Claude models |
|
|
OpenAI and OpenAI-compatible APIs |
|
|
Gemini Developer API |
|
|
Gemini and Anthropic on Vertex AI |
|
|
ChatGPT via OAuth |
OpenAITransport speaks two endpoints. Its api field picks between them, and
defaults to "responses" (/v1/responses). The OpenAI-compatible subclasses -
OpenAICompatibleTransport, NebiusTransport, OpenRouterTransport - set
api="chat", because compatible servers rarely implement /v1/responses.
The core axio package does not bundle any transport implementation. Install
the appropriate package for your model provider.
Every shipped transport reads its text/event-stream response through
axio-sse rather than parsing SSE by hand. axio-transport-openai and
axio-transport-codex also share axio-responses, which holds the Responses
API’s request builders and stream reader - the vocabulary those two
transports speak. Neither package is a transport on its own; install them
transitively as a transport’s dependency, not directly. See axio-sse
and axio-responses.
A transport is also where two contracts outside the protocol are honoured. Every
published provider stop reason is mapped onto a StopReason. An unmapped one
becomes StopReason.error, which the agent’s wildcard turns into a terminated
run. Provider token counts are converted into the inclusive-totals rule
described in Model Registry.
See Writing Transports for a step-by-step guide.
ContextStore¶
The context store manages conversation history. It is an abstract base class
with async methods. Only append and get_history are truly abstract.
Everything else has a working default implementation:
from axio.messages import Message
from axio import ContextStore
class MyContextStore(ContextStore):
def __init__(self) -> None:
self._messages: list[Message] = []
async def append(self, message: Message) -> None:
self._messages.append(message)
async def get_history(self) -> list[Message]:
return list(self._messages)
# Default implementations provided by ContextStore (override as needed):
# session_id - lazy UUID hex property
# clear() - raises NotImplementedError by default
# fork() - deep-copies history into a MemoryContextStore
# close() - no-op by default
# set_context_tokens(input, output) - no-op by default
# get_context_tokens() - returns (0, 0) by default
# add_context_tokens(input, output) - increments via get/set above
# list_sessions() - returns a single SessionInfo for the current session
store = MyContextStore()
assert store.session_id # auto-generated UUID hex
Built-in implementations:
MemoryContextStore(inaxio) - in-memory, with no persistence. It suits short-lived agents, tests, and prototypes.SQLiteContextStore(inaxio-context-sqlite) - persistent and SQLite-backed. It survives process restarts and supports multiple named sessions per project.
Implement your own ContextStore to back conversations with Redis, a database,
or any other storage layer.
See Context & Messages for details on the message model.
PermissionGuard¶
Guards gate tool execution. They sit between parameter validation and handler
invocation. PermissionGuard is an abstract base class (ABC) - not a
Protocol. Subclass it and implement check():
from typing import Any
from axio import PermissionGuard, Tool
class MyGuard(PermissionGuard):
async def check(self, tool: Tool[Any], **kwargs: Any) -> dict[str, Any]:
# return kwargs to allow, raise GuardError to deny
return kwargs
A guard receives the Tool object and the raw keyword arguments. It must
either return a dict of (possibly modified) kwargs to allow execution, or
raise GuardError to deny. Guards can modify the kwargs before returning them.
Tool calls are made via await guard(tool, **kwargs), which delegates to check().
The ConcurrentGuard subclass additionally wraps check() in an
asyncio.Semaphore to control parallelism.
Axio ships three built-in guards:
AllowAllGuardAlways returns kwargs unchanged. Useful as a no-op default.
DenyAllGuardAlways raises
GuardError("denied"). Useful for locked-down environments.ConcurrentGuardAbstract base that serializes (or rate-limits) concurrent
check()calls via a semaphore. Set theconcurrencyclass attribute to control parallelism (default: 1).
Multiple guards compose sequentially. Each guard’s output is passed to the next.
See Permission Guards for the full guard system and Writing Guards for a how-to guide.
Additional transport protocols¶
Beyond CompletionTransport, axio defines protocols for other AI modalities.
Some are implemented by a shipped transport, and some are interfaces waiting
for an implementation. Each section below says which.
ImageGenTransport¶
Generates images from text prompts.
from typing import runtime_checkable, Protocol
@runtime_checkable
class ImageGenTransport(Protocol):
async def generate_images(
self,
prompt: str,
*,
model: str | None = None,
n: int = 1,
) -> list[bytes]:
"""Generate ``n`` image samples for a text prompt.
Args:
prompt: Text description of the image to generate.
model: Optional model id, overriding the transport's default.
n: Number of samples to generate (default: 1).
Returns:
List of raw image bytes - PNG, JPEG or WebP, provider-defined.
"""
...
There is no size argument. Aspect ratio, resolution and the rest are
provider-specific knobs that live as extra keyword arguments on the
implementation rather than in the protocol.
Usage pattern:
async def example():
from axio.transport import ImageGenTransport
transport: ImageGenTransport = ... # your implementation
images = await transport.generate_images("a red moon", n=1)
assert len(images) == 1
assert isinstance(images[0], bytes)
Implementation status: implemented by
GoogleTransport, which declares
CompletionTransport, ImageGenTransport and VideoGenTransport together.
VideoGenTransport¶
Generates videos from text prompts, optionally seeded with an image.
from typing import runtime_checkable, Protocol
@runtime_checkable
class VideoGenTransport(Protocol):
async def generate_videos(
self,
prompt: str,
*,
model: str | None = None,
n: int = 1,
image: bytes | None = None,
duration_seconds: int | None = None,
aspect_ratio: str | None = None,
) -> list[bytes]:
"""Generate ``n`` video samples. Returns raw MP4 / WebM bytes."""
...
Implementation status: implemented by
GoogleTransport (Veo).
AudioGenTransport¶
Generates non-speech audio - music, sound effects, ambience. Distinct from
TTSTransport, which is text-to-speech.
from typing import runtime_checkable, Protocol
@runtime_checkable
class AudioGenTransport(Protocol):
async def generate_audios(
self,
prompt: str,
*,
model: str | None = None,
n: int = 1,
) -> list[bytes]:
"""Generate ``n`` audio samples. Returns raw MP3 / WAV / OGG bytes."""
...
Implementation status: Protocol-only. No package that ships with axio implements it.
TTSTransport¶
Synthesizes speech from text (text-to-speech).
from typing import runtime_checkable, Protocol
from collections.abc import AsyncIterator
@runtime_checkable
class TTSTransport(Protocol):
def synthesize(
self,
text: str,
*,
voice: str | None = None,
) -> AsyncIterator[bytes]:
"""Synthesize speech from text.
Args:
text: Text to convert to speech.
voice: Optional voice identifier (provider-specific).
Yields:
Chunks of audio data (e.g., WAV or MP3 bytes).
"""
...
Purpose: Convert agent responses to audio for voice assistants or accessibility features.
Usage pattern:
async def example():
from axio.transport import TTSTransport
transport: TTSTransport = ... # your implementation
audio_chunks = [
chunk
async for chunk in transport.synthesize("Hello world", voice="alloy")
]
audio_data = b"".join(audio_chunks)
assert isinstance(audio_data, bytes)
Implementation status: Protocol-only. No official implementation package ships with Axio. Implement your own or use a third-party package.
STTTransport¶
Transcribes audio to text (speech-to-text).
from typing import runtime_checkable, Protocol
@runtime_checkable
class STTTransport(Protocol):
async def transcribe(
self,
audio: bytes,
media_type: str = "audio/wav",
) -> str:
"""Transcribe audio to text.
Args:
audio: Raw audio bytes.
media_type: MIME type of the audio (e.g., "audio/wav", "audio/mp3").
Returns:
Transcribed text.
"""
...
Purpose: Convert user voice input to text for processing by the agent.
Usage pattern:
async def example():
from axio.transport import STTTransport
transport: STTTransport = ... # your implementation
audio_data = b"..." # raw audio bytes
text = await transport.transcribe(audio_data, media_type="audio/wav")
assert isinstance(text, str)
Implementation status: Protocol-only. No official implementation package ships with Axio. Implement your own or use a third-party package.
EmbeddingTransport¶
Generates vector embeddings from text.
from typing import runtime_checkable, Protocol
@runtime_checkable
class EmbeddingTransport(Protocol):
async def embed(
self,
texts: list[str],
) -> list[list[float]]:
"""Generate embeddings for a list of texts.
Args:
texts: List of text strings to embed.
Returns:
List of embedding vectors (one per input text).
"""
...
Purpose: Generate embeddings for RAG (retrieval-augmented generation), semantic search, or similarity comparisons.
Usage pattern:
async def example():
from axio.transport import EmbeddingTransport
transport: EmbeddingTransport = ... # your implementation
embeddings = await transport.embed(["hello", "world"])
assert len(embeddings) == 2
assert isinstance(embeddings[0], list)
assert isinstance(embeddings[0][0], float)
Implementation status: implemented by
OpenAITransport, which declares
CompletionTransport and EmbeddingTransport together and calls
/v1/embeddings with its own model.
RealtimeTransport and RealtimeSession¶
A duplex session - audio, text and tools travelling both ways at once - rather
than a request that yields a response. RealtimeTransport.connect() opens one:
from typing import Any, runtime_checkable, Protocol
from collections.abc import AsyncIterator
from axio import StreamEvent, Tool
from axio.blocks import ContentBlock
from axio.types import ToolCallID, ToolName
@runtime_checkable
class RealtimeSession(Protocol):
async def send(self, content: ContentBlock | list[ContentBlock]) -> None: ...
async def commit(self) -> None: ...
async def interrupt(self) -> None: ...
async def send_tool_result(
self, tool_use_id: ToolCallID, name: ToolName, content: str | list[ContentBlock]
) -> None: ...
def events(self) -> AsyncIterator[StreamEvent]: ...
async def close(self) -> None: ...
@runtime_checkable
class RealtimeTransport(Protocol):
async def connect(
self,
*,
system: str,
tools: list[Tool[Any]],
voice: str | None = None,
input_audio_format: str = "audio/pcm;rate=16000",
output_audio_format: str = "audio/pcm;rate=24000",
) -> RealtimeSession: ...
send_tool_result takes the tool name beside the call id because some
providers (Gemini Live) require it. OpenAI realtime ignores it. commit()
signals end-of-utterance for manual voice-activity detection. It is a no-op
under server VAD.
A session yields the realtime events described in Stream Events -
AudioOutputDelta, TranscriptDelta, SpeechStarted, SpeechStopped,
TurnComplete - alongside the ordinary content events.
Implementation status: implemented by OpenAIRealtimeTransport
(axio-transport-openai) and by GeminiLiveTransport / VertexLiveTransport
(axio-transport-google). See Realtime Audio.
Placeholder transports¶
axio.transport also ships a Dummy* implementation of each protocol -
DummyCompletionTransport, DummyImageGenTransport, DummyVideoGenTransport,
DummyAudioGenTransport, DummyTTSTransport, DummySTTTransport,
DummyEmbeddingTransport. Each one raises RuntimeError when called.
They exist so an agent prototype can be declared before a provider is chosen.
Construct the agent with a placeholder. Swap the real transport in with
agent.copy(transport=...) at runtime. A prototype that was never configured
then fails loudly at the call site, instead of quietly doing nothing.
Note: All transport protocols follow the same design principles:
Stateless: All state is passed via arguments. No state is hidden between calls.
Type-safe: Protocols are
@runtime_checkablefor isinstance checks.Composable: Multiple transports can be combined or wrapped.
To implement a custom transport, see Writing Transports.