aether-context 0.3.0__py3-none-any.whl
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.
- aether_context/__init__.py +29 -0
- aether_context/_log.py +33 -0
- aether_context/cli.py +1191 -0
- aether_context/config.py +206 -0
- aether_context/context_pool.py +819 -0
- aether_context/encoder.py +213 -0
- aether_context/errors.py +93 -0
- aether_context/local_llm.py +846 -0
- aether_context/mpo.py +151 -0
- aether_context/py.typed +0 -0
- aether_context/quantize.py +86 -0
- aether_context/session.py +829 -0
- aether_context/slice_loader.py +501 -0
- aether_context/tokenizer.py +64 -0
- aether_context/ui.py +253 -0
- aether_context/witness.py +356 -0
- aether_context-0.3.0.dist-info/METADATA +429 -0
- aether_context-0.3.0.dist-info/RECORD +23 -0
- aether_context-0.3.0.dist-info/WHEEL +5 -0
- aether_context-0.3.0.dist-info/entry_points.txt +2 -0
- aether_context-0.3.0.dist-info/licenses/LICENSE +201 -0
- aether_context-0.3.0.dist-info/licenses/NOTICE.md +19 -0
- aether_context-0.3.0.dist-info/top_level.txt +1 -0
aether_context/config.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# aether-context (Unlimited Context)
|
|
2
|
+
# Copyright (c) 2026 Aether AI
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Configuration dataclasses + ~/.aether-context persistence.
|
|
5
|
+
|
|
6
|
+
``PoolConfig`` governs the on-disk context pool (size = reach, index kind, slice size).
|
|
7
|
+
``SessionConfig`` governs a single run (which model, the window fractions that drive
|
|
8
|
+
trigger/target/verbatim behavior). Both are plain dataclasses (no pydantic) to keep the
|
|
9
|
+
core dependency surface at numpy-only.
|
|
10
|
+
|
|
11
|
+
Constants:
|
|
12
|
+
* window fractions TRIGGER 0.75 / TARGET 0.50 / VERBATIM 0.30
|
|
13
|
+
* pool reach math ``reach ≈ pool_gb × 233M tokens`` (README table)
|
|
14
|
+
* the 5 GB pool floor (README minimum for a usable reach)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import shutil
|
|
20
|
+
from dataclasses import asdict, dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from aether_context._log import get_logger
|
|
24
|
+
from aether_context.errors import PoolBudgetError, PoolCorrupt
|
|
25
|
+
|
|
26
|
+
_log = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
# --- engine constants --------------------------------------------------------
|
|
29
|
+
#: Minimum usable pool size in GB (README floor).
|
|
30
|
+
POOL_GB_FLOOR = 5
|
|
31
|
+
#: Tokens of reach per GB of pool (README: reach ≈ pool_gb × 233M).
|
|
32
|
+
TOKENS_PER_GB = 233_000_000
|
|
33
|
+
#: Window fraction at which overflow encoding triggers.
|
|
34
|
+
TRIGGER_FRACTION = 0.75
|
|
35
|
+
#: Target window occupancy after a paged compaction.
|
|
36
|
+
TARGET_FRACTION = 0.50
|
|
37
|
+
#: Fraction of the window kept verbatim (never encoded away).
|
|
38
|
+
VERBATIM_FRACTION = 0.30
|
|
39
|
+
|
|
40
|
+
#: Default retrieval embedding dimensionality (the 256-dim retrieval embedding).
|
|
41
|
+
DEFAULT_DIM = 256
|
|
42
|
+
#: Default tokens per encoded slice.
|
|
43
|
+
DEFAULT_SLICE_TOKENS = 512
|
|
44
|
+
#: Config file name inside the pool dir.
|
|
45
|
+
CONFIG_FILENAME = "config.json"
|
|
46
|
+
|
|
47
|
+
_VALID_INDEX = ("flat", "hnsw", "tiered")
|
|
48
|
+
_VALID_MODE = ("separate", "shared")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reach_tokens(pool_gb: int) -> int:
|
|
52
|
+
"""Token reach for a given pool size: ``pool_gb × TOKENS_PER_GB``."""
|
|
53
|
+
return pool_gb * TOKENS_PER_GB
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def default_pool_dir() -> Path:
|
|
57
|
+
"""The default pool directory: ``~/.aether-context``."""
|
|
58
|
+
return Path.home() / ".aether-context"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#: Bytes per gigabyte (binary), for disk / RAM math.
|
|
62
|
+
BYTES_PER_GB = 1024 ** 3
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def free_disk_bytes(path: "Path | str") -> int | None:
|
|
66
|
+
"""Free bytes on the filesystem that holds ``path`` (nearest existing ancestor).
|
|
67
|
+
|
|
68
|
+
The pool directory may not exist yet, so we walk up to the closest existing parent before
|
|
69
|
+
probing. Returns ``None`` if free space cannot be read (so callers can skip the check
|
|
70
|
+
rather than wrongly block). Never raises.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
p = Path(path)
|
|
74
|
+
while not p.exists() and p != p.parent:
|
|
75
|
+
p = p.parent
|
|
76
|
+
return int(shutil.disk_usage(str(p)).free)
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class PoolConfig:
|
|
83
|
+
"""On-disk context pool configuration.
|
|
84
|
+
|
|
85
|
+
``pool_gb`` is *reach*, not window. Rejects ``pool_gb < POOL_GB_FLOOR`` with a reason.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
pool_gb: int = POOL_GB_FLOOR
|
|
89
|
+
mode: str = "separate"
|
|
90
|
+
index: str = "flat"
|
|
91
|
+
dim: int = DEFAULT_DIM
|
|
92
|
+
slice_tokens: int = DEFAULT_SLICE_TOKENS
|
|
93
|
+
dir: Path = field(default_factory=default_pool_dir)
|
|
94
|
+
quantize_bits: int = 0 # TurboVec: 0 = float32 (default); 8 = recall-safe (~4x); 4 = lossy/flagged
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
# Path coercion (callers may pass a str).
|
|
98
|
+
if not isinstance(self.dir, Path):
|
|
99
|
+
self.dir = Path(self.dir)
|
|
100
|
+
if self.quantize_bits not in (0, 4, 8):
|
|
101
|
+
raise PoolBudgetError(
|
|
102
|
+
f"quantize_bits={self.quantize_bits} is not one of (0, 4, 8)",
|
|
103
|
+
hint="0=float32, 8=recall-safe TurboVec (~4x), 4=lossy (flag-only).",
|
|
104
|
+
)
|
|
105
|
+
if self.pool_gb < POOL_GB_FLOOR:
|
|
106
|
+
raise PoolBudgetError(
|
|
107
|
+
f"pool_gb={self.pool_gb} is below the {POOL_GB_FLOOR} GB pool floor "
|
|
108
|
+
f"(reach would be too small to be useful)"
|
|
109
|
+
)
|
|
110
|
+
if self.index not in _VALID_INDEX:
|
|
111
|
+
raise PoolBudgetError(
|
|
112
|
+
f"index={self.index!r} is not one of {_VALID_INDEX}",
|
|
113
|
+
hint="Use index='flat' (numpy, always works), 'hnsw', or 'tiered'.",
|
|
114
|
+
)
|
|
115
|
+
if self.mode not in _VALID_MODE:
|
|
116
|
+
raise PoolBudgetError(
|
|
117
|
+
f"mode={self.mode!r} is not one of {_VALID_MODE}",
|
|
118
|
+
hint="Use mode='separate' (default) or 'shared'.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def reach(self) -> int:
|
|
123
|
+
"""Token reach of this pool (``pool_gb × TOKENS_PER_GB``)."""
|
|
124
|
+
return reach_tokens(self.pool_gb)
|
|
125
|
+
|
|
126
|
+
def config_path(self) -> Path:
|
|
127
|
+
"""Path to the persisted ``config.json`` inside :attr:`dir`."""
|
|
128
|
+
return self.dir / CONFIG_FILENAME
|
|
129
|
+
|
|
130
|
+
def save(self) -> Path:
|
|
131
|
+
"""Write this config to ``<dir>/config.json`` (creating the dir). Returns the path."""
|
|
132
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
path = self.config_path()
|
|
134
|
+
data = asdict(self)
|
|
135
|
+
data["dir"] = str(self.dir) # JSON cannot hold a Path
|
|
136
|
+
path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8")
|
|
137
|
+
_log.debug("saved pool config to %s", path)
|
|
138
|
+
return path
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def load(cls, dir: Path | str | None = None) -> "PoolConfig":
|
|
142
|
+
"""Load config from ``<dir>/config.json``; defaults if the file is absent.
|
|
143
|
+
|
|
144
|
+
Raises :class:`~aether_context.errors.PoolCorrupt` if the file exists but is
|
|
145
|
+
malformed.
|
|
146
|
+
"""
|
|
147
|
+
base = Path(dir) if dir is not None else default_pool_dir()
|
|
148
|
+
path = base / CONFIG_FILENAME
|
|
149
|
+
if not path.exists():
|
|
150
|
+
return cls(dir=base)
|
|
151
|
+
try:
|
|
152
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
153
|
+
except (json.JSONDecodeError, OSError, ValueError) as exc:
|
|
154
|
+
raise PoolCorrupt(f"could not read pool config at {path}: {exc}") from exc
|
|
155
|
+
raw.pop("reach", None) # derived; never persisted but be defensive
|
|
156
|
+
raw["dir"] = base
|
|
157
|
+
try:
|
|
158
|
+
return cls(**raw)
|
|
159
|
+
except TypeError as exc:
|
|
160
|
+
raise PoolCorrupt(f"pool config at {path} has unexpected fields: {exc}") from exc
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class SessionConfig:
|
|
165
|
+
"""Per-run configuration.
|
|
166
|
+
|
|
167
|
+
``model`` is required (a spec string like ``"ollama/qwen2.5"`` or ``"mock"``, or a
|
|
168
|
+
``LocalLLM`` object). The window fractions use the engine defaults.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
model: object
|
|
172
|
+
system: str | None = None
|
|
173
|
+
max_tokens: int | None = None
|
|
174
|
+
verbatim_fraction: float = VERBATIM_FRACTION
|
|
175
|
+
trigger_fraction: float = TRIGGER_FRACTION
|
|
176
|
+
target_fraction: float = TARGET_FRACTION
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
for name, value in (
|
|
180
|
+
("verbatim_fraction", self.verbatim_fraction),
|
|
181
|
+
("trigger_fraction", self.trigger_fraction),
|
|
182
|
+
("target_fraction", self.target_fraction),
|
|
183
|
+
):
|
|
184
|
+
if not (0.0 <= float(value) <= 1.0):
|
|
185
|
+
raise PoolBudgetError(
|
|
186
|
+
f"{name}={value} must be within [0.0, 1.0]",
|
|
187
|
+
hint="Window fractions are proportions of the model's native window.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
"PoolConfig",
|
|
193
|
+
"SessionConfig",
|
|
194
|
+
"reach_tokens",
|
|
195
|
+
"default_pool_dir",
|
|
196
|
+
"free_disk_bytes",
|
|
197
|
+
"BYTES_PER_GB",
|
|
198
|
+
"POOL_GB_FLOOR",
|
|
199
|
+
"TOKENS_PER_GB",
|
|
200
|
+
"TRIGGER_FRACTION",
|
|
201
|
+
"TARGET_FRACTION",
|
|
202
|
+
"VERBATIM_FRACTION",
|
|
203
|
+
"DEFAULT_DIM",
|
|
204
|
+
"DEFAULT_SLICE_TOKENS",
|
|
205
|
+
"CONFIG_FILENAME",
|
|
206
|
+
]
|