axio 0.4.1__tar.gz → 0.5.1__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.4.1 → axio-0.5.1}/PKG-INFO +37 -6
- {axio-0.4.1 → axio-0.5.1}/README.md +36 -5
- {axio-0.4.1 → axio-0.5.1}/pyproject.toml +3 -1
- {axio-0.4.1 → axio-0.5.1}/src/axio/agent.py +7 -2
- axio-0.5.1/src/axio/compaction.py +67 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/context.py +22 -77
- {axio-0.4.1 → axio-0.5.1}/src/axio/models.py +12 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/transport.py +9 -1
- {axio-0.4.1 → axio-0.5.1}/tests/test_context.py +2 -1
- axio-0.5.1/tests/test_models.py +61 -0
- {axio-0.4.1 → axio-0.5.1}/.github/workflows/publish.yml +0 -0
- {axio-0.4.1 → axio-0.5.1}/.github/workflows/tests.yml +0 -0
- {axio-0.4.1 → axio-0.5.1}/.gitignore +0 -0
- {axio-0.4.1 → axio-0.5.1}/LICENSE +0 -0
- {axio-0.4.1 → axio-0.5.1}/Makefile +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/__init__.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/blocks.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/events.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/exceptions.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/messages.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/permission.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/py.typed +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/selector.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/stream.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/testing.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/tool.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/tool_args.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/src/axio/types.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/conftest.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_agent_branch.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_agent_permission.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_agent_run.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_agent_stream.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_agent_tools.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_blocks.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_events.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_exceptions.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_permission.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_stream.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_tool.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_tool_args.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_transport.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/tests/test_types.py +0 -0
- {axio-0.4.1 → axio-0.5.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -38,9 +38,24 @@ pip install axio
|
|
|
38
38
|
|
|
39
39
|
## Quick start
|
|
40
40
|
|
|
41
|
+
<!--
|
|
42
|
+
name: test_readme_quick_start
|
|
43
|
+
```python
|
|
44
|
+
import sys, types
|
|
45
|
+
from axio.testing import StubTransport, make_text_response
|
|
46
|
+
|
|
47
|
+
_m = types.ModuleType("axio_transport_openai")
|
|
48
|
+
class OpenAITransport(StubTransport):
|
|
49
|
+
def __init__(self, api_key: str = "", model: str = "") -> None:
|
|
50
|
+
super().__init__([make_text_response("Hello, Alice!")])
|
|
51
|
+
_m.OpenAITransport = OpenAITransport # type: ignore[attr-defined]
|
|
52
|
+
sys.modules["axio_transport_openai"] = _m
|
|
53
|
+
```
|
|
54
|
+
-->
|
|
55
|
+
<!-- name: test_readme_quick_start -->
|
|
41
56
|
```python
|
|
42
57
|
import asyncio
|
|
43
|
-
from axio import Agent
|
|
58
|
+
from axio.agent import Agent
|
|
44
59
|
from axio.context import MemoryContextStore
|
|
45
60
|
from axio.tool import Tool, ToolHandler
|
|
46
61
|
|
|
@@ -97,6 +112,7 @@ asyncio.run(main())
|
|
|
97
112
|
|
|
98
113
|
### CompletionTransport
|
|
99
114
|
|
|
115
|
+
<!-- name: test_readme_completion_transport -->
|
|
100
116
|
```python
|
|
101
117
|
from typing import Protocol, runtime_checkable
|
|
102
118
|
from collections.abc import AsyncIterator
|
|
@@ -113,18 +129,30 @@ class CompletionTransport(Protocol):
|
|
|
113
129
|
|
|
114
130
|
### ContextStore
|
|
115
131
|
|
|
132
|
+
<!-- name: test_readme_context_store -->
|
|
116
133
|
```python
|
|
117
|
-
|
|
134
|
+
from abc import ABC, abstractmethod
|
|
135
|
+
from axio.context import ContextStore
|
|
136
|
+
from axio.messages import Message
|
|
137
|
+
|
|
138
|
+
class MyContextStore(ContextStore, ABC):
|
|
139
|
+
@abstractmethod
|
|
118
140
|
async def append(self, message: Message) -> None: ...
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
119
143
|
async def get_history(self) -> list[Message]: ...
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
|
|
145
|
+
# Everything else — session_id, close(), fork(), clear(),
|
|
146
|
+
# get/set_context_tokens() — has a default implementation.
|
|
123
147
|
```
|
|
124
148
|
|
|
125
149
|
### PermissionGuard
|
|
126
150
|
|
|
151
|
+
<!-- name: test_readme_permission_guard -->
|
|
127
152
|
```python
|
|
153
|
+
from typing import Protocol, runtime_checkable
|
|
154
|
+
from axio.tool import ToolHandler
|
|
155
|
+
|
|
128
156
|
@runtime_checkable
|
|
129
157
|
class PermissionGuard(Protocol):
|
|
130
158
|
async def check(self, handler: ToolHandler) -> ToolHandler: ...
|
|
@@ -144,6 +172,7 @@ class PermissionGuard(Protocol):
|
|
|
144
172
|
|
|
145
173
|
## Tools
|
|
146
174
|
|
|
175
|
+
<!-- name: test_readme_tools -->
|
|
147
176
|
```python
|
|
148
177
|
from axio.tool import Tool, ToolHandler
|
|
149
178
|
|
|
@@ -166,7 +195,9 @@ tool = Tool(
|
|
|
166
195
|
|
|
167
196
|
## Testing
|
|
168
197
|
|
|
198
|
+
<!-- name: test_readme_testing -->
|
|
169
199
|
```python
|
|
200
|
+
from axio.agent import Agent
|
|
170
201
|
from axio.testing import (
|
|
171
202
|
StubTransport,
|
|
172
203
|
make_tool_use_response,
|
|
@@ -25,9 +25,24 @@ pip install axio
|
|
|
25
25
|
|
|
26
26
|
## Quick start
|
|
27
27
|
|
|
28
|
+
<!--
|
|
29
|
+
name: test_readme_quick_start
|
|
30
|
+
```python
|
|
31
|
+
import sys, types
|
|
32
|
+
from axio.testing import StubTransport, make_text_response
|
|
33
|
+
|
|
34
|
+
_m = types.ModuleType("axio_transport_openai")
|
|
35
|
+
class OpenAITransport(StubTransport):
|
|
36
|
+
def __init__(self, api_key: str = "", model: str = "") -> None:
|
|
37
|
+
super().__init__([make_text_response("Hello, Alice!")])
|
|
38
|
+
_m.OpenAITransport = OpenAITransport # type: ignore[attr-defined]
|
|
39
|
+
sys.modules["axio_transport_openai"] = _m
|
|
40
|
+
```
|
|
41
|
+
-->
|
|
42
|
+
<!-- name: test_readme_quick_start -->
|
|
28
43
|
```python
|
|
29
44
|
import asyncio
|
|
30
|
-
from axio import Agent
|
|
45
|
+
from axio.agent import Agent
|
|
31
46
|
from axio.context import MemoryContextStore
|
|
32
47
|
from axio.tool import Tool, ToolHandler
|
|
33
48
|
|
|
@@ -84,6 +99,7 @@ asyncio.run(main())
|
|
|
84
99
|
|
|
85
100
|
### CompletionTransport
|
|
86
101
|
|
|
102
|
+
<!-- name: test_readme_completion_transport -->
|
|
87
103
|
```python
|
|
88
104
|
from typing import Protocol, runtime_checkable
|
|
89
105
|
from collections.abc import AsyncIterator
|
|
@@ -100,18 +116,30 @@ class CompletionTransport(Protocol):
|
|
|
100
116
|
|
|
101
117
|
### ContextStore
|
|
102
118
|
|
|
119
|
+
<!-- name: test_readme_context_store -->
|
|
103
120
|
```python
|
|
104
|
-
|
|
121
|
+
from abc import ABC, abstractmethod
|
|
122
|
+
from axio.context import ContextStore
|
|
123
|
+
from axio.messages import Message
|
|
124
|
+
|
|
125
|
+
class MyContextStore(ContextStore, ABC):
|
|
126
|
+
@abstractmethod
|
|
105
127
|
async def append(self, message: Message) -> None: ...
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
106
130
|
async def get_history(self) -> list[Message]: ...
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
|
|
132
|
+
# Everything else — session_id, close(), fork(), clear(),
|
|
133
|
+
# get/set_context_tokens() — has a default implementation.
|
|
110
134
|
```
|
|
111
135
|
|
|
112
136
|
### PermissionGuard
|
|
113
137
|
|
|
138
|
+
<!-- name: test_readme_permission_guard -->
|
|
114
139
|
```python
|
|
140
|
+
from typing import Protocol, runtime_checkable
|
|
141
|
+
from axio.tool import ToolHandler
|
|
142
|
+
|
|
115
143
|
@runtime_checkable
|
|
116
144
|
class PermissionGuard(Protocol):
|
|
117
145
|
async def check(self, handler: ToolHandler) -> ToolHandler: ...
|
|
@@ -131,6 +159,7 @@ class PermissionGuard(Protocol):
|
|
|
131
159
|
|
|
132
160
|
## Tools
|
|
133
161
|
|
|
162
|
+
<!-- name: test_readme_tools -->
|
|
134
163
|
```python
|
|
135
164
|
from axio.tool import Tool, ToolHandler
|
|
136
165
|
|
|
@@ -153,7 +182,9 @@ tool = Tool(
|
|
|
153
182
|
|
|
154
183
|
## Testing
|
|
155
184
|
|
|
185
|
+
<!-- name: test_readme_testing -->
|
|
156
186
|
```python
|
|
187
|
+
from axio.agent import Agent
|
|
157
188
|
from axio.testing import (
|
|
158
189
|
StubTransport,
|
|
159
190
|
make_tool_use_response,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "axio"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "Minimal, streaming-first, protocol-driven foundation for LLM-powered agents"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -21,6 +21,7 @@ packages = ["src/axio"]
|
|
|
21
21
|
|
|
22
22
|
[tool.pytest.ini_options]
|
|
23
23
|
asyncio_mode = "auto"
|
|
24
|
+
testpaths = ["tests", "README.md"]
|
|
24
25
|
|
|
25
26
|
[tool.ruff]
|
|
26
27
|
line-length = 119
|
|
@@ -40,4 +41,5 @@ dev = [
|
|
|
40
41
|
"mypy>=1.14",
|
|
41
42
|
"ruff>=0.9",
|
|
42
43
|
"pytest-cov>=7.1.0",
|
|
44
|
+
"markdown-pytest>=0.6.0",
|
|
43
45
|
]
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import dataclasses
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
from collections.abc import AsyncGenerator, Iterable
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Self
|
|
11
12
|
|
|
12
13
|
from axio.blocks import TextBlock, ToolResultBlock, ToolUseBlock
|
|
13
14
|
from axio.context import ContextStore
|
|
@@ -34,11 +35,15 @@ logger = logging.getLogger(__name__)
|
|
|
34
35
|
@dataclass(slots=True)
|
|
35
36
|
class Agent:
|
|
36
37
|
system: str
|
|
37
|
-
tools: list[Tool]
|
|
38
38
|
transport: CompletionTransport
|
|
39
|
+
tools: list[Tool] = field(default_factory=list)
|
|
39
40
|
selector: ToolSelector | None = field(default=None)
|
|
40
41
|
max_iterations: int = field(default=50)
|
|
41
42
|
|
|
43
|
+
def copy(self, **overrides: Any) -> Self:
|
|
44
|
+
"""Return a new Agent with *overrides* applied."""
|
|
45
|
+
return dataclasses.replace(self, **overrides)
|
|
46
|
+
|
|
42
47
|
def run_stream(self, user_message: str, context: ContextStore) -> AgentStream:
|
|
43
48
|
return AgentStream(self._run_loop(user_message, context))
|
|
44
49
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Context compaction: summarise old history to stay within token limits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from axio.agent import Agent
|
|
8
|
+
from axio.blocks import TextBlock, ToolResultBlock
|
|
9
|
+
from axio.context import ContextStore, MemoryContextStore
|
|
10
|
+
from axio.messages import Message
|
|
11
|
+
from axio.transport import CompletionTransport
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_DEFAULT_COMPACTION_PROMPT = (
|
|
16
|
+
"You are a conversation summarizer. You will see a conversation between"
|
|
17
|
+
" a user and an AI assistant, including tool calls and their results."
|
|
18
|
+
" Produce a concise summary preserving: user goals, decisions made,"
|
|
19
|
+
" key facts, tool outcomes, and state changes. Write as narrative prose,"
|
|
20
|
+
" not as a transcript."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def compact_context(
|
|
25
|
+
context: ContextStore,
|
|
26
|
+
transport: CompletionTransport,
|
|
27
|
+
*,
|
|
28
|
+
max_messages: int = 20,
|
|
29
|
+
keep_recent: int = 6,
|
|
30
|
+
system_prompt: str = _DEFAULT_COMPACTION_PROMPT,
|
|
31
|
+
) -> list[Message] | None:
|
|
32
|
+
"""Summarize old messages from *context*, keeping recent ones verbatim.
|
|
33
|
+
|
|
34
|
+
Returns a compacted message list ready to populate a fresh store,
|
|
35
|
+
or ``None`` when no compaction is needed.
|
|
36
|
+
"""
|
|
37
|
+
history = await context.get_history()
|
|
38
|
+
if len(history) <= max_messages:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
split = _find_safe_boundary(history, keep_recent)
|
|
42
|
+
if split <= 0:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
old, recent = history[:split], history[split:]
|
|
46
|
+
|
|
47
|
+
summary_ctx = MemoryContextStore(old)
|
|
48
|
+
agent = Agent(system=system_prompt, transport=transport, max_iterations=1)
|
|
49
|
+
try:
|
|
50
|
+
summary = await agent.run("Summarize the conversation above.", summary_ctx)
|
|
51
|
+
except Exception:
|
|
52
|
+
logger.warning("Context compaction failed, keeping original history", exc_info=True)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
Message(role="user", content=[TextBlock(text=summary)]),
|
|
57
|
+
Message(role="assistant", content=[TextBlock(text="Understood, context restored.")]),
|
|
58
|
+
*recent,
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _find_safe_boundary(history: list[Message], keep_recent: int) -> int:
|
|
63
|
+
"""Return a split index that never separates a tool_use from its tool_result."""
|
|
64
|
+
split = len(history) - keep_recent
|
|
65
|
+
while split > 0 and any(isinstance(b, ToolResultBlock) for b in history[split].content):
|
|
66
|
+
split -= 1
|
|
67
|
+
return split
|
|
@@ -9,9 +9,8 @@ from dataclasses import dataclass
|
|
|
9
9
|
from typing import Self
|
|
10
10
|
from uuid import uuid4
|
|
11
11
|
|
|
12
|
-
from axio.blocks import TextBlock
|
|
12
|
+
from axio.blocks import TextBlock
|
|
13
13
|
from axio.messages import Message
|
|
14
|
-
from axio.transport import CompletionTransport
|
|
15
14
|
|
|
16
15
|
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
@@ -28,8 +27,11 @@ class SessionInfo:
|
|
|
28
27
|
|
|
29
28
|
class ContextStore(ABC):
|
|
30
29
|
@property
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
def session_id(self) -> str:
|
|
31
|
+
"""Lazy-init UUID hex; works without calling super().__init__()."""
|
|
32
|
+
if "_session_id" not in self.__dict__:
|
|
33
|
+
self.__dict__["_session_id"] = uuid4().hex
|
|
34
|
+
return str(self.__dict__["_session_id"])
|
|
33
35
|
|
|
34
36
|
@abstractmethod
|
|
35
37
|
async def append(self, message: Message) -> None: ...
|
|
@@ -37,20 +39,26 @@ class ContextStore(ABC):
|
|
|
37
39
|
@abstractmethod
|
|
38
40
|
async def get_history(self) -> list[Message]: ...
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
async def clear(self) -> None:
|
|
43
|
+
raise NotImplementedError(f"{type(self).__name__} does not support clear()")
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
async def fork(self) -> ContextStore:
|
|
46
|
+
"""Returns a MemoryContextStore deep copy by default."""
|
|
47
|
+
messages = copy.deepcopy(await self.get_history())
|
|
48
|
+
in_tok, out_tok = await self.get_context_tokens()
|
|
49
|
+
store = MemoryContextStore(messages)
|
|
50
|
+
await store.set_context_tokens(in_tok, out_tok)
|
|
51
|
+
return store
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
async def set_context_tokens(self, input_tokens: int, output_tokens: int) -> None:
|
|
54
|
+
"""No-op by default; tokens are silently dropped."""
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
async def get_context_tokens(self) -> tuple[int, int]:
|
|
57
|
+
"""Returns (0, 0) by default."""
|
|
58
|
+
return 0, 0
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
async def close(self) -> None:
|
|
61
|
+
"""No-op by default."""
|
|
54
62
|
|
|
55
63
|
async def list_sessions(self) -> list[SessionInfo]:
|
|
56
64
|
"""List available sessions. Default: returns a single entry for the current session."""
|
|
@@ -132,66 +140,3 @@ class MemoryContextStore(ContextStore):
|
|
|
132
140
|
|
|
133
141
|
async def close(self) -> None:
|
|
134
142
|
pass
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
_DEFAULT_COMPACTION_PROMPT = (
|
|
138
|
-
"You are a conversation summarizer. You will see a conversation between"
|
|
139
|
-
" a user and an AI assistant, including tool calls and their results."
|
|
140
|
-
" Produce a concise summary preserving: user goals, decisions made,"
|
|
141
|
-
" key facts, tool outcomes, and state changes. Write as narrative prose,"
|
|
142
|
-
" not as a transcript."
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
async def compact_context(
|
|
147
|
-
context: ContextStore,
|
|
148
|
-
transport: CompletionTransport,
|
|
149
|
-
*,
|
|
150
|
-
max_messages: int = 20,
|
|
151
|
-
keep_recent: int = 6,
|
|
152
|
-
system_prompt: str | None = None,
|
|
153
|
-
) -> list[Message] | None:
|
|
154
|
-
"""Summarize old messages from *context*, keeping recent ones verbatim.
|
|
155
|
-
|
|
156
|
-
Returns a compacted message list ready to populate a fresh store,
|
|
157
|
-
or ``None`` when no compaction is needed.
|
|
158
|
-
"""
|
|
159
|
-
history = await context.get_history()
|
|
160
|
-
if len(history) <= max_messages:
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
split = _find_safe_boundary(history, keep_recent)
|
|
164
|
-
if split <= 0:
|
|
165
|
-
return None
|
|
166
|
-
|
|
167
|
-
old, recent = history[:split], history[split:]
|
|
168
|
-
|
|
169
|
-
# Deferred import to avoid circular dependency (context ↔ agent)
|
|
170
|
-
from axio.agent import Agent
|
|
171
|
-
|
|
172
|
-
summary_ctx = MemoryContextStore(old)
|
|
173
|
-
agent = Agent(
|
|
174
|
-
system=system_prompt or _DEFAULT_COMPACTION_PROMPT,
|
|
175
|
-
tools=[],
|
|
176
|
-
transport=transport,
|
|
177
|
-
max_iterations=1,
|
|
178
|
-
)
|
|
179
|
-
try:
|
|
180
|
-
summary = await agent.run("Summarize the conversation above.", summary_ctx)
|
|
181
|
-
except Exception:
|
|
182
|
-
logger.warning("Context compaction failed, keeping original history", exc_info=True)
|
|
183
|
-
return None
|
|
184
|
-
|
|
185
|
-
return [
|
|
186
|
-
Message(role="user", content=[TextBlock(text=summary)]),
|
|
187
|
-
Message(role="assistant", content=[TextBlock(text="Understood, context restored.")]),
|
|
188
|
-
*recent,
|
|
189
|
-
]
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _find_safe_boundary(history: list[Message], keep_recent: int) -> int:
|
|
193
|
-
"""Return a split index that never separates a tool_use from its tool_result."""
|
|
194
|
-
split = len(history) - keep_recent
|
|
195
|
-
while split > 0 and any(isinstance(b, ToolResultBlock) for b in history[split].content):
|
|
196
|
-
split -= 1
|
|
197
|
-
return split
|
|
@@ -91,3 +91,15 @@ class ModelRegistry(MutableMapping[str, ModelSpec]):
|
|
|
91
91
|
|
|
92
92
|
def ids(self) -> list[str]:
|
|
93
93
|
return list(self._models)
|
|
94
|
+
|
|
95
|
+
def _get_model_by_index(self, index: int) -> ModelSpec:
|
|
96
|
+
vals = list(iter(self._models.values()))
|
|
97
|
+
if len(vals) >= 1:
|
|
98
|
+
return vals[index]
|
|
99
|
+
raise IndexError("ModelRegistry is empty")
|
|
100
|
+
|
|
101
|
+
def first(self) -> ModelSpec:
|
|
102
|
+
return self._get_model_by_index(0)
|
|
103
|
+
|
|
104
|
+
def last(self) -> ModelSpec:
|
|
105
|
+
return self._get_model_by_index(-1)
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
"""Transport protocols: completion, image gen, TTS, STT.
|
|
1
|
+
"""Transport protocols: completion, image gen, TTS, STT.
|
|
2
|
+
|
|
3
|
+
Transports should be stateless — all request state lives in the arguments
|
|
4
|
+
passed to each method call. This allows multiple agents to share a single
|
|
5
|
+
transport instance and call it concurrently without interference.
|
|
6
|
+
|
|
7
|
+
The one allowed exception is a reusable connection pool (e.g. an
|
|
8
|
+
``aiohttp.ClientSession``), which is safe to share across concurrent calls.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
@@ -6,7 +6,8 @@ from collections.abc import AsyncIterator
|
|
|
6
6
|
from typing import Literal
|
|
7
7
|
|
|
8
8
|
from axio.blocks import TextBlock, ToolResultBlock
|
|
9
|
-
from axio.
|
|
9
|
+
from axio.compaction import _find_safe_boundary, compact_context
|
|
10
|
+
from axio.context import ContextStore, MemoryContextStore
|
|
10
11
|
from axio.events import StreamEvent
|
|
11
12
|
from axio.messages import Message
|
|
12
13
|
from axio.testing import StubTransport, make_text_response
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Tests for ModelRegistry.first() and ModelRegistry.last()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from axio.models import ModelRegistry, ModelSpec
|
|
8
|
+
|
|
9
|
+
A = ModelSpec(id="a", input_cost=1.0)
|
|
10
|
+
B = ModelSpec(id="b", input_cost=2.0)
|
|
11
|
+
C = ModelSpec(id="c", input_cost=3.0)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_first_single() -> None:
|
|
15
|
+
reg = ModelRegistry([A])
|
|
16
|
+
assert reg.first() == A
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_last_single() -> None:
|
|
20
|
+
reg = ModelRegistry([A])
|
|
21
|
+
assert reg.last() == A
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_first_last_same_for_single_element() -> None:
|
|
25
|
+
reg = ModelRegistry([A])
|
|
26
|
+
assert reg.first() == reg.last()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_first_multi() -> None:
|
|
30
|
+
reg = ModelRegistry([A, B, C])
|
|
31
|
+
assert reg.first() == A
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_last_multi() -> None:
|
|
35
|
+
reg = ModelRegistry([A, B, C])
|
|
36
|
+
assert reg.last() == C
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_first_empty_raises() -> None:
|
|
40
|
+
reg = ModelRegistry()
|
|
41
|
+
with pytest.raises(IndexError, match="ModelRegistry is empty"):
|
|
42
|
+
reg.first()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_last_empty_raises() -> None:
|
|
46
|
+
reg = ModelRegistry()
|
|
47
|
+
with pytest.raises(IndexError, match="ModelRegistry is empty"):
|
|
48
|
+
reg.last()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_first_after_by_cost() -> None:
|
|
52
|
+
reg = ModelRegistry([C, A, B])
|
|
53
|
+
cheapest = reg.by_cost()
|
|
54
|
+
assert cheapest.first() == A
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_last_after_by_cost() -> None:
|
|
58
|
+
reg = ModelRegistry([C, A, B])
|
|
59
|
+
priciest = reg.by_cost(desc=True)
|
|
60
|
+
assert priciest.first() == C
|
|
61
|
+
assert priciest.last() == A
|
|
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
|