Writing Transports¶
A transport connects Axio to an LLM provider. Implement the
CompletionTransport protocol to add support for any API.
The protocol¶
@runtime_checkable
class CompletionTransport(Protocol):
def stream(
self,
messages: list[Message],
tools: list[Tool],
system: str,
) -> AsyncIterator[StreamEvent]: ...
Your transport must yield StreamEvent values as they arrive from the LLM.
The agent expects the stream to end with an IterationEnd event.
Minimal implementation¶
from collections.abc import AsyncIterator
from axio import CompletionTransport, Message, Tool, StreamEvent
from axio.events import TextDelta, IterationEnd
from axio.types import StopReason, Usage
class EchoTransport:
"""Transport that echoes the last user message (for testing)."""
async def stream(
self,
messages: list[Message],
tools: list[Tool],
system: str,
) -> AsyncIterator[StreamEvent]:
# Find the last user message text
last_text = ""
for msg in reversed(messages):
if msg.role == "user":
for block in msg.content:
if hasattr(block, "text"):
last_text = block.text
break
break
# Yield it back as a text delta
yield TextDelta(index=0, delta=f"Echo: {last_text}")
# Always end with IterationEnd
yield IterationEnd(
iteration=1,
stop_reason=StopReason.end_turn,
usage=Usage(input_tokens=0, output_tokens=0),
)
Event contract¶
Your transport should yield these events in order:
Content events — any mix of:
TextDeltafor text chunksReasoningDeltafor reasoning/thinking chunksToolUseStartfollowed byToolInputDeltafor tool calls
IterationEnd— exactly once at the end, with:iteration: the agent passes this, but transports can use1stop_reason:end_turn,tool_use,max_tokens, orerrorusage: token counts for this call
Tool calls¶
When the LLM wants to call a tool, yield:
yield ToolUseStart(index=0, tool_use_id="call_abc", name="my_tool")
yield ToolInputDelta(index=0, tool_use_id="call_abc", partial_json='{"arg": "value"}')
yield IterationEnd(iteration=1, stop_reason=StopReason.tool_use, usage=usage)
The agent assembles ToolInputDelta fragments into complete JSON. You can
yield multiple ToolInputDelta events for the same tool call if the API
streams the JSON incrementally.
Multiple tool calls¶
For parallel tool calls, use different index values:
yield ToolUseStart(index=0, tool_use_id="call_1", name="tool_a")
yield ToolUseStart(index=1, tool_use_id="call_2", name="tool_b")
yield ToolInputDelta(index=0, tool_use_id="call_1", partial_json='{"x": 1}')
yield ToolInputDelta(index=1, tool_use_id="call_2", partial_json='{"y": 2}')
yield IterationEnd(iteration=1, stop_reason=StopReason.tool_use, usage=usage)
Registering as a plugin¶
Add entry points to your pyproject.toml:
[project.entry-points."axio.transport"]
my_llm = "my_package:MyTransport"
Optionally provide a settings screen for the TUI:
[project.entry-points."axio.transport.settings"]
my_llm = "my_package:MySettingsScreen"
Tips¶
Stream tokens as they arrive — don’t buffer the full response.
Track token usage accurately for cost monitoring.
Handle API errors gracefully: yield
IterationEndwithstop_reason=StopReason.errorrather than letting exceptions propagate.Look at
axio-transport-openaifor a production-grade reference implementation usingaiohttpand SSE parsing.