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)[source]
Bases:
object- copy(**overrides: Any) Self[source]
Return a new Agent with overrides applied.
- async dispatch_tools(blocks: list[ToolUseBlock], iteration: int) list[ToolResultBlock][source]
- last_iteration_message: Message | None
- max_iterations: int
- async run(user_message: str, context: ContextStore) str[source]
- run_stream(user_message: str, context: ContextStore) AgentStream[source]
- selector: ToolSelector | None
- system: str
- 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({}))[source]
Bases:
Generic- 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
- 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)[source]¶
Bases:
object- async dispatch_tools(blocks: list[ToolUseBlock], iteration: int) list[ToolResultBlock][source]¶
- max_iterations: int¶
- async run(user_message: str, context: ContextStore) str[source]¶
- run_stream(user_message: str, context: ContextStore) AgentStream[source]¶
- selector: ToolSelector | None¶
- system: str¶
- 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({}))[source]¶
Bases:
Generic- 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¶
- schema: MappingProxyType¶
- property supports_streaming: bool¶
Handler supports streaming if it exposes a
.streamasync-generator attribute.
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 protocols: completion, image gen, TTS, STT.
Transports should be stateless - all request state lives in the arguments passed to each method call. This allows multiple agents to share a single transport instance and call it concurrently without interference.
The one allowed exception is a reusable connection pool (e.g. an
aiohttp.ClientSession), which is safe to share across concurrent calls.
- 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.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.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.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.
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_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.
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_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.