axio 0.6.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {axio-0.6.1 → axio-0.7.0}/.gitignore +2 -0
- {axio-0.6.1 → axio-0.7.0}/PKG-INFO +32 -25
- {axio-0.6.1 → axio-0.7.0}/README.md +29 -22
- {axio-0.6.1 → axio-0.7.0}/pyproject.toml +3 -3
- {axio-0.6.1 → axio-0.7.0}/src/axio/agent.py +12 -12
- axio-0.7.0/src/axio/agent_loader.py +386 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/blocks.py +1 -1
- axio-0.7.0/src/axio/compaction.py +167 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/context.py +2 -2
- {axio-0.6.1 → axio-0.7.0}/src/axio/events.py +1 -1
- {axio-0.6.1 → axio-0.7.0}/src/axio/messages.py +1 -1
- {axio-0.6.1 → axio-0.7.0}/src/axio/permission.py +12 -1
- {axio-0.6.1 → axio-0.7.0}/src/axio/selector.py +4 -4
- {axio-0.6.1 → axio-0.7.0}/src/axio/stream.py +2 -2
- {axio-0.6.1 → axio-0.7.0}/src/axio/testing.py +9 -9
- {axio-0.6.1 → axio-0.7.0}/src/axio/tool.py +10 -8
- {axio-0.6.1 → axio-0.7.0}/src/axio/tool_args.py +1 -1
- axio-0.7.0/src/axio/transport.py +107 -0
- {axio-0.6.1 → axio-0.7.0}/tests/conftest.py +3 -1
- {axio-0.6.1 → axio-0.7.0}/tests/test_agent_branch.py +7 -6
- axio-0.7.0/tests/test_agent_loader.py +342 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_agent_permission.py +12 -12
- {axio-0.6.1 → axio-0.7.0}/tests/test_agent_run.py +4 -3
- {axio-0.6.1 → axio-0.7.0}/tests/test_agent_tools.py +12 -12
- axio-0.7.0/tests/test_compaction.py +287 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_context.py +16 -16
- {axio-0.6.1 → axio-0.7.0}/tests/test_tool.py +9 -8
- {axio-0.6.1 → axio-0.7.0}/tests/test_transport.py +2 -1
- axio-0.6.1/src/axio/compaction.py +0 -67
- axio-0.6.1/src/axio/transport.py +0 -43
- {axio-0.6.1 → axio-0.7.0}/.github/workflows/publish.yml +0 -0
- {axio-0.6.1 → axio-0.7.0}/.github/workflows/tests.yml +0 -0
- {axio-0.6.1 → axio-0.7.0}/LICENSE +0 -0
- {axio-0.6.1 → axio-0.7.0}/Makefile +0 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/__init__.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/exceptions.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/models.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/py.typed +0 -0
- {axio-0.6.1 → axio-0.7.0}/src/axio/types.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_agent_stream.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_blocks.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_events.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_exceptions.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_models.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_permission.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_stream.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_tool_args.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/tests/test_types.py +0 -0
- {axio-0.6.1 → axio-0.7.0}/uv.lock +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Minimal, streaming-first, protocol-driven foundation for LLM-powered agents
|
|
5
|
-
Project-URL: Homepage, https://github.com/axio-agent
|
|
6
|
-
Project-URL: Repository, https://github.com/axio-agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/mosquito/axio-agent
|
|
6
|
+
Project-URL: Repository, https://github.com/mosquito/axio-agent
|
|
7
7
|
License: MIT
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Keywords: agent,ai,async,llm,protocol,pydantic,streaming
|
|
@@ -73,7 +73,7 @@ greet_tool = Tool(name="greet", description="Greet someone by name", handler=Gre
|
|
|
73
73
|
from axio_transport_openai import OpenAITransport
|
|
74
74
|
|
|
75
75
|
transport = OpenAITransport(api_key="sk-...", model="gpt-4o-mini")
|
|
76
|
-
agent = Agent(system="You are helpful.", tools=[greet_tool]
|
|
76
|
+
agent = Agent(system="You are helpful.", transport=transport, tools=[greet_tool])
|
|
77
77
|
|
|
78
78
|
# 3. Run
|
|
79
79
|
async def main() -> None:
|
|
@@ -131,31 +131,38 @@ class CompletionTransport(Protocol):
|
|
|
131
131
|
|
|
132
132
|
<!-- name: test_readme_context_store -->
|
|
133
133
|
```python
|
|
134
|
-
from abc import ABC, abstractmethod
|
|
135
134
|
from axio.context import ContextStore
|
|
136
135
|
from axio.messages import Message
|
|
137
136
|
|
|
138
|
-
class MyContextStore(ContextStore
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
class MyContextStore(ContextStore):
|
|
138
|
+
def __init__(self) -> None:
|
|
139
|
+
self._messages: list[Message] = []
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
async def append(self, message: Message) -> None:
|
|
142
|
+
self._messages.append(message)
|
|
143
|
+
|
|
144
|
+
async def get_history(self) -> list[Message]:
|
|
145
|
+
return list(self._messages)
|
|
144
146
|
|
|
145
147
|
# Everything else — session_id, close(), fork(), clear(),
|
|
146
|
-
# get/set_context_tokens()
|
|
148
|
+
# get/set_context_tokens(), add_context_tokens(), list_sessions()
|
|
149
|
+
# — has a default implementation.
|
|
147
150
|
```
|
|
148
151
|
|
|
149
152
|
### PermissionGuard
|
|
150
153
|
|
|
154
|
+
`PermissionGuard` is an abstract base class (ABC). Subclass it and implement
|
|
155
|
+
`check()`:
|
|
156
|
+
|
|
151
157
|
<!-- name: test_readme_permission_guard -->
|
|
152
158
|
```python
|
|
153
|
-
from typing import
|
|
154
|
-
from axio.
|
|
159
|
+
from typing import Any
|
|
160
|
+
from axio.permission import PermissionGuard
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
class MyGuard(PermissionGuard):
|
|
163
|
+
async def check(self, handler: Any) -> Any:
|
|
164
|
+
# return handler to allow, raise GuardError to deny
|
|
165
|
+
return handler
|
|
159
166
|
```
|
|
160
167
|
|
|
161
168
|
## Stream events
|
|
@@ -233,15 +240,15 @@ my_guard = "my_package:MyGuard"
|
|
|
233
240
|
|
|
234
241
|
| Package | Purpose |
|
|
235
242
|
|---|---|
|
|
236
|
-
| [axio-transport-anthropic](https://github.com/axio-agent
|
|
237
|
-
| [axio-transport-openai](https://github.com/axio-agent
|
|
238
|
-
| [axio-transport-codex](https://github.com/axio-agent
|
|
239
|
-
| [axio-
|
|
240
|
-
| [axio-tools-
|
|
241
|
-
| [axio-tools-
|
|
242
|
-
| [axio-
|
|
243
|
-
| [axio-tui
|
|
244
|
-
| [axio-tui-guards](https://github.com/axio-agent
|
|
243
|
+
| [axio-transport-anthropic](https://github.com/mosquito/axio-agent) | Anthropic Claude transport |
|
|
244
|
+
| [axio-transport-openai](https://github.com/mosquito/axio-agent) | OpenAI-compatible transport (OpenAI, Nebius, OpenRouter, custom) |
|
|
245
|
+
| [axio-transport-codex](https://github.com/mosquito/axio-agent) | ChatGPT OAuth transport |
|
|
246
|
+
| [axio-context-sqlite](https://github.com/mosquito/axio-agent) | SQLite-backed persistent context store |
|
|
247
|
+
| [axio-tools-local](https://github.com/mosquito/axio-agent) | Shell, file, Python tools |
|
|
248
|
+
| [axio-tools-mcp](https://github.com/mosquito/axio-agent) | MCP server bridge |
|
|
249
|
+
| [axio-tools-docker](https://github.com/mosquito/axio-agent) | Docker sandbox tools |
|
|
250
|
+
| [axio-tui](https://github.com/mosquito/axio-agent) | Textual TUI application |
|
|
251
|
+
| [axio-tui-guards](https://github.com/mosquito/axio-agent) | Permission guard plugins |
|
|
245
252
|
|
|
246
253
|
## License
|
|
247
254
|
|
|
@@ -60,7 +60,7 @@ greet_tool = Tool(name="greet", description="Greet someone by name", handler=Gre
|
|
|
60
60
|
from axio_transport_openai import OpenAITransport
|
|
61
61
|
|
|
62
62
|
transport = OpenAITransport(api_key="sk-...", model="gpt-4o-mini")
|
|
63
|
-
agent = Agent(system="You are helpful.", tools=[greet_tool]
|
|
63
|
+
agent = Agent(system="You are helpful.", transport=transport, tools=[greet_tool])
|
|
64
64
|
|
|
65
65
|
# 3. Run
|
|
66
66
|
async def main() -> None:
|
|
@@ -118,31 +118,38 @@ class CompletionTransport(Protocol):
|
|
|
118
118
|
|
|
119
119
|
<!-- name: test_readme_context_store -->
|
|
120
120
|
```python
|
|
121
|
-
from abc import ABC, abstractmethod
|
|
122
121
|
from axio.context import ContextStore
|
|
123
122
|
from axio.messages import Message
|
|
124
123
|
|
|
125
|
-
class MyContextStore(ContextStore
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
class MyContextStore(ContextStore):
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
self._messages: list[Message] = []
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
async def append(self, message: Message) -> None:
|
|
129
|
+
self._messages.append(message)
|
|
130
|
+
|
|
131
|
+
async def get_history(self) -> list[Message]:
|
|
132
|
+
return list(self._messages)
|
|
131
133
|
|
|
132
134
|
# Everything else — session_id, close(), fork(), clear(),
|
|
133
|
-
# get/set_context_tokens()
|
|
135
|
+
# get/set_context_tokens(), add_context_tokens(), list_sessions()
|
|
136
|
+
# — has a default implementation.
|
|
134
137
|
```
|
|
135
138
|
|
|
136
139
|
### PermissionGuard
|
|
137
140
|
|
|
141
|
+
`PermissionGuard` is an abstract base class (ABC). Subclass it and implement
|
|
142
|
+
`check()`:
|
|
143
|
+
|
|
138
144
|
<!-- name: test_readme_permission_guard -->
|
|
139
145
|
```python
|
|
140
|
-
from typing import
|
|
141
|
-
from axio.
|
|
146
|
+
from typing import Any
|
|
147
|
+
from axio.permission import PermissionGuard
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
class MyGuard(PermissionGuard):
|
|
150
|
+
async def check(self, handler: Any) -> Any:
|
|
151
|
+
# return handler to allow, raise GuardError to deny
|
|
152
|
+
return handler
|
|
146
153
|
```
|
|
147
154
|
|
|
148
155
|
## Stream events
|
|
@@ -220,15 +227,15 @@ my_guard = "my_package:MyGuard"
|
|
|
220
227
|
|
|
221
228
|
| Package | Purpose |
|
|
222
229
|
|---|---|
|
|
223
|
-
| [axio-transport-anthropic](https://github.com/axio-agent
|
|
224
|
-
| [axio-transport-openai](https://github.com/axio-agent
|
|
225
|
-
| [axio-transport-codex](https://github.com/axio-agent
|
|
226
|
-
| [axio-
|
|
227
|
-
| [axio-tools-
|
|
228
|
-
| [axio-tools-
|
|
229
|
-
| [axio-
|
|
230
|
-
| [axio-tui
|
|
231
|
-
| [axio-tui-guards](https://github.com/axio-agent
|
|
230
|
+
| [axio-transport-anthropic](https://github.com/mosquito/axio-agent) | Anthropic Claude transport |
|
|
231
|
+
| [axio-transport-openai](https://github.com/mosquito/axio-agent) | OpenAI-compatible transport (OpenAI, Nebius, OpenRouter, custom) |
|
|
232
|
+
| [axio-transport-codex](https://github.com/mosquito/axio-agent) | ChatGPT OAuth transport |
|
|
233
|
+
| [axio-context-sqlite](https://github.com/mosquito/axio-agent) | SQLite-backed persistent context store |
|
|
234
|
+
| [axio-tools-local](https://github.com/mosquito/axio-agent) | Shell, file, Python tools |
|
|
235
|
+
| [axio-tools-mcp](https://github.com/mosquito/axio-agent) | MCP server bridge |
|
|
236
|
+
| [axio-tools-docker](https://github.com/mosquito/axio-agent) | Docker sandbox tools |
|
|
237
|
+
| [axio-tui](https://github.com/mosquito/axio-agent) | Textual TUI application |
|
|
238
|
+
| [axio-tui-guards](https://github.com/mosquito/axio-agent) | Permission guard plugins |
|
|
232
239
|
|
|
233
240
|
## License
|
|
234
241
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "axio"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Minimal, streaming-first, protocol-driven foundation for LLM-powered agents"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -10,8 +10,8 @@ dependencies = ["pydantic>=2"]
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
13
|
-
Homepage = "https://github.com/axio-agent
|
|
14
|
-
Repository = "https://github.com/axio-agent
|
|
13
|
+
Homepage = "https://github.com/mosquito/axio-agent"
|
|
14
|
+
Repository = "https://github.com/mosquito/axio-agent"
|
|
15
15
|
[build-system]
|
|
16
16
|
requires = ["hatchling"]
|
|
17
17
|
build-backend = "hatchling.build"
|
|
@@ -10,9 +10,9 @@ from collections.abc import AsyncGenerator, Iterable
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any, Self
|
|
12
12
|
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
13
|
+
from .blocks import TextBlock, ToolResultBlock, ToolUseBlock
|
|
14
|
+
from .context import ContextStore
|
|
15
|
+
from .events import (
|
|
16
16
|
Error,
|
|
17
17
|
IterationEnd,
|
|
18
18
|
SessionEndEvent,
|
|
@@ -22,12 +22,12 @@ from axio.events import (
|
|
|
22
22
|
ToolResult,
|
|
23
23
|
ToolUseStart,
|
|
24
24
|
)
|
|
25
|
-
from
|
|
26
|
-
from
|
|
27
|
-
from
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
from
|
|
25
|
+
from .messages import Message
|
|
26
|
+
from .selector import ToolSelector
|
|
27
|
+
from .stream import AgentStream
|
|
28
|
+
from .tool import Tool
|
|
29
|
+
from .transport import CompletionTransport
|
|
30
|
+
from .types import StopReason, Usage
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
|
|
36
36
|
class Agent:
|
|
37
37
|
system: str
|
|
38
38
|
transport: CompletionTransport
|
|
39
|
-
tools: list[Tool] = field(default_factory=list)
|
|
39
|
+
tools: list[Tool[Any]] = field(default_factory=list)
|
|
40
40
|
selector: ToolSelector | None = field(default=None)
|
|
41
41
|
max_iterations: int = field(default=50)
|
|
42
42
|
last_iteration_message: Message | None = field(default=None)
|
|
@@ -74,7 +74,7 @@ class Agent:
|
|
|
74
74
|
logger.info("Tools complete: %d total, %d errors", len(results), error_count)
|
|
75
75
|
return results
|
|
76
76
|
|
|
77
|
-
def _find_tool(self, name: str) -> Tool | None:
|
|
77
|
+
def _find_tool(self, name: str) -> Tool[Any] | None:
|
|
78
78
|
for tool in self.tools:
|
|
79
79
|
if tool.name == name:
|
|
80
80
|
return tool
|
|
@@ -128,7 +128,7 @@ class Agent:
|
|
|
128
128
|
blocks.append(ToolUseBlock(id=tid, name=info["name"], input=inp))
|
|
129
129
|
return blocks, malformed
|
|
130
130
|
|
|
131
|
-
async def _select_tools(self, history: list[Message], tools: list[Tool]) -> Iterable[Tool]:
|
|
131
|
+
async def _select_tools(self, history: list[Message], tools: list[Tool[Any]]) -> Iterable[Tool[Any]]:
|
|
132
132
|
if not tools:
|
|
133
133
|
return []
|
|
134
134
|
if not self.selector:
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Load Agent prototypes from declarative config files (TOML, JSON, INI).
|
|
2
|
+
|
|
3
|
+
Each file describes one agent. The loader scans a directory, parses every
|
|
4
|
+
recognised file, resolves tool names against a caller-supplied toolbox, and
|
|
5
|
+
returns a ``dict[str, tuple[str, Agent]]`` that matches the shape used by
|
|
6
|
+
``agent_swarm`` and similar orchestrators.
|
|
7
|
+
|
|
8
|
+
Supported formats
|
|
9
|
+
-----------------
|
|
10
|
+
TOML::
|
|
11
|
+
|
|
12
|
+
name = "architect"
|
|
13
|
+
description = "System design and interface specs"
|
|
14
|
+
max_iterations = 100
|
|
15
|
+
tools = ["read_file", "write_file"]
|
|
16
|
+
|
|
17
|
+
[system]
|
|
18
|
+
text = \"\"\"
|
|
19
|
+
You are an expert software architect...
|
|
20
|
+
\"\"\"
|
|
21
|
+
|
|
22
|
+
JSON::
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
"name": "architect",
|
|
26
|
+
"description": "System design and interface specs",
|
|
27
|
+
"max_iterations": 100,
|
|
28
|
+
"tools": ["read_file", "write_file"],
|
|
29
|
+
"system": "You are an expert software architect..."
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
INI::
|
|
33
|
+
|
|
34
|
+
[agent]
|
|
35
|
+
name = architect
|
|
36
|
+
description = System design and interface specs
|
|
37
|
+
max_iterations = 100
|
|
38
|
+
tools = read_file, write_file
|
|
39
|
+
|
|
40
|
+
[system]
|
|
41
|
+
text = You are an expert software architect...
|
|
42
|
+
|
|
43
|
+
In all formats ``name`` falls back to the file stem when omitted. ``system``
|
|
44
|
+
may be a plain string or a ``{"text": "..."}`` mapping (TOML/JSON). INI tools
|
|
45
|
+
are comma-separated.
|
|
46
|
+
|
|
47
|
+
Custom sources
|
|
48
|
+
--------------
|
|
49
|
+
Subclass :class:`AgentLoader` and implement :meth:`~AgentLoader.load` — the
|
|
50
|
+
base :meth:`~AgentLoader.load_file` will handle reading the file and calling
|
|
51
|
+
your implementation automatically::
|
|
52
|
+
|
|
53
|
+
class DbAgentLoader(AgentLoader):
|
|
54
|
+
def load(self, content: str) -> AgentSpec:
|
|
55
|
+
row = json.loads(content)
|
|
56
|
+
return AgentSpec(name=row["name"], ...)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import configparser
|
|
62
|
+
import dataclasses
|
|
63
|
+
import json
|
|
64
|
+
import tomllib
|
|
65
|
+
from collections.abc import Callable, Mapping
|
|
66
|
+
from dataclasses import dataclass, field
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from types import MappingProxyType
|
|
69
|
+
from typing import Annotated, Any, TypedDict
|
|
70
|
+
|
|
71
|
+
from pydantic import Field
|
|
72
|
+
|
|
73
|
+
from .agent import Agent
|
|
74
|
+
from .context import ContextStore, MemoryContextStore
|
|
75
|
+
from .events import StreamEvent, TextDelta
|
|
76
|
+
from .tool import Tool, ToolHandler
|
|
77
|
+
from .transport import CompletionTransport, DummyCompletionTransport
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class AgentSpec:
|
|
82
|
+
"""Parsed agent definition before transport/tools are injected."""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
description: str
|
|
86
|
+
system: str
|
|
87
|
+
max_iterations: int = 50
|
|
88
|
+
tools: tuple[str, ...] = ()
|
|
89
|
+
model: str | None = None
|
|
90
|
+
|
|
91
|
+
def to_agent(self, toolbox: Mapping[str, Tool[Any]] = MappingProxyType({})) -> Agent:
|
|
92
|
+
"""Return a prototype Agent with *toolbox* tools attached.
|
|
93
|
+
|
|
94
|
+
The agent uses :class:`~axio.transport.DummyCompletionTransport` —
|
|
95
|
+
call ``agent.copy(transport=real_transport)`` before running it.
|
|
96
|
+
|
|
97
|
+
Raises :exc:`KeyError` if any name in ``self.tools`` is absent from
|
|
98
|
+
*toolbox*.
|
|
99
|
+
"""
|
|
100
|
+
resolved: list[Tool[Any]] = []
|
|
101
|
+
for name in self.tools:
|
|
102
|
+
if name not in toolbox:
|
|
103
|
+
raise KeyError(f"Tool {name!r} not found in toolbox")
|
|
104
|
+
resolved.append(toolbox[name])
|
|
105
|
+
return Agent(
|
|
106
|
+
system=self.system,
|
|
107
|
+
transport=DummyCompletionTransport(),
|
|
108
|
+
tools=resolved,
|
|
109
|
+
max_iterations=self.max_iterations,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AgentLoader:
|
|
114
|
+
"""Base class for format-specific agent loaders.
|
|
115
|
+
|
|
116
|
+
Subclasses implement :meth:`load` to parse a raw string. The source of
|
|
117
|
+
that string is entirely up to the caller — files, databases, HTTP, etc.
|
|
118
|
+
:meth:`load_file` is provided on the base class and calls :meth:`load`
|
|
119
|
+
automatically.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
extensions: tuple[str, ...] = ()
|
|
123
|
+
|
|
124
|
+
def load(self, content: str) -> AgentSpec:
|
|
125
|
+
"""Parse *content* and return an :class:`AgentSpec`.
|
|
126
|
+
|
|
127
|
+
``name`` defaults to ``""`` when not present; :meth:`load_file`
|
|
128
|
+
patches it from the file stem after calling this method.
|
|
129
|
+
"""
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
|
|
132
|
+
def load_file(self, path: Path) -> AgentSpec:
|
|
133
|
+
"""Read *path* and delegate to :meth:`load`, patching ``name`` from the stem.
|
|
134
|
+
|
|
135
|
+
Any :exc:`ValueError` raised by :meth:`load` is re-raised with *path*
|
|
136
|
+
prepended so errors are easy to locate.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
spec = self.load(path.read_text(encoding="utf-8"))
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
raise ValueError(f"{path}: {exc}") from exc
|
|
142
|
+
if not spec.name:
|
|
143
|
+
return dataclasses.replace(spec, name=path.stem)
|
|
144
|
+
return spec
|
|
145
|
+
|
|
146
|
+
def scan(
|
|
147
|
+
self,
|
|
148
|
+
directory: Path,
|
|
149
|
+
toolbox: Mapping[str, Tool[Any]] = MappingProxyType({}),
|
|
150
|
+
) -> dict[str, tuple[str, Agent]]:
|
|
151
|
+
"""Scan *directory* for agent files and return a name→(description, agent) dict."""
|
|
152
|
+
result: dict[str, tuple[str, Agent]] = {}
|
|
153
|
+
for ext in self.extensions:
|
|
154
|
+
for path in sorted(directory.glob(f"*.{ext}")):
|
|
155
|
+
spec = self.load_file(path)
|
|
156
|
+
result[spec.name] = (spec.description, spec.to_agent(toolbox))
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def _parse_dict(self, data: dict[str, object]) -> AgentSpec:
|
|
160
|
+
"""Normalise a parsed dict (any format) into an :class:`AgentSpec`."""
|
|
161
|
+
name = str(data.get("name", ""))
|
|
162
|
+
description = str(data.get("description", ""))
|
|
163
|
+
|
|
164
|
+
system_raw = data.get("system", "")
|
|
165
|
+
if isinstance(system_raw, dict):
|
|
166
|
+
system = str(system_raw.get("text", ""))
|
|
167
|
+
else:
|
|
168
|
+
system = str(system_raw)
|
|
169
|
+
|
|
170
|
+
raw_iter = data.get("max_iterations", 50)
|
|
171
|
+
max_iterations = int(raw_iter) # type: ignore[call-overload]
|
|
172
|
+
|
|
173
|
+
tools_raw = data.get("tools", ())
|
|
174
|
+
tools = tuple(tools_raw) if isinstance(tools_raw, (list, tuple)) else ()
|
|
175
|
+
|
|
176
|
+
model_raw = data.get("model")
|
|
177
|
+
model = str(model_raw) if model_raw is not None else None
|
|
178
|
+
|
|
179
|
+
return AgentSpec(
|
|
180
|
+
name=name,
|
|
181
|
+
description=description,
|
|
182
|
+
system=system,
|
|
183
|
+
max_iterations=max_iterations,
|
|
184
|
+
tools=tools,
|
|
185
|
+
model=model,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TomlAgentLoader(AgentLoader):
|
|
190
|
+
"""Load agent definitions from ``.toml`` files."""
|
|
191
|
+
|
|
192
|
+
extensions = ("toml",)
|
|
193
|
+
|
|
194
|
+
def load(self, content: str) -> AgentSpec:
|
|
195
|
+
try:
|
|
196
|
+
data = tomllib.loads(content)
|
|
197
|
+
except tomllib.TOMLDecodeError as exc:
|
|
198
|
+
raise ValueError(f"Invalid TOML: {exc}") from exc
|
|
199
|
+
return self._parse_dict(data)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class JsonAgentLoader(AgentLoader):
|
|
203
|
+
"""Load agent definitions from ``.json`` files."""
|
|
204
|
+
|
|
205
|
+
extensions = ("json",)
|
|
206
|
+
|
|
207
|
+
def load(self, content: str) -> AgentSpec:
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(content)
|
|
210
|
+
except json.JSONDecodeError as exc:
|
|
211
|
+
raise ValueError(f"Invalid JSON: {exc}") from exc
|
|
212
|
+
if not isinstance(data, dict):
|
|
213
|
+
raise ValueError(f"Expected a JSON object, got {type(data).__name__}")
|
|
214
|
+
return self._parse_dict(data)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class IniAgentLoader(AgentLoader):
|
|
218
|
+
"""Load agent definitions from ``.ini`` files.
|
|
219
|
+
|
|
220
|
+
Expected sections: ``[agent]`` for metadata, ``[system]`` for the prompt.
|
|
221
|
+
The ``tools`` key is comma-separated. Multiline system prompts work via
|
|
222
|
+
standard ConfigParser continuation (indent subsequent lines).
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
extensions = ("ini",)
|
|
226
|
+
|
|
227
|
+
def load(self, content: str) -> AgentSpec:
|
|
228
|
+
cp = configparser.ConfigParser(interpolation=None)
|
|
229
|
+
cp.read_string(content)
|
|
230
|
+
|
|
231
|
+
agent_section = cp["agent"] if cp.has_section("agent") else {}
|
|
232
|
+
system_section = cp["system"] if cp.has_section("system") else {}
|
|
233
|
+
|
|
234
|
+
name = agent_section.get("name", "")
|
|
235
|
+
description = agent_section.get("description", "")
|
|
236
|
+
max_iterations = int(agent_section.get("max_iterations", 50))
|
|
237
|
+
model_raw = agent_section.get("model")
|
|
238
|
+
model = model_raw if model_raw else None
|
|
239
|
+
system = system_section.get("text", agent_section.get("system", ""))
|
|
240
|
+
tools_raw = agent_section.get("tools", "")
|
|
241
|
+
tools = tuple(t.strip() for t in tools_raw.split(",") if t.strip()) if tools_raw else ()
|
|
242
|
+
|
|
243
|
+
return AgentSpec(
|
|
244
|
+
name=name,
|
|
245
|
+
description=description,
|
|
246
|
+
system=system,
|
|
247
|
+
max_iterations=max_iterations,
|
|
248
|
+
tools=tools,
|
|
249
|
+
model=model,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class MultiFormatLoader:
|
|
255
|
+
"""Aggregate loader that handles TOML, JSON, and INI files."""
|
|
256
|
+
|
|
257
|
+
loaders: list[AgentLoader] = field(
|
|
258
|
+
default_factory=lambda: [TomlAgentLoader(), JsonAgentLoader(), IniAgentLoader()]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def scan(
|
|
262
|
+
self,
|
|
263
|
+
directory: Path,
|
|
264
|
+
toolbox: Mapping[str, Tool[Any]] = MappingProxyType({}),
|
|
265
|
+
) -> dict[str, tuple[str, Agent]]:
|
|
266
|
+
"""Scan *directory* with all registered loaders, later loaders win on name collision."""
|
|
267
|
+
result: dict[str, tuple[str, Agent]] = {}
|
|
268
|
+
for loader in self.loaders:
|
|
269
|
+
result.update(loader.scan(directory, toolbox))
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def load_agents(
|
|
274
|
+
directory: Path,
|
|
275
|
+
toolbox: Mapping[str, Tool[Any]] = MappingProxyType({}),
|
|
276
|
+
) -> dict[str, tuple[str, Agent]]:
|
|
277
|
+
"""Scan *directory* for ``.toml``, ``.json``, and ``.ini`` agent files.
|
|
278
|
+
|
|
279
|
+
Returns ``dict[name, (description, agent)]`` — same shape as the
|
|
280
|
+
``AGENTS`` registry used in ``agent_swarm`` and similar examples.
|
|
281
|
+
|
|
282
|
+
Example::
|
|
283
|
+
|
|
284
|
+
from pathlib import Path
|
|
285
|
+
from axio.agent_loader import load_agents
|
|
286
|
+
|
|
287
|
+
AGENTS = load_agents(
|
|
288
|
+
Path(__file__).parent / "roles",
|
|
289
|
+
toolbox={"read_file": read_file_tool, "write_file": write_file_tool},
|
|
290
|
+
)
|
|
291
|
+
"""
|
|
292
|
+
return MultiFormatLoader().scan(directory, toolbox)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class AgentContext(TypedDict):
|
|
296
|
+
agent_name: str
|
|
297
|
+
proto: Agent
|
|
298
|
+
transport: CompletionTransport
|
|
299
|
+
context_factory: Callable[[], ContextStore]
|
|
300
|
+
on_event: Callable[[str, StreamEvent], None] | None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class AgentTool(ToolHandler[AgentContext]):
|
|
304
|
+
"""Handler for a single delegate agent — receives runtime deps via Tool.context."""
|
|
305
|
+
|
|
306
|
+
task: Annotated[str, Field(description="Full task instructions.")]
|
|
307
|
+
|
|
308
|
+
async def __call__(
|
|
309
|
+
self,
|
|
310
|
+
context: AgentContext,
|
|
311
|
+
) -> str:
|
|
312
|
+
agent = context["proto"].copy(transport=context["transport"])
|
|
313
|
+
ctx = context["context_factory"]()
|
|
314
|
+
if context["on_event"] is None:
|
|
315
|
+
return await agent.run(self.task, ctx)
|
|
316
|
+
parts: list[str] = []
|
|
317
|
+
async for event in agent.run_stream(self.task, ctx):
|
|
318
|
+
context["on_event"](context["agent_name"], event)
|
|
319
|
+
if isinstance(event, TextDelta):
|
|
320
|
+
parts.append(event.delta)
|
|
321
|
+
return "".join(parts)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def make_agent_tools(
|
|
325
|
+
agents: dict[str, tuple[str, Agent]],
|
|
326
|
+
transport: CompletionTransport,
|
|
327
|
+
context_factory: Callable[[], ContextStore] = MemoryContextStore,
|
|
328
|
+
on_event: Callable[[str, StreamEvent], None] | None = None,
|
|
329
|
+
agent_name_prefix: str = "",
|
|
330
|
+
) -> list[Tool[AgentContext]]:
|
|
331
|
+
"""Convert each agent into its own :class:`~axio.tool.Tool`.
|
|
332
|
+
|
|
333
|
+
Each tool is named after the agent and accepts a single ``task`` field.
|
|
334
|
+
Runtime dependencies (transport, context factory, event callback) are
|
|
335
|
+
stored in :attr:`~axio.tool.Tool.context` and injected on each call.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
agents:
|
|
340
|
+
``dict[name, (description, prototype_agent)]`` — e.g. the value
|
|
341
|
+
returned by :func:`load_agents`.
|
|
342
|
+
transport:
|
|
343
|
+
Transport assigned to the selected agent via ``agent.copy()``.
|
|
344
|
+
context_factory:
|
|
345
|
+
Called once per invocation to produce a fresh
|
|
346
|
+
:class:`~axio.context.ContextStore`. Defaults to
|
|
347
|
+
:class:`~axio.context.MemoryContextStore`.
|
|
348
|
+
on_event:
|
|
349
|
+
Optional callback receiving ``(agent_name, event)`` for every
|
|
350
|
+
:class:`~axio.events.StreamEvent` the agent emits.
|
|
351
|
+
agent_name_prefix:
|
|
352
|
+
Prefix to prepend to each agent name.
|
|
353
|
+
|
|
354
|
+
Example::
|
|
355
|
+
|
|
356
|
+
from axio.agent_loader import load_agents, make_agent_tools
|
|
357
|
+
|
|
358
|
+
agents = load_agents(Path("roles"), toolbox={"read_file": read_file_tool})
|
|
359
|
+
tools = make_agent_tools(agents, transport=my_transport)
|
|
360
|
+
orchestrator = Agent(system="...", transport=my_transport, tools=tools)
|
|
361
|
+
"""
|
|
362
|
+
result = []
|
|
363
|
+
for agent_name, (desc, proto) in agents.items():
|
|
364
|
+
tool = Tool(
|
|
365
|
+
name=f"{agent_name_prefix}{agent_name}",
|
|
366
|
+
description=desc,
|
|
367
|
+
handler=AgentTool,
|
|
368
|
+
context=AgentContext(
|
|
369
|
+
proto=proto,
|
|
370
|
+
transport=transport,
|
|
371
|
+
context_factory=context_factory,
|
|
372
|
+
on_event=on_event,
|
|
373
|
+
agent_name=agent_name,
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
result.append(tool)
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def load_agents_from_dir(
|
|
381
|
+
directory: Path,
|
|
382
|
+
transport: CompletionTransport,
|
|
383
|
+
context_factory: Callable[[], ContextStore] = MemoryContextStore,
|
|
384
|
+
on_event: Callable[[str, StreamEvent], None] | None = None,
|
|
385
|
+
) -> list[Tool[AgentContext]]:
|
|
386
|
+
return make_agent_tools(load_agents(directory), transport, context_factory, on_event)
|