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.
Files changed (44) hide show
  1. {axio-0.5.1 → axio-0.6.0}/PKG-INFO +1 -1
  2. {axio-0.5.1 → axio-0.6.0}/pyproject.toml +1 -1
  3. {axio-0.5.1 → axio-0.6.0}/src/axio/agent.py +8 -2
  4. {axio-0.5.1 → axio-0.6.0}/src/axio/messages.py +1 -1
  5. {axio-0.5.1 → axio-0.6.0}/tests/test_agent_run.py +81 -0
  6. {axio-0.5.1 → axio-0.6.0}/.github/workflows/publish.yml +0 -0
  7. {axio-0.5.1 → axio-0.6.0}/.github/workflows/tests.yml +0 -0
  8. {axio-0.5.1 → axio-0.6.0}/.gitignore +0 -0
  9. {axio-0.5.1 → axio-0.6.0}/LICENSE +0 -0
  10. {axio-0.5.1 → axio-0.6.0}/Makefile +0 -0
  11. {axio-0.5.1 → axio-0.6.0}/README.md +0 -0
  12. {axio-0.5.1 → axio-0.6.0}/src/axio/__init__.py +0 -0
  13. {axio-0.5.1 → axio-0.6.0}/src/axio/blocks.py +0 -0
  14. {axio-0.5.1 → axio-0.6.0}/src/axio/compaction.py +0 -0
  15. {axio-0.5.1 → axio-0.6.0}/src/axio/context.py +0 -0
  16. {axio-0.5.1 → axio-0.6.0}/src/axio/events.py +0 -0
  17. {axio-0.5.1 → axio-0.6.0}/src/axio/exceptions.py +0 -0
  18. {axio-0.5.1 → axio-0.6.0}/src/axio/models.py +0 -0
  19. {axio-0.5.1 → axio-0.6.0}/src/axio/permission.py +0 -0
  20. {axio-0.5.1 → axio-0.6.0}/src/axio/py.typed +0 -0
  21. {axio-0.5.1 → axio-0.6.0}/src/axio/selector.py +0 -0
  22. {axio-0.5.1 → axio-0.6.0}/src/axio/stream.py +0 -0
  23. {axio-0.5.1 → axio-0.6.0}/src/axio/testing.py +0 -0
  24. {axio-0.5.1 → axio-0.6.0}/src/axio/tool.py +0 -0
  25. {axio-0.5.1 → axio-0.6.0}/src/axio/tool_args.py +0 -0
  26. {axio-0.5.1 → axio-0.6.0}/src/axio/transport.py +0 -0
  27. {axio-0.5.1 → axio-0.6.0}/src/axio/types.py +0 -0
  28. {axio-0.5.1 → axio-0.6.0}/tests/conftest.py +0 -0
  29. {axio-0.5.1 → axio-0.6.0}/tests/test_agent_branch.py +0 -0
  30. {axio-0.5.1 → axio-0.6.0}/tests/test_agent_permission.py +0 -0
  31. {axio-0.5.1 → axio-0.6.0}/tests/test_agent_stream.py +0 -0
  32. {axio-0.5.1 → axio-0.6.0}/tests/test_agent_tools.py +0 -0
  33. {axio-0.5.1 → axio-0.6.0}/tests/test_blocks.py +0 -0
  34. {axio-0.5.1 → axio-0.6.0}/tests/test_context.py +0 -0
  35. {axio-0.5.1 → axio-0.6.0}/tests/test_events.py +0 -0
  36. {axio-0.5.1 → axio-0.6.0}/tests/test_exceptions.py +0 -0
  37. {axio-0.5.1 → axio-0.6.0}/tests/test_models.py +0 -0
  38. {axio-0.5.1 → axio-0.6.0}/tests/test_permission.py +0 -0
  39. {axio-0.5.1 → axio-0.6.0}/tests/test_stream.py +0 -0
  40. {axio-0.5.1 → axio-0.6.0}/tests/test_tool.py +0 -0
  41. {axio-0.5.1 → axio-0.6.0}/tests/test_tool_args.py +0 -0
  42. {axio-0.5.1 → axio-0.6.0}/tests/test_transport.py +0 -0
  43. {axio-0.5.1 → axio-0.6.0}/tests/test_types.py +0 -0
  44. {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.5.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio"
3
- version = "0.5.1"
3
+ version = "0.6.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"
@@ -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
- active_tools = list(await self._select_tools(history, self.tools))
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(history, active_tools, self.system):
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