chuk-ai-session-manager 0.7.1__py3-none-any.whl → 0.8__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 +238 -197
- 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.dist-info}/METADATA +79 -3
- chuk_ai_session_manager-0.8.dist-info/RECORD +45 -0
- {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.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.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# chuk_ai_session_manager/memory/prefetcher.py
|
|
2
|
+
"""
|
|
3
|
+
Simple Prefetcher for AI Virtual Memory.
|
|
4
|
+
|
|
5
|
+
Don't wait for ML prediction. Start with dumb heuristics that work:
|
|
6
|
+
1. Last segment summary (almost always needed for "what did we discuss")
|
|
7
|
+
2. Most-referenced claim pages (high access_count claims)
|
|
8
|
+
3. Tool traces for likely tools (based on recent tool usage)
|
|
9
|
+
|
|
10
|
+
Keep it simple: no ML prediction required.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Set
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
17
|
+
|
|
18
|
+
from .models import PageType
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .page_table import PageTable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolUsagePattern(BaseModel):
|
|
25
|
+
"""Track tool usage patterns for prefetch prediction."""
|
|
26
|
+
|
|
27
|
+
tool_name: str
|
|
28
|
+
call_count: int = Field(default=0)
|
|
29
|
+
last_turn: int = Field(default=0)
|
|
30
|
+
prereq_pages: List[str] = Field(
|
|
31
|
+
default_factory=list
|
|
32
|
+
) # Pages often read before calling
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SimplePrefetcher(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Basic prefetch that doesn't need prediction models.
|
|
38
|
+
Good enough for v0.9.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# How many claim pages to prefetch
|
|
42
|
+
max_claims_to_prefetch: int = Field(default=3)
|
|
43
|
+
|
|
44
|
+
# How many recent tools to consider
|
|
45
|
+
max_recent_tools: int = Field(default=3)
|
|
46
|
+
|
|
47
|
+
# Tool usage tracking
|
|
48
|
+
_tool_usage: Dict[str, ToolUsagePattern] = PrivateAttr(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
# Recent page accesses per tool (tool_name -> list of page_ids accessed before call)
|
|
51
|
+
_tool_prereqs: Dict[str, List[str]] = PrivateAttr(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
# Page access counts (for finding high-value claims)
|
|
54
|
+
_page_access_counts: Counter = PrivateAttr(default_factory=Counter)
|
|
55
|
+
|
|
56
|
+
# Last segment summary page ID (updated when segments roll)
|
|
57
|
+
_last_segment_summary_id: Optional[str] = PrivateAttr(default=None)
|
|
58
|
+
|
|
59
|
+
def record_page_access(self, page_id: str) -> None:
|
|
60
|
+
"""Record a page access for statistics."""
|
|
61
|
+
self._page_access_counts[page_id] += 1
|
|
62
|
+
|
|
63
|
+
def record_tool_call(
|
|
64
|
+
self,
|
|
65
|
+
tool_name: str,
|
|
66
|
+
turn: int,
|
|
67
|
+
pages_accessed_before: Optional[List[str]] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Record a tool call and what pages were accessed before it.
|
|
71
|
+
|
|
72
|
+
This builds up patterns like:
|
|
73
|
+
"When calling weather_tool, we usually read location claims first"
|
|
74
|
+
"""
|
|
75
|
+
if tool_name not in self._tool_usage:
|
|
76
|
+
self._tool_usage[tool_name] = ToolUsagePattern(tool_name=tool_name)
|
|
77
|
+
|
|
78
|
+
pattern = self._tool_usage[tool_name]
|
|
79
|
+
pattern.call_count += 1
|
|
80
|
+
pattern.last_turn = turn
|
|
81
|
+
|
|
82
|
+
# Track pages accessed before this tool call
|
|
83
|
+
if pages_accessed_before:
|
|
84
|
+
if tool_name not in self._tool_prereqs:
|
|
85
|
+
self._tool_prereqs[tool_name] = []
|
|
86
|
+
self._tool_prereqs[tool_name].extend(pages_accessed_before)
|
|
87
|
+
|
|
88
|
+
def set_last_segment_summary(self, page_id: str) -> None:
|
|
89
|
+
"""Update the last segment summary page ID."""
|
|
90
|
+
self._last_segment_summary_id = page_id
|
|
91
|
+
|
|
92
|
+
def get_likely_tools(self, recent_turns: int = 5) -> List[str]:
|
|
93
|
+
"""Get tools likely to be called based on recent usage."""
|
|
94
|
+
# Sort by recency and frequency
|
|
95
|
+
tools = list(self._tool_usage.values())
|
|
96
|
+
tools.sort(key=lambda t: (t.last_turn, t.call_count), reverse=True)
|
|
97
|
+
return [t.tool_name for t in tools[: self.max_recent_tools]]
|
|
98
|
+
|
|
99
|
+
def get_tool_prereq_pages(self, tool_name: str) -> List[str]:
|
|
100
|
+
"""
|
|
101
|
+
Get pages commonly accessed before calling a tool.
|
|
102
|
+
|
|
103
|
+
Returns most frequently accessed prereq pages.
|
|
104
|
+
"""
|
|
105
|
+
prereqs = self._tool_prereqs.get(tool_name, [])
|
|
106
|
+
if not prereqs:
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
# Count occurrences and return top ones
|
|
110
|
+
counts = Counter(prereqs)
|
|
111
|
+
return [page_id for page_id, _ in counts.most_common(3)]
|
|
112
|
+
|
|
113
|
+
def get_top_claims(
|
|
114
|
+
self,
|
|
115
|
+
page_table: Optional["PageTable"] = None,
|
|
116
|
+
limit: Optional[int] = None,
|
|
117
|
+
) -> List[str]:
|
|
118
|
+
"""
|
|
119
|
+
Get most-referenced claim pages.
|
|
120
|
+
|
|
121
|
+
If page_table is provided, filters to only claim-type pages.
|
|
122
|
+
"""
|
|
123
|
+
if limit is None:
|
|
124
|
+
limit = self.max_claims_to_prefetch
|
|
125
|
+
|
|
126
|
+
# Get top accessed pages
|
|
127
|
+
top_pages = [
|
|
128
|
+
page_id for page_id, _ in self._page_access_counts.most_common(limit * 3)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
if page_table is None:
|
|
132
|
+
return top_pages[:limit]
|
|
133
|
+
|
|
134
|
+
# Filter to claims only
|
|
135
|
+
claims = []
|
|
136
|
+
for page_id in top_pages:
|
|
137
|
+
entry = page_table.lookup(page_id)
|
|
138
|
+
if entry and entry.page_type == PageType.CLAIM:
|
|
139
|
+
claims.append(page_id)
|
|
140
|
+
if len(claims) >= limit:
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
return claims
|
|
144
|
+
|
|
145
|
+
async def prefetch_on_turn_start(
|
|
146
|
+
self,
|
|
147
|
+
session_id: str,
|
|
148
|
+
page_table: Optional["PageTable"] = None,
|
|
149
|
+
) -> List[str]:
|
|
150
|
+
"""
|
|
151
|
+
Get pages to prefetch at the start of a turn.
|
|
152
|
+
|
|
153
|
+
Returns list of page_ids to prefetch.
|
|
154
|
+
"""
|
|
155
|
+
pages_to_prefetch: List[str] = []
|
|
156
|
+
seen: Set[str] = set()
|
|
157
|
+
|
|
158
|
+
def add_page(page_id: str) -> None:
|
|
159
|
+
if page_id and page_id not in seen:
|
|
160
|
+
pages_to_prefetch.append(page_id)
|
|
161
|
+
seen.add(page_id)
|
|
162
|
+
|
|
163
|
+
# 1. Last segment summary (almost always needed for "what did we discuss")
|
|
164
|
+
if self._last_segment_summary_id:
|
|
165
|
+
add_page(self._last_segment_summary_id)
|
|
166
|
+
|
|
167
|
+
# 2. Most-referenced claim pages (high access_count claims)
|
|
168
|
+
for claim_id in self.get_top_claims(page_table):
|
|
169
|
+
add_page(claim_id)
|
|
170
|
+
|
|
171
|
+
# 3. Tool prereqs for likely tools
|
|
172
|
+
likely_tools = self.get_likely_tools()
|
|
173
|
+
for tool_name in likely_tools:
|
|
174
|
+
for prereq_id in self.get_tool_prereq_pages(tool_name):
|
|
175
|
+
add_page(prereq_id)
|
|
176
|
+
|
|
177
|
+
return pages_to_prefetch
|
|
178
|
+
|
|
179
|
+
def clear(self) -> None:
|
|
180
|
+
"""Clear all tracking data."""
|
|
181
|
+
self._tool_usage.clear()
|
|
182
|
+
self._tool_prereqs.clear()
|
|
183
|
+
self._page_access_counts.clear()
|
|
184
|
+
self._last_segment_summary_id = None
|
|
185
|
+
|
|
186
|
+
def get_stats(self) -> Dict[str, int]:
|
|
187
|
+
"""Get prefetcher statistics."""
|
|
188
|
+
return {
|
|
189
|
+
"tools_tracked": len(self._tool_usage),
|
|
190
|
+
"pages_tracked": len(self._page_access_counts),
|
|
191
|
+
"total_accesses": sum(self._page_access_counts.values()),
|
|
192
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# chuk_ai_session_manager/memory/tlb.py
|
|
2
|
+
"""
|
|
3
|
+
Translation Lookaside Buffer (TLB) for AI Virtual Memory.
|
|
4
|
+
|
|
5
|
+
The TLB is a small, fast cache for recently accessed page table entries.
|
|
6
|
+
It avoids the overhead of full PageTable lookups for hot pages.
|
|
7
|
+
|
|
8
|
+
Without a TLB, every page access requires a PageTable lookup plus potentially
|
|
9
|
+
a storage lookup. This becomes a bottleneck at scale - you end up measuring
|
|
10
|
+
metadata latency, not content latency.
|
|
11
|
+
|
|
12
|
+
The TLB provides O(1) lookups for recently accessed pages with LRU eviction.
|
|
13
|
+
|
|
14
|
+
Design principles:
|
|
15
|
+
- Pydantic-native: BaseModel subclass with proper validation
|
|
16
|
+
- No magic strings: Uses StorageTier enum
|
|
17
|
+
- Type-safe: Full type annotations throughout
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections import OrderedDict
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
24
|
+
|
|
25
|
+
from .models import CombinedPageTableStats, PageTableEntry, TLBStats
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PageTLB(BaseModel):
|
|
29
|
+
"""
|
|
30
|
+
Translation Lookaside Buffer - fast cache for page table entries.
|
|
31
|
+
|
|
32
|
+
Uses LRU eviction to keep the most recently accessed entries.
|
|
33
|
+
|
|
34
|
+
Typical usage:
|
|
35
|
+
1. Check TLB first (O(1))
|
|
36
|
+
2. If miss, check PageTable
|
|
37
|
+
3. Insert result into TLB
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
max_entries: int = Field(default=512, description="Maximum entries to cache")
|
|
41
|
+
|
|
42
|
+
# LRU cache: OrderedDict maintains insertion order, move_to_end on access
|
|
43
|
+
_entries: OrderedDict = PrivateAttr(default_factory=OrderedDict)
|
|
44
|
+
|
|
45
|
+
# Stats
|
|
46
|
+
hits: int = Field(default=0)
|
|
47
|
+
misses: int = Field(default=0)
|
|
48
|
+
|
|
49
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
50
|
+
|
|
51
|
+
def __len__(self) -> int:
|
|
52
|
+
return len(self._entries)
|
|
53
|
+
|
|
54
|
+
def __contains__(self, page_id: str) -> bool:
|
|
55
|
+
return page_id in self._entries
|
|
56
|
+
|
|
57
|
+
def lookup(self, page_id: str) -> Optional[PageTableEntry]:
|
|
58
|
+
"""
|
|
59
|
+
Look up a page entry in the TLB.
|
|
60
|
+
|
|
61
|
+
Returns the entry if found (TLB hit), None otherwise (TLB miss).
|
|
62
|
+
Updates LRU order on hit.
|
|
63
|
+
"""
|
|
64
|
+
if page_id in self._entries:
|
|
65
|
+
# Move to end (most recently used)
|
|
66
|
+
self._entries.move_to_end(page_id)
|
|
67
|
+
self.hits += 1
|
|
68
|
+
return self._entries[page_id]
|
|
69
|
+
else:
|
|
70
|
+
self.misses += 1
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def insert(self, entry: PageTableEntry) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Insert or update an entry in the TLB.
|
|
76
|
+
|
|
77
|
+
If the TLB is full, evicts the least recently used entry.
|
|
78
|
+
"""
|
|
79
|
+
page_id = entry.page_id
|
|
80
|
+
|
|
81
|
+
# If already present, update and move to end
|
|
82
|
+
if page_id in self._entries:
|
|
83
|
+
self._entries[page_id] = entry
|
|
84
|
+
self._entries.move_to_end(page_id)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Check if we need to evict
|
|
88
|
+
if len(self._entries) >= self.max_entries:
|
|
89
|
+
self._evict_lru()
|
|
90
|
+
|
|
91
|
+
# Insert new entry
|
|
92
|
+
self._entries[page_id] = entry
|
|
93
|
+
|
|
94
|
+
def _evict_lru(self) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Evict the least recently used entry.
|
|
97
|
+
|
|
98
|
+
Returns the evicted page_id, or None if TLB was empty.
|
|
99
|
+
"""
|
|
100
|
+
if not self._entries:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# OrderedDict.popitem(last=False) removes first item (oldest)
|
|
104
|
+
page_id, _ = self._entries.popitem(last=False)
|
|
105
|
+
return page_id
|
|
106
|
+
|
|
107
|
+
def invalidate(self, page_id: str) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Remove an entry from the TLB.
|
|
110
|
+
|
|
111
|
+
Call this when a page is modified, evicted, or deleted.
|
|
112
|
+
Returns True if the entry was present and removed.
|
|
113
|
+
"""
|
|
114
|
+
if page_id in self._entries:
|
|
115
|
+
del self._entries[page_id]
|
|
116
|
+
return True
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def invalidate_tier(self, tier: str) -> int:
|
|
120
|
+
"""
|
|
121
|
+
Invalidate all entries in a specific tier.
|
|
122
|
+
|
|
123
|
+
Useful when flushing a tier or during checkpoints.
|
|
124
|
+
Returns the number of entries invalidated.
|
|
125
|
+
"""
|
|
126
|
+
to_remove = [
|
|
127
|
+
page_id for page_id, entry in self._entries.items() if entry.tier == tier
|
|
128
|
+
]
|
|
129
|
+
for page_id in to_remove:
|
|
130
|
+
del self._entries[page_id]
|
|
131
|
+
return len(to_remove)
|
|
132
|
+
|
|
133
|
+
def flush(self) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Clear the entire TLB.
|
|
136
|
+
|
|
137
|
+
Call this on context switches, checkpoints, or when consistency
|
|
138
|
+
with the PageTable is required.
|
|
139
|
+
Returns the number of entries cleared.
|
|
140
|
+
"""
|
|
141
|
+
count = len(self._entries)
|
|
142
|
+
self._entries.clear()
|
|
143
|
+
return count
|
|
144
|
+
|
|
145
|
+
def get_all(self) -> list[PageTableEntry]:
|
|
146
|
+
"""Get all cached entries (for debugging/inspection)."""
|
|
147
|
+
return list(self._entries.values())
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def hit_rate(self) -> float:
|
|
151
|
+
"""Calculate the TLB hit rate."""
|
|
152
|
+
total = self.hits + self.misses
|
|
153
|
+
if total == 0:
|
|
154
|
+
return 0.0
|
|
155
|
+
return self.hits / total
|
|
156
|
+
|
|
157
|
+
def reset_stats(self) -> None:
|
|
158
|
+
"""Reset hit/miss counters."""
|
|
159
|
+
self.hits = 0
|
|
160
|
+
self.misses = 0
|
|
161
|
+
|
|
162
|
+
def get_stats(self) -> TLBStats:
|
|
163
|
+
"""Get TLB statistics."""
|
|
164
|
+
return TLBStats(
|
|
165
|
+
size=len(self._entries),
|
|
166
|
+
max_size=self.max_entries,
|
|
167
|
+
utilization=len(self._entries) / self.max_entries
|
|
168
|
+
if self.max_entries > 0
|
|
169
|
+
else 0,
|
|
170
|
+
hits=self.hits,
|
|
171
|
+
misses=self.misses,
|
|
172
|
+
hit_rate=self.hit_rate,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TLBWithPageTable:
|
|
177
|
+
"""
|
|
178
|
+
Convenience wrapper that combines TLB with PageTable for lookups.
|
|
179
|
+
|
|
180
|
+
Provides a unified interface that automatically checks TLB first,
|
|
181
|
+
then falls back to PageTable, and keeps TLB updated.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, page_table, tlb: Optional[PageTLB] = None):
|
|
185
|
+
"""
|
|
186
|
+
Initialize with a PageTable and optional TLB.
|
|
187
|
+
|
|
188
|
+
If no TLB is provided, creates one with default settings.
|
|
189
|
+
"""
|
|
190
|
+
from .page_table import PageTable
|
|
191
|
+
|
|
192
|
+
self.page_table: PageTable = page_table
|
|
193
|
+
# Use 'is None' check because empty PageTLB has __len__=0 which is falsy
|
|
194
|
+
self.tlb = tlb if tlb is not None else PageTLB()
|
|
195
|
+
|
|
196
|
+
def lookup(self, page_id: str) -> Optional[PageTableEntry]:
|
|
197
|
+
"""
|
|
198
|
+
Look up a page, checking TLB first.
|
|
199
|
+
|
|
200
|
+
Returns the entry if found, None otherwise.
|
|
201
|
+
Automatically populates TLB on cache miss.
|
|
202
|
+
"""
|
|
203
|
+
# Check TLB first
|
|
204
|
+
entry = self.tlb.lookup(page_id)
|
|
205
|
+
if entry is not None:
|
|
206
|
+
return entry
|
|
207
|
+
|
|
208
|
+
# TLB miss - check page table
|
|
209
|
+
entry = self.page_table.lookup(page_id)
|
|
210
|
+
if entry is not None:
|
|
211
|
+
# Populate TLB for next time
|
|
212
|
+
self.tlb.insert(entry)
|
|
213
|
+
|
|
214
|
+
return entry
|
|
215
|
+
|
|
216
|
+
def register(self, page) -> PageTableEntry:
|
|
217
|
+
"""Register a page in both PageTable and TLB."""
|
|
218
|
+
entry = self.page_table.register(page)
|
|
219
|
+
self.tlb.insert(entry)
|
|
220
|
+
return entry
|
|
221
|
+
|
|
222
|
+
def update_location(self, page_id: str, **kwargs) -> bool:
|
|
223
|
+
"""Update location in PageTable and invalidate TLB entry."""
|
|
224
|
+
success = self.page_table.update_location(page_id, **kwargs)
|
|
225
|
+
if success:
|
|
226
|
+
# Invalidate stale TLB entry - will be refreshed on next lookup
|
|
227
|
+
self.tlb.invalidate(page_id)
|
|
228
|
+
return success
|
|
229
|
+
|
|
230
|
+
def mark_dirty(self, page_id: str) -> bool:
|
|
231
|
+
"""Mark page dirty and invalidate TLB."""
|
|
232
|
+
success = self.page_table.mark_dirty(page_id)
|
|
233
|
+
if success:
|
|
234
|
+
self.tlb.invalidate(page_id)
|
|
235
|
+
return success
|
|
236
|
+
|
|
237
|
+
def remove(self, page_id: str) -> Optional[PageTableEntry]:
|
|
238
|
+
"""Remove from both PageTable and TLB."""
|
|
239
|
+
self.tlb.invalidate(page_id)
|
|
240
|
+
return self.page_table.remove(page_id)
|
|
241
|
+
|
|
242
|
+
def get_stats(self) -> CombinedPageTableStats:
|
|
243
|
+
"""Get combined stats."""
|
|
244
|
+
return CombinedPageTableStats(
|
|
245
|
+
page_table=self.page_table.get_stats(),
|
|
246
|
+
tlb=self.tlb.get_stats(),
|
|
247
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# chuk_ai_session_manager/memory/vm_prompts.py
|
|
2
|
+
"""
|
|
3
|
+
Virtual Memory system prompts for Chat Completions integration.
|
|
4
|
+
|
|
5
|
+
These prompts enforce VM semantics when injected into the developer message.
|
|
6
|
+
|
|
7
|
+
Design principles:
|
|
8
|
+
- Pydantic-native: Tool definitions as proper models
|
|
9
|
+
- No magic strings: Uses enums for modes and types
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from .models import (
|
|
16
|
+
Modality,
|
|
17
|
+
ToolDefinition,
|
|
18
|
+
ToolFunction,
|
|
19
|
+
ToolParameter,
|
|
20
|
+
ToolParameters,
|
|
21
|
+
ToolType,
|
|
22
|
+
VMMode,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Strict mode: No hallucinated memory, citations required
|
|
27
|
+
VM_STRICT_PROMPT = """You are operating under STRICT Virtual Memory grounding rules.
|
|
28
|
+
|
|
29
|
+
Your ONLY valid sources of information are:
|
|
30
|
+
1) The content inside <VM:CONTEXT> (the currently mapped working set), and
|
|
31
|
+
2) The content returned by tools (e.g., page_fault) in messages with role="tool".
|
|
32
|
+
|
|
33
|
+
Everything listed in <VM:MANIFEST_JSON> is DISCOVERY METADATA ONLY.
|
|
34
|
+
- You MUST NOT quote, paraphrase, or "use" hint text from the manifest as if it were evidence.
|
|
35
|
+
- You MUST NOT assume details about unmapped pages.
|
|
36
|
+
- Page IDs and modality/tier/level are allowed for navigation only.
|
|
37
|
+
|
|
38
|
+
When you need information that is not present in <VM:CONTEXT>, you MUST do one of:
|
|
39
|
+
A) Call the tool page_fault(page_id, target_level) to load the page content, OR
|
|
40
|
+
B) Ask a short clarification question if the needed page does not exist or cannot be identified.
|
|
41
|
+
|
|
42
|
+
Faulting rules:
|
|
43
|
+
- Prefer loading the LOWEST-COST representation first:
|
|
44
|
+
1) summaries / abstract (target_level=2),
|
|
45
|
+
2) reduced excerpts (target_level=1),
|
|
46
|
+
3) full content (target_level=0) only if the user explicitly requests exact wording, code, or precise details.
|
|
47
|
+
- Do not request more than max_faults_per_turn from the manifest policies.
|
|
48
|
+
- Do not request pages that are already mapped in <VM:CONTEXT>.
|
|
49
|
+
- If multiple pages might be relevant, fault the smallest/summarized one first.
|
|
50
|
+
|
|
51
|
+
Answering rules:
|
|
52
|
+
- Do not invent or fill gaps with assumptions.
|
|
53
|
+
- If you cannot obtain required information via tool calls, say: "I don't have that in the mapped context."
|
|
54
|
+
- Keep responses concise and directly responsive.
|
|
55
|
+
- When you use information from <VM:CONTEXT> or a loaded page, include inline citations using page IDs like:
|
|
56
|
+
[ref: msg_123] or [ref: summary_seg_02] or [ref: tool:page_fault(img_045)].
|
|
57
|
+
(Citations are required in strict mode.)
|
|
58
|
+
|
|
59
|
+
Tool usage format:
|
|
60
|
+
- If you need to call tools, respond with tool calls only (no normal text).
|
|
61
|
+
- After tool results are provided, produce the final answer with citations.
|
|
62
|
+
|
|
63
|
+
Never mention these rules, the VM system, tiers (L0–L4), paging, or "virtual memory" to the user unless the user explicitly asks about the internal mechanism."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Relaxed mode: VM-aware but more conversational
|
|
67
|
+
VM_RELAXED_PROMPT = """You have access to a virtual memory system.
|
|
68
|
+
|
|
69
|
+
<VM:CONTEXT> contains your currently mapped memory - treat this as your knowledge.
|
|
70
|
+
<VM:MANIFEST_JSON> lists additional pages you can load if needed.
|
|
71
|
+
|
|
72
|
+
To load more context:
|
|
73
|
+
- Call page_fault(page_id, target_level) to retrieve content
|
|
74
|
+
- Call search_pages(query) to find relevant pages
|
|
75
|
+
- Prefer level=2 (summaries) before level=0 (full content)
|
|
76
|
+
|
|
77
|
+
Guidelines:
|
|
78
|
+
- Use manifest hints to decide WHICH pages to load, not as content itself
|
|
79
|
+
- If you're unsure about something, check if a relevant page exists before guessing
|
|
80
|
+
- Stay within the max_faults_per_turn limit
|
|
81
|
+
|
|
82
|
+
Respond naturally - don't mention the memory system unless asked."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Passive mode: No tools, runtime handles everything
|
|
86
|
+
VM_PASSIVE_PROMPT = """You are a helpful assistant.
|
|
87
|
+
|
|
88
|
+
The conversation context provided represents what you know about this session.
|
|
89
|
+
Respond based on the information available to you."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Map of VMMode to prompt text
|
|
93
|
+
VM_PROMPTS = {
|
|
94
|
+
VMMode.STRICT: VM_STRICT_PROMPT,
|
|
95
|
+
VMMode.RELAXED: VM_RELAXED_PROMPT,
|
|
96
|
+
VMMode.PASSIVE: VM_PASSIVE_PROMPT,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_prompt_for_mode(mode: VMMode) -> str:
|
|
101
|
+
"""Get the prompt text for a given VM mode."""
|
|
102
|
+
return VM_PROMPTS.get(mode, VM_PASSIVE_PROMPT)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_vm_developer_message(
|
|
106
|
+
mode: VMMode,
|
|
107
|
+
manifest_json: str,
|
|
108
|
+
context: str,
|
|
109
|
+
system_prompt: str = "",
|
|
110
|
+
max_faults_per_turn: int = 2,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Build the complete developer message with VM rules, manifest, and context.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
mode: VMMode enum value (STRICT, RELAXED, or PASSIVE)
|
|
117
|
+
manifest_json: JSON string of the VM manifest
|
|
118
|
+
context: The VM:CONTEXT block content
|
|
119
|
+
system_prompt: Optional additional system instructions
|
|
120
|
+
max_faults_per_turn: Policy value to inject into strict prompt
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Complete developer message string
|
|
124
|
+
"""
|
|
125
|
+
if mode == VMMode.STRICT:
|
|
126
|
+
rules = VM_STRICT_PROMPT.replace(
|
|
127
|
+
"max_faults_per_turn from the manifest policies",
|
|
128
|
+
f"max_faults_per_turn ({max_faults_per_turn}) from the manifest policies",
|
|
129
|
+
)
|
|
130
|
+
elif mode == VMMode.RELAXED:
|
|
131
|
+
rules = VM_RELAXED_PROMPT
|
|
132
|
+
else:
|
|
133
|
+
rules = VM_PASSIVE_PROMPT
|
|
134
|
+
|
|
135
|
+
parts: List[str] = []
|
|
136
|
+
|
|
137
|
+
if system_prompt:
|
|
138
|
+
parts.append(system_prompt)
|
|
139
|
+
|
|
140
|
+
if mode != VMMode.PASSIVE:
|
|
141
|
+
parts.append(f"<VM:RULES>\n{rules}\n</VM:RULES>")
|
|
142
|
+
parts.append(f"<VM:MANIFEST_JSON>\n{manifest_json}\n</VM:MANIFEST_JSON>")
|
|
143
|
+
|
|
144
|
+
parts.append(f"<VM:CONTEXT>\n{context}\n</VM:CONTEXT>")
|
|
145
|
+
|
|
146
|
+
return "\n\n".join(parts)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Tool definitions as Pydantic models
|
|
150
|
+
PAGE_FAULT_TOOL = ToolDefinition(
|
|
151
|
+
type=ToolType.FUNCTION,
|
|
152
|
+
function=ToolFunction(
|
|
153
|
+
name="page_fault",
|
|
154
|
+
description="Load a memory page into context at specified compression level. Use when you need content from a known page_id.",
|
|
155
|
+
parameters=ToolParameters(
|
|
156
|
+
type="object",
|
|
157
|
+
properties={
|
|
158
|
+
"page_id": ToolParameter(
|
|
159
|
+
type="string",
|
|
160
|
+
description="ID of the page to load",
|
|
161
|
+
),
|
|
162
|
+
"target_level": ToolParameter(
|
|
163
|
+
type="integer",
|
|
164
|
+
minimum=0,
|
|
165
|
+
maximum=3,
|
|
166
|
+
default=2,
|
|
167
|
+
description="0=full, 1=reduced, 2=abstract/summary, 3=reference only",
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
required=["page_id"],
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
SEARCH_PAGES_TOOL = ToolDefinition(
|
|
176
|
+
type=ToolType.FUNCTION,
|
|
177
|
+
function=ToolFunction(
|
|
178
|
+
name="search_pages",
|
|
179
|
+
description="Search for pages matching a query. Use when you need to find relevant pages but don't know their IDs.",
|
|
180
|
+
parameters=ToolParameters(
|
|
181
|
+
type="object",
|
|
182
|
+
properties={
|
|
183
|
+
"query": ToolParameter(
|
|
184
|
+
type="string",
|
|
185
|
+
description="Search query (semantic or keyword)",
|
|
186
|
+
),
|
|
187
|
+
"modality": ToolParameter(
|
|
188
|
+
type="string",
|
|
189
|
+
enum=[m.value for m in Modality],
|
|
190
|
+
description="Filter by content type",
|
|
191
|
+
),
|
|
192
|
+
"limit": ToolParameter(
|
|
193
|
+
type="integer",
|
|
194
|
+
default=5,
|
|
195
|
+
description="Maximum results to return",
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
required=["query"],
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# List of all VM tools as Pydantic models
|
|
205
|
+
VM_TOOL_DEFINITIONS: List[ToolDefinition] = [PAGE_FAULT_TOOL, SEARCH_PAGES_TOOL]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_vm_tools(include_search: bool = True) -> List[ToolDefinition]:
|
|
209
|
+
"""
|
|
210
|
+
Get the VM tool definitions as Pydantic models.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
include_search: Whether to include search_pages tool
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
List of ToolDefinition models
|
|
217
|
+
"""
|
|
218
|
+
if include_search:
|
|
219
|
+
return VM_TOOL_DEFINITIONS
|
|
220
|
+
return [PAGE_FAULT_TOOL]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_vm_tools_as_dicts(include_search: bool = True) -> List[dict]:
|
|
224
|
+
"""
|
|
225
|
+
Get the VM tool definitions as dicts (for Chat Completions API).
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
include_search: Whether to include search_pages tool
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of tool definition dicts
|
|
232
|
+
"""
|
|
233
|
+
tools = get_vm_tools(include_search)
|
|
234
|
+
return [tool.model_dump(exclude_none=True) for tool in tools]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Legacy: Keep VM_TOOLS as dicts for backward compatibility
|
|
238
|
+
VM_TOOLS = get_vm_tools_as_dicts(include_search=True)
|