axio 0.3.4__tar.gz → 0.4.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.3.4 → axio-0.4.0}/PKG-INFO +3 -2
  2. {axio-0.3.4 → axio-0.4.0}/README.md +2 -1
  3. {axio-0.3.4 → axio-0.4.0}/pyproject.toml +1 -1
  4. {axio-0.3.4 → axio-0.4.0}/src/axio/agent.py +11 -3
  5. {axio-0.3.4 → axio-0.4.0}/src/axio/events.py +33 -1
  6. {axio-0.3.4 → axio-0.4.0}/src/axio/models.py +0 -9
  7. axio-0.4.0/src/axio/selector.py +10 -0
  8. axio-0.4.0/src/axio/tool_args.py +243 -0
  9. axio-0.4.0/tests/test_tool_args.py +753 -0
  10. axio-0.3.4/src/axio/selector.py +0 -121
  11. axio-0.3.4/tests/test_selector.py +0 -237
  12. {axio-0.3.4 → axio-0.4.0}/.github/workflows/publish.yml +0 -0
  13. {axio-0.3.4 → axio-0.4.0}/.github/workflows/tests.yml +0 -0
  14. {axio-0.3.4 → axio-0.4.0}/.gitignore +0 -0
  15. {axio-0.3.4 → axio-0.4.0}/LICENSE +0 -0
  16. {axio-0.3.4 → axio-0.4.0}/Makefile +0 -0
  17. {axio-0.3.4 → axio-0.4.0}/src/axio/__init__.py +0 -0
  18. {axio-0.3.4 → axio-0.4.0}/src/axio/blocks.py +0 -0
  19. {axio-0.3.4 → axio-0.4.0}/src/axio/context.py +0 -0
  20. {axio-0.3.4 → axio-0.4.0}/src/axio/exceptions.py +0 -0
  21. {axio-0.3.4 → axio-0.4.0}/src/axio/messages.py +0 -0
  22. {axio-0.3.4 → axio-0.4.0}/src/axio/permission.py +0 -0
  23. {axio-0.3.4 → axio-0.4.0}/src/axio/py.typed +0 -0
  24. {axio-0.3.4 → axio-0.4.0}/src/axio/stream.py +0 -0
  25. {axio-0.3.4 → axio-0.4.0}/src/axio/testing.py +0 -0
  26. {axio-0.3.4 → axio-0.4.0}/src/axio/tool.py +0 -0
  27. {axio-0.3.4 → axio-0.4.0}/src/axio/transport.py +0 -0
  28. {axio-0.3.4 → axio-0.4.0}/src/axio/types.py +0 -0
  29. {axio-0.3.4 → axio-0.4.0}/tests/conftest.py +0 -0
  30. {axio-0.3.4 → axio-0.4.0}/tests/test_agent_branch.py +0 -0
  31. {axio-0.3.4 → axio-0.4.0}/tests/test_agent_permission.py +0 -0
  32. {axio-0.3.4 → axio-0.4.0}/tests/test_agent_run.py +0 -0
  33. {axio-0.3.4 → axio-0.4.0}/tests/test_agent_stream.py +0 -0
  34. {axio-0.3.4 → axio-0.4.0}/tests/test_agent_tools.py +0 -0
  35. {axio-0.3.4 → axio-0.4.0}/tests/test_blocks.py +0 -0
  36. {axio-0.3.4 → axio-0.4.0}/tests/test_context.py +0 -0
  37. {axio-0.3.4 → axio-0.4.0}/tests/test_events.py +0 -0
  38. {axio-0.3.4 → axio-0.4.0}/tests/test_exceptions.py +0 -0
  39. {axio-0.3.4 → axio-0.4.0}/tests/test_permission.py +0 -0
  40. {axio-0.3.4 → axio-0.4.0}/tests/test_stream.py +0 -0
  41. {axio-0.3.4 → axio-0.4.0}/tests/test_tool.py +0 -0
  42. {axio-0.3.4 → axio-0.4.0}/tests/test_transport.py +0 -0
  43. {axio-0.3.4 → axio-0.4.0}/tests/test_types.py +0 -0
  44. {axio-0.3.4 → axio-0.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio
3
- Version: 0.3.4
3
+ Version: 0.4.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
@@ -77,7 +77,7 @@ asyncio.run(main())
77
77
 
78
78
  ┌─────────┐ stream() ┌─────────────────────┐
79
79
  │ Agent │ ────────────▶ │ CompletionTransport │
80
- │ loop │ ◀──────────── │ (OpenAI, Nebius, …)
80
+ │ loop │ ◀──────────── │ (Anthropic, OpenAI, …)
81
81
  └─────────┘ StreamEvent └─────────────────────┘
82
82
 
83
83
  │ tool_use?
@@ -202,6 +202,7 @@ my_guard = "my_package:MyGuard"
202
202
 
203
203
  | Package | Purpose |
204
204
  |---|---|
205
+ | [axio-transport-anthropic](https://github.com/axio-agent/axio-transport-anthropic) | Anthropic Claude transport |
205
206
  | [axio-transport-openai](https://github.com/axio-agent/axio-transport-openai) | OpenAI-compatible transport |
206
207
  | [axio-transport-nebius](https://github.com/axio-agent/axio-transport-nebius) | Nebius AI Studio transport |
207
208
  | [axio-transport-codex](https://github.com/axio-agent/axio-transport-codex) | ChatGPT OAuth transport |
@@ -64,7 +64,7 @@ asyncio.run(main())
64
64
 
65
65
  ┌─────────┐ stream() ┌─────────────────────┐
66
66
  │ Agent │ ────────────▶ │ CompletionTransport │
67
- │ loop │ ◀──────────── │ (OpenAI, Nebius, …)
67
+ │ loop │ ◀──────────── │ (Anthropic, OpenAI, …)
68
68
  └─────────┘ StreamEvent └─────────────────────┘
69
69
 
70
70
  │ tool_use?
@@ -189,6 +189,7 @@ my_guard = "my_package:MyGuard"
189
189
 
190
190
  | Package | Purpose |
191
191
  |---|---|
192
+ | [axio-transport-anthropic](https://github.com/axio-agent/axio-transport-anthropic) | Anthropic Claude transport |
192
193
  | [axio-transport-openai](https://github.com/axio-agent/axio-transport-openai) | OpenAI-compatible transport |
193
194
  | [axio-transport-nebius](https://github.com/axio-agent/axio-transport-nebius) | Nebius AI Studio transport |
194
195
  | [axio-transport-codex](https://github.com/axio-agent/axio-transport-codex) | ChatGPT OAuth transport |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio"
3
- version = "0.3.4"
3
+ version = "0.4.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"
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
- from collections.abc import AsyncGenerator
8
+ from collections.abc import AsyncGenerator, Iterable
9
9
  from dataclasses import dataclass, field
10
10
  from typing import Any
11
11
 
@@ -22,6 +22,7 @@ from axio.events import (
22
22
  ToolUseStart,
23
23
  )
24
24
  from axio.messages import Message
25
+ from axio.selector import ToolSelector
25
26
  from axio.stream import AgentStream
26
27
  from axio.tool import Tool
27
28
  from axio.transport import CompletionTransport
@@ -35,6 +36,7 @@ class Agent:
35
36
  system: str
36
37
  tools: list[Tool]
37
38
  transport: CompletionTransport
39
+ selector: ToolSelector | None = field(default=None)
38
40
  max_iterations: int = field(default=50)
39
41
 
40
42
  def run_stream(self, user_message: str, context: ContextStore) -> AgentStream:
@@ -120,6 +122,13 @@ class Agent:
120
122
  blocks.append(ToolUseBlock(id=tid, name=info["name"], input=inp))
121
123
  return blocks, malformed
122
124
 
125
+ async def _select_tools(self, history: list[Message], tools: list[Tool]) -> Iterable[Tool]:
126
+ if not tools:
127
+ return []
128
+ if not self.selector:
129
+ return tools
130
+ return await self.selector.select(history, tools)
131
+
123
132
  async def _run_loop(self, user_message: str, context: ContextStore) -> AsyncGenerator[StreamEvent, None]:
124
133
  total_usage = Usage(0, 0)
125
134
  session_end_emitted = False
@@ -129,8 +138,7 @@ class Agent:
129
138
  for iteration in range(1, self.max_iterations + 1):
130
139
  history = await context.get_history()
131
140
  logger.info("Iteration %d, history length=%d", iteration, len(history))
132
- active_tools = self.tools
133
-
141
+ active_tools = list(await self._select_tools(history, self.tools))
134
142
  content: list[TextBlock | ToolUseBlock] = []
135
143
  pending: dict[str, dict[str, Any]] = {}
136
144
  stop_reason = StopReason.end_turn
@@ -34,6 +34,28 @@ class ToolInputDelta:
34
34
  partial_json: str
35
35
 
36
36
 
37
+ @dataclass(frozen=True, slots=True)
38
+ class ToolFieldStart:
39
+ index: int
40
+ tool_use_id: ToolCallID
41
+ key: str
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class ToolFieldDelta:
46
+ index: int
47
+ tool_use_id: ToolCallID
48
+ key: str
49
+ text: str
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class ToolFieldEnd:
54
+ index: int
55
+ tool_use_id: ToolCallID
56
+ key: str
57
+
58
+
37
59
  @dataclass(frozen=True, slots=True)
38
60
  class ToolResult:
39
61
  tool_use_id: ToolCallID
@@ -62,5 +84,15 @@ class SessionEndEvent:
62
84
 
63
85
 
64
86
  type StreamEvent = (
65
- ReasoningDelta | TextDelta | ToolUseStart | ToolInputDelta | ToolResult | IterationEnd | Error | SessionEndEvent
87
+ ReasoningDelta
88
+ | TextDelta
89
+ | ToolUseStart
90
+ | ToolInputDelta
91
+ | ToolFieldStart
92
+ | ToolFieldDelta
93
+ | ToolFieldEnd
94
+ | ToolResult
95
+ | IterationEnd
96
+ | Error
97
+ | SessionEndEvent
66
98
  )
@@ -7,15 +7,6 @@ from dataclasses import dataclass
7
7
  from enum import StrEnum
8
8
 
9
9
 
10
- @dataclass(frozen=True, slots=True)
11
- class TransportMeta:
12
- """Metadata a transport plugin declares about itself."""
13
-
14
- label: str
15
- api_key_env: str
16
- role_defaults: dict[str, str]
17
-
18
-
19
10
  class Capability(StrEnum):
20
11
  text = "text"
21
12
  vision = "vision"
@@ -0,0 +1,10 @@
1
+ from collections.abc import Iterable
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ from axio.messages import Message
5
+ from axio.tool import Tool
6
+
7
+
8
+ @runtime_checkable
9
+ class ToolSelector(Protocol):
10
+ async def select(self, messages: Iterable[Message], tools: Iterable[Tool]) -> Iterable[Tool]: ...
@@ -0,0 +1,243 @@
1
+ """Incremental streaming parser for tool call JSON arguments.
2
+
3
+ Feeds partial JSON chunks (from ``ToolInputDelta.partial_json``) and emits
4
+ structured ``ToolField*`` events as top-level object fields are discovered.
5
+
6
+ Top-level *string* values are decoded (escape sequences resolved, quotes
7
+ stripped). All other top-level values are emitted as raw JSON fragments.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Mapping
13
+ from enum import IntEnum
14
+ from types import MappingProxyType
15
+
16
+ from axio.events import ToolFieldDelta, ToolFieldEnd, ToolFieldStart
17
+
18
+ type ToolFieldEvent = ToolFieldStart | ToolFieldDelta | ToolFieldEnd
19
+
20
+
21
+ class State(IntEnum):
22
+ INIT = 0
23
+ OBJ = 1
24
+ KEY = 2
25
+ COLON = 3
26
+ VAL = 4
27
+ STR = 5
28
+ RAW = 6
29
+ AFTER = 7
30
+ ESC = 8
31
+ UESC = 9
32
+
33
+
34
+ ESCAPES: Mapping[str, str] = MappingProxyType(
35
+ {
36
+ "n": "\n",
37
+ "t": "\t",
38
+ "r": "\r",
39
+ "b": "\b",
40
+ "f": "\f",
41
+ '"': '"',
42
+ "\\": "\\",
43
+ "/": "/",
44
+ }
45
+ )
46
+
47
+
48
+ class ToolArgStream:
49
+ """O(1)-per-character streaming parser for tool argument JSON.
50
+
51
+ Usage::
52
+
53
+ stream = ToolArgStream("call_1")
54
+ events = stream.feed('{"path":"/tmp/f')
55
+ # [ToolFieldStart(0, "call_1", "path"), ToolFieldDelta(0, "call_1", "path", "/tmp/f")]
56
+ events = stream.feed('oo.py"}')
57
+ # [ToolFieldDelta(0, "call_1", "path", "oo.py"), ToolFieldEnd(0, "call_1", "path")]
58
+ """
59
+
60
+ __slots__ = (
61
+ "_id",
62
+ "_idx",
63
+ "_st",
64
+ "_key_chars",
65
+ "_key",
66
+ "_buf",
67
+ "_u",
68
+ "_high",
69
+ "_depth",
70
+ "_raw_str",
71
+ "_raw_esc",
72
+ "_esc_key",
73
+ "_esc_ret",
74
+ "_events",
75
+ )
76
+
77
+ def __init__(self, tool_use_id: str, index: int = 0) -> None:
78
+ self._id = tool_use_id
79
+ self._idx = index
80
+ self._st = State.INIT
81
+ self._key_chars: list[str] = []
82
+ self._key = ""
83
+ self._buf: list[str] = []
84
+ self._u: list[str] = []
85
+ self._high = 0
86
+ self._depth = 0
87
+ self._raw_str = False
88
+ self._raw_esc = False
89
+ self._esc_key = False
90
+ self._esc_ret = State.KEY
91
+ self._events: list[ToolFieldEvent] = []
92
+
93
+ @property
94
+ def current_key(self) -> str:
95
+ """The field currently being streamed, or ``""``."""
96
+ return self._key
97
+
98
+ def feed(self, chunk: str) -> list[ToolFieldEvent]:
99
+ """Process a partial JSON chunk and return any field events produced."""
100
+ self._events = []
101
+ for ch in chunk:
102
+ self._step(ch)
103
+ self._flush()
104
+ return self._events
105
+
106
+ def _flush(self) -> None:
107
+ if self._buf:
108
+ self._events.append(ToolFieldDelta(self._idx, self._id, self._key, "".join(self._buf)))
109
+ self._buf.clear()
110
+
111
+ def _start(self) -> None:
112
+ self._flush()
113
+ self._events.append(ToolFieldStart(self._idx, self._id, self._key))
114
+
115
+ def _end(self) -> None:
116
+ self._flush()
117
+ self._events.append(ToolFieldEnd(self._idx, self._id, self._key))
118
+
119
+ def _step(self, ch: str) -> None: # noqa: PLR0912
120
+ match self._st:
121
+ case State.INIT:
122
+ if ch == "{":
123
+ self._st = State.OBJ
124
+
125
+ case State.OBJ:
126
+ if ch == '"':
127
+ self._key_chars.clear()
128
+ self._st = State.KEY
129
+ elif ch == "}":
130
+ self._st = State.INIT
131
+
132
+ case State.KEY:
133
+ if ch == "\\":
134
+ self._esc_key = True
135
+ self._esc_ret = State.KEY
136
+ self._st = State.ESC
137
+ elif ch == '"':
138
+ self._key = "".join(self._key_chars)
139
+ self._st = State.COLON
140
+ else:
141
+ self._key_chars.append(ch)
142
+
143
+ case State.COLON:
144
+ if ch == ":":
145
+ self._start()
146
+ self._st = State.VAL
147
+
148
+ case State.VAL:
149
+ if ch in " \t\r\n":
150
+ pass
151
+ elif ch == '"':
152
+ self._st = State.STR
153
+ else:
154
+ self._buf.append(ch)
155
+ self._depth = 1 if ch in "{[" else 0
156
+ self._raw_str = False
157
+ self._raw_esc = False
158
+ self._st = State.RAW
159
+
160
+ case State.STR:
161
+ if ch == "\\":
162
+ self._esc_key = False
163
+ self._esc_ret = State.STR
164
+ self._st = State.ESC
165
+ elif ch == '"':
166
+ if self._high:
167
+ self._buf.append("\ufffd")
168
+ self._high = 0
169
+ self._end()
170
+ self._st = State.AFTER
171
+ else:
172
+ if self._high:
173
+ self._buf.append("\ufffd")
174
+ self._high = 0
175
+ self._buf.append(ch)
176
+
177
+ case State.RAW:
178
+ if self._raw_str:
179
+ self._buf.append(ch)
180
+ if self._raw_esc:
181
+ self._raw_esc = False
182
+ elif ch == "\\":
183
+ self._raw_esc = True
184
+ elif ch == '"':
185
+ self._raw_str = False
186
+ elif self._depth == 0 and ch in " \t\r\n,}":
187
+ # simple value (number/bool/null) ends on whitespace or delimiter
188
+ self._end()
189
+ self._st = State.AFTER
190
+ if ch in ",}":
191
+ self._step(ch) # reprocess delimiter
192
+ elif ch == '"':
193
+ self._buf.append(ch)
194
+ self._raw_str = True
195
+ elif ch in "{[":
196
+ self._buf.append(ch)
197
+ self._depth += 1
198
+ elif ch in "}]":
199
+ self._buf.append(ch)
200
+ self._depth -= 1
201
+ if self._depth == 0:
202
+ self._end()
203
+ self._st = State.AFTER
204
+ else:
205
+ self._buf.append(ch)
206
+
207
+ case State.AFTER:
208
+ if ch == ",":
209
+ self._st = State.OBJ
210
+ elif ch == "}":
211
+ self._st = State.INIT
212
+
213
+ case State.ESC:
214
+ if ch == "u":
215
+ self._u.clear()
216
+ self._st = State.UESC
217
+ else:
218
+ dec = ESCAPES.get(ch, ch)
219
+ if self._esc_key:
220
+ self._key_chars.append(dec)
221
+ else:
222
+ self._buf.append(dec)
223
+ self._st = self._esc_ret
224
+
225
+ case State.UESC:
226
+ self._u.append(ch)
227
+ if len(self._u) == 4:
228
+ code = int("".join(self._u), 16)
229
+ if self._esc_key:
230
+ self._key_chars.append(chr(code))
231
+ elif self._high:
232
+ if 0xDC00 <= code <= 0xDFFF:
233
+ full = 0x10000 + (self._high - 0xD800) * 0x400 + (code - 0xDC00)
234
+ self._buf.append(chr(full))
235
+ else:
236
+ self._buf.append("\ufffd")
237
+ self._buf.append(chr(code))
238
+ self._high = 0
239
+ elif 0xD800 <= code <= 0xDBFF:
240
+ self._high = code
241
+ else:
242
+ self._buf.append(chr(code))
243
+ self._st = self._esc_ret