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.
Files changed (46) hide show
  1. chuk_ai_session_manager/__init__.py +84 -40
  2. chuk_ai_session_manager/api/__init__.py +1 -1
  3. chuk_ai_session_manager/api/simple_api.py +53 -59
  4. chuk_ai_session_manager/exceptions.py +31 -17
  5. chuk_ai_session_manager/guards/__init__.py +118 -0
  6. chuk_ai_session_manager/guards/bindings.py +217 -0
  7. chuk_ai_session_manager/guards/cache.py +163 -0
  8. chuk_ai_session_manager/guards/manager.py +819 -0
  9. chuk_ai_session_manager/guards/models.py +498 -0
  10. chuk_ai_session_manager/guards/ungrounded.py +159 -0
  11. chuk_ai_session_manager/infinite_conversation.py +86 -79
  12. chuk_ai_session_manager/memory/__init__.py +247 -0
  13. chuk_ai_session_manager/memory/artifacts_bridge.py +469 -0
  14. chuk_ai_session_manager/memory/context_packer.py +347 -0
  15. chuk_ai_session_manager/memory/fault_handler.py +507 -0
  16. chuk_ai_session_manager/memory/manifest.py +307 -0
  17. chuk_ai_session_manager/memory/models.py +1084 -0
  18. chuk_ai_session_manager/memory/mutation_log.py +186 -0
  19. chuk_ai_session_manager/memory/pack_cache.py +206 -0
  20. chuk_ai_session_manager/memory/page_table.py +275 -0
  21. chuk_ai_session_manager/memory/prefetcher.py +192 -0
  22. chuk_ai_session_manager/memory/tlb.py +247 -0
  23. chuk_ai_session_manager/memory/vm_prompts.py +238 -0
  24. chuk_ai_session_manager/memory/working_set.py +574 -0
  25. chuk_ai_session_manager/models/__init__.py +21 -9
  26. chuk_ai_session_manager/models/event_source.py +3 -1
  27. chuk_ai_session_manager/models/event_type.py +10 -1
  28. chuk_ai_session_manager/models/session.py +103 -68
  29. chuk_ai_session_manager/models/session_event.py +69 -68
  30. chuk_ai_session_manager/models/session_metadata.py +9 -10
  31. chuk_ai_session_manager/models/session_run.py +21 -22
  32. chuk_ai_session_manager/models/token_usage.py +76 -76
  33. chuk_ai_session_manager/procedural_memory/__init__.py +70 -0
  34. chuk_ai_session_manager/procedural_memory/formatter.py +407 -0
  35. chuk_ai_session_manager/procedural_memory/manager.py +523 -0
  36. chuk_ai_session_manager/procedural_memory/models.py +371 -0
  37. chuk_ai_session_manager/sample_tools.py +79 -46
  38. chuk_ai_session_manager/session_aware_tool_processor.py +27 -16
  39. chuk_ai_session_manager/session_manager.py +259 -232
  40. chuk_ai_session_manager/session_prompt_builder.py +163 -111
  41. chuk_ai_session_manager/session_storage.py +45 -52
  42. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/METADATA +80 -4
  43. chuk_ai_session_manager-0.8.1.dist-info/RECORD +45 -0
  44. {chuk_ai_session_manager-0.7.1.dist-info → chuk_ai_session_manager-0.8.1.dist-info}/WHEEL +1 -1
  45. chuk_ai_session_manager-0.7.1.dist-info/RECORD +0 -22
  46. {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,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)