axio 0.6.2__tar.gz → 0.8.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.2 → axio-0.8.0}/PKG-INFO +12 -12
- {axio-0.6.2 → axio-0.8.0}/README.md +9 -9
- {axio-0.6.2 → axio-0.8.0}/pyproject.toml +3 -3
- {axio-0.6.2 → axio-0.8.0}/src/axio/agent.py +12 -12
- axio-0.8.0/src/axio/agent_loader.py +386 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/blocks.py +1 -1
- axio-0.8.0/src/axio/compaction.py +167 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/context.py +2 -2
- {axio-0.6.2 → axio-0.8.0}/src/axio/events.py +1 -1
- {axio-0.6.2 → axio-0.8.0}/src/axio/messages.py +1 -1
- {axio-0.6.2 → axio-0.8.0}/src/axio/permission.py +12 -1
- {axio-0.6.2 → axio-0.8.0}/src/axio/selector.py +4 -4
- {axio-0.6.2 → axio-0.8.0}/src/axio/stream.py +2 -2
- {axio-0.6.2 → axio-0.8.0}/src/axio/testing.py +9 -9
- {axio-0.6.2 → axio-0.8.0}/src/axio/tool.py +10 -8
- {axio-0.6.2 → axio-0.8.0}/src/axio/tool_args.py +1 -1
- axio-0.8.0/src/axio/transport.py +107 -0
- {axio-0.6.2 → axio-0.8.0}/tests/conftest.py +3 -1
- {axio-0.6.2 → axio-0.8.0}/tests/test_agent_branch.py +7 -6
- axio-0.8.0/tests/test_agent_loader.py +342 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_agent_permission.py +12 -12
- {axio-0.6.2 → axio-0.8.0}/tests/test_agent_run.py +4 -3
- {axio-0.6.2 → axio-0.8.0}/tests/test_agent_tools.py +12 -12
- axio-0.8.0/tests/test_compaction.py +287 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_context.py +16 -16
- {axio-0.6.2 → axio-0.8.0}/tests/test_tool.py +9 -8
- {axio-0.6.2 → axio-0.8.0}/tests/test_transport.py +2 -1
- axio-0.6.2/src/axio/compaction.py +0 -67
- axio-0.6.2/src/axio/transport.py +0 -43
- {axio-0.6.2 → axio-0.8.0}/.github/workflows/publish.yml +0 -0
- {axio-0.6.2 → axio-0.8.0}/.github/workflows/tests.yml +0 -0
- {axio-0.6.2 → axio-0.8.0}/.gitignore +0 -0
- {axio-0.6.2 → axio-0.8.0}/LICENSE +0 -0
- {axio-0.6.2 → axio-0.8.0}/Makefile +0 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/__init__.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/exceptions.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/models.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/py.typed +0 -0
- {axio-0.6.2 → axio-0.8.0}/src/axio/types.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_agent_stream.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_blocks.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_events.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_exceptions.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_models.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_permission.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_stream.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_tool_args.py +0 -0
- {axio-0.6.2 → axio-0.8.0}/tests/test_types.py +0 -0
- {axio-0.6.2 → axio-0.8.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.8.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
|
|
@@ -240,15 +240,15 @@ my_guard = "my_package:MyGuard"
|
|
|
240
240
|
|
|
241
241
|
| Package | Purpose |
|
|
242
242
|
|---|---|
|
|
243
|
-
| [axio-transport-anthropic](https://github.com/axio-agent
|
|
244
|
-
| [axio-transport-openai](https://github.com/axio-agent
|
|
245
|
-
| [axio-transport-codex](https://github.com/axio-agent
|
|
246
|
-
| [axio-context-sqlite](https://github.com/axio-agent
|
|
247
|
-
| [axio-tools-local](https://github.com/axio-agent
|
|
248
|
-
| [axio-tools-mcp](https://github.com/axio-agent
|
|
249
|
-
| [axio-tools-docker](https://github.com/axio-agent
|
|
250
|
-
| [axio-tui](https://github.com/axio-agent
|
|
251
|
-
| [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 |
|
|
252
252
|
|
|
253
253
|
## License
|
|
254
254
|
|
|
@@ -227,15 +227,15 @@ my_guard = "my_package:MyGuard"
|
|
|
227
227
|
|
|
228
228
|
| Package | Purpose |
|
|
229
229
|
|---|---|
|
|
230
|
-
| [axio-transport-anthropic](https://github.com/axio-agent
|
|
231
|
-
| [axio-transport-openai](https://github.com/axio-agent
|
|
232
|
-
| [axio-transport-codex](https://github.com/axio-agent
|
|
233
|
-
| [axio-context-sqlite](https://github.com/axio-agent
|
|
234
|
-
| [axio-tools-local](https://github.com/axio-agent
|
|
235
|
-
| [axio-tools-mcp](https://github.com/axio-agent
|
|
236
|
-
| [axio-tools-docker](https://github.com/axio-agent
|
|
237
|
-
| [axio-tui](https://github.com/axio-agent
|
|
238
|
-
| [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 |
|
|
239
239
|
|
|
240
240
|
## License
|
|
241
241
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "axio"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.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)
|