Plugin System¶
Axio uses Python’s entry point groups for plugin discovery. Packages register their components as entry points, and the framework discovers them at startup - no import-time coupling, no centralized registry.
Entry point groups¶
flowchart TD
EP["Entry Points in pyproject.toml"]
EP --> AT["axio.tools - Tool handler functions"]
EP --> ATS["axio.tools.settings - ToolsPlugin (dynamic providers)"]
EP --> ATR["axio.transport - CompletionTransport classes"]
EP --> ATRS["axio.transport.settings - Transport settings screens"]
EP --> AG["axio.guards - PermissionGuard classes"]
EP --> AS["axio.selector - ToolSelector classes"]
Group |
Registers |
Example |
|---|---|---|
|
Individual tool handler functions |
|
|
|
|
|
|
|
|
Transport settings UI screens |
|
|
|
|
|
|
|
Registering entry points¶
In your package’s pyproject.toml:
[project.entry-points."axio.tools"]
my_tool = "my_package.tools:my_tool"
[project.entry-points."axio.transport"]
my_transport = "my_package.transport:MyTransport"
[project.entry-points."axio.guards"]
my_guard = "my_package.guards:MyGuard"
[project.entry-points."axio.selector"]
my_selector = "my_package.selector:MySelector"
After installing the package (or running uv sync in the workspace), Axio
will automatically discover and load your components.
Discovery functions¶
The axio_tui.plugin module provides discovery functions:
from axio import Tool, CompletionTransport, PermissionGuard
def discover_tools() -> list[Tool]:
"""Load all tools from the axio.tools entry point group."""
def discover_tools_by_package() -> dict[str, list[Tool]]:
"""Return tools from axio.tools entry points grouped by distribution package name."""
def discover_tools_plugins() -> dict[str, "ToolsPlugin"]:
"""Load and instantiate tool plugins from axio.tools.settings."""
def discover_transports() -> dict[str, type]:
"""Load transport classes from axio.transport."""
def discover_transport_settings() -> dict[str, type]:
"""Load settings screen classes from axio.transport.settings."""
def discover_selectors() -> dict[str, type]:
"""Return selector classes from axio.selector entry points."""
def discover_guards() -> dict[str, type[PermissionGuard]]:
"""Load all guards from axio.guards."""
The functions that return dictionaries key the results by the entry point
name. discover_tools() returns a flat list[Tool] while
discover_tools_by_package() returns the same tools grouped by distribution
package name.
ToolsPlugin protocol¶
For packages that provide a dynamic set of tools (like MCP or Docker
sandboxes), implement the ToolsPlugin protocol. Unlike static axio.tools
entries (one handler per entry point), a ToolsPlugin can return any number
of tools based on runtime configuration, and it integrates with the TUI’s
settings screens.
The full protocol (defined in axio_tui.plugin) is:
from typing import Any, Protocol, runtime_checkable
from axio import Tool
@runtime_checkable
class ToolsPlugin(Protocol):
"""Protocol for dynamic tool provider plugins.
Plugins register via the ``axio.tools.settings`` entry point group.
The TUI discovers them, calls ``init()``, collects tools, and shows
settings screens - without knowing anything about the plugin internals.
"""
@property
def label(self) -> str:
"""Human-readable display name for the plugin (shown in the TUI)."""
...
async def init(self, config: Any = None, global_config: Any = None) -> None:
"""Initialise the plugin, optionally with saved config."""
...
@property
def all_tools(self) -> list[Tool]:
"""Return the current list of tools this plugin provides."""
...
def settings_screen(self) -> Any:
"""Return a Textual Screen (or compatible object) for configuring this plugin."""
...
async def close(self) -> None:
"""Tear down connections or resources held by the plugin."""
...
The TUI lifecycle for a plugin is:
discover_tools_plugins()instantiates the class (no arguments).await plugin.init(config, global_config)is called with any saved configuration.plugin.all_toolsis read to obtain the tools to register with the agent.plugin.settings_screen()is called when the user opens the plugin’s settings in the TUI.await plugin.close()is called on shutdown.
Register a plugin under axio.tools.settings:
[project.entry-points."axio.tools.settings"]
my_plugin = "my_package.plugin:MyPlugin"
Transport display name¶
Each transport class declares its display name via a name: str field:
import os
from dataclasses import dataclass, field
from axio import CompletionTransport
@dataclass(slots=True)
class MyTransport(CompletionTransport):
name: str = "My Provider"
api_key: str = field(default_factory=lambda: os.environ.get("MY_API_KEY", ""))
...
The TUI uses transport.name to label the transport in the welcome screen
and command palette. A transport is considered available when its
fetch_models() call succeeds; if it raises (e.g. because no API key is
present), the transport is shown as unavailable. API key lookup is each
transport’s own responsibility - typically via a field(default_factory=...)
that reads the appropriate environment variable.