axio¶
Core agent loop, protocols, messages, tools, events, and context interfaces.
Public API¶
axio - public API.
- class axio.Agent(system: 'str', transport: 'CompletionTransport', tools: 'list[Tool[Any]]' = <factory>, selector: 'ToolSelector | None' = None, max_iterations: 'int' = 50, last_iteration_message: 'Message | None' = None, *, before_turn: 'Callable[[int], Awaitable[None]] | None' = None, before_tools: 'Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]] | None' = None, providers: 'list[ToolProvider]' = <factory>, instructions: 'list[Instruction]' = <factory>, required_instructions: 'tuple[str, ...]' = (), before_turn_hooks: 'tuple[Callable[[int], Awaitable[None]], ...]' = (), before_tools_hooks: 'tuple[Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]], ...]' = (), system_contributions: 'tuple[Callable[[], str], ...]' = (), tool_guards: 'tuple[PermissionGuard, ...]' = ())[source]
Bases:
object- before_tools: Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]] | None
- before_tools_hooks: tuple[Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]], ...]
- before_turn: Callable[[int], Awaitable[None]] | None
- before_turn_hooks: tuple[Callable[[int], Awaitable[None]], ...]
- copy(**overrides: Any) Self[source]
Return a new Agent with fresh instruction state and overrides applied.
- async dispatch_tools(blocks: list[ToolUseBlock], iteration: int) list[ToolResultBlock][source]
- get_tool(name: str) Tool[Any] | None[source]
Return the dispatchable tool, including its access declaration and guards.
During a run this uses the current iteration’s selected tool set. Preflight hooks can inspect effects without accessing a second registry.
- get_tools() list[Tool[Any]][source]
Return collected tools before selection, including activated instructions.
- instructions: list[Instruction]
- last_iteration_message: Message | None
- async load_instruction(id: str) LoadedInstruction[source]
Load provider content and stage it for the next model iteration.
- max_iterations: int
- providers: list[ToolProvider]
- required_instructions: tuple[str, ...]
- async run(user_message: str, context: ContextStore) str[source]
- run_stream(user_message: str, context: ContextStore) AgentStream[source]
- selector: ToolSelector | None
- system: str
- system_contributions: tuple[Callable[[], str], ...]
- tool_guards: tuple[PermissionGuard, ...]
- tools: list[Tool[Any]]
- transport: CompletionTransport
- class axio.AgentStream(generator: AsyncGenerator[StreamEvent, None])[source]
Bases:
object- async aclose() None[source]
- async get_final_text() str[source]
Everything the turn said, and nothing about whether it finished saying it.
A run ending on one of
INCOMPLETEreturns a truncated answer that reads exactly like a whole one, because astrhas nowhere to put the reason. It is logged as a warning here, andget_session_end()carries it for a caller that needs to branch on it.Errorstill raises, so a broken turn is never returned as an answer.
- async get_session_end() SessionEndEvent[source]
- class axio.AudioOutput(index: int, data: bytes, media_type: AudioMediaType)[source]
Bases:
objectAudio content from a tool result (e.g. read_file on an audio file).
- data: bytes
- index: int
- media_type: AudioMediaType
- class axio.AudioOutputDelta(data: bytes, media_type: str = 'audio/pcm;rate=24000')[source]
Bases:
objectStreaming audio chunk from the assistant in a realtime session.
- data: bytes
- media_type: str
- class axio.BlockEnd(index: int)[source]
Bases:
objectThe content block at
indexis complete.The point at which accumulated
ToolInputDeltafragments are guaranteed to parse. Every provider marks it and axio has had no terminator for it until now.- index: int
- class axio.Citation(index: int, cited_text: str = '', title: str | None = None, url: str | None = None, source_id: str | None = None, start: int | None = None, end: int | None = None, unit: Literal['char', 'byte', 'page', 'block', 'unknown']='unknown', raw: dict[str, ~typing.Any]=<factory>)[source]
Bases:
objectA span of generated text attributed to a source.
What Anthropic’s citation shapes, OpenAI’s annotations and Google’s grounding metadata have in common.
rawkeeps the provider’s whole object for the fields that differ.- cited_text: str
- end: int | None
- index: int
- raw: dict[str, Any]
- source_id: str | None
Whatever identifies the source inside this request: a file id, a document index, a chunk id.
- start: int | None
- title: str | None
- unit: Literal['char', 'byte', 'page', 'block', 'unknown']
What
startandendcount. Stated because the providers disagree — OpenAI counts characters and Google counts bytes — so offsets from different units must never be compared.
- url: str | None
- class axio.CompletionTransport(*args, **kwargs)[source]
Bases:
Protocol- stream(messages: list[Message], tools: list[Tool[Any]], system: str) AsyncIterator[StreamEvent][source]
- class axio.ConcurrentGuard[source]
Bases:
PermissionGuard,ABCGuard with concurrency control.
Subclass and override
check().__call__acquires the semaphore then delegates tocheck(). Setconcurrencyto control parallelism (default 1 - one check at a time).- concurrency: int = 1
- class axio.ContextStore[source]
Bases:
ABC- async add_context_tokens(input_tokens: int, output_tokens: int) None[source]
- async clear() None[source]
- async close() None[source]
No-op by default.
- async fork() ContextStore[source]
Returns a MemoryContextStore deep copy by default.
- async classmethod from_context(context: ContextStore) Self[source]
- async classmethod from_history(history: list[Message]) Self[source]
Create a new ContextStore pre-populated with history.
- async get_context_tokens() tuple[int, int][source]
Returns (0, 0) by default.
- async list_sessions() list[SessionInfo][source]
List available sessions. Default: returns a single entry for the current session.
- property session_id: str
Lazy-init UUID hex; works without calling super().__init__().
- async set_context_tokens(input_tokens: int, output_tokens: int) None[source]
No-op by default; tokens are silently dropped.
- class axio.Error(exception: 'BaseException')[source]
Bases:
object- exception: BaseException
- axio.Field(description: str = '', default: Any = MISSING, ge: int | float | None = None, le: int | float | None = None) FieldInfo[source]
Annotate a handler parameter with metadata (description, default, constraints).
Usage:
async def search( query: Annotated[str, Field(description="Search query")], limit: Annotated[int, Field(default=10, ge=1, le=100)], ) -> str: ...
- class axio.FieldInfo(description: str = '', default: Any = MISSING, ge: int | float | None = None, le: int | float | None = None, strict: bool = False)[source]
Bases:
objectMetadata attached to a handler parameter via
Annotated[T, FieldInfo(...)].- default: Any = MISSING
- description: str = ''
- ge: int | float | None = None
- le: int | float | None = None
- strict: bool = False
- validate(value: Any, name: str, hint: Any) None[source]
Validate value against this FieldInfo’s constraints, raising if invalid.
- class axio.ImageOutput(index: int, data: bytes, media_type: ImageMediaType)[source]
Bases:
objectModel generated an image inline (e.g. Nano Banana / Gemini Image).
- data: bytes
- index: int
- media_type: ImageMediaType
- class axio.IterationEnd(iteration: 'int', stop_reason: 'StopReason', usage: 'Usage', raw: 'str' = '')[source]
Bases:
object- iteration: int
- raw: str
The provider’s own word for why it stopped, where it differs from what axio calls it. Kept so a caller can act on a reason this vocabulary has no name for.
- stop_reason: StopReason
- usage: Usage
- class axio.IterationStart(iteration: int, id: str | None = None, model: str | None = None)[source]
Bases:
objectOne provider request has begun.
modelis the model that actually served the turn, which need not be the one asked for. Server-side fallback, sticky routing and dated-snapshot resolution all substitute a different model at a different price. A cost lookup therefore keys off this rather than off the request.- id: str | None
- iteration: int
- model: str | None
- class axio.MemoryContextStore(history: list[Message] | None = None)[source]
Bases:
ContextStoreSimple in-memory context store. fork() returns a deep copy.
- async clear() None[source]
- async close() None[source]
No-op by default.
- async fork() MemoryContextStore[source]
Returns a MemoryContextStore deep copy by default.
- async get_context_tokens() tuple[int, int][source]
Returns (0, 0) by default.
- property session_id: str
Lazy-init UUID hex; works without calling super().__init__().
- async set_context_tokens(input_tokens: int, output_tokens: int) None[source]
No-op by default; tokens are silently dropped.
- class axio.Message(role: "Literal['user', 'assistant', 'system']", content: 'list[ContentBlock]' = <factory>)[source]
Bases:
object- content: list[ContentBlock]
- role: Literal['user', 'assistant', 'system']
- to_dict() dict[str, Any][source]
- class axio.PermissionGuard[source]
Bases:
ABCGate for tool calls. Return modified kwargs to allow, raise to deny.
Tool invokes guards via
await guard(tool, **kwargs).Guards receive the
Toolobject and the raw keyword arguments before execution. Return the (possibly modified) dict to allow; raiseGuardErrorto deny. Because guards see all inputs up front, they are also the right place for logging and auditing:class AuditGuard(PermissionGuard): async def check(self, tool: Tool, **kwargs: Any) -> dict[str, Any]: logger.info("tool=%s args=%s", tool.name, kwargs) return kwargs # always allow
See
examples/agent_swarm/agent_swarm/__main__.py(RoleGuard) for a production example.
- class axio.ProviderBlock(provider: str, kind: str, data: dict[str, Any], id: str = '')[source]
Bases:
ContentBlockOne item of the turn that only the protocol which produced it can read.
An endpoint that runs its own tools answers with items this vocabulary has no shape for: a web search it ran, a file it read, code it executed, and whatever it adds next. Where the application keeps the history rather than the provider —
store=Falseon the Responses API — the provider expects every one of those items back, in order and exactly once. Dropped, the next request is missing what the model was answering from, and reasoning continuity, tool-call association and anything built on a newer item type break with nothing said.datais the item exactly as it arrived and is never interpreted here.providernames the protocol that issued it, so it is replayed only to an endpoint that speaks the same one, andkindis the item’s own type, which is all a caller needs to render or count it.- data: dict[str, Any]
- id: str
How the provider names this item, where it names them.
- kind: str
- provider: str
- class axio.ProviderEvent(provider: str, kind: str, data: dict[str, Any], index: int | None = None)[source]
Bases:
objectA provider payload axio does not model, forwarded verbatim.
datais the provider’s own JSON object exactly as it was parsed: no renaming, no coercion, no filtering.How completely a transport forwards depends on what its stream names. A stream that names each event has a reader. That reader forwards every payload it does not interpret, so nothing is dropped. A stream with no discriminator — one shape per payload, read field by field — has no such catch. Those transports forward the parts they know how to name. A field the provider adds inside a payload reaches nobody until someone reads it.
A consumer that does not recognise
(provider, kind)ignores it.- data: dict[str, Any]
- index: int | None
The content-block or output index, where the payload carries one.
- kind: str
The provider’s own discriminator, verbatim, never a name axio invented: matching on it is matching the vocabulary the provider publishes.
- provider: str
Which transport produced it:
"anthropic","openai","google". The Codex transport reads its stream through the shared Responses reader, so its events say"openai"rather than naming a fourth provider.
- class axio.ProviderOutput(provider: str, kind: str, data: dict[str, Any], index: int, id: str = '')[source]
Bases:
objectOne output item of this turn that the next request has to send back unread.
ProviderEventforwards what a caller may want to watch; this one names what the turn is not complete without. An endpoint that keeps no history of its own —store=Falseon the Responses API — expects every item it produced back on the next request, including those from the tools it ran itself: a web search, a file search, a code interpreter, and item types that do not exist yet. Read as news rather than as content, they were watched and dropped, and the next request was missing what the model had answered from.The agent stores it as a
ProviderBlock, and the transport that speaks the same protocol replaysdataverbatim.- data: dict[str, Any]
The item exactly as it arrived, never interpreted here.
- id: str
- index: int
The output index this item occupied. Declared in the same position as on
ProviderEvent, whose first three fields these are: with the two orders crossed, one built positionally in the shape of the other assigned every field to the wrong name.
- kind: str
The item’s own type, verbatim.
- provider: str
Which protocol produced it, in the names
ProviderEventuses.
- class axio.RealtimeAgent(system: str, transport: RealtimeTransport, tools: list[Tool[Any]] = <factory>, voice: str | None = None, input_audio_format: str = 'audio/pcm;rate=16000', output_audio_format: str = 'audio/pcm;rate=24000', raise_on_error: bool = True)[source]
Bases:
objectDrives a duplex realtime session against a
RealtimeTransport.The agent intercepts
ToolUseStart/ToolInputDelta/TurnCompleteevents to assemble tool calls and dispatches them as background tasks so that streaming audio output from the provider is not blocked. Each tool result is sent back to the session viaRealtimeSession.send_tool_result()as soon as the handler returns.interrupt()cancels in-flight tasks.Lifecycle:
async with RealtimeAgent(system="...", transport=t, tools=[...]) as agent: await agent.send(AudioBlock(data=mic_chunk, media_type="audio/pcm")) async for event in agent.events(): match event: case AudioOutputDelta(data=pcm): speaker.feed(pcm) ...
- async close() None[source]
- async commit() None[source]
- async connect() None[source]
- async events() AsyncIterator[StreamEvent][source]
Yield events from the session, dispatching tool calls as side-effects.
Tool dispatch runs as background tasks — slow handlers do not block the stream of
AudioOutputDeltafrom the provider.
- input_audio_format: str
- async interrupt() None[source]
- output_audio_format: str
- raise_on_error: bool
Re-raise the exception wrapped in any
Errorevent yielded by the session. Set toFalseto receiveErrorevents as data and decide what to do per error (transient retry, log-and-continue, etc.).
- async send(content: ContentBlock | list[ContentBlock]) None[source]
- property session: RealtimeSession
- system: str
- tools: list[Tool[Any]]
- transport: RealtimeTransport
- voice: str | None
- class axio.RealtimeSession(*args, **kwargs)[source]
Bases:
ProtocolActive duplex realtime session — bidirectional audio / text / tools.
Returned by
RealtimeTransport.connect(). Events from the provider arrive onevents(). User input is pushed viasend().- async close() None[source]
Tear down the session and release resources.
- async commit() None[source]
Signal end-of-utterance for manual VAD; no-op with server VAD.
- events() AsyncIterator[StreamEvent][source]
Async iterator over server events for the lifetime of this session.
- async interrupt() None[source]
Abort in-flight assistant generation (e.g. user interrupted).
- async send(content: ContentBlock | list[ContentBlock]) None[source]
Append user content (audio chunk, text, image) to the input buffer.
- async send_tool_result(tool_use_id: ToolCallID, name: ToolName, content: str | list[ContentBlock]) None[source]
Deliver a tool’s result to the provider so generation can resume.
nameis included because some providers (e.g. Gemini Live) require the tool name alongside the call id. OpenAI realtime can ignore it.
- class axio.RealtimeTransport(*args, **kwargs)[source]
Bases:
ProtocolProvider that supports duplex realtime sessions (e.g. OpenAI Realtime, Gemini Live). Distinct from
CompletionTransportbecause the interaction is bidirectional, not request/response.- async connect(*, 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[source]
- class axio.ReasoningBlock(text: str = '', signature: str = '', redacted: bool = False, id: str = '', provider: str = '')[source]
Bases:
ContentBlockThe model’s own reasoning, kept so the turn can be sent back unaltered.
signatureis the provider’s proof that the block is its own. Anthropic refuses a returned thinking block whose signature is missing or changed, and Google reportsMISSING_THOUGHT_SIGNATUREfor the same failure. A stored turn that dropped the signature cannot be replayed. Never inspect, decode or truncate it.redactedmarks a block whose reasoning the provider withheld. The signature still has to travel, andtextis empty.- id: str
How the provider names this block, where it names them. Required beside the signature.
- provider: str
Which protocol issued
signature:"anthropic","google","openai", the same namesProviderEventuses. The value means nothing outside the protocol that made it — Anthropic sends it as a thinking signature, Google asthoughtSignature, Responses asencrypted_content— so a session that changes transport must not replay it to the next one. Empty says nobody recorded it, which is what a turn stored before this field existed looks like;proof()lets that through rather than dropping proofs it cannot judge.
- redacted: bool
- signature: str
Out of
repron purpose: this is the one field documented as never to be inspected, and a debug log of the block printed it in full beside everything else.
- text: str
- class axio.ReasoningDelta(index: 'int', delta: 'str')[source]
Bases:
object- delta: str
- index: int
- class axio.ReasoningSignature(index: int, signature: str, redacted: bool = False, id: str = '', provider: str = '')[source]
Bases:
objectOpaque proof that a reasoning block is the provider’s own, to be replayed unaltered.
Anthropic refuses a returned
thinkingblock whose signature is missing or changed, and Google publishes aMISSING_THOUGHT_SIGNATUREfinish reason for the same failure. Never inspect, decode, re-encode or truncatedata.- id: str
How the provider names the block this proves, where it names them. Replayed beside the proof, because a provider that identifies reasoning by id refuses the pair without it.
- index: int
- provider: str
Which protocol issued this proof, in the names
ProviderEventuses.
- redacted: bool
True where the payload replaces the reasoning text instead of accompanying it.
- signature: str
- class axio.Refusal(index: int, text: str = '', spoken: bool = True, category: str | None = None, blocked_input: bool = False, raw: dict[str, ~typing.Any]=<factory>)[source]
Bases:
objectThe model declined, or the provider blocked the turn.
This is deliberately not a
TextDelta. As ordinary assistant text, or as an empty turn that succeeded, a refusal is indistinguishable from an answer and no consumer can act on it.- blocked_input: bool
True where the provider rejected the prompt rather than the answer, so nothing was generated and sending the same prompt again cannot succeed.
- category: str | None
The provider’s own category, verbatim. Not normalised: the taxonomies do not overlap, and a mapping between them would state something no provider says.
- index: int
- raw: dict[str, Any]
- spoken: bool
Whether
textis the model’s own words.True on the endpoints that stream a refusal as output content, which is what OpenAI and the Responses API do. False where the text is the provider explaining why it stopped: Anthropic sends stop_details.explanation, which its own schema documents as unstable and not to be parsed, and the model generated nothing at all.
The agent stores either kind as the turn’s text, because a stored turn with no content is refused by the next request and the explanation is the only account of the decline there is. A consumer that renders the two differently — the model declining, against the provider reporting a block — reads this.
- text: str
- class axio.SessionEndEvent(stop_reason: 'StopReason', total_usage: 'Usage')[source]
Bases:
object- stop_reason: StopReason
- total_usage: Usage
- class axio.SpeechStarted[source]
Bases:
objectServer VAD detected the user started speaking (realtime).
- class axio.SpeechStopped[source]
Bases:
objectServer VAD detected the user stopped speaking (realtime).
- class axio.StopReason(*values)[source]
Bases:
StrEnumWhy the provider stopped generating.
Anything that is not
tool_useorpause_turnends the run. See the match inAgent._run_loop, whose wildcard keeps a member added here from falling through into another paid iteration.- cancelled = 'cancelled'
The caller or the provider stopped the turn before it finished.
- context_window_exceeded = 'context_window_exceeded'
The conversation outgrew the model’s window. Truncated, like
max_tokens.
- end_turn = 'end_turn'
- error = 'error'
- max_tokens = 'max_tokens'
- pause_turn = 'pause_turn'
A server-side tool loop reached its iteration limit. Resumable: the provider expects the assistant content back so it can finish. This is the one reason that does not end the run.
- refusal = 'refusal'
The model declined, or the provider blocked the turn. Not an error: the same prompt sent again will be declined again.
- repetition = 'repetition'
Axio stopped the turn, because the model was repeating itself. The only reason here the provider did not give. Reported as
end_turn, a caller could not tell an answer the model finished from one cut off mid-word, which is the same objection this vocabulary raises against reading a truncated response as a whole one.
- tool_use = 'tool_use'
- unknown = 'unknown'
The provider said something this vocabulary does not have. Terminal, and it vouches for nothing. Named rather than folded into one of the others, because each of those claims something the provider did not say: that the turn finished, that it was truncated, or that the transport broke.
IterationEnd.rawcarries the word itself.
- type axio.StreamEvent = ReasoningDelta | ReasoningSignature | TextDelta | TextSignature | Refusal | Citation | ImageOutput | AudioOutput | VideoOutput | ToolUseStart | ToolInputDelta | ToolFieldStart | ToolFieldDelta | ToolFieldEnd | ToolOutputDelta | ToolResult | BlockEnd | IterationStart | IterationEnd | Error | ProviderEvent | ProviderOutput | SessionEndEvent | AudioOutputDelta | TranscriptDelta | SpeechStarted | SpeechStopped | TurnComplete
- class axio.TextBlock(text: 'str', signature: 'str' = '', provider: 'str' = '')[source]
Bases:
ContentBlock- provider: str
Which protocol issued
signature. SeeReasoningBlock.provider.
- signature: str
Opaque proof for this text, where the provider signs a text part. Replayed with it. Kept out of
reprwith the rest of them: seeproviderbelow.
- text: str
- class axio.TextDelta(index: 'int', delta: 'str')[source]
Bases:
object- delta: str
- index: int
- class axio.TextSignature(index: int, signature: str, provider: str = '')[source]
Bases:
objectOpaque proof that a block of answer text is the provider’s own, to be replayed unaltered.
Google signs the part it issued the proof for, and answer text is one such part. The proof belongs to the text block, not to the reasoning or the call beside it: replayed on another part it proves nothing, and the turn fails with
MISSING_THOUGHT_SIGNATURE. Never inspect, decode, re-encode or truncatedata.Emitted after the text it signs, never before.
- index: int
- provider: str
Which protocol issued this proof, in the names
ProviderEventuses.
- signature: str
- class axio.Tool(name: 'ToolName', handler: 'Callable[..., Awaitable[Any]]', description: 'str' = '', guards: 'tuple[PermissionGuard, ...]' = (), concurrency: 'int | None' = None, context: 'T' = mappingproxy({}), schema: 'MappingProxyType[str, Any]' = mappingproxy({}), *, access: 'AccessDescriptor | None' = None, permission_identity: 'str' = '')[source]
Bases:
Generic- access: AccessDescriptor | None
- async call_streaming(**kwargs: Any) AsyncGenerator[tuple[str, str], None][source]
Execute handler, yielding
(key, text)chunks for streaming output.Uses
handler.stream(**kwargs)if the handler exposes one. Otherwise falls back to__call__()and yields the full result as a single("output", ...)chunk. Semaphore is held for the entire iteration.
- concurrency: int | None
- context: T
- description: str
- format_stream_result(chunks: list[tuple[float, str, str]]) str[source]
Aggregate streamed chunks into the final tool result string.
Handlers may attach a
format_stream_resultcallable for structured output (e.g. shell log records). Defaults to text concatenation.
- guards: tuple[PermissionGuard, ...]
- handler: Callable[[...], Awaitable[Any]]
- property input_schema: JSONSchema
- name: ToolName
- permission_identity: str
- schema: MappingProxyType
- property supports_streaming: bool
Handler supports streaming if it exposes a
.streamasync-generator attribute.
- class axio.ToolFieldDelta(index: 'int', tool_use_id: 'ToolCallID', key: 'str', text: 'str')[source]
Bases:
object- index: int
- key: str
- text: str
- tool_use_id: ToolCallID
- class axio.ToolFieldEnd(index: 'int', tool_use_id: 'ToolCallID', key: 'str')[source]
Bases:
object- index: int
- key: str
- tool_use_id: ToolCallID
- class axio.ToolFieldStart(index: 'int', tool_use_id: 'ToolCallID', key: 'str')[source]
Bases:
object- index: int
- key: str
- tool_use_id: ToolCallID
- class axio.ToolInputDelta(index: 'int', tool_use_id: 'ToolCallID', partial_json: 'str')[source]
Bases:
object- index: int
- partial_json: str
- tool_use_id: ToolCallID
- class axio.ToolOutputDelta(tool_use_id: 'ToolCallID', name: 'ToolName', key: 'str', delta: 'str')[source]
Bases:
object- delta: str
- key: str
- name: ToolName
- tool_use_id: ToolCallID
- class axio.ToolResult(tool_use_id: 'ToolCallID', name: 'ToolName', is_error: 'bool', content: 'str' = '', input: 'dict[str, Any]'=<factory>)[source]
Bases:
object- content: str
- input: dict[str, Any]
- is_error: bool
- name: ToolName
- tool_use_id: ToolCallID
- class axio.ToolResultBlock(tool_use_id: 'ToolCallID', content: 'str | list[TextBlock | ImageBlock | AudioBlock | VideoBlock]', is_error: 'bool' = False)[source]
Bases:
ContentBlock- content: str | list[TextBlock | ImageBlock | AudioBlock | VideoBlock]
- is_error: bool
- tool_use_id: ToolCallID
- class axio.ToolSelector(*args, **kwargs)[source]
Bases:
Protocol
- class axio.ToolUseBlock(id: 'ToolCallID', name: 'ToolName', input: 'dict[str, Any]', signature: 'str' = '', provider: 'str' = '')[source]
Bases:
ContentBlock- id: ToolCallID
- input: dict[str, Any]
- name: ToolName
- provider: str
Which protocol issued
signature. SeeReasoningBlock.provider.
- signature: str
Opaque proof for this call. Stored rather than held in the transport, which a restart empties.
- class axio.ToolUseStart(index: 'int', tool_use_id: 'ToolCallID', name: 'ToolName', signature: 'str' = '', provider: 'str' = '')[source]
Bases:
object- index: int
- name: ToolName
- provider: str
Which protocol issued
signature, in the namesProviderEventuses. Stored with the proof so a session that changes transport does not replay it to one that never made it.
- signature: str
Opaque proof that this call is the model’s own, where the provider issues one for the call rather than for the reasoning beside it. Replayed on the call itself: attached to a reasoning block instead, the call comes back unsigned and the provider refuses the turn.
- tool_use_id: ToolCallID
- class axio.TranscriptDelta(role: Literal['user', 'assistant'], delta: str)[source]
Bases:
objectLive transcript delta — server-side STT of user mic, or assistant speech transcription, depending on
role.- delta: str
- role: Literal['user', 'assistant']
- class axio.TurnComplete(stop_reason: StopReason, usage: Usage | None = None)[source]
Bases:
objectAssistant turn finished in a realtime session.
stop_reasonmay beStopReason.tool_useto signal that pending tool calls should run before the next turn starts.- stop_reason: StopReason
- usage: Usage | None
- class axio.Usage(input_tokens: int, output_tokens: int, *, cache_read_tokens: int = 0, cache_write_tokens: int = 0, reasoning_tokens: int = 0)[source]
Bases:
objectToken counts for one provider request.
The rule:
input_tokensandoutput_tokensare always inclusive grand totals, and every other field is a disjoint slice of one of them:cache_read_tokens + cache_write_tokens <= input_tokens reasoning_tokens <= output_tokens
Providers disagree about whether their own headline number already contains the slices, and they disagree in opposite directions. Anthropic counts only the tokens after the last cache breakpoint, so its cache counts have to be added. Google reports thinking beside the candidates rather than inside them. Each transport adds or does not add to satisfy the rule here, so nothing downstream has to know which provider answered.
Counts only, never money. A cached token and a written one bill at different multipliers, so a caller that wants cost multiplies these slices by its own per-model rates. A zero slice means the provider billed none of it, or reported no breakdown at all. Axio cannot tell those apart.
- property answer_tokens: int
Output that was the answer rather than reasoning.
- cache_read_tokens: int
The slice of
input_tokensserved from cache, billed at a discount.
- cache_write_tokens: int
The slice of
input_tokenswritten to cache, billed at a premium. Disjoint from the read.
- input_tokens: int
- output_tokens: int
- reasoning_tokens: int
The slice of
output_tokensspent on reasoning the caller never sees.
- classmethod reported(input_tokens: int, output_tokens: int, *, cache_read_tokens: int = 0, cache_write_tokens: int = 0, reasoning_tokens: int = 0) Usage[source]
What a provider said, held to the rule above rather than trusted to follow it.
Constructed directly, a provider that reports a slice larger than the total it belongs to fails
__post_init__, and a whole answer is lost over an accounting discrepancy. Every transport reads its provider’s numbers through here instead.A slice that outgrew its total means the total was reported without it. The total is therefore raised to hold the slices, which keeps the tokens the provider billed for; the other repair, cutting the slice down, throws them away and under-reports the cost.
- property total_tokens: int
- property uncached_input_tokens: int
Input the provider had to read in full, which is what most of the bill is.
- class axio.VideoOutput(index: int, data: bytes, media_type: VideoMediaType)[source]
Bases:
objectModel generated a video inline.
- data: bytes
- index: int
- media_type: VideoMediaType
Agent: the core agentic loop orchestrating transport, tools, and context.
- class axio.agent.Agent(system: 'str', transport: 'CompletionTransport', tools: 'list[Tool[Any]]' = <factory>, selector: 'ToolSelector | None' = None, max_iterations: 'int' = 50, last_iteration_message: 'Message | None' = None, *, before_turn: 'Callable[[int], Awaitable[None]] | None' = None, before_tools: 'Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]] | None' = None, providers: 'list[ToolProvider]' = <factory>, instructions: 'list[Instruction]' = <factory>, required_instructions: 'tuple[str, ...]' = (), before_turn_hooks: 'tuple[Callable[[int], Awaitable[None]], ...]' = (), before_tools_hooks: 'tuple[Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]], ...]' = (), system_contributions: 'tuple[Callable[[], str], ...]' = (), tool_guards: 'tuple[PermissionGuard, ...]' = ())[source]¶
Bases:
object- before_tools: Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]] | None¶
- before_tools_hooks: tuple[Callable[[list[ToolUseBlock], int], Awaitable[list[ToolResultBlock]]], ...]¶
- before_turn: Callable[[int], Awaitable[None]] | None¶
- before_turn_hooks: tuple[Callable[[int], Awaitable[None]], ...]¶
- copy(**overrides: Any) Self[source]¶
Return a new Agent with fresh instruction state and overrides applied.
- async dispatch_tools(blocks: list[ToolUseBlock], iteration: int) list[ToolResultBlock][source]¶
- get_tool(name: str) Tool[Any] | None[source]¶
Return the dispatchable tool, including its access declaration and guards.
During a run this uses the current iteration’s selected tool set. Preflight hooks can inspect effects without accessing a second registry.
- get_tools() list[Tool[Any]][source]¶
Return collected tools before selection, including activated instructions.
- instructions: list[Instruction]¶
- async load_instruction(id: str) LoadedInstruction[source]¶
Load provider content and stage it for the next model iteration.
- max_iterations: int¶
- providers: list[ToolProvider]¶
- required_instructions: tuple[str, ...]¶
- async run(user_message: str, context: ContextStore) str[source]¶
- run_stream(user_message: str, context: ContextStore) AgentStream[source]¶
- selector: ToolSelector | None¶
- system: str¶
- system_contributions: tuple[Callable[[], str], ...]¶
- tool_guards: tuple[PermissionGuard, ...]¶
- transport: CompletionTransport¶
- type axio.agent.TurnBlock = TextBlock | ReasoningBlock | ImageBlock | AudioBlock | VideoBlock | ToolUseBlock | ProviderBlock¶
What one iteration accumulates while the transport streams it.
Tool: frozen dataclass binding a handler callable to a name, guards, and concurrency.
- type axio.tool.JSONSchema = dict[str, Any]¶
- class axio.tool.Tool(name: 'ToolName', handler: 'Callable[..., Awaitable[Any]]', description: 'str' = '', guards: 'tuple[PermissionGuard, ...]' = (), concurrency: 'int | None' = None, context: 'T' = mappingproxy({}), schema: 'MappingProxyType[str, Any]' = mappingproxy({}), *, access: 'AccessDescriptor | None' = None, permission_identity: 'str' = '')[source]¶
Bases:
Generic- access: AccessDescriptor | None¶
- async call_streaming(**kwargs: Any) AsyncGenerator[tuple[str, str], None][source]¶
Execute handler, yielding
(key, text)chunks for streaming output.Uses
handler.stream(**kwargs)if the handler exposes one. Otherwise falls back to__call__()and yields the full result as a single("output", ...)chunk. Semaphore is held for the entire iteration.
- concurrency: int | None¶
- context: T¶
- description: str¶
- format_stream_result(chunks: list[tuple[float, str, str]]) str[source]¶
Aggregate streamed chunks into the final tool result string.
Handlers may attach a
format_stream_resultcallable for structured output (e.g. shell log records). Defaults to text concatenation.
- guards: tuple[PermissionGuard, ...]¶
- handler: Callable[[...], Awaitable[Any]]¶
- property input_schema: JSONSchema¶
- permission_identity: str¶
- schema: MappingProxyType¶
- property supports_streaming: bool¶
Handler supports streaming if it exposes a
.streamasync-generator attribute.
Instructions¶
axio.instructions.Instruction is an abstract base class for instructions in
general: rules, reference material, procedures, and skills. A skill is one use
of this contract. Core does not interpret skill formats or provider locations.
A provider supplies an identifier, a short summary, an optional source label,
and an asynchronous load() implementation. Reading the metadata does not load
the content. Use an identifier that distinguishes sources with the same name.
from __future__ import annotations
import asyncio
from axio.instructions import Instruction, LoadedInstruction
from axio.tool import Tool
async def convert_temperature(celsius: float) -> str:
"""Convert degrees Celsius to degrees Fahrenheit."""
return str(celsius * 9 / 5 + 32)
class TemperatureInstructions(Instruction):
async def load(self) -> LoadedInstruction:
return LoadedInstruction(
"Use convert_temperature and include the unit in the answer.",
(Tool(name="convert_temperature", handler=convert_temperature),),
)
async def example() -> None:
instruction = TemperatureInstructions("example:temperature", "Temperature conversion")
assert instruction.summary == "Temperature conversion"
content = await instruction.load()
assert await content.tools[0](celsius=0.0) == "32.0"
asyncio.run(example())
Instructions without tools return LoadedInstruction(text). Each call to
load() requests current content; providers may cache it only while they can
validate its freshness. Loading does not execute the described procedure or
change an agent. The caller decides when to load mandatory or optional content.
Exceptions and cancellation propagate to the caller. A provider cleans up resources acquired by a failed load and owns sessions used by returned tools. Those sessions must remain open while the tools are in use. Tools keep their guards and access declarations; loading an instruction does not grant access.
Use instructions with an agent¶
Pass instruction objects in Agent(instructions=[...]). The agent adds their
short summaries and sources to the system prompt and exposes load_instruction.
The model calls this tool with an instruction ID to read the full content.
Associated tools become available on the next model iteration. They cannot run
in the same batch that loads them.
For instructions that must apply before the first model request, pass their IDs
in required_instructions. These instructions are refreshed before each model
iteration. Providers should reuse their tool objects when the tools are unchanged.
Conflicting instruction IDs or tool names raise an error; they never silently
replace another provider’s declaration.
from __future__ import annotations
import asyncio
from axio import Agent, MemoryContextStore
from axio.instructions import Instruction, LoadedInstruction
from axio.testing import StubTransport, make_text_response, make_tool_use_response
class StyleRules(Instruction):
async def load(self) -> LoadedInstruction:
return LoadedInstruction("Use short sentences and state assumptions explicitly.")
async def example() -> None:
rules = StyleRules("example:style", "Writing conventions")
transport = StubTransport([
make_tool_use_response("load_instruction", tool_input={"id": rules.id}),
make_text_response("The rules are loaded."),
])
agent = Agent("Answer the user.", transport, instructions=[rules])
assert await agent.run("Read the writing conventions.", MemoryContextStore()) == "The rules are loaded."
asyncio.run(example())
The agent collects base tools and loaded instruction tools, applies tool_guards,
and then calls its ToolSelector. Only tools in that iteration’s selected set
can execute. Application guards supplement the guards supplied by each tool.
They also apply to tools added by instructions and to load_instruction itself.
before_turn_hooks and before_tools_hooks run in tuple order after the existing
before_turn and before_tools callbacks. Tool hooks receive the calls that
earlier hooks did not defer. Each hook must return results with distinct IDs
from those calls. A failed hook prevents the whole batch from executing.
A preflight hook can call await agent.load_instruction(id) to load newly
applicable mandatory instructions. If content changes, the whole batch is
deferred so the model can reconsider it with the updated instructions. Unchanged
content and identical tool objects do not trigger another deferral. Hook errors
and cancelled runs discard staged changes.
system_contributions contains synchronous callbacks that supply additional
prompt sections. The agent combines the base system, these sections, and its
current instruction content without changing the base string.
Replace agent.instructions between runs to reload a catalog. Removed or replaced
objects lose their active content and associated tools at the next iteration.
agent.copy() creates independent instruction state and a separate catalog list.
The caller must keep each provider open while any agent uses its tools.
Pass initialized providers with Agent(..., providers=[provider]). The agent reads
their catalogs at each iteration; applications own initialization, reload, and cleanup.
agent.get_tools() returns currently available tools before selection, including
activated tools and application guards.
Instruction.load_access() can declare the effects of loading content for
permission guards. The default is unknown effects. Local skills declare their
source file as a read. These declarations do not grant permission. Required
instructions are loaded by the application-controlled preflight path.
Provider-independent instructions with content loaded on demand.
- class axio.instructions.Instruction(id: str, summary: str, *, source: str = '')[source]¶
Bases:
ABCAn instruction whose provider implements asynchronous content loading.
ididentifies the instruction within a catalog. Providers must include enough source identity to distinguish instructions with the same name.summaryis the short description available before loading the content.sourceis an optional display location; core does not interpret it.Rules, reference material, procedures, and skills use the same contract. The caller decides when loading is required. This class does not infer applicability from a filename, URI, or instruction type.
- abstractmethod async load() LoadedInstruction[source]¶
Return complete content and any tools needed to follow it.
Implementations prepare dependencies before returning. They must not change an agent or execute the procedure described by the instruction. Exceptions, including cancellation, propagate to the caller. Providers must clean up resources acquired by a failed or cancelled load.
Each call requests the current content. A provider may use a cache if it can validate its freshness. Core does not cache this method or share activation state between agents. Providers retain ownership of any sessions used by the returned tools.
- class axio.instructions.InstructionState[source]¶
Bases:
objectOne agent’s catalog, loaded content, and changes awaiting an iteration.
Providers retain their resources. This object only owns the agent’s view of their instructions. A successful load stages changes;
commitmakes them available together at the next model boundary.- static collect_tools(base: Iterable[Tool[Any]], content: Iterable[LoadedInstruction]) list[Tool[Any]][source]¶
Preserve tool identities and reject conflicting names before activation.
- commit(base: Iterable[Tool[Any]]) list[Tool[Any]][source]¶
Apply staged content atomically and return the current tool set.
- async load(id: str, base: Iterable[Tool[Any]]) LoadedInstruction[source]¶
Stage one complete provider result; failure leaves the state unchanged.
- synchronize(instructions: Iterable[Instruction]) None[source]¶
Replace catalog metadata and discard removed or replaced content.
- class axio.instructions.LoadedInstruction(text: str, tools: tuple[Tool[Any], ...] = ())[source]¶
Bases:
objectA complete instruction and the tools needed to follow it.
Tools retain their guards and access declarations. Their provider owns any resources they use and must keep those resources open while they are in use. Returning a tool does not execute it or grant permission to call it.
- text: str¶
Provider discovery¶
See Register and discover plugins for all entry-point groups, installation and registration examples, lifecycle contracts, and discovery diagnostics.
axio.plugins loads installed entry points into PluginCatalog. Each catalog
retains entries, load errors, and the declaring distribution. The first
declaration reserves its name even when loading fails. require(name) reports
a stored failure or lists the available names when the name is unknown.
The groups have separate contracts:
Group |
Target |
|---|---|
|
A plain async tool handler |
|
A zero-argument |
|
A |
|
A |
|
A zero-argument |
|
A transport class or plugin, through |
A tool provider implements init(config=None, global_config=None), close(),
the all_tools property, and get_instructions(). The last method returns
Instruction objects with metadata available before loading. Provider factories
must not acquire resources: initialization and cleanup belong to the lifecycle.
Register a provider in its package:
[project.entry-points."axio.tools.providers"]
example = "example_tools:Provider"
Use open_tool_providers() as an async context manager. It registers cleanup
before each initialization and closes providers in reverse order, including
on partial startup, cancellation, and exceptions in the context body. A startup
failure propagates after cleanup. Use separate contexts to isolate independent
startup failures. Providers must permit close() after partial initialization.
collect_tools() reads the current tool sets and rejects conflicting names.
Multiple references to the same tool object are allowed. It does not cache a
provider’s tools, so callers can collect them again after a provider reload.
standalone_tools() excludes plain handlers also supplied by an initialized
provider from the same distribution. Tools from different distributions still
conflict when their names match.
The TUI retains its settings groups and ToolsPlugin interface. Settings screens
are separate from the core provider contract. Core imports no UI implementation.
Common entry-point discovery and tool-provider lifecycle, without UI imports.
- class axio.plugins.ContextBackend(*args, **kwargs)[source]¶
Bases:
ProtocolAn installed store factory that owns resources shared by its sessions.
- create(session_id: str | None = None) ContextStore[source]¶
- class axio.plugins.PluginCatalog(entries: dict[str, T]=<factory>, errors: dict[str, str]=<factory>, sources: dict[str, PluginSource]=<factory>)[source]¶
Bases:
GenericSuccessfully loaded extensions, failures, and their installed sources.
- entries: dict[str, T]¶
- errors: dict[str, str]¶
- sources: dict[str, PluginSource]¶
- exception axio.plugins.PluginError[source]¶
Bases:
RuntimeErrorAn extension could not be loaded or conflicts with another extension.
- class axio.plugins.PluginSource(group: str, distribution: str, value: str)[source]¶
Bases:
objectThe installed declaration that owns an entry-point name.
- distribution: str¶
- group: str¶
- value: str¶
- class axio.plugins.RefreshableToolProvider(*args, **kwargs)[source]¶
Bases:
ProtocolAn optional capability for refreshing tools and instructions in place.
- class axio.plugins.ToolLifecycle(*args, **kwargs)[source]¶
Bases:
ProtocolThe working interface shared by current and legacy tool providers.
- class axio.plugins.ToolProvider(*args, **kwargs)[source]¶
Bases:
ToolLifecycle,ProtocolA dynamic tool set and instruction catalog owned by an installed provider.
Register a zero-argument factory in
axio.tools.providers. The factory must not acquire resources.initreceives application configuration; only the provider interprets its contents.closemust also work after a failed or cancelled initialization.- get_instructions() Sequence[Instruction][source]¶
- axio.plugins.collect_tools(providers: Iterable[ToolLifecycle], *, tools: Iterable[Tool[Any]] = ()) list[Tool[Any]][source]¶
Collect current tools, rejecting different tools with the same name.
- axio.plugins.context_backend_from_target(name: str, target: Any) ContextBackend[source]¶
- axio.plugins.discover_context_backends() PluginCatalog[ContextBackend][source]¶
- axio.plugins.discover_guards(*, points: Iterable[EntryPoint] | None = None) PluginCatalog[type[PermissionGuard]][source]¶
- axio.plugins.discover_selectors(*, points: Iterable[EntryPoint] | None = None) PluginCatalog[type[ToolSelector]][source]¶
- axio.plugins.discover_tool_providers(*, points: Iterable[EntryPoint] | None = None) PluginCatalog[ToolProvider][source]¶
- axio.plugins.discover_tools(*, points: Iterable[EntryPoint] | None = None) PluginCatalog[Tool[Any]][source]¶
- axio.plugins.guard_from_target(name: str, target: Any) type[PermissionGuard][source]¶
- axio.plugins.load_entry_points(group: str, adapt: Callable[[str, Any], T], *, points: Iterable[EntryPoint] | None = None) PluginCatalog[source]¶
Load each name once, retaining failures and reserving the first declaration.
Discovery does not initialize providers. An adapter validates the contract for its own group. A broken declaration cannot be replaced by a duplicate.
- axio.plugins.open_tool_providers(providers: Iterable, *, config: Any = None, global_config: Any = None) AsyncIterator[tuple[T, ...]][source]¶
Initialize providers and close them in reverse order on every exit path.
Cleanup is registered before initialization so partial startup is covered. A startup error propagates after closing the failing and earlier providers. The caller may use separate contexts when startup failures must be isolated.
- axio.plugins.provider_from_target(name: str, target: Any) ToolProvider[source]¶
- axio.plugins.selector_from_target(name: str, target: Any) type[ToolSelector][source]¶
- axio.plugins.standalone_tools(tools: PluginCatalog[Tool[Any]], providers: PluginCatalog[ToolProvider]) list[Tool[Any]][source]¶
Prefer a distribution’s initialized provider over its duplicate plain handlers.
Different distributions still conflict. Plain handlers without a matching provider tool remain available.
Context and events¶
ContextStore: protocol for conversation history storage.
- class axio.context.ContextStore[source]¶
Bases:
ABC- async fork() ContextStore[source]¶
Returns a MemoryContextStore deep copy by default.
- async classmethod from_context(context: ContextStore) Self[source]¶
- async classmethod from_history(history: list[Message]) Self[source]¶
Create a new ContextStore pre-populated with history.
- async list_sessions() list[SessionInfo][source]¶
List available sessions. Default: returns a single entry for the current session.
- property session_id: str¶
Lazy-init UUID hex; works without calling super().__init__().
- class axio.context.MemoryContextStore(history: list[Message] | None = None)[source]¶
Bases:
ContextStoreSimple in-memory context store. fork() returns a deep copy.
- async fork() MemoryContextStore[source]¶
Returns a MemoryContextStore deep copy by default.
- property session_id: str¶
Lazy-init UUID hex; works without calling super().__init__().
- class axio.context.SessionInfo(session_id: 'str', message_count: 'int', preview: 'str', created_at: 'str', input_tokens: 'int' = 0, output_tokens: 'int' = 0)[source]¶
Bases:
object- created_at: str¶
- input_tokens: int¶
- message_count: int¶
- output_tokens: int¶
- preview: str¶
- session_id: str¶
Stream events: all variants emitted by AgentStream.
- class axio.events.AudioOutput(index: int, data: bytes, media_type: AudioMediaType)[source]¶
Bases:
objectAudio content from a tool result (e.g. read_file on an audio file).
- data: bytes¶
- index: int¶
- media_type: AudioMediaType¶
- class axio.events.AudioOutputDelta(data: bytes, media_type: str = 'audio/pcm;rate=24000')[source]¶
Bases:
objectStreaming audio chunk from the assistant in a realtime session.
- data: bytes¶
- media_type: str¶
- class axio.events.BlockEnd(index: int)[source]¶
Bases:
objectThe content block at
indexis complete.The point at which accumulated
ToolInputDeltafragments are guaranteed to parse. Every provider marks it and axio has had no terminator for it until now.- index: int¶
- class axio.events.Citation(index: int, cited_text: str = '', title: str | None = None, url: str | None = None, source_id: str | None = None, start: int | None = None, end: int | None = None, unit: Literal['char', 'byte', 'page', 'block', 'unknown']='unknown', raw: dict[str, ~typing.Any]=<factory>)[source]¶
Bases:
objectA span of generated text attributed to a source.
What Anthropic’s citation shapes, OpenAI’s annotations and Google’s grounding metadata have in common.
rawkeeps the provider’s whole object for the fields that differ.- cited_text: str¶
- end: int | None¶
- index: int¶
- raw: dict[str, Any]¶
- source_id: str | None¶
Whatever identifies the source inside this request: a file id, a document index, a chunk id.
- start: int | None¶
- title: str | None¶
- unit: Literal['char', 'byte', 'page', 'block', 'unknown']¶
What
startandendcount. Stated because the providers disagree — OpenAI counts characters and Google counts bytes — so offsets from different units must never be compared.
- url: str | None¶
- class axio.events.Error(exception: 'BaseException')[source]¶
Bases:
object- exception: BaseException¶
- class axio.events.ImageOutput(index: int, data: bytes, media_type: ImageMediaType)[source]¶
Bases:
objectModel generated an image inline (e.g. Nano Banana / Gemini Image).
- data: bytes¶
- index: int¶
- media_type: ImageMediaType¶
- class axio.events.IterationEnd(iteration: 'int', stop_reason: 'StopReason', usage: 'Usage', raw: 'str' = '')[source]¶
Bases:
object- iteration: int¶
- raw: str¶
The provider’s own word for why it stopped, where it differs from what axio calls it. Kept so a caller can act on a reason this vocabulary has no name for.
- stop_reason: StopReason¶
- class axio.events.IterationStart(iteration: int, id: str | None = None, model: str | None = None)[source]¶
Bases:
objectOne provider request has begun.
modelis the model that actually served the turn, which need not be the one asked for. Server-side fallback, sticky routing and dated-snapshot resolution all substitute a different model at a different price. A cost lookup therefore keys off this rather than off the request.- id: str | None¶
- iteration: int¶
- model: str | None¶
- class axio.events.ProviderEvent(provider: str, kind: str, data: dict[str, Any], index: int | None = None)[source]¶
Bases:
objectA provider payload axio does not model, forwarded verbatim.
datais the provider’s own JSON object exactly as it was parsed: no renaming, no coercion, no filtering.How completely a transport forwards depends on what its stream names. A stream that names each event has a reader. That reader forwards every payload it does not interpret, so nothing is dropped. A stream with no discriminator — one shape per payload, read field by field — has no such catch. Those transports forward the parts they know how to name. A field the provider adds inside a payload reaches nobody until someone reads it.
A consumer that does not recognise
(provider, kind)ignores it.- data: dict[str, Any]¶
- index: int | None¶
The content-block or output index, where the payload carries one.
- kind: str¶
The provider’s own discriminator, verbatim, never a name axio invented: matching on it is matching the vocabulary the provider publishes.
- provider: str¶
Which transport produced it:
"anthropic","openai","google". The Codex transport reads its stream through the shared Responses reader, so its events say"openai"rather than naming a fourth provider.
- class axio.events.ProviderOutput(provider: str, kind: str, data: dict[str, Any], index: int, id: str = '')[source]¶
Bases:
objectOne output item of this turn that the next request has to send back unread.
ProviderEventforwards what a caller may want to watch; this one names what the turn is not complete without. An endpoint that keeps no history of its own —store=Falseon the Responses API — expects every item it produced back on the next request, including those from the tools it ran itself: a web search, a file search, a code interpreter, and item types that do not exist yet. Read as news rather than as content, they were watched and dropped, and the next request was missing what the model had answered from.The agent stores it as a
ProviderBlock, and the transport that speaks the same protocol replaysdataverbatim.- data: dict[str, Any]¶
The item exactly as it arrived, never interpreted here.
- id: str¶
- index: int¶
The output index this item occupied. Declared in the same position as on
ProviderEvent, whose first three fields these are: with the two orders crossed, one built positionally in the shape of the other assigned every field to the wrong name.
- kind: str¶
The item’s own type, verbatim.
- provider: str¶
Which protocol produced it, in the names
ProviderEventuses.
- class axio.events.ReasoningDelta(index: 'int', delta: 'str')[source]¶
Bases:
object- delta: str¶
- index: int¶
- class axio.events.ReasoningSignature(index: int, signature: str, redacted: bool = False, id: str = '', provider: str = '')[source]¶
Bases:
objectOpaque proof that a reasoning block is the provider’s own, to be replayed unaltered.
Anthropic refuses a returned
thinkingblock whose signature is missing or changed, and Google publishes aMISSING_THOUGHT_SIGNATUREfinish reason for the same failure. Never inspect, decode, re-encode or truncatedata.- id: str¶
How the provider names the block this proves, where it names them. Replayed beside the proof, because a provider that identifies reasoning by id refuses the pair without it.
- index: int¶
- provider: str¶
Which protocol issued this proof, in the names
ProviderEventuses.
- redacted: bool¶
True where the payload replaces the reasoning text instead of accompanying it.
- signature: str¶
- class axio.events.Refusal(index: int, text: str = '', spoken: bool = True, category: str | None = None, blocked_input: bool = False, raw: dict[str, ~typing.Any]=<factory>)[source]¶
Bases:
objectThe model declined, or the provider blocked the turn.
This is deliberately not a
TextDelta. As ordinary assistant text, or as an empty turn that succeeded, a refusal is indistinguishable from an answer and no consumer can act on it.- blocked_input: bool¶
True where the provider rejected the prompt rather than the answer, so nothing was generated and sending the same prompt again cannot succeed.
- category: str | None¶
The provider’s own category, verbatim. Not normalised: the taxonomies do not overlap, and a mapping between them would state something no provider says.
- index: int¶
- raw: dict[str, Any]¶
- spoken: bool¶
Whether
textis the model’s own words.True on the endpoints that stream a refusal as output content, which is what OpenAI and the Responses API do. False where the text is the provider explaining why it stopped: Anthropic sends stop_details.explanation, which its own schema documents as unstable and not to be parsed, and the model generated nothing at all.
The agent stores either kind as the turn’s text, because a stored turn with no content is refused by the next request and the explanation is the only account of the decline there is. A consumer that renders the two differently — the model declining, against the provider reporting a block — reads this.
- text: str¶
- class axio.events.SessionEndEvent(stop_reason: 'StopReason', total_usage: 'Usage')[source]¶
Bases:
object- stop_reason: StopReason¶
- class axio.events.SpeechStarted[source]¶
Bases:
objectServer VAD detected the user started speaking (realtime).
- class axio.events.SpeechStopped[source]¶
Bases:
objectServer VAD detected the user stopped speaking (realtime).
- type axio.events.StreamEvent = ReasoningDelta | ReasoningSignature | TextDelta | TextSignature | Refusal | Citation | ImageOutput | AudioOutput | VideoOutput | ToolUseStart | ToolInputDelta | ToolFieldStart | ToolFieldDelta | ToolFieldEnd | ToolOutputDelta | ToolResult | BlockEnd | IterationStart | IterationEnd | Error | ProviderEvent | ProviderOutput | SessionEndEvent | AudioOutputDelta | TranscriptDelta | SpeechStarted | SpeechStopped | TurnComplete¶
- class axio.events.TextDelta(index: 'int', delta: 'str')[source]¶
Bases:
object- delta: str¶
- index: int¶
- class axio.events.TextSignature(index: int, signature: str, provider: str = '')[source]¶
Bases:
objectOpaque proof that a block of answer text is the provider’s own, to be replayed unaltered.
Google signs the part it issued the proof for, and answer text is one such part. The proof belongs to the text block, not to the reasoning or the call beside it: replayed on another part it proves nothing, and the turn fails with
MISSING_THOUGHT_SIGNATURE. Never inspect, decode, re-encode or truncatedata.Emitted after the text it signs, never before.
- index: int¶
- provider: str¶
Which protocol issued this proof, in the names
ProviderEventuses.
- signature: str¶
- class axio.events.ToolFieldDelta(index: 'int', tool_use_id: 'ToolCallID', key: 'str', text: 'str')[source]¶
Bases:
object- index: int¶
- key: str¶
- text: str¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolFieldEnd(index: 'int', tool_use_id: 'ToolCallID', key: 'str')[source]¶
Bases:
object- index: int¶
- key: str¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolFieldStart(index: 'int', tool_use_id: 'ToolCallID', key: 'str')[source]¶
Bases:
object- index: int¶
- key: str¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolInputDelta(index: 'int', tool_use_id: 'ToolCallID', partial_json: 'str')[source]¶
Bases:
object- index: int¶
- partial_json: str¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolOutputDelta(tool_use_id: 'ToolCallID', name: 'ToolName', key: 'str', delta: 'str')[source]¶
Bases:
object- delta: str¶
- key: str¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolResult(tool_use_id: 'ToolCallID', name: 'ToolName', is_error: 'bool', content: 'str' = '', input: 'dict[str, Any]'=<factory>)[source]¶
Bases:
object- content: str¶
- input: dict[str, Any]¶
- is_error: bool¶
- tool_use_id: ToolCallID¶
- class axio.events.ToolUseStart(index: 'int', tool_use_id: 'ToolCallID', name: 'ToolName', signature: 'str' = '', provider: 'str' = '')[source]¶
Bases:
object- index: int¶
- provider: str¶
Which protocol issued
signature, in the namesProviderEventuses. Stored with the proof so a session that changes transport does not replay it to one that never made it.
- signature: str¶
Opaque proof that this call is the model’s own, where the provider issues one for the call rather than for the reasoning beside it. Replayed on the call itself: attached to a reasoning block instead, the call comes back unsigned and the provider refuses the turn.
- tool_use_id: ToolCallID¶
- class axio.events.TranscriptDelta(role: Literal['user', 'assistant'], delta: str)[source]¶
Bases:
objectLive transcript delta — server-side STT of user mic, or assistant speech transcription, depending on
role.- delta: str¶
- role: Literal['user', 'assistant']¶
- class axio.events.TurnComplete(stop_reason: StopReason, usage: Usage | None = None)[source]¶
Bases:
objectAssistant turn finished in a realtime session.
stop_reasonmay beStopReason.tool_useto signal that pending tool calls should run before the next turn starts.- stop_reason: StopReason¶
- class axio.events.VideoOutput(index: int, data: bytes, media_type: VideoMediaType)[source]¶
Bases:
objectModel generated a video inline.
- data: bytes¶
- index: int¶
- media_type: VideoMediaType¶
Protocols¶
Transport discovery is available to user code through axio.transport.
discover_transports() returns a TransportCatalog; use require(name) to get
a TransportEntry, open() to enter its lifetime, and select_model() to choose
a model. See Choose a transport in application code for runnable application examples,
authentication settings, and error handling. These helpers do not require REPL
or TUI and are not re-exported from top-level axio.
Transport protocols and application plugin discovery.
- class axio.transport.AudioGenTransport(*args, **kwargs)[source]¶
Bases:
ProtocolGenerate
nnon-speech audio samples for a text prompt — music, sound effects, ambient. Returns raw audio bytes (MP3 / WAV / OGG — provider-defined). Distinct fromTTSTransport, which is text-to-speech.
- class axio.transport.CompletionTransport(*args, **kwargs)[source]¶
Bases:
Protocol- stream(messages: list[Message], tools: list[Tool[Any]], system: str) AsyncIterator[StreamEvent][source]¶
- class axio.transport.DummyAudioGenTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,AudioGenTransport
- class axio.transport.DummyCompletionTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,CompletionTransport- stream(messages: list[Message], tools: list[Tool[Any]], system: str) AsyncIterator[StreamEvent][source]¶
- class axio.transport.DummyEmbeddingTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,EmbeddingTransport
- class axio.transport.DummyImageEditTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,ImageEditTransport- async edit_images(prompt: str, *, images: list[ImageBlock], mask: ImageBlock | None = None, model: str | None = None, n: int = 1) list[bytes][source]¶
- class axio.transport.DummyImageGenTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,ImageGenTransport
- class axio.transport.DummySTTTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,STTTransport
- class axio.transport.DummyTTSTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,TTSTransport
- class axio.transport.DummyTransport[source]¶
Bases:
objectPlaceholder transport that fails loudly if actually used.
Assign this as the default transport when constructing agent prototypes that will be configured later via
agent.copy(transport=real_transport).Example:
from axio.agent import Agent from axio.transport import DummyCompletionTransport researcher = Agent( system="You are a research assistant...", transport=DummyCompletionTransport(), ) # At runtime, swap in the real transport: active = researcher.copy(transport=OpenAITransport()) result = await active.run(task, context)
- class axio.transport.DummyVideoGenTransport(*args, **kwargs)[source]¶
Bases:
DummyTransport,VideoGenTransport
- class axio.transport.ImageEditTransport(*args, **kwargs)[source]¶
Bases:
ProtocolEdit supplied images according to a prompt, returning complete image bytes.
Sources are required. Implementations must reject unsupported masks instead of ignoring them; supported formats and counts are provider-specific.
- async edit_images(prompt: str, *, images: list[ImageBlock], mask: ImageBlock | None = None, model: str | None = None, n: int = 1) list[bytes][source]¶
- class axio.transport.ImageGenTransport(*args, **kwargs)[source]¶
Bases:
ProtocolGenerate
nimage samples for a text prompt. Returns raw image bytes (PNG / JPEG / WebP — provider-defined).
- class axio.transport.RealtimeSession(*args, **kwargs)[source]¶
Bases:
ProtocolActive duplex realtime session — bidirectional audio / text / tools.
Returned by
RealtimeTransport.connect(). Events from the provider arrive onevents(). User input is pushed viasend().- events() AsyncIterator[StreamEvent][source]¶
Async iterator over server events for the lifetime of this session.
- async send(content: ContentBlock | list[ContentBlock]) None[source]¶
Append user content (audio chunk, text, image) to the input buffer.
- async send_tool_result(tool_use_id: ToolCallID, name: ToolName, content: str | list[ContentBlock]) None[source]¶
Deliver a tool’s result to the provider so generation can resume.
nameis included because some providers (e.g. Gemini Live) require the tool name alongside the call id. OpenAI realtime can ignore it.
- class axio.transport.RealtimeTransport(*args, **kwargs)[source]¶
Bases:
ProtocolProvider that supports duplex realtime sessions (e.g. OpenAI Realtime, Gemini Live). Distinct from
CompletionTransportbecause the interaction is bidirectional, not request/response.- async connect(*, 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[source]¶
- class axio.transport.TransportAuth(*args, **kwargs)[source]¶
Bases:
ProtocolAccount operations owned by a transport package, without frontend imports.
- class axio.transport.TransportCatalog(entries: dict[str, ~axio.transport.TransportEntry]=<factory>, errors: dict[str, str]=<factory>, sources: dict[str, ~axio.plugins.PluginSource]=<factory>)[source]¶
Bases:
objectLoaded entries and failures, including failures explicitly requested by name.
- entries: dict[str, TransportEntry]¶
- errors: dict[str, str]¶
- require(name: str) TransportEntry[source]¶
- sources: dict[str, PluginSource]¶
- class axio.transport.TransportEntry(name: str, cls: Any, display_name: str, env_vars: tuple[str, ...] = (), supports_temperature: bool = True, supports_max_tokens: bool = True, supports_debug: bool = True, supports_thinking: bool = True, auth: TransportAuth | None = None, auto_select: bool = True)[source]¶
Bases:
objectAdapt a plugin object or a legacy transport class to one application API.
- auth: TransportAuth | None¶
- auto_select: bool¶
- bind_tools(transport: Any | None) None[source]¶
Let the provider connect its optional tool handlers to a transport.
- cls: Any¶
- display_name: str¶
- env_vars: tuple[str, ...]¶
- classmethod from_target(name: str, target: Any) TransportEntry[source]¶
- name: str¶
- open(*, session: Any, account: str | None = None, credentials: Path | str | None = None, on_auth_refresh: Callable[[...], Any] | None = None, **settings: Any) AsyncIterator[Any][source]¶
Yield a ready transport; the caller owns the shared HTTP session.
Managed factories own catalog loading and cleanup. Legacy constructors receive session and saved settings; this adapter loads their catalog.
- supports_debug: bool¶
- supports_max_tokens: bool¶
- supports_temperature: bool¶
- supports_thinking: bool¶
- exception axio.transport.TransportError[source]¶
Bases:
RuntimeErrorAn installed transport could not be loaded, configured, or opened.
- class axio.transport.TransportPlugin(*args, **kwargs)[source]¶
Bases:
ProtocolAn entry point may expose a plugin object or a transport class.
Plugin objects implement this contract. Legacy classes use TransportEntry’s defaults and can supply the same metadata as class attributes. Factories yield a ready transport with its model catalog loaded.
- auth: TransportAuth | None¶
- display_name: str¶
- env_vars: tuple[str, ...]¶
- supports_debug: bool¶
- supports_max_tokens: bool¶
- supports_temperature: bool¶
- supports_thinking: bool¶
- class axio.transport.VideoGenTransport(*args, **kwargs)[source]¶
Bases:
ProtocolGenerate
nvideo samples for a text prompt. Returns raw video bytes (MP4 / WebM — provider-defined). Provider-specific knobs (duration, aspect ratio, seed image, etc.) live as extra kwargs on the implementation.
- axio.transport.discover_transports() TransportCatalog[source]¶
Load axio.transport entries. The first declaration of each name wins.
A failed first declaration reserves its name too. This prevents an unrelated package from silently replacing the requested provider after a load failure.
- axio.transport.select_transport_model(transport: Any, model: ModelSpec) Any[source]¶
Clone a transport while preserving its session and provider settings.
Permission system: guards that gate tool execution.
- class axio.permission.AllowAllGuard[source]¶
Bases:
PermissionGuard
- class axio.permission.ConcurrentGuard[source]¶
Bases:
PermissionGuard,ABCGuard with concurrency control.
Subclass and override
check().__call__acquires the semaphore then delegates tocheck(). Setconcurrencyto control parallelism (default 1 - one check at a time).- concurrency: int = 1¶
- class axio.permission.DenyAllGuard[source]¶
Bases:
PermissionGuard
- class axio.permission.PermissionGuard[source]¶
Bases:
ABCGate for tool calls. Return modified kwargs to allow, raise to deny.
Tool invokes guards via
await guard(tool, **kwargs).Guards receive the
Toolobject and the raw keyword arguments before execution. Return the (possibly modified) dict to allow; raiseGuardErrorto deny. Because guards see all inputs up front, they are also the right place for logging and auditing:class AuditGuard(PermissionGuard): async def check(self, tool: Tool, **kwargs: Any) -> dict[str, Any]: logger.info("tool=%s args=%s", tool.name, kwargs) return kwargs # always allow
See
examples/agent_swarm/agent_swarm/__main__.py(RoleGuard) for a production example.
Realtime duplex agent: drives a RealtimeSession, dispatches
tools concurrently with audio output, exposes a single event stream to the
caller.
- class axio.realtime.RealtimeAgent(system: str, transport: RealtimeTransport, tools: list[Tool[Any]] = <factory>, voice: str | None = None, input_audio_format: str = 'audio/pcm;rate=16000', output_audio_format: str = 'audio/pcm;rate=24000', raise_on_error: bool = True)[source]¶
Bases:
objectDrives a duplex realtime session against a
RealtimeTransport.The agent intercepts
ToolUseStart/ToolInputDelta/TurnCompleteevents to assemble tool calls and dispatches them as background tasks so that streaming audio output from the provider is not blocked. Each tool result is sent back to the session viaRealtimeSession.send_tool_result()as soon as the handler returns.interrupt()cancels in-flight tasks.Lifecycle:
async with RealtimeAgent(system="...", transport=t, tools=[...]) as agent: await agent.send(AudioBlock(data=mic_chunk, media_type="audio/pcm")) async for event in agent.events(): match event: case AudioOutputDelta(data=pcm): speaker.feed(pcm) ...
- async events() AsyncIterator[StreamEvent][source]¶
Yield events from the session, dispatching tool calls as side-effects.
Tool dispatch runs as background tasks — slow handlers do not block the stream of
AudioOutputDeltafrom the provider.
- input_audio_format: str¶
- output_audio_format: str¶
- raise_on_error: bool¶
Re-raise the exception wrapped in any
Errorevent yielded by the session. Set toFalseto receiveErrorevents as data and decide what to do per error (transient retry, log-and-continue, etc.).
- async send(content: ContentBlock | list[ContentBlock]) None[source]¶
- property session: RealtimeSession¶
- system: str¶
- transport: RealtimeTransport¶
- voice: str | None¶
Data types¶
Message: the fundamental unit of conversation history.
- class axio.messages.Message(role: "Literal['user', 'assistant', 'system']", content: 'list[ContentBlock]' = <factory>)[source]¶
Bases:
object- content: list[ContentBlock]¶
- role: Literal['user', 'assistant', 'system']¶
Content blocks: TextBlock, ImageBlock, AudioBlock, ToolUseBlock, ToolResultBlock.
- class axio.blocks.AudioBlock(media_type: 'AudioMediaType', data: 'bytes')[source]¶
Bases:
ContentBlock- data: bytes¶
- media_type: AudioMediaType¶
- type axio.blocks.AudioMediaType = Literal['audio/x-aac', 'audio/flac', 'audio/mp3', 'audio/m4a', 'audio/mpeg', 'audio/mpga', 'audio/mp4', 'audio/ogg', 'audio/pcm', 'audio/wav', 'audio/webm']¶
- class axio.blocks.ImageBlock(media_type: 'ImageMediaType', data: 'bytes')[source]¶
Bases:
ContentBlock- data: bytes¶
- media_type: ImageMediaType¶
- type axio.blocks.ImageMediaType = Literal['image/jpeg', 'image/png', 'image/gif', 'image/webp']¶
- class axio.blocks.ProviderBlock(provider: str, kind: str, data: dict[str, Any], id: str = '')[source]¶
Bases:
ContentBlockOne item of the turn that only the protocol which produced it can read.
An endpoint that runs its own tools answers with items this vocabulary has no shape for: a web search it ran, a file it read, code it executed, and whatever it adds next. Where the application keeps the history rather than the provider —
store=Falseon the Responses API — the provider expects every one of those items back, in order and exactly once. Dropped, the next request is missing what the model was answering from, and reasoning continuity, tool-call association and anything built on a newer item type break with nothing said.datais the item exactly as it arrived and is never interpreted here.providernames the protocol that issued it, so it is replayed only to an endpoint that speaks the same one, andkindis the item’s own type, which is all a caller needs to render or count it.- data: dict[str, Any]¶
- id: str¶
How the provider names this item, where it names them.
- kind: str¶
- provider: str¶
- class axio.blocks.ReasoningBlock(text: str = '', signature: str = '', redacted: bool = False, id: str = '', provider: str = '')[source]¶
Bases:
ContentBlockThe model’s own reasoning, kept so the turn can be sent back unaltered.
signatureis the provider’s proof that the block is its own. Anthropic refuses a returned thinking block whose signature is missing or changed, and Google reportsMISSING_THOUGHT_SIGNATUREfor the same failure. A stored turn that dropped the signature cannot be replayed. Never inspect, decode or truncate it.redactedmarks a block whose reasoning the provider withheld. The signature still has to travel, andtextis empty.- id: str¶
How the provider names this block, where it names them. Required beside the signature.
- provider: str¶
Which protocol issued
signature:"anthropic","google","openai", the same namesProviderEventuses. The value means nothing outside the protocol that made it — Anthropic sends it as a thinking signature, Google asthoughtSignature, Responses asencrypted_content— so a session that changes transport must not replay it to the next one. Empty says nobody recorded it, which is what a turn stored before this field existed looks like;proof()lets that through rather than dropping proofs it cannot judge.
- redacted: bool¶
- signature: str¶
Out of
repron purpose: this is the one field documented as never to be inspected, and a debug log of the block printed it in full beside everything else.
- text: str¶
- class axio.blocks.TextBlock(text: 'str', signature: 'str' = '', provider: 'str' = '')[source]¶
Bases:
ContentBlock- provider: str¶
Which protocol issued
signature. SeeReasoningBlock.provider.
- signature: str¶
Opaque proof for this text, where the provider signs a text part. Replayed with it. Kept out of
reprwith the rest of them: seeproviderbelow.
- text: str¶
- class axio.blocks.ToolResultBlock(tool_use_id: 'ToolCallID', content: 'str | list[TextBlock | ImageBlock | AudioBlock | VideoBlock]', is_error: 'bool' = False)[source]¶
Bases:
ContentBlock- content: str | list[TextBlock | ImageBlock | AudioBlock | VideoBlock]¶
- is_error: bool¶
- tool_use_id: ToolCallID¶
- class axio.blocks.ToolUseBlock(id: 'ToolCallID', name: 'ToolName', input: 'dict[str, Any]', signature: 'str' = '', provider: 'str' = '')[source]¶
Bases:
ContentBlock- id: ToolCallID¶
- input: dict[str, Any]¶
- provider: str¶
Which protocol issued
signature. SeeReasoningBlock.provider.
- signature: str¶
Opaque proof for this call. Stored rather than held in the transport, which a restart empties.
- class axio.blocks.VideoBlock(media_type: 'VideoMediaType', data: 'bytes')[source]¶
Bases:
ContentBlock- data: bytes¶
- media_type: VideoMediaType¶
- type axio.blocks.VideoMediaType = Literal['video/mp4', 'video/mpeg', 'video/mov', 'video/avi', 'video/x-flv', 'video/mpg', 'video/webm', 'video/wmv', 'video/3gpp']¶
- axio.blocks.from_dict(data: dict[str, Any]) ContentBlock[source]¶
Deserialize a plain dict to a ContentBlock.
- axio.blocks.proof(block: TextBlock | ReasoningBlock | ToolUseBlock, provider: str) str[source]¶
This block’s opaque proof, where the protocol about to receive it is the one that issued it.
Each converter reads the same
signaturefield by its own rules, so a session that changed transport sent a proof one provider made to another that never made it. At best the request is refused; at worst the value is accepted somewhere it was never meant to sit, or printed by whatever logs the request.A block with no recorded provider was stored before anyone recorded one, and the transport reading it is almost always the one that wrote it. Those are replayed, because dropping them loses proofs that are valid and breaks the sessions that already exist.
- axio.blocks.replayable(block: ProviderBlock, provider: str) bool[source]¶
Whether this opaque item goes back to the endpoint about to be asked.
Unlike a proof, an item with no provider recorded is not replayed: every one of them was made by a reader that names itself, so an empty name is a block from somewhere else entirely.
- axio.blocks.to_dict(block: ContentBlock) dict[str, Any][source]¶
- axio.blocks.to_dict(block: TextBlock) dict[str, Any]
- axio.blocks.to_dict(block: ImageBlock) dict[str, Any]
- axio.blocks.to_dict(block: AudioBlock) dict[str, Any]
- axio.blocks.to_dict(block: VideoBlock) dict[str, Any]
- axio.blocks.to_dict(block: ReasoningBlock) dict[str, Any]
- axio.blocks.to_dict(block: ToolUseBlock) dict[str, Any]
- axio.blocks.to_dict(block: ProviderBlock) dict[str, Any]
- axio.blocks.to_dict(block: ToolResultBlock) dict[str, Any]
Serialize a ContentBlock to a plain dict.
Primitive types: ToolName, ToolCallID, StopReason, Usage.
- axio.types.INCOMPLETE: Final = frozenset({StopReason.cancelled, StopReason.context_window_exceeded, StopReason.max_tokens, StopReason.unknown})¶
Reasons that end a run holding an answer that is not finished, and say so nowhere else.
end_turnfinished one.refusalanderrorannounce themselves through events of their own, andrepetitionwrites its own note into the text it cut. These four say nothing unless the caller shows them, and a truncated answer then reads exactly like a whole one.
- class axio.types.StopReason(*values)[source]¶
Bases:
StrEnumWhy the provider stopped generating.
Anything that is not
tool_useorpause_turnends the run. See the match inAgent._run_loop, whose wildcard keeps a member added here from falling through into another paid iteration.- cancelled = 'cancelled'¶
The caller or the provider stopped the turn before it finished.
- context_window_exceeded = 'context_window_exceeded'¶
The conversation outgrew the model’s window. Truncated, like
max_tokens.
- end_turn = 'end_turn'¶
- error = 'error'¶
- max_tokens = 'max_tokens'¶
- pause_turn = 'pause_turn'¶
A server-side tool loop reached its iteration limit. Resumable: the provider expects the assistant content back so it can finish. This is the one reason that does not end the run.
- refusal = 'refusal'¶
The model declined, or the provider blocked the turn. Not an error: the same prompt sent again will be declined again.
- repetition = 'repetition'¶
Axio stopped the turn, because the model was repeating itself. The only reason here the provider did not give. Reported as
end_turn, a caller could not tell an answer the model finished from one cut off mid-word, which is the same objection this vocabulary raises against reading a truncated response as a whole one.
- tool_use = 'tool_use'¶
- unknown = 'unknown'¶
The provider said something this vocabulary does not have. Terminal, and it vouches for nothing. Named rather than folded into one of the others, because each of those claims something the provider did not say: that the turn finished, that it was truncated, or that the transport broke.
IterationEnd.rawcarries the word itself.
- type axio.types.ToolCallID = str¶
- type axio.types.ToolName = str¶
- class axio.types.Usage(input_tokens: int, output_tokens: int, *, cache_read_tokens: int = 0, cache_write_tokens: int = 0, reasoning_tokens: int = 0)[source]¶
Bases:
objectToken counts for one provider request.
The rule:
input_tokensandoutput_tokensare always inclusive grand totals, and every other field is a disjoint slice of one of them:cache_read_tokens + cache_write_tokens <= input_tokens reasoning_tokens <= output_tokens
Providers disagree about whether their own headline number already contains the slices, and they disagree in opposite directions. Anthropic counts only the tokens after the last cache breakpoint, so its cache counts have to be added. Google reports thinking beside the candidates rather than inside them. Each transport adds or does not add to satisfy the rule here, so nothing downstream has to know which provider answered.
Counts only, never money. A cached token and a written one bill at different multipliers, so a caller that wants cost multiplies these slices by its own per-model rates. A zero slice means the provider billed none of it, or reported no breakdown at all. Axio cannot tell those apart.
- property answer_tokens: int¶
Output that was the answer rather than reasoning.
- cache_read_tokens: int¶
The slice of
input_tokensserved from cache, billed at a discount.
- cache_write_tokens: int¶
The slice of
input_tokenswritten to cache, billed at a premium. Disjoint from the read.
- input_tokens: int¶
- output_tokens: int¶
- reasoning_tokens: int¶
The slice of
output_tokensspent on reasoning the caller never sees.
- classmethod reported(input_tokens: int, output_tokens: int, *, cache_read_tokens: int = 0, cache_write_tokens: int = 0, reasoning_tokens: int = 0) Usage[source]¶
What a provider said, held to the rule above rather than trusted to follow it.
Constructed directly, a provider that reports a slice larger than the total it belongs to fails
__post_init__, and a whole answer is lost over an accounting discrepancy. Every transport reads its provider’s numbers through here instead.A slice that outgrew its total means the total was reported without it. The total is therefore raised to hold the slices, which keeps the tokens the provider billed for; the other repair, cutting the slice down, throws them away and under-reports the cost.
- property total_tokens: int¶
- property uncached_input_tokens: int¶
Input the provider had to read in full, which is what most of the bill is.
- axio.types.stop_reason_from(raw: str, table: Mapping[str, StopReason], *, provider: str) StopReason[source]¶
What a provider’s own stop value means here, or
unknownwhere the table does not say.There is no fifth answer to give. Folded into
end_turnit claims the turn finished, intomax_tokensthat it was truncated, intoerrorthat the transport broke; raising throws away an answer the caller has already read.unknownsays only what is true, andIterationEnd.rawcarries the provider’s own word for the caller to act on.
Transport-agnostic model types: Capability, ModelSpec, ModelRegistry.
- class axio.models.Capability(*values)[source]¶
Bases:
StrEnum- audio = 'audio'¶
- embedding = 'embedding'¶
- image_editing = 'image_editing'¶
- image_generation = 'image_generation'¶
- json_mode = 'json_mode'¶
- reasoning = 'reasoning'¶
- structured_outputs = 'structured_outputs'¶
- text = 'text'¶
- tool_use = 'tool_use'¶
- video = 'video'¶
- video_generation = 'video_generation'¶
- vision = 'vision'¶
- class axio.models.ModelRegistry(models: Iterable[ModelSpec] | None = None)[source]¶
Bases:
MutableMapping[str,ModelSpec]- by_capability(*caps: Capability) ModelRegistry[source]¶
- by_cost(*, output: bool = False, desc: bool = False) ModelRegistry[source]¶
Return registry ordered by cost (input by default, output if output=True).
- by_prefix(prefix: str) ModelRegistry[source]¶
- search(*q: str) ModelRegistry[source]¶
search by parts of id
- class axio.models.ModelSpec(id: 'str', capabilities: 'frozenset[Capability]' = frozenset(), max_output_tokens: 'int' = 8192, context_window: 'int' = 128000, input_cost: 'float' = 0.0, output_cost: 'float' = 0.0)[source]¶
Bases:
object- capabilities: frozenset[Capability]¶
- context_window: int¶
- id: str¶
- input_cost: float¶
- max_output_tokens: int¶
- output_cost: float¶
Field metadata for tool handler functions - lightweight replacement for pydantic.Field.
- axio.field.Field(description: str = '', default: Any = MISSING, ge: int | float | None = None, le: int | float | None = None) FieldInfo[source]¶
Annotate a handler parameter with metadata (description, default, constraints).
Usage:
async def search( query: Annotated[str, Field(description="Search query")], limit: Annotated[int, Field(default=10, ge=1, le=100)], ) -> str: ...
- class axio.field.FieldInfo(description: str = '', default: Any = MISSING, ge: int | float | None = None, le: int | float | None = None, strict: bool = False)[source]¶
Bases:
objectMetadata attached to a handler parameter via
Annotated[T, FieldInfo(...)].- default: Any = MISSING¶
- description: str = ''¶
- ge: int | float | None = None¶
- le: int | float | None = None¶
- strict: bool = False¶
- axio.field.MISSING: Final[MissingSentinel] = MISSING¶
Sentinel value meaning “this field has no default and is required”.
- class axio.field.MissingSentinel[source]¶
Bases:
objectSingleton sentinel - distinguishes ‘no default’ from
None.
- axio.field.StrictStr¶
Drop-in replacement for
from pydantic import StrictStr. Rejects non-string values (e.g. integers) without coercion.alias of
Annotated[str,FieldInfo(description=, default=MISSING, ge=None, le=None, strict=True)]
- axio.field.bare_type(hint: Any) type[source]¶
Return the base Python type, stripping
Annotated,Optional, and generic wrappers.
- axio.field.check_list_items(value: list[Any], name: str, inner: Any) None[source]¶
Raise TypeError when any list element violates the generic item type.
- axio.field.check_scalar(value: Any, name: str, b: type, strict: bool) None[source]¶
Raise TypeError when value does not satisfy the scalar type b.
- axio.field.check_type(value: Any, name: str, inner: Any, *, strict: bool) None[source]¶
Dispatch type validation for value against inner (already unwrapped).
- axio.field.get_field_info(annotation: Any) FieldInfo | None[source]¶
Extract a
FieldInfofrom anAnnotatedannotation, or returnNone.
- axio.field.is_classvar(annotation: Any) bool[source]¶
Return True if annotation is
ClassVarorClassVar[X].
- axio.field.unwrap_hint(hint: Any) tuple[Any, bool][source]¶
Strip
AnnotatedandOptionalwrappers.Returns
(inner_type, is_optional)where inner_type has no Annotated or Union-with-None wrappers and is_optional is True whenNonewas one of the Union members.
JSON-schema builder for plain async handler functions.
Produces clean schemas with no "title" keys - no post-processing needed
in transports.
- axio.schema.build_tool_schema(fn: Any, hints: dict[str, Any] | None = None) dict[str, Any][source]¶
Return a JSON schema object for fn (a callable or class).
The schema has the form:
{ "type": "object", "properties": {"field": {"type": "string"}, ...}, "required": ["field", ...] # only when non-empty }
No
"title"keys are emitted anywhere in the schema.Parameters¶
- fn:
A plain async function or a class whose annotations define the fields.
- hints:
Pre-computed
get_type_hints(fn, include_extras=True)result. When supplied, the call toget_type_hintsis skipped.
- axio.schema.property_schema(annotation: Any) dict[str, Any][source]¶
Recursively convert a Python type annotation to a JSON schema fragment.
- axio.schema.strip_title(schema: dict[str, Any]) dict[str, Any][source]¶
The schema without its
titlekeywords, at every depth.build_tool_schemawrites none, so this is for a schema that came from somewhere else: a pydantic model names every model and field, no provider reads those names, and a large tool set pays for them on every request.Only the keywords that hold schemas are walked.
const,default,enumandexampleshold values the caller declared, so a value with atitlefield of its own survives: walked as schemas, anenumof objects came out as a list of empty ones.
Credentials¶
axio.credentials provides typed credential storage without provider dependencies.
Credentials[T] supplies locking, serialization, and record selection for the
file and SQLite backends. CredentialStore[T] is the protocol for other backends.
A provider supplies its dataclass and a default filename. Mark exactly one string
field with metadata={"credential_key": True}. The key can be inside a nested
dataclass. Use repr=False for secret fields.
For an additive schema change, a new field with a default or default factory can
declare metadata={"credential_optional": True}. Older records that omit it then
use that default. All unmarked fields remain required even if their dataclass has
a default; missing recovery markers must not silently reset operation state.
Unknown fields and incorrectly typed values are still rejected.
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import TemporaryDirectory
from axio.credentials import FileCredentialStore
@dataclass(frozen=True, slots=True)
class ServiceAccount:
name: str = field(metadata={"credential_key": True})
api_key: str = field(repr=False)
class ServiceCredentials(FileCredentialStore[ServiceAccount]):
filename = "service.auth"
async def example(path: Path) -> None:
store = ServiceCredentials(path)
async with store.lock("work"):
await store.save(ServiceAccount("work", "example-key"))
async with store.lock("work"):
account = await store.load("work") # ServiceAccount | None
assert account == ServiceAccount("work", "example-key")
with TemporaryDirectory() as directory:
asyncio.run(example(Path(directory) / "service.auth"))
For a caller-selected path, FileCredentialStore[ServiceAccount](path) also
retains the record type. Alternatively, pass record_type=ServiceAccount.
SQLiteCredentialStore[ServiceAccount](database_path) has the same record API.
The default file path is $XDG_CONFIG_HOME/axio/<filename>, falling back to
~/.config/axio/<filename> when XDG_CONFIG_HOME is unset or relative.
The shared codec uses asdict() and restores nested dataclasses, optional values,
unions, literals, enums, lists, tuples, sets, frozensets, and dictionaries with
string keys. Scalar types are checked without coercing strings or booleans into
numbers. Non-finite numbers, missing fields, and unknown fields are rejected.
Dataclass __post_init__ methods can check relationships between fields; raise
ValueError for invalid data. Codec errors do not include stored values.
Call load() and save() under the corresponding lock(). Each save commits
independently, even if a later operation fails. Cancellation waits for an active
write before releasing the lock. Keep locks held across credential renewal and
its writes. Local backends require POSIX permissions and flock; stored data is
not encrypted. Preserve the adjacent .locks directory while processes use it.
list_records() returns complete records, including secrets. Applications must
remove secret fields before displaying account metadata. select_account() and
selected_account() store a preference for future sessions; host_id() retains
a stable local identity. The storage layer does not perform OAuth or refresh.
For Redis, PostgreSQL, or another backend, implement CredentialStore[T] with
lock(key), load(key), and save(record). Use record_id(record) for the key
and CredentialCodec(T) for shared validation and serialization. Distributed
locks must remain exclusive throughout each operation. Saves must be atomic and
durable before returning.
Local backend extensions override read_value() and write_value() with
dictionary values. These methods run in worker threads; JSON encoding belongs
at the persistence boundary. SQLite opens and closes a connection within each
worker-thread operation. Existing ChatGPT JSON records and SQLite databases
remain readable, with their original lock names and recovery markers.
- class axio.credentials.Credentials(directory: Path, locks: Path, *, record_type: type[T] | None = None)[source]¶
Shared process locking and cancellation handling for local backends.
Override read_value/write_value to change persistence. These synchronous hooks run in a worker thread. Each write must commit before returning. load/save require lock(key_id) in the same asyncio task. Locks are not reentrant. The local filesystem must support flock and durable writes.
- property codec: CredentialCodec[T][source]¶
Resolve T from a typed instance, a typed subclass, or record_type.
- async list_records() list[T][source]¶
Read a typed snapshot. Records can contain secrets; do not print them.
- abstractmethod read_value(key: str) dict[str, Any] | None[source]¶
Read the latest committed value. Called under the corresponding lock.
- read_values() dict[str, Any][source]¶
Read a snapshot for account discovery. Optional for custom adapters.
- async select_account(key_id: str) None[source]¶
Select an existing registration for future sessions.
- class axio.credentials.CredentialStore(*args, **kwargs)[source]¶
Storage contract implemented by the application, with any backend.
lock() excludes all writers for one record key, including other processes and hosts that share these credentials. Keep it held across HTTP requests. A distributed lease must remain exclusive until exit; a lease that silently expires can cause refresh-token reuse.
load() reads the latest record, never a stale process-local cache. save() atomically and durably replaces the whole record BEFORE returning. Each save is committed separately, even inside lock(); lock() is not a database transaction that rolls back writes on exit. Recovery markers must survive a process crash during the subsequent HTTP exchange.
Call load/save under lock. Report failures as CredentialError with safe messages. Protect credentials through filesystem permissions, database access controls, or equivalent. Keep secrets out of logs and repr. Retain the host ID separately across restarts and sign-outs. Axio selects neither a backend nor a storage path.
- class axio.credentials.FileCredentialStore(path: str | Path | None = None, *, record_type: type[T] | None = None)[source]¶
Store registrations in one JSON file with atomic replacement.
Defaults to $XDG_CONFIG_HOME/axio/<filename> or ~/.config/axio/<filename>. Creates directories with mode 0700 and files with mode 0600. Rejects files with unsafe permissions, symlinks, and hard links. Keep the adjacent .locks directory intact while any process uses the store. Credentials are protected by filesystem access controls, not encrypted at rest.
- read_value(key: str) dict[str, Any] | None[source]¶
Read the latest committed value. Called under the corresponding lock.
- class axio.credentials.SQLiteCredentialStore(path: str | Path, *, record_type: type[T] | None = None)[source]¶
Store registrations in a dedicated SQLite database in a private directory.
Each load opens a mode=ro connection, reads, and closes in one worker-thread call. Each write opens its own connection, commits, and closes. The adjacent <database>.locks directory coordinates refresh independently of SQL commits. Connections never span an HTTP request. No close() call is necessary.
- connect(*, readonly: bool) Connection[source]¶
Open one connection. The calling worker thread must close it.
- read_value(key: str) dict[str, Any] | None[source]¶
Read the latest committed value. Called under the corresponding lock.
- read_values() dict[str, Any][source]¶
Read a snapshot for account discovery. Optional for custom adapters.
Utilities¶
AgentStream: async iterator wrapper over the agent event generator.
- class axio.stream.AgentStream(generator: AsyncGenerator[StreamEvent, None])[source]¶
Bases:
object- async get_final_text() str[source]¶
Everything the turn said, and nothing about whether it finished saying it.
A run ending on one of
INCOMPLETEreturns a truncated answer that reads exactly like a whole one, because astrhas nowhere to put the reason. It is logged as a warning here, andget_session_end()carries it for a caller that needs to branch on it.Errorstill raises, so a broken turn is never returned as an answer.
- async get_session_end() SessionEndEvent[source]¶
Load Agent prototypes from declarative config files (TOML, JSON, INI).
Each file describes one agent. The loader scans a directory, parses every
recognised file, resolves tool names against a caller-supplied toolbox, and
returns a dict[str, tuple[str, Agent]] that matches the shape used by
agent_swarm and similar orchestrators.
Supported formats¶
TOML:
name = "architect"
description = "System design and interface specs"
max_iterations = 100
tools = ["read_file", "write_file"]
[system]
text = """
You are an expert software architect...
"""
JSON:
{
"name": "architect",
"description": "System design and interface specs",
"max_iterations": 100,
"tools": ["read_file", "write_file"],
"system": "You are an expert software architect..."
}
INI:
[agent]
name = architect
description = System design and interface specs
max_iterations = 100
tools = read_file, write_file
[system]
text = You are an expert software architect...
In all formats name falls back to the file stem when omitted. system
may be a plain string or a {"text": "..."} mapping (TOML/JSON). INI tools
are comma-separated.
Custom sources¶
Subclass AgentLoader and implement load() - the
base load_file() will handle reading the file and calling
your implementation automatically:
class DbAgentLoader(AgentLoader):
def load(self, content: str) -> AgentSpec:
row = json.loads(content)
return AgentSpec(name=row["name"], ...)
- class axio.agent_loader.AgentContext[source]¶
Bases:
TypedDict- agent_name: str¶
- context_factory: Callable[[], ContextStore]¶
- on_event: Callable[[str, StreamEvent], None] | None¶
- transport: CompletionTransport¶
- class axio.agent_loader.AgentLoader[source]¶
Bases:
objectBase class for format-specific agent loaders.
Subclasses implement
load()to parse a raw string. The source of that string is entirely up to the caller - files, databases, HTTP, etc.load_file()is provided on the base class and callsload()automatically.- extensions: tuple[str, ...] = ()¶
- load(content: str) AgentSpec[source]¶
Parse content and return an
AgentSpec.namedefaults to""when not present.load_file()patches it from the file stem after calling this method.
- class axio.agent_loader.AgentSpec(name: str, description: str, system: str, max_iterations: int = 50, tools: tuple[str, ...] = (), model: str | None = None)[source]¶
Bases:
objectParsed agent definition before transport/tools are injected.
- description: str¶
- max_iterations: int = 50¶
- model: str | None = None¶
- name: str¶
- system: str¶
- to_agent(toolbox: Mapping[str, Tool[Any]] = mappingproxy({})) Agent[source]¶
Return a prototype Agent with toolbox tools attached.
The agent uses
DummyCompletionTransport- callagent.copy(transport=real_transport)before running it.Raises
KeyErrorif any name inself.toolsis absent from toolbox.
- tools: tuple[str, ...] = ()¶
- class axio.agent_loader.IniAgentLoader[source]¶
Bases:
AgentLoaderLoad agent definitions from
.inifiles.Expected sections:
[agent]for metadata,[system]for the prompt. Thetoolskey is comma-separated. Multiline system prompts work via standard ConfigParser continuation (indent subsequent lines).- extensions: tuple[str, ...] = ('ini',)¶
- class axio.agent_loader.JsonAgentLoader[source]¶
Bases:
AgentLoaderLoad agent definitions from
.jsonfiles.- extensions: tuple[str, ...] = ('json',)¶
- class axio.agent_loader.MultiFormatLoader(loaders: list[AgentLoader] = <factory>)[source]¶
Bases:
objectAggregate loader that handles TOML, JSON, and INI files.
- loaders: list[AgentLoader]¶
- class axio.agent_loader.TomlAgentLoader[source]¶
Bases:
AgentLoaderLoad agent definitions from
.tomlfiles.- extensions: tuple[str, ...] = ('toml',)¶
- async axio.agent_loader.agent_tool(task: ~typing.Annotated[str, ~axio.field.FieldInfo(description=Full task instructions., default=MISSING, ge=None, le=None, strict=False)]) str[source]¶
Delegate a task to a sub-agent.
- axio.agent_loader.load_agents(directory: Path, toolbox: Mapping[str, Tool[Any]] = mappingproxy({})) dict[str, tuple[str, Agent]][source]¶
Scan directory for
.toml,.json, and.iniagent files.Returns
dict[name, (description, agent)]- same shape as theAGENTSregistry used inagent_swarmand similar examples.Example:
from pathlib import Path from axio.agent_loader import load_agents AGENTS = load_agents( Path(__file__).parent / "roles", toolbox={"read_file": read_file_tool, "write_file": write_file_tool}, )
- axio.agent_loader.load_agents_from_dir(directory: ~pathlib.Path, transport: ~axio.transport.CompletionTransport, context_factory: ~collections.abc.Callable[[], ~axio.context.ContextStore] = <class 'axio.context.MemoryContextStore'>, on_event: ~collections.abc.Callable[[str, ~axio.events.StreamEvent], None] | None = None) list[Tool[AgentContext]][source]¶
- axio.agent_loader.make_agent_tools(agents: dict[str, tuple[str, ~axio.agent.Agent]], transport: ~axio.transport.CompletionTransport, context_factory: ~collections.abc.Callable[[], ~axio.context.ContextStore] = <class 'axio.context.MemoryContextStore'>, on_event: ~collections.abc.Callable[[str, ~axio.events.StreamEvent], None] | None = None, agent_name_prefix: str = '') list[Tool[AgentContext]][source]¶
Convert each agent into its own
Tool.Each tool is named after the agent and accepts a single
taskfield. Runtime dependencies (transport, context factory, event callback) are stored incontextand injected on each call.Parameters¶
- agents:
dict[name, (description, prototype_agent)]- e.g. the value returned byload_agents().- transport:
Transport assigned to the selected agent via
agent.copy().- context_factory:
Called once per invocation to produce a fresh
ContextStore. Defaults toMemoryContextStore.- on_event:
Optional callback receiving
(agent_name, event)for everyStreamEventthe agent emits.- agent_name_prefix:
Prefix to prepend to each agent name.
Example:
from axio.agent_loader import load_agents, make_agent_tools agents = load_agents(Path("roles"), toolbox={"read_file": read_file_tool}) tools = make_agent_tools(agents, transport=my_transport) orchestrator = Agent(system="...", transport=my_transport, tools=tools)
Incremental streaming parser for tool call JSON arguments.
Feeds partial JSON chunks (from ToolInputDelta.partial_json) and emits
structured ToolField* events as top-level object fields are discovered.
Top-level string values are decoded (escape sequences resolved, quotes stripped). All other top-level values are emitted as raw JSON fragments.
- class axio.tool_args.State(*values)[source]¶
Bases:
IntEnum- AFTER = 7¶
- COLON = 3¶
- ESC = 8¶
- INIT = 0¶
- KEY = 2¶
- OBJ = 1¶
- RAW = 6¶
- STR = 5¶
- UESC = 9¶
- VAL = 4¶
- class axio.tool_args.ToolArgStream(tool_use_id: str, index: int = 0)[source]¶
Bases:
objectO(1)-per-character streaming parser for tool argument JSON.
Usage:
stream = ToolArgStream("call_1") events = stream.feed('{"path":"/tmp/f') # [ToolFieldStart(0, "call_1", "path"), ToolFieldDelta(0, "call_1", "path", "/tmp/f")] events = stream.feed('oo.py"}') # [ToolFieldDelta(0, "call_1", "path", "oo.py"), ToolFieldEnd(0, "call_1", "path")]
- property current_key: str¶
The field currently being streamed, or
"".
- property done: bool¶
Whether the top-level JSON object has been fully parsed.
- feed(chunk: str) list[ToolFieldEvent][source]¶
Process a partial JSON chunk and return any field events produced.
- type axio.tool_args.ToolFieldEvent = ToolFieldStart | ToolFieldDelta | ToolFieldEnd¶
Context compaction: summarise old history to stay within token limits.
- class axio.compaction.AutoCompactStore(store: ContextStore, transport: CompletionTransport, *, keep_recent: int = 6, max_tokens: int | None = None, threshold: float = 0.75)[source]¶
Bases:
ContextStoreDelegating
ContextStorewrapper that auto-compacts the inner store when token usage exceeds a threshold. Works with anyContextStorebackend -MemoryContextStore,SQLiteContextStore, etc.The threshold defaults to 75 % of
transport.model.context_window(read viagetattr; falls back to 128 000 if the transport has nomodelattribute). Passmax_tokensexplicitly to override.Compaction fires from
add_context_tokens(), which the agent loop calls immediately afterIterationEnd.input_tokensthere equals the real context size sent to the model in that iteration.Internally,
_do_compact()forks the inner store before callingcompact_context. The fork acts as a stable snapshot. The summarisation agent reads from it while the live store remains writable. The live store is only cleared and repopulated after the (async) summary call returns.Example:
from axio.compaction import AutoCompactStore from axio.context import MemoryContextStore store = AutoCompactStore(MemoryContextStore(), transport, keep_recent=6) result = await agent.run(task, store)
- async fork() AutoCompactStore[source]¶
Return an
AutoCompactStorewrapping a fork of the inner store.
- async list_sessions() list[SessionInfo][source]¶
List available sessions. Default: returns a single entry for the current session.
- property session_id: str¶
Lazy-init UUID hex; works without calling super().__init__().
- async axio.compaction.compact_context(context: ContextStore, transport: CompletionTransport, *, keep_recent: int = 6, system_prompt: str = 'You are a conversation summarizer. You will see a conversation between a user and an AI assistant, including tool calls and their results. Produce a concise summary preserving: user goals, decisions made, key facts, tool outcomes, and state changes. Write as narrative prose, not as a transcript.') list[Message] | None[source]¶
Summarize old messages from context, keeping recent ones verbatim.
Returns a compacted message list ready to populate a fresh store, or
Noneif the history is too short to compact (split <= 0).The caller is responsible for deciding when to compact (e.g. by tracking
usage.input_tokensfromIterationEndevents).
Exception hierarchy for axio.
- exception axio.exceptions.GuardError[source]¶
Bases:
ToolErrorGuard denied or crashed during permission check.
Shared test helpers: StubTransport, fixtures, response builders.
- class axio.testing.StubTransport(responses: Sequence[Sequence[StreamEvent | BaseException]] | None = None)[source]¶
Bases:
objectA CompletionTransport that yields pre-configured event sequences.
Each call to stream() pops the next sequence from the list.
- stream(messages: list[Message], tools: list[Tool[Any]], system: str) AsyncIterator[StreamEvent][source]¶
- axio.testing.assert_static_model_transport_contract(transport: _StaticModelTransport) None[source]¶
Check invariants shared by transports with a ready, bundled model catalog.
- axio.testing.assert_stream_contract(events: Sequence[StreamEvent]) None[source]¶
Check what every
CompletionTransport.stream()must produce.A transport that breaks one of these still passes its own tests, because the agent papers over the difference. Call this from each transport’s tests on whatever its fake server produced.
- axio.testing.make_ephemeral_context() MemoryContextStore[source]¶
- axio.testing.make_stub_transport() StubTransport[source]¶
- axio.testing.make_text_response(text: str = 'Done', iteration: int = 2, usage: Usage | None = None) list[StreamEvent][source]¶
Build a standard end_turn text response event sequence.
- axio.testing.make_tool_use_response(tool_name: str = 'echo', tool_id: str = 'call_1', tool_input: dict[str, Any] | None = None, iteration: int = 1, usage: Usage | None = None) list[StreamEvent][source]¶
Build a standard tool_use response event sequence.