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.
@@ -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
+ ]