axio-context-sqlite 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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio-context-sqlite
3
- Version: 0.6.2
3
+ Version: 0.8.0
4
4
  Summary: SQLite-backed context store for Axio
5
- Project-URL: Homepage, https://github.com/axio-agent/monorepo
6
- Project-URL: Repository, https://github.com/axio-agent/monorepo
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
  Keywords: agent,ai,context,llm,sqlite,storage
9
9
  Requires-Python: >=3.12
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  [![Python](https://img.shields.io/pypi/pyversions/axio-context-sqlite)](https://pypi.org/project/axio-context-sqlite/)
18
18
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
19
19
 
20
- SQLite-backed persistent context store for [axio](https://github.com/axio-agent/monorepo).
20
+ SQLite-backed persistent context store for [axio](https://github.com/mosquito/axio-agent).
21
21
 
22
22
  ## Installation
23
23
 
@@ -4,7 +4,7 @@
4
4
  [![Python](https://img.shields.io/pypi/pyversions/axio-context-sqlite)](https://pypi.org/project/axio-context-sqlite/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
 
7
- SQLite-backed persistent context store for [axio](https://github.com/axio-agent/monorepo).
7
+ SQLite-backed persistent context store for [axio](https://github.com/mosquito/axio-agent).
8
8
 
9
9
  ## Installation
10
10
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio-context-sqlite"
3
- version = "0.6.2"
3
+ version = "0.8.0"
4
4
  description = "SQLite-backed context store for Axio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -9,8 +9,8 @@ keywords = ["llm", "agent", "ai", "sqlite", "context", "storage"]
9
9
  dependencies = ["axio", "aiosqlite>=0.20"]
10
10
 
11
11
  [project.urls]
12
- Homepage = "https://github.com/axio-agent/monorepo"
13
- Repository = "https://github.com/axio-agent/monorepo"
12
+ Homepage = "https://github.com/mosquito/axio-agent"
13
+ Repository = "https://github.com/mosquito/axio-agent"
14
14
 
15
15
  [build-system]
16
16
  requires = ["hatchling"]
@@ -0,0 +1,158 @@
1
+ """Tests for AutoCompactStore wrapping SQLiteContextStore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncGenerator, AsyncIterator
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ import aiosqlite
11
+ import pytest
12
+ from axio.blocks import TextBlock
13
+ from axio.compaction import AutoCompactStore
14
+ from axio.events import StreamEvent
15
+ from axio.messages import Message
16
+ from axio.testing import StubTransport, make_text_response
17
+ from axio.tool import Tool
18
+
19
+ from axio_context_sqlite import SQLiteContextStore, connect
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ def _msg(role: str = "user", text: str = "x") -> Message:
27
+ return Message(role=role, content=[TextBlock(text=text)]) # type: ignore[arg-type]
28
+
29
+
30
+ class FailTransport:
31
+ def stream(self, messages: list[Message], tools: list[Tool[Any]], system: str) -> AsyncIterator[StreamEvent]:
32
+ raise RuntimeError("boom")
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Fixtures
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ @pytest.fixture
41
+ def db_path(tmp_path: Path) -> Path:
42
+ return tmp_path / "autocompact_test.db"
43
+
44
+
45
+ @pytest.fixture
46
+ async def conn(db_path: Path) -> AsyncGenerator[aiosqlite.Connection, None]:
47
+ c = await connect(db_path)
48
+ yield c
49
+ await c.close()
50
+
51
+
52
+ @pytest.fixture
53
+ async def sqlite_store(conn: aiosqlite.Connection) -> SQLiteContextStore:
54
+ return SQLiteContextStore(conn, uuid4().hex, "test-project")
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Tests
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ class TestAutoCompactWithSQLite:
63
+ async def test_history_written_back_to_sqlite(self, sqlite_store: SQLiteContextStore) -> None:
64
+ """After compaction, messages in the DB are the compacted ones."""
65
+ for i in range(14):
66
+ role = "user" if i % 2 == 0 else "assistant"
67
+ await sqlite_store.append(_msg(role, f"msg-{i}"))
68
+
69
+ transport = StubTransport([make_text_response("Summary of earlier work")])
70
+ store = AutoCompactStore(sqlite_store, transport, max_tokens=1, keep_recent=4)
71
+
72
+ await store.add_context_tokens(2, 0)
73
+
74
+ history = await sqlite_store.get_history()
75
+ # 2 (summary pair) + 4 recent
76
+ assert len(history) == 6
77
+ assert history[0].role == "user"
78
+ block0 = history[0].content[0]
79
+ assert isinstance(block0, TextBlock)
80
+ assert "Summary of earlier work" in block0.text
81
+ assert history[1].role == "assistant"
82
+
83
+ async def test_cumulative_tokens_persisted_in_sqlite(self, sqlite_store: SQLiteContextStore) -> None:
84
+ """Cumulative token counts survive compaction in the SQLite table."""
85
+ for i in range(14):
86
+ role = "user" if i % 2 == 0 else "assistant"
87
+ await sqlite_store.append(_msg(role, f"msg-{i}"))
88
+ await sqlite_store.set_context_tokens(400_000, 15_000)
89
+
90
+ transport = StubTransport([make_text_response("summary")])
91
+ store = AutoCompactStore(sqlite_store, transport, max_tokens=1, keep_recent=4)
92
+
93
+ await store.add_context_tokens(2, 0)
94
+
95
+ in_tok, out_tok = await sqlite_store.get_context_tokens()
96
+ assert in_tok == 400_002
97
+ assert out_tok == 15_000
98
+
99
+ async def test_sqlite_session_id_preserved(self, sqlite_store: SQLiteContextStore) -> None:
100
+ """session_id of wrapper matches the inner SQLiteContextStore."""
101
+ store = AutoCompactStore(sqlite_store, StubTransport([]), max_tokens=999_999)
102
+ assert store.session_id == sqlite_store.session_id
103
+
104
+ async def test_compact_does_not_corrupt_on_failure(self, sqlite_store: SQLiteContextStore) -> None:
105
+ """If compaction fails, the original history in the DB is untouched."""
106
+ msgs = [_msg("user" if i % 2 == 0 else "assistant", f"msg-{i}") for i in range(14)]
107
+ for m in msgs:
108
+ await sqlite_store.append(m)
109
+
110
+ store = AutoCompactStore(sqlite_store, FailTransport(), max_tokens=1, keep_recent=4)
111
+ await store.add_context_tokens(2, 0)
112
+
113
+ history = await sqlite_store.get_history()
114
+ assert len(history) == 14
115
+
116
+ async def test_fork_creates_independent_sqlite_session(self, sqlite_store: SQLiteContextStore) -> None:
117
+ """fork() returns an AutoCompactStore with a new session_id; mutations are independent."""
118
+ for i in range(6):
119
+ role = "user" if i % 2 == 0 else "assistant"
120
+ await sqlite_store.append(_msg(role, f"msg-{i}"))
121
+
122
+ store = AutoCompactStore(sqlite_store, StubTransport([]), max_tokens=999_999)
123
+ forked = await store.fork()
124
+
125
+ assert forked.session_id != store.session_id
126
+
127
+ # append to fork — parent unaffected
128
+ await forked.append(_msg(text="extra"))
129
+ assert len(await store.get_history()) == 6
130
+ assert len(await forked.get_history()) == 7
131
+
132
+ async def test_compact_uses_fork_as_snapshot(self, sqlite_store: SQLiteContextStore) -> None:
133
+ """compact_context receives a fork of the store, not the live store itself."""
134
+ for i in range(14):
135
+ role = "user" if i % 2 == 0 else "assistant"
136
+ await sqlite_store.append(_msg(role, f"msg-{i}"))
137
+
138
+ seen_session_ids: list[str] = []
139
+
140
+ import axio.compaction as _mod
141
+
142
+ original = _mod.compact_context
143
+
144
+ async def _capturing(ctx, transport, **kw): # type: ignore[no-untyped-def]
145
+ seen_session_ids.append(ctx.session_id)
146
+ return await original(ctx, StubTransport([make_text_response("summary")]), **kw)
147
+
148
+ _mod.compact_context = _capturing # type: ignore[assignment]
149
+ try:
150
+ store = AutoCompactStore(
151
+ sqlite_store, StubTransport([make_text_response("summary")]), max_tokens=1, keep_recent=4
152
+ )
153
+ await store.add_context_tokens(2, 0)
154
+ finally:
155
+ _mod.compact_context = original
156
+
157
+ assert len(seen_session_ids) == 1
158
+ assert seen_session_ids[0] != sqlite_store.session_id