axio 0.5.1__tar.gz → 0.6.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.5.1 → axio-0.6.0}/PKG-INFO +1 -1
- {axio-0.5.1 → axio-0.6.0}/pyproject.toml +1 -1
- {axio-0.5.1 → axio-0.6.0}/src/axio/agent.py +8 -2
- {axio-0.5.1 → axio-0.6.0}/src/axio/messages.py +1 -1
- {axio-0.5.1 → axio-0.6.0}/tests/test_agent_run.py +81 -0
- {axio-0.5.1 → axio-0.6.0}/.github/workflows/publish.yml +0 -0
- {axio-0.5.1 → axio-0.6.0}/.github/workflows/tests.yml +0 -0
- {axio-0.5.1 → axio-0.6.0}/.gitignore +0 -0
- {axio-0.5.1 → axio-0.6.0}/LICENSE +0 -0
- {axio-0.5.1 → axio-0.6.0}/Makefile +0 -0
- {axio-0.5.1 → axio-0.6.0}/README.md +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/__init__.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/blocks.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/compaction.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/context.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/events.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/exceptions.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/models.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/permission.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/py.typed +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/selector.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/stream.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/testing.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/tool.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/tool_args.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/transport.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/src/axio/types.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/conftest.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_agent_branch.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_agent_permission.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_agent_stream.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_agent_tools.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_blocks.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_context.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_events.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_exceptions.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_models.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_permission.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_stream.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_tool.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_tool_args.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_transport.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/tests/test_types.py +0 -0
- {axio-0.5.1 → axio-0.6.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Minimal, streaming-first, protocol-driven foundation for LLM-powered agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/axio-agent/axio
|
|
6
6
|
Project-URL: Repository, https://github.com/axio-agent/axio
|
|
@@ -39,6 +39,7 @@ class Agent:
|
|
|
39
39
|
tools: list[Tool] = field(default_factory=list)
|
|
40
40
|
selector: ToolSelector | None = field(default=None)
|
|
41
41
|
max_iterations: int = field(default=50)
|
|
42
|
+
last_iteration_message: Message | None = field(default=None)
|
|
42
43
|
|
|
43
44
|
def copy(self, **overrides: Any) -> Self:
|
|
44
45
|
"""Return a new Agent with *overrides* applied."""
|
|
@@ -143,14 +144,19 @@ class Agent:
|
|
|
143
144
|
for iteration in range(1, self.max_iterations + 1):
|
|
144
145
|
history = await context.get_history()
|
|
145
146
|
logger.info("Iteration %d, history length=%d", iteration, len(history))
|
|
146
|
-
|
|
147
|
+
effective_history = (
|
|
148
|
+
[*history, self.last_iteration_message]
|
|
149
|
+
if self.last_iteration_message and iteration == self.max_iterations
|
|
150
|
+
else history
|
|
151
|
+
)
|
|
152
|
+
active_tools = list(await self._select_tools(effective_history, self.tools))
|
|
147
153
|
content: list[TextBlock | ToolUseBlock] = []
|
|
148
154
|
pending: dict[str, dict[str, Any]] = {}
|
|
149
155
|
stop_reason = StopReason.end_turn
|
|
150
156
|
malformed: set[str] = set()
|
|
151
157
|
|
|
152
158
|
try:
|
|
153
|
-
async for event in self.transport.stream(
|
|
159
|
+
async for event in self.transport.stream(effective_history, active_tools, self.system):
|
|
154
160
|
yield event
|
|
155
161
|
match event:
|
|
156
162
|
case TextDelta(delta=delta):
|
|
@@ -10,7 +10,7 @@ from axio.blocks import ContentBlock, from_dict, to_dict
|
|
|
10
10
|
|
|
11
11
|
@dataclass(slots=True)
|
|
12
12
|
class Message:
|
|
13
|
-
role: Literal["user", "assistant"]
|
|
13
|
+
role: Literal["user", "assistant", "system"]
|
|
14
14
|
content: list[ContentBlock] = field(default_factory=list)
|
|
15
15
|
|
|
16
16
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -2,14 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
5
7
|
from axio.agent import Agent
|
|
8
|
+
from axio.blocks import TextBlock
|
|
6
9
|
from axio.context import MemoryContextStore
|
|
7
10
|
from axio.events import IterationEnd, ReasoningDelta, SessionEndEvent, StreamEvent, TextDelta, ToolResult
|
|
11
|
+
from axio.messages import Message
|
|
8
12
|
from axio.testing import MsgInput, StubTransport, make_text_response, make_tool_use_response
|
|
9
13
|
from axio.tool import Tool, ToolHandler
|
|
10
14
|
from axio.types import StopReason, Usage
|
|
11
15
|
|
|
12
16
|
|
|
17
|
+
class CapturingTransport:
|
|
18
|
+
"""Records messages passed to each stream() call."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, responses: list[list[StreamEvent]]) -> None:
|
|
21
|
+
self._responses = responses
|
|
22
|
+
self._call_count = 0
|
|
23
|
+
self.calls: list[list[Message]] = []
|
|
24
|
+
|
|
25
|
+
async def _generate(self, events: list[StreamEvent]) -> AsyncIterator[StreamEvent]:
|
|
26
|
+
for event in events:
|
|
27
|
+
yield event
|
|
28
|
+
|
|
29
|
+
def stream(self, messages: list[Message], tools: list[Tool], system: str) -> AsyncIterator[StreamEvent]:
|
|
30
|
+
self.calls.append(list(messages))
|
|
31
|
+
idx = min(self._call_count, len(self._responses) - 1)
|
|
32
|
+
self._call_count += 1
|
|
33
|
+
return self._generate(self._responses[idx])
|
|
34
|
+
|
|
35
|
+
|
|
13
36
|
class OkHandler(ToolHandler):
|
|
14
37
|
msg: str
|
|
15
38
|
|
|
@@ -190,3 +213,61 @@ class TestMaxIterations:
|
|
|
190
213
|
last = events[-1]
|
|
191
214
|
assert isinstance(last, SessionEndEvent)
|
|
192
215
|
assert last.stop_reason == StopReason.error
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestLastIterationMessage:
|
|
219
|
+
async def test_injected_only_on_last_iteration(self) -> None:
|
|
220
|
+
"""last_iteration_message is appended to history only on the final iteration."""
|
|
221
|
+
tool = Tool(name="echo", description="echo", handler=OkHandler)
|
|
222
|
+
hint = Message(role="system", content=[TextBlock(text="wrap up now")])
|
|
223
|
+
transport = CapturingTransport(
|
|
224
|
+
[
|
|
225
|
+
make_tool_use_response("echo", "c1", {"msg": "hi"}, 1),
|
|
226
|
+
make_text_response("Done", 2),
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
agent = Agent(
|
|
230
|
+
system="test",
|
|
231
|
+
tools=[tool],
|
|
232
|
+
transport=transport,
|
|
233
|
+
max_iterations=2,
|
|
234
|
+
last_iteration_message=hint,
|
|
235
|
+
)
|
|
236
|
+
await agent.run("go", MemoryContextStore())
|
|
237
|
+
|
|
238
|
+
# iteration 1: hint NOT in history
|
|
239
|
+
assert hint not in transport.calls[0]
|
|
240
|
+
# iteration 2 (last): hint IS the final message
|
|
241
|
+
assert transport.calls[1][-1] is hint
|
|
242
|
+
|
|
243
|
+
async def test_not_injected_when_none(self) -> None:
|
|
244
|
+
"""No injection when last_iteration_message is None (default)."""
|
|
245
|
+
transport = CapturingTransport([make_text_response("hi", 1)])
|
|
246
|
+
agent = Agent(system="test", tools=[], transport=transport)
|
|
247
|
+
await agent.run("go", MemoryContextStore())
|
|
248
|
+
|
|
249
|
+
history = transport.calls[0]
|
|
250
|
+
assert all(m.role != "system" for m in history)
|
|
251
|
+
|
|
252
|
+
async def test_not_stored_in_context(self) -> None:
|
|
253
|
+
"""last_iteration_message is injected into the stream but not persisted."""
|
|
254
|
+
tool = Tool(name="echo", description="echo", handler=OkHandler)
|
|
255
|
+
hint = Message(role="system", content=[TextBlock(text="wrap up")])
|
|
256
|
+
transport = CapturingTransport(
|
|
257
|
+
[
|
|
258
|
+
make_tool_use_response("echo", "c1", {"msg": "hi"}, 1),
|
|
259
|
+
make_text_response("Done", 2),
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
agent = Agent(
|
|
263
|
+
system="test",
|
|
264
|
+
tools=[tool],
|
|
265
|
+
transport=transport,
|
|
266
|
+
max_iterations=2,
|
|
267
|
+
last_iteration_message=hint,
|
|
268
|
+
)
|
|
269
|
+
context = MemoryContextStore()
|
|
270
|
+
await agent.run("go", context)
|
|
271
|
+
|
|
272
|
+
history = await context.get_history()
|
|
273
|
+
assert hint not in history
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|