chuk-ai-session-manager 0.7.1__py3-none-any.whl → 0.8.1__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.
- chuk_ai_session_manager/__init__.py +84 -40
- chuk_ai_session_manager/api/__init__.py +1 -1
- chuk_ai_session_manager/api/simple_api.py +53 -59
- chuk_ai_session_manager/exceptions.py +31 -17
- chuk_ai_session_manager/guards/__init__.py +118 -0
- chuk_ai_session_manager/guards/bindings.py +217 -0
- chuk_ai_session_manager/guards/cache.py +163 -0
- chuk_ai_session_manager/guards/manager.py +819 -0
- chuk_ai_session_manager/guards/models.py +498 -0
- chuk_ai_session_manager/guards/ungrounded.py +159 -0
- chuk_ai_session_manager/infinite_conversation.py +86 -79
- chuk_ai_session_manager/memory/__init__.py +247 -0
- chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
- chuk_ai_session_manager/memory/context_packer.py +347 -0
- chuk_ai_session_manager/memory/fault_handler.py +507 -0
- chuk_ai_session_manager/memory/manifest.py +307 -0
- chuk_ai_session_manager/memory/models.py +1084 -0
- chuk_ai_session_manager/memory/mutation_log.py +186 -0
- chuk_ai_session_manager/memory/pack_cache.py +206 -0
- chuk_ai_session_manager/memory/page_table.py +275 -0
- chuk_ai_session_manager/memory/prefetcher.py +192 -0
- chuk_ai_session_manager/memory/tlb.py +247 -0
- chuk_ai_session_manager/memory/vm_prompts.py +238 -0
- chuk_ai_session_manager/memory/working_set.py +574 -0
- chuk_ai_session_manager/models/__init__.py +21 -9
- chuk_ai_session_manager/models/event_source.py +3 -1
- chuk_ai_session_manager/models/event_type.py +10 -1
- chuk_ai_session_manager/models/session.py +103 -68
- chuk_ai_session_manager/models/session_event.py +69 -68
- chuk_ai_session_manager/models/session_metadata.py +9 -10
- chuk_ai_session_manager/models/session_run.py +21 -22
- chuk_ai_session_manager/models/token_usage.py +76 -76
- chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
- chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
- chuk_ai_session_manager/procedural_memory/manager.py +523 -0
- chuk_ai_session_manager/procedural_memory/models.py +371 -0
- chuk_ai_session_manager/sample_tools.py +79 -46
- chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
- chuk_ai_session_manager/session_manager.py +259 -232
- chuk_ai_session_manager/session_prompt_builder.py +163 -111
- chuk_ai_session_manager/session_storage.py +45 -52
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/METADATA +80 -4
- chuk_ai_session_manager-0.8.1.dist-info/RECORD +45 -0
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/WHEEL +1 -1
- chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# chuk_ai_session_manager/memory/working_set.py
|
|
2
|
+
"""
|
|
3
|
+
Working Set Manager for AI Virtual Memory.
|
|
4
|
+
|
|
5
|
+
The WorkingSetManager tracks which pages are currently "hot" (in L0/L1)
|
|
6
|
+
and manages capacity constraints. It's the gatekeeper for what's in context.
|
|
7
|
+
|
|
8
|
+
Design principles:
|
|
9
|
+
- Pydantic-native: BaseModel subclass with proper validation
|
|
10
|
+
- Token-aware: Tracks token budget across modalities
|
|
11
|
+
- Eviction-ready: Provides candidates when under pressure
|
|
12
|
+
- Pinning support: Critical pages are never evicted
|
|
13
|
+
- Anti-thrash: Prevent evicting recently faulted pages
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
20
|
+
|
|
21
|
+
from .models import (
|
|
22
|
+
MemoryPage,
|
|
23
|
+
StorageTier,
|
|
24
|
+
TokenBudget,
|
|
25
|
+
WorkingSetStats,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Pinned Set
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PinnedSet(BaseModel):
|
|
35
|
+
"""
|
|
36
|
+
Pages that are never evicted from working set.
|
|
37
|
+
Pinning prevents thrash on critical context.
|
|
38
|
+
|
|
39
|
+
Auto-pinned by default:
|
|
40
|
+
- System prompt page
|
|
41
|
+
- Active goal/plan page
|
|
42
|
+
- User preferences page
|
|
43
|
+
- Current tool schemas
|
|
44
|
+
- Last N turns (configurable, typically 2-4)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Explicitly pinned pages
|
|
48
|
+
pinned: Set[str] = Field(default_factory=set)
|
|
49
|
+
|
|
50
|
+
# Auto-pin configuration
|
|
51
|
+
auto_pin_last_n_turns: int = Field(
|
|
52
|
+
default=3, description="Auto-pin last N user+assistant turn pairs"
|
|
53
|
+
)
|
|
54
|
+
auto_pin_system_prompt: bool = Field(default=True)
|
|
55
|
+
auto_pin_claims: bool = Field(
|
|
56
|
+
default=True, description="Auto-pin claim pages (high-value)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Pages auto-pinned (tracked separately for debugging)
|
|
60
|
+
auto_pinned: Set[str] = Field(default_factory=set)
|
|
61
|
+
|
|
62
|
+
def pin(self, page_id: str) -> None:
|
|
63
|
+
"""Explicitly pin a page."""
|
|
64
|
+
self.pinned.add(page_id)
|
|
65
|
+
|
|
66
|
+
def unpin(self, page_id: str) -> None:
|
|
67
|
+
"""Unpin a page (only affects explicit pins)."""
|
|
68
|
+
self.pinned.discard(page_id)
|
|
69
|
+
self.auto_pinned.discard(page_id)
|
|
70
|
+
|
|
71
|
+
def is_pinned(self, page_id: str) -> bool:
|
|
72
|
+
"""Check if a page is pinned (explicitly or auto)."""
|
|
73
|
+
return page_id in self.pinned or page_id in self.auto_pinned
|
|
74
|
+
|
|
75
|
+
def auto_pin(self, page_id: str) -> None:
|
|
76
|
+
"""Auto-pin a page (e.g., recent turn, claim)."""
|
|
77
|
+
self.auto_pinned.add(page_id)
|
|
78
|
+
|
|
79
|
+
def clear_auto_pins(self) -> None:
|
|
80
|
+
"""Clear all auto-pins (before recalculating)."""
|
|
81
|
+
self.auto_pinned.clear()
|
|
82
|
+
|
|
83
|
+
def get_all_pinned(self) -> Set[str]:
|
|
84
|
+
"""Get all pinned pages (explicit + auto)."""
|
|
85
|
+
return self.pinned | self.auto_pinned
|
|
86
|
+
|
|
87
|
+
def count(self) -> int:
|
|
88
|
+
"""Total number of pinned pages."""
|
|
89
|
+
return len(self.get_all_pinned())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Anti-Thrash Policy
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AntiThrashPolicy(BaseModel):
|
|
98
|
+
"""
|
|
99
|
+
Prevent evicting pages that were just faulted in.
|
|
100
|
+
|
|
101
|
+
OS working sets fail when you thrash. LLM working sets thrash
|
|
102
|
+
when user toggles between topics.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Recently-evicted pages get a "do not evict again" window
|
|
106
|
+
eviction_cooldown_turns: int = Field(default=3)
|
|
107
|
+
|
|
108
|
+
# Recently-faulted pages get temporary protection
|
|
109
|
+
fault_protection_turns: int = Field(default=2)
|
|
110
|
+
|
|
111
|
+
# Track eviction/fault history: page_id -> turn number
|
|
112
|
+
_eviction_history: Dict[str, int] = PrivateAttr(default_factory=dict)
|
|
113
|
+
_fault_history: Dict[str, int] = PrivateAttr(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
def record_eviction(self, page_id: str, turn: int) -> None:
|
|
116
|
+
"""Record that a page was evicted at this turn."""
|
|
117
|
+
self._eviction_history[page_id] = turn
|
|
118
|
+
|
|
119
|
+
def record_fault(self, page_id: str, turn: int) -> None:
|
|
120
|
+
"""Record that a page was faulted in at this turn."""
|
|
121
|
+
self._fault_history[page_id] = turn
|
|
122
|
+
|
|
123
|
+
def can_evict(self, page_id: str, current_turn: int) -> bool:
|
|
124
|
+
"""Check if page is in cooldown period."""
|
|
125
|
+
# Check fault protection
|
|
126
|
+
if page_id in self._fault_history:
|
|
127
|
+
fault_turn = self._fault_history[page_id]
|
|
128
|
+
if current_turn - fault_turn < self.fault_protection_turns:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Check eviction cooldown (avoid re-evicting)
|
|
132
|
+
if page_id in self._eviction_history:
|
|
133
|
+
evict_turn = self._eviction_history[page_id]
|
|
134
|
+
if current_turn - evict_turn < self.eviction_cooldown_turns:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def get_eviction_penalty(self, page_id: str, current_turn: int) -> float:
|
|
140
|
+
"""
|
|
141
|
+
Higher penalty = less likely to evict.
|
|
142
|
+
|
|
143
|
+
Recently faulted = high penalty (we just loaded it!)
|
|
144
|
+
Recently evicted = high penalty (avoid re-evicting)
|
|
145
|
+
"""
|
|
146
|
+
penalty = 0.0
|
|
147
|
+
|
|
148
|
+
if page_id in self._fault_history:
|
|
149
|
+
fault_turn = self._fault_history[page_id]
|
|
150
|
+
turns_since = current_turn - fault_turn
|
|
151
|
+
if turns_since < self.fault_protection_turns:
|
|
152
|
+
# High penalty if recently faulted
|
|
153
|
+
penalty += 1.0 - (turns_since / self.fault_protection_turns)
|
|
154
|
+
|
|
155
|
+
if page_id in self._eviction_history:
|
|
156
|
+
evict_turn = self._eviction_history[page_id]
|
|
157
|
+
turns_since = current_turn - evict_turn
|
|
158
|
+
if turns_since < self.eviction_cooldown_turns:
|
|
159
|
+
# Moderate penalty if recently evicted
|
|
160
|
+
penalty += 0.5 * (1.0 - (turns_since / self.eviction_cooldown_turns))
|
|
161
|
+
|
|
162
|
+
return min(1.0, penalty)
|
|
163
|
+
|
|
164
|
+
def cleanup_old_history(self, current_turn: int, max_age: int = 20) -> None:
|
|
165
|
+
"""Remove old history entries to prevent memory growth."""
|
|
166
|
+
self._fault_history = {
|
|
167
|
+
pid: turn
|
|
168
|
+
for pid, turn in self._fault_history.items()
|
|
169
|
+
if current_turn - turn <= max_age
|
|
170
|
+
}
|
|
171
|
+
self._eviction_history = {
|
|
172
|
+
pid: turn
|
|
173
|
+
for pid, turn in self._eviction_history.items()
|
|
174
|
+
if current_turn - turn <= max_age
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class WorkingSetConfig(BaseModel):
|
|
179
|
+
"""Configuration for working set management."""
|
|
180
|
+
|
|
181
|
+
# Token limits
|
|
182
|
+
max_l0_tokens: int = Field(
|
|
183
|
+
default=128_000, description="Maximum tokens in L0 (context window)"
|
|
184
|
+
)
|
|
185
|
+
max_l1_pages: int = Field(default=100, description="Maximum pages in L1 cache")
|
|
186
|
+
|
|
187
|
+
# Eviction thresholds
|
|
188
|
+
eviction_threshold: float = Field(
|
|
189
|
+
default=0.85,
|
|
190
|
+
ge=0.0,
|
|
191
|
+
le=1.0,
|
|
192
|
+
description="Trigger eviction when utilization exceeds this",
|
|
193
|
+
)
|
|
194
|
+
target_utilization: float = Field(
|
|
195
|
+
default=0.70, ge=0.0, le=1.0, description="Target utilization after eviction"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Reserved tokens
|
|
199
|
+
reserved_tokens: int = Field(
|
|
200
|
+
default=4000, description="Reserved for system prompt, tools, etc."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class WorkingSetManager(BaseModel):
|
|
205
|
+
"""
|
|
206
|
+
Manages the working set (L0 + L1 pages).
|
|
207
|
+
|
|
208
|
+
The working set is what's currently "hot" - either in the context window
|
|
209
|
+
(L0) or in fast cache (L1). This manager tracks capacity, handles
|
|
210
|
+
promotion/demotion, and identifies eviction candidates.
|
|
211
|
+
|
|
212
|
+
Includes pinning (never evict critical pages) and anti-thrash
|
|
213
|
+
(don't evict recently-faulted pages).
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
config: WorkingSetConfig = Field(default_factory=WorkingSetConfig)
|
|
217
|
+
|
|
218
|
+
# Token budget tracking
|
|
219
|
+
budget: TokenBudget = Field(default_factory=TokenBudget)
|
|
220
|
+
|
|
221
|
+
# L0 pages (in context) - ordered by position
|
|
222
|
+
l0_pages: List[str] = Field(
|
|
223
|
+
default_factory=list, description="Page IDs in L0, ordered"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# L1 pages (hot cache) - maps page_id -> MemoryPage
|
|
227
|
+
l1_cache: Dict[str, MemoryPage] = Field(
|
|
228
|
+
default_factory=dict, description="Pages in L1 cache"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Page importance overrides
|
|
232
|
+
importance_overrides: Dict[str, float] = Field(
|
|
233
|
+
default_factory=dict, description="Manual importance adjustments"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Pinned set - pages that are never evicted
|
|
237
|
+
pinned_set: PinnedSet = Field(default_factory=PinnedSet)
|
|
238
|
+
|
|
239
|
+
# Anti-thrash policy
|
|
240
|
+
anti_thrash: AntiThrashPolicy = Field(default_factory=AntiThrashPolicy)
|
|
241
|
+
|
|
242
|
+
# Current turn number (for anti-thrash)
|
|
243
|
+
current_turn: int = Field(default=0)
|
|
244
|
+
|
|
245
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
246
|
+
|
|
247
|
+
def __len__(self) -> int:
|
|
248
|
+
"""Total pages in working set."""
|
|
249
|
+
return len(self.l0_pages) + len(self.l1_cache)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def l0_count(self) -> int:
|
|
253
|
+
"""Number of pages in L0."""
|
|
254
|
+
return len(self.l0_pages)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def l1_count(self) -> int:
|
|
258
|
+
"""Number of pages in L1."""
|
|
259
|
+
return len(self.l1_cache)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def utilization(self) -> float:
|
|
263
|
+
"""Current L0 token utilization (0-1)."""
|
|
264
|
+
return self.budget.utilization
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def tokens_used(self) -> int:
|
|
268
|
+
"""Total tokens in L0."""
|
|
269
|
+
return self.budget.used
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def tokens_available(self) -> int:
|
|
273
|
+
"""Available tokens for new content."""
|
|
274
|
+
return self.budget.available
|
|
275
|
+
|
|
276
|
+
def needs_eviction(self) -> bool:
|
|
277
|
+
"""Check if eviction is needed."""
|
|
278
|
+
return self.utilization > self.config.eviction_threshold
|
|
279
|
+
|
|
280
|
+
def can_fit(self, tokens: int) -> bool:
|
|
281
|
+
"""Check if additional tokens can fit in L0."""
|
|
282
|
+
return self.budget.can_fit(tokens)
|
|
283
|
+
|
|
284
|
+
def add_to_l0(self, page: MemoryPage) -> bool:
|
|
285
|
+
"""
|
|
286
|
+
Add a page to L0 (context window).
|
|
287
|
+
|
|
288
|
+
Returns True if successful, False if insufficient space.
|
|
289
|
+
Does NOT automatically evict - call get_eviction_candidates first.
|
|
290
|
+
"""
|
|
291
|
+
tokens = page.size_tokens or page.estimate_tokens()
|
|
292
|
+
|
|
293
|
+
if not self.budget.can_fit(tokens):
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
# Add to budget
|
|
297
|
+
self.budget.add(tokens, page.modality)
|
|
298
|
+
|
|
299
|
+
# Add to L0 list (at end = most recent)
|
|
300
|
+
if page.page_id not in self.l0_pages:
|
|
301
|
+
self.l0_pages.append(page.page_id)
|
|
302
|
+
|
|
303
|
+
# Remove from L1 if present (promoted to L0)
|
|
304
|
+
self.l1_cache.pop(page.page_id, None)
|
|
305
|
+
|
|
306
|
+
# Update page state
|
|
307
|
+
page.storage_tier = StorageTier.L0
|
|
308
|
+
page.mark_accessed()
|
|
309
|
+
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def add_to_l1(self, page: MemoryPage) -> bool:
|
|
313
|
+
"""
|
|
314
|
+
Add a page to L1 (hot cache).
|
|
315
|
+
|
|
316
|
+
Returns True if successful, False if L1 is full.
|
|
317
|
+
"""
|
|
318
|
+
if len(self.l1_cache) >= self.config.max_l1_pages:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
# Remove from L0 if present (demoted to L1)
|
|
322
|
+
if page.page_id in self.l0_pages:
|
|
323
|
+
self.l0_pages.remove(page.page_id)
|
|
324
|
+
tokens = page.size_tokens or page.estimate_tokens()
|
|
325
|
+
self.budget.remove(tokens, page.modality)
|
|
326
|
+
|
|
327
|
+
# Add to L1
|
|
328
|
+
self.l1_cache[page.page_id] = page
|
|
329
|
+
page.storage_tier = StorageTier.L1
|
|
330
|
+
page.mark_accessed()
|
|
331
|
+
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
def remove(self, page_id: str) -> Optional[MemoryPage]:
|
|
335
|
+
"""
|
|
336
|
+
Remove a page from the working set entirely.
|
|
337
|
+
|
|
338
|
+
Returns the removed page, or None if not found.
|
|
339
|
+
"""
|
|
340
|
+
# Check L1 first
|
|
341
|
+
page = self.l1_cache.pop(page_id, None)
|
|
342
|
+
|
|
343
|
+
# Check L0
|
|
344
|
+
if page_id in self.l0_pages:
|
|
345
|
+
self.l0_pages.remove(page_id)
|
|
346
|
+
# We need the page to update budget - caller should handle this
|
|
347
|
+
# For now, assume average tokens per modality
|
|
348
|
+
|
|
349
|
+
return page
|
|
350
|
+
|
|
351
|
+
def remove_from_l0(self, page_id: str, page: MemoryPage) -> bool:
|
|
352
|
+
"""
|
|
353
|
+
Remove a specific page from L0, updating budget.
|
|
354
|
+
|
|
355
|
+
Returns True if removed, False if not in L0.
|
|
356
|
+
"""
|
|
357
|
+
if page_id not in self.l0_pages:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
self.l0_pages.remove(page_id)
|
|
361
|
+
tokens = page.size_tokens or page.estimate_tokens()
|
|
362
|
+
self.budget.remove(tokens, page.modality)
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
def promote_to_l0(self, page: MemoryPage) -> bool:
|
|
366
|
+
"""
|
|
367
|
+
Promote a page from L1 to L0.
|
|
368
|
+
|
|
369
|
+
Returns True if successful.
|
|
370
|
+
"""
|
|
371
|
+
if page.page_id not in self.l1_cache:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
return self.add_to_l0(page)
|
|
375
|
+
|
|
376
|
+
def demote_to_l1(self, page: MemoryPage) -> bool:
|
|
377
|
+
"""
|
|
378
|
+
Demote a page from L0 to L1.
|
|
379
|
+
|
|
380
|
+
Returns True if successful.
|
|
381
|
+
"""
|
|
382
|
+
if page.page_id not in self.l0_pages:
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
return self.add_to_l1(page)
|
|
386
|
+
|
|
387
|
+
def get_page(self, page_id: str) -> Optional[MemoryPage]:
|
|
388
|
+
"""Get a page from L1 cache (L0 pages are tracked by ID only)."""
|
|
389
|
+
return self.l1_cache.get(page_id)
|
|
390
|
+
|
|
391
|
+
def is_in_l0(self, page_id: str) -> bool:
|
|
392
|
+
"""Check if a page is in L0."""
|
|
393
|
+
return page_id in self.l0_pages
|
|
394
|
+
|
|
395
|
+
def is_in_l1(self, page_id: str) -> bool:
|
|
396
|
+
"""Check if a page is in L1."""
|
|
397
|
+
return page_id in self.l1_cache
|
|
398
|
+
|
|
399
|
+
def is_in_working_set(self, page_id: str) -> bool:
|
|
400
|
+
"""Check if a page is in the working set (L0 or L1)."""
|
|
401
|
+
return self.is_in_l0(page_id) or self.is_in_l1(page_id)
|
|
402
|
+
|
|
403
|
+
def get_eviction_candidates(
|
|
404
|
+
self,
|
|
405
|
+
tokens_needed: int = 0,
|
|
406
|
+
from_tier: StorageTier = StorageTier.L0,
|
|
407
|
+
) -> List[Tuple[str, float]]:
|
|
408
|
+
"""
|
|
409
|
+
Get pages that are candidates for eviction, scored by priority.
|
|
410
|
+
|
|
411
|
+
Returns list of (page_id, eviction_score) tuples.
|
|
412
|
+
Lower score = evict first.
|
|
413
|
+
|
|
414
|
+
Scoring considers:
|
|
415
|
+
- Pinning (pinned pages are excluded)
|
|
416
|
+
- Anti-thrash (recently faulted pages get penalty)
|
|
417
|
+
- Recency (LRU component)
|
|
418
|
+
- Access frequency (LFU component)
|
|
419
|
+
- Importance (user/system-assigned)
|
|
420
|
+
- Position (older messages first)
|
|
421
|
+
"""
|
|
422
|
+
candidates = []
|
|
423
|
+
|
|
424
|
+
if from_tier == StorageTier.L0:
|
|
425
|
+
# For L0, we only have page IDs - score by position
|
|
426
|
+
for i, page_id in enumerate(self.l0_pages):
|
|
427
|
+
# Skip pinned pages
|
|
428
|
+
if self.pinned_set.is_pinned(page_id):
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Check anti-thrash policy
|
|
432
|
+
if not self.anti_thrash.can_evict(page_id, self.current_turn):
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
# Earlier position = lower score = evict first
|
|
436
|
+
position_score = i / max(len(self.l0_pages), 1)
|
|
437
|
+
importance = self.importance_overrides.get(page_id, 0.5)
|
|
438
|
+
|
|
439
|
+
# Add anti-thrash penalty (higher penalty = higher score = less likely to evict)
|
|
440
|
+
thrash_penalty = self.anti_thrash.get_eviction_penalty(
|
|
441
|
+
page_id, self.current_turn
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Combine position, importance, and anti-thrash
|
|
445
|
+
score = position_score * 0.4 + importance * 0.4 + thrash_penalty * 0.2
|
|
446
|
+
candidates.append((page_id, score))
|
|
447
|
+
else:
|
|
448
|
+
# For L1, we have full pages with access tracking
|
|
449
|
+
now = datetime.utcnow()
|
|
450
|
+
for page_id, page in self.l1_cache.items():
|
|
451
|
+
# Skip pinned pages
|
|
452
|
+
if self.pinned_set.is_pinned(page_id) or page.pinned:
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
# Check anti-thrash policy
|
|
456
|
+
if not self.anti_thrash.can_evict(page_id, self.current_turn):
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# Recency score (seconds since access, normalized)
|
|
460
|
+
age_seconds = (now - page.last_accessed).total_seconds()
|
|
461
|
+
recency_score = 1.0 / (1.0 + age_seconds / 3600) # Decay over hours
|
|
462
|
+
|
|
463
|
+
# Frequency score (log scale)
|
|
464
|
+
import math
|
|
465
|
+
|
|
466
|
+
frequency_score = math.log1p(page.access_count) / 10.0
|
|
467
|
+
frequency_score = min(1.0, frequency_score)
|
|
468
|
+
|
|
469
|
+
# Importance (page type affects this)
|
|
470
|
+
importance = self.importance_overrides.get(page_id, page.importance)
|
|
471
|
+
|
|
472
|
+
# Add anti-thrash penalty
|
|
473
|
+
thrash_penalty = self.anti_thrash.get_eviction_penalty(
|
|
474
|
+
page_id, self.current_turn
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Combined score (higher = keep longer)
|
|
478
|
+
score = (
|
|
479
|
+
recency_score * 0.3
|
|
480
|
+
+ frequency_score * 0.2
|
|
481
|
+
+ importance * 0.3
|
|
482
|
+
+ thrash_penalty * 0.2
|
|
483
|
+
)
|
|
484
|
+
candidates.append((page_id, score))
|
|
485
|
+
|
|
486
|
+
# Sort by score (lowest first = evict first)
|
|
487
|
+
candidates.sort(key=lambda x: x[1])
|
|
488
|
+
|
|
489
|
+
return candidates
|
|
490
|
+
|
|
491
|
+
def new_turn(self) -> None:
|
|
492
|
+
"""Advance to a new turn, updating anti-thrash tracking."""
|
|
493
|
+
self.current_turn += 1
|
|
494
|
+
# Cleanup old history periodically
|
|
495
|
+
if self.current_turn % 10 == 0:
|
|
496
|
+
self.anti_thrash.cleanup_old_history(self.current_turn)
|
|
497
|
+
|
|
498
|
+
def pin_page(self, page_id: str) -> None:
|
|
499
|
+
"""Pin a page (will not be evicted)."""
|
|
500
|
+
self.pinned_set.pin(page_id)
|
|
501
|
+
|
|
502
|
+
def unpin_page(self, page_id: str) -> None:
|
|
503
|
+
"""Unpin a page."""
|
|
504
|
+
self.pinned_set.unpin(page_id)
|
|
505
|
+
|
|
506
|
+
def is_pinned(self, page_id: str) -> bool:
|
|
507
|
+
"""Check if a page is pinned."""
|
|
508
|
+
return self.pinned_set.is_pinned(page_id)
|
|
509
|
+
|
|
510
|
+
def record_fault(self, page_id: str) -> None:
|
|
511
|
+
"""Record a page fault for anti-thrash tracking."""
|
|
512
|
+
self.anti_thrash.record_fault(page_id, self.current_turn)
|
|
513
|
+
|
|
514
|
+
def record_eviction(self, page_id: str) -> None:
|
|
515
|
+
"""Record an eviction for anti-thrash tracking."""
|
|
516
|
+
self.anti_thrash.record_eviction(page_id, self.current_turn)
|
|
517
|
+
|
|
518
|
+
def calculate_eviction_target(self, tokens_needed: int = 0) -> int:
|
|
519
|
+
"""
|
|
520
|
+
Calculate how many tokens to free to reach target utilization.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
tokens_needed: Additional tokens we need to fit
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Number of tokens to evict
|
|
527
|
+
"""
|
|
528
|
+
current = self.budget.used
|
|
529
|
+
max_tokens = self.config.max_l0_tokens - self.config.reserved_tokens
|
|
530
|
+
target = int(max_tokens * self.config.target_utilization)
|
|
531
|
+
|
|
532
|
+
# Need to get below target AND fit new tokens
|
|
533
|
+
required_free = current - target + tokens_needed
|
|
534
|
+
|
|
535
|
+
return max(0, required_free)
|
|
536
|
+
|
|
537
|
+
def set_importance(self, page_id: str, importance: float) -> None:
|
|
538
|
+
"""Set importance override for a page."""
|
|
539
|
+
self.importance_overrides[page_id] = max(0.0, min(1.0, importance))
|
|
540
|
+
|
|
541
|
+
def clear_importance(self, page_id: str) -> None:
|
|
542
|
+
"""Remove importance override for a page."""
|
|
543
|
+
self.importance_overrides.pop(page_id, None)
|
|
544
|
+
|
|
545
|
+
def get_l0_page_ids(self) -> List[str]:
|
|
546
|
+
"""Get all page IDs in L0, in order."""
|
|
547
|
+
return list(self.l0_pages)
|
|
548
|
+
|
|
549
|
+
def get_l1_pages(self) -> List[MemoryPage]:
|
|
550
|
+
"""Get all pages in L1."""
|
|
551
|
+
return list(self.l1_cache.values())
|
|
552
|
+
|
|
553
|
+
def get_stats(self) -> WorkingSetStats:
|
|
554
|
+
"""Get working set statistics."""
|
|
555
|
+
return WorkingSetStats(
|
|
556
|
+
l0_pages=len(self.l0_pages),
|
|
557
|
+
l1_pages=len(self.l1_cache),
|
|
558
|
+
total_pages=len(self),
|
|
559
|
+
tokens_used=self.tokens_used,
|
|
560
|
+
tokens_available=self.tokens_available,
|
|
561
|
+
utilization=self.utilization,
|
|
562
|
+
needs_eviction=self.needs_eviction(),
|
|
563
|
+
tokens_by_modality=dict(self.budget.tokens_by_modality),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def clear(self) -> None:
|
|
567
|
+
"""Clear the entire working set."""
|
|
568
|
+
self.l0_pages.clear()
|
|
569
|
+
self.l1_cache.clear()
|
|
570
|
+
self.budget = TokenBudget(
|
|
571
|
+
total_limit=self.config.max_l0_tokens,
|
|
572
|
+
reserved=self.config.reserved_tokens,
|
|
573
|
+
)
|
|
574
|
+
self.importance_overrides.clear()
|
|
@@ -2,35 +2,40 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Core models for the chuk session manager.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
# Import each model separately to avoid circular imports
|
|
7
|
+
# These are re-exported as part of the public API
|
|
6
8
|
try:
|
|
7
|
-
from chuk_ai_session_manager.models.event_source import EventSource
|
|
9
|
+
from chuk_ai_session_manager.models.event_source import EventSource # noqa: F401
|
|
8
10
|
except ImportError:
|
|
9
11
|
pass
|
|
10
12
|
|
|
11
13
|
try:
|
|
12
|
-
from chuk_ai_session_manager.models.event_type import EventType
|
|
14
|
+
from chuk_ai_session_manager.models.event_type import EventType # noqa: F401
|
|
13
15
|
except ImportError:
|
|
14
16
|
pass
|
|
15
17
|
|
|
16
18
|
try:
|
|
17
|
-
from chuk_ai_session_manager.models.session_event import SessionEvent
|
|
19
|
+
from chuk_ai_session_manager.models.session_event import SessionEvent # noqa: F401
|
|
18
20
|
except ImportError:
|
|
19
21
|
pass
|
|
20
22
|
|
|
21
23
|
try:
|
|
22
|
-
from chuk_ai_session_manager.models.session_metadata import SessionMetadata
|
|
24
|
+
from chuk_ai_session_manager.models.session_metadata import SessionMetadata # noqa: F401
|
|
23
25
|
except ImportError:
|
|
24
26
|
pass
|
|
25
27
|
|
|
26
28
|
try:
|
|
27
|
-
from chuk_ai_session_manager.models.session_run import
|
|
29
|
+
from chuk_ai_session_manager.models.session_run import ( # noqa: F401
|
|
30
|
+
RunStatus,
|
|
31
|
+
SessionRun,
|
|
32
|
+
)
|
|
28
33
|
except ImportError:
|
|
29
34
|
pass
|
|
30
35
|
|
|
31
36
|
# Import Session last since it might depend on the above
|
|
32
37
|
try:
|
|
33
|
-
from chuk_ai_session_manager.models.session import Session
|
|
38
|
+
from chuk_ai_session_manager.models.session import Session # noqa: F401
|
|
34
39
|
except ImportError:
|
|
35
40
|
pass
|
|
36
41
|
|
|
@@ -38,7 +43,14 @@ except ImportError:
|
|
|
38
43
|
__all__ = []
|
|
39
44
|
|
|
40
45
|
# Check which imports succeeded and add them to __all__
|
|
41
|
-
for name in [
|
|
42
|
-
|
|
46
|
+
for name in [
|
|
47
|
+
"EventSource",
|
|
48
|
+
"EventType",
|
|
49
|
+
"SessionEvent",
|
|
50
|
+
"SessionMetadata",
|
|
51
|
+
"SessionRun",
|
|
52
|
+
"RunStatus",
|
|
53
|
+
"Session",
|
|
54
|
+
]:
|
|
43
55
|
if name in globals():
|
|
44
|
-
__all__.append(name)
|
|
56
|
+
__all__.append(name)
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# chuk_ai_session_manager/models/event_type.py
|
|
2
2
|
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
3
5
|
class EventType(str, Enum):
|
|
4
6
|
"""Type of the session event."""
|
|
7
|
+
|
|
8
|
+
# Episodic memory
|
|
5
9
|
MESSAGE = "message"
|
|
6
10
|
SUMMARY = "summary"
|
|
7
11
|
TOOL_CALL = "tool_call"
|
|
8
12
|
REFERENCE = "reference"
|
|
9
|
-
CONTEXT_BRIDGE = "context_bridge"
|
|
13
|
+
CONTEXT_BRIDGE = "context_bridge"
|
|
14
|
+
|
|
15
|
+
# Procedural memory
|
|
16
|
+
TOOL_TRACE = "tool_trace" # Rich tool invocation record
|
|
17
|
+
TOOL_PATTERN = "tool_pattern" # Aggregated tool patterns
|
|
18
|
+
TOOL_FIX = "tool_fix" # Fix relationship (failure -> success)
|