chuk-ai-session-manager 0.7__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.
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 +238 -197
  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.dist-info → chuk_ai_session_manager-0.8.dist-info}/METADATA +78 -2
  43. chuk_ai_session_manager-0.8.dist-info/RECORD +45 -0
  44. {chuk_ai_session_manager-0.7.dist-info → chuk_ai_session_manager-0.8.dist-info}/WHEEL +1 -1
  45. chuk_ai_session_manager-0.7.dist-info/RECORD +0 -22
  46. {chuk_ai_session_manager-0.7.dist-info → chuk_ai_session_manager-0.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,347 @@
1
+ # chuk_ai_session_manager/memory/context_packer.py
2
+ """
3
+ Context Packer for AI Virtual Memory.
4
+
5
+ The ContextPacker transforms the working set into the VM:CONTEXT block -
6
+ a compact, human-readable representation of mapped pages that goes into
7
+ the model's context window.
8
+
9
+ Design principles:
10
+ - Human-readable: Format is easy for models to parse and reference
11
+ - Token-efficient: Compact representation with clear structure
12
+ - Cross-modal: Handles text, images, audio, video with appropriate formats
13
+ - Pydantic-native: All models are BaseModel subclasses
14
+ - No magic strings: Uses enums for all categorical values
15
+ """
16
+
17
+ from typing import List, Optional
18
+
19
+ from pydantic import BaseModel, Field
20
+
21
+ from .models import (
22
+ ContextPrefix,
23
+ FormattedPage,
24
+ MemoryPage,
25
+ MessageRole,
26
+ Modality,
27
+ PageType,
28
+ )
29
+
30
+
31
+ class PackedContext(BaseModel):
32
+ """Result of packing the working set into context."""
33
+
34
+ content: str = Field(..., description="The VM:CONTEXT block content")
35
+ tokens_est: int = Field(default=0, description="Estimated token count")
36
+ pages_included: List[str] = Field(
37
+ default_factory=list, description="Page IDs included"
38
+ )
39
+ pages_truncated: List[str] = Field(
40
+ default_factory=list, description="Page IDs that were truncated"
41
+ )
42
+ pages_omitted: List[str] = Field(
43
+ default_factory=list, description="Page IDs omitted due to budget"
44
+ )
45
+
46
+
47
+ class ContextPackerConfig(BaseModel):
48
+ """Configuration for context packing."""
49
+
50
+ include_page_ids: bool = Field(
51
+ default=True, description="Include page IDs in output"
52
+ )
53
+ include_timestamps: bool = Field(default=False, description="Include timestamps")
54
+ max_text_length: int = Field(
55
+ default=0, description="Max chars per text page (0=unlimited)"
56
+ )
57
+
58
+
59
+ class ContextPacker(BaseModel):
60
+ """
61
+ Packs MemoryPages into the VM:CONTEXT format.
62
+
63
+ The output format is:
64
+ ```
65
+ <VM:CONTEXT>
66
+ U (msg_301): "User message text here"
67
+ A (msg_302): "Assistant response here"
68
+ T (tool_result_045): {"calculator": {"result": 4}}
69
+ S (summary_seg_02): "Key points: 1) First point, 2) Second point..."
70
+ I (img_045): [IMAGE: architecture diagram, 1200x800]
71
+ D (audio_012): [AUDIO: 5:42 duration, transcript: "So the key insight is..."]
72
+ V (video_007): [VIDEO: 12:30 duration, 8 scenes, topic: "system walkthrough"]
73
+ </VM:CONTEXT>
74
+ ```
75
+ """
76
+
77
+ config: ContextPackerConfig = Field(default_factory=ContextPackerConfig)
78
+
79
+ def pack(
80
+ self,
81
+ pages: List[MemoryPage],
82
+ token_budget: Optional[int] = None,
83
+ ) -> PackedContext:
84
+ """
85
+ Pack a list of pages into VM:CONTEXT format.
86
+
87
+ Args:
88
+ pages: List of MemoryPages to pack
89
+ token_budget: Optional token limit (will truncate/omit if exceeded)
90
+
91
+ Returns:
92
+ PackedContext with the formatted content
93
+ """
94
+ lines: List[str] = []
95
+ pages_included: List[str] = []
96
+ pages_truncated: List[str] = []
97
+ pages_omitted: List[str] = []
98
+ tokens_used = 0
99
+
100
+ for page in pages:
101
+ # Format the page
102
+ formatted = self._format_page(page)
103
+
104
+ # Check budget
105
+ if token_budget and tokens_used + formatted.tokens_est > token_budget:
106
+ # Try truncating
107
+ if page.modality == Modality.TEXT and self.config.max_text_length == 0:
108
+ # Can truncate text
109
+ remaining = token_budget - tokens_used
110
+ if remaining > 50: # Minimum useful content
111
+ formatted = self._format_page(page, max_tokens=remaining)
112
+ pages_truncated.append(page.page_id)
113
+ else:
114
+ pages_omitted.append(page.page_id)
115
+ continue
116
+ else:
117
+ pages_omitted.append(page.page_id)
118
+ continue
119
+
120
+ lines.append(formatted.content)
121
+ pages_included.append(page.page_id)
122
+ tokens_used += formatted.tokens_est
123
+
124
+ return PackedContext(
125
+ content="\n".join(lines),
126
+ tokens_est=tokens_used,
127
+ pages_included=pages_included,
128
+ pages_truncated=pages_truncated,
129
+ pages_omitted=pages_omitted,
130
+ )
131
+
132
+ def _format_page(
133
+ self,
134
+ page: MemoryPage,
135
+ max_tokens: Optional[int] = None,
136
+ ) -> FormattedPage:
137
+ """
138
+ Format a single page for VM:CONTEXT.
139
+
140
+ Returns FormattedPage with formatted content and token estimate.
141
+ """
142
+ if page.modality == Modality.TEXT:
143
+ return self._format_text(page, max_tokens)
144
+ elif page.modality == Modality.IMAGE:
145
+ return self._format_image(page)
146
+ elif page.modality == Modality.AUDIO:
147
+ return self._format_audio(page)
148
+ elif page.modality == Modality.VIDEO:
149
+ return self._format_video(page)
150
+ elif page.modality == Modality.STRUCTURED:
151
+ return self._format_structured(page, max_tokens)
152
+ else:
153
+ return self._format_generic(page)
154
+
155
+ def _format_text(
156
+ self,
157
+ page: MemoryPage,
158
+ max_tokens: Optional[int] = None,
159
+ ) -> FormattedPage:
160
+ """Format a text page."""
161
+ content = page.content or ""
162
+
163
+ # Determine prefix based on metadata
164
+ prefix = self._get_text_prefix(page)
165
+
166
+ # Truncate if needed
167
+ max_chars = self.config.max_text_length
168
+ if max_tokens:
169
+ max_chars = max_tokens * 4 # ~4 chars per token
170
+
171
+ if max_chars > 0 and len(content) > max_chars:
172
+ content = content[:max_chars] + "..."
173
+
174
+ # Format
175
+ if self.config.include_page_ids:
176
+ line = f'{prefix.value} ({page.page_id}): "{content}"'
177
+ else:
178
+ line = f'{prefix.value}: "{content}"'
179
+
180
+ tokens_est = len(line) // 4
181
+ return FormattedPage(content=line, tokens_est=tokens_est)
182
+
183
+ def _get_text_prefix(self, page: MemoryPage) -> ContextPrefix:
184
+ """Determine the prefix for a text page based on metadata."""
185
+ role = page.metadata.get(MessageRole.USER.value, "")
186
+ if not role:
187
+ role = page.metadata.get("role", "")
188
+
189
+ page_type = page.metadata.get("type", "")
190
+
191
+ if role == MessageRole.USER.value:
192
+ return ContextPrefix.USER
193
+ elif role == MessageRole.ASSISTANT.value:
194
+ return ContextPrefix.ASSISTANT
195
+ elif role == MessageRole.TOOL.value or page_type == PageType.TOOL_RESULT.value:
196
+ return ContextPrefix.TOOL
197
+ elif (
198
+ page_type == PageType.SUMMARY.value
199
+ or PageType.SUMMARY.value in page.page_id
200
+ ):
201
+ return ContextPrefix.SUMMARY
202
+ else:
203
+ return ContextPrefix.USER # Default
204
+
205
+ def _format_image(self, page: MemoryPage) -> FormattedPage:
206
+ """Format an image page."""
207
+ parts = ["[IMAGE:"]
208
+
209
+ # Add caption if available
210
+ if page.caption:
211
+ parts.append(f" {page.caption}")
212
+ elif page.content and isinstance(page.content, str):
213
+ parts.append(f" {page.content}")
214
+
215
+ # Add dimensions
216
+ if page.dimensions:
217
+ parts.append(f", {page.dimensions[0]}x{page.dimensions[1]}")
218
+
219
+ parts.append("]")
220
+ description = "".join(parts)
221
+
222
+ if self.config.include_page_ids:
223
+ line = f"{ContextPrefix.IMAGE.value} ({page.page_id}): {description}"
224
+ else:
225
+ line = f"{ContextPrefix.IMAGE.value}: {description}"
226
+
227
+ tokens_est = len(line) // 4
228
+ return FormattedPage(content=line, tokens_est=tokens_est)
229
+
230
+ def _format_audio(self, page: MemoryPage) -> FormattedPage:
231
+ """Format an audio page."""
232
+ parts = ["[AUDIO:"]
233
+
234
+ # Duration
235
+ if page.duration_seconds:
236
+ mins = int(page.duration_seconds // 60)
237
+ secs = int(page.duration_seconds % 60)
238
+ parts.append(f" {mins}:{secs:02d} duration")
239
+
240
+ # Transcript excerpt
241
+ if page.transcript:
242
+ excerpt = page.transcript[:200]
243
+ if len(page.transcript) > 200:
244
+ excerpt += "..."
245
+ parts.append(f', transcript: "{excerpt}"')
246
+ elif page.content and isinstance(page.content, str):
247
+ excerpt = page.content[:200]
248
+ if len(page.content) > 200:
249
+ excerpt += "..."
250
+ parts.append(f', transcript: "{excerpt}"')
251
+
252
+ parts.append("]")
253
+ description = "".join(parts)
254
+
255
+ if self.config.include_page_ids:
256
+ line = f"{ContextPrefix.AUDIO.value} ({page.page_id}): {description}"
257
+ else:
258
+ line = f"{ContextPrefix.AUDIO.value}: {description}"
259
+
260
+ tokens_est = len(line) // 4
261
+ return FormattedPage(content=line, tokens_est=tokens_est)
262
+
263
+ def _format_video(self, page: MemoryPage) -> FormattedPage:
264
+ """Format a video page."""
265
+ parts = ["[VIDEO:"]
266
+
267
+ # Duration
268
+ if page.duration_seconds:
269
+ mins = int(page.duration_seconds // 60)
270
+ secs = int(page.duration_seconds % 60)
271
+ parts.append(f" {mins}:{secs:02d} duration")
272
+
273
+ # Scene count from metadata
274
+ scene_count = page.metadata.get("scene_count")
275
+ if scene_count:
276
+ parts.append(f", {scene_count} scenes")
277
+
278
+ # Topic/description
279
+ topic = page.metadata.get("topic") or page.caption
280
+ if topic:
281
+ parts.append(f', topic: "{topic}"')
282
+
283
+ parts.append("]")
284
+ description = "".join(parts)
285
+
286
+ if self.config.include_page_ids:
287
+ line = f"{ContextPrefix.VIDEO.value} ({page.page_id}): {description}"
288
+ else:
289
+ line = f"{ContextPrefix.VIDEO.value}: {description}"
290
+
291
+ tokens_est = len(line) // 4
292
+ return FormattedPage(content=line, tokens_est=tokens_est)
293
+
294
+ def _format_structured(
295
+ self,
296
+ page: MemoryPage,
297
+ max_tokens: Optional[int] = None,
298
+ ) -> FormattedPage:
299
+ """Format a structured (JSON) page."""
300
+ import json
301
+
302
+ content = page.content
303
+ if isinstance(content, dict):
304
+ content = json.dumps(content, separators=(",", ":"))
305
+ elif content is None:
306
+ content = "{}"
307
+
308
+ # Truncate if needed
309
+ max_chars = max_tokens * 4 if max_tokens else 0
310
+ if max_chars > 0 and len(str(content)) > max_chars:
311
+ content = str(content)[:max_chars] + "..."
312
+
313
+ if self.config.include_page_ids:
314
+ line = f"{ContextPrefix.STRUCTURED.value} ({page.page_id}): {content}"
315
+ else:
316
+ line = f"{ContextPrefix.STRUCTURED.value}: {content}"
317
+
318
+ tokens_est = len(line) // 4
319
+ return FormattedPage(content=line, tokens_est=tokens_est)
320
+
321
+ def _format_generic(self, page: MemoryPage) -> FormattedPage:
322
+ """Format any other page type."""
323
+ content = str(page.content)[:500] if page.content else "[no content]"
324
+
325
+ if self.config.include_page_ids:
326
+ line = f"{ContextPrefix.UNKNOWN.value} ({page.page_id}): {content}"
327
+ else:
328
+ line = f"{ContextPrefix.UNKNOWN.value}: {content}"
329
+
330
+ tokens_est = len(line) // 4
331
+ return FormattedPage(content=line, tokens_est=tokens_est)
332
+
333
+ def pack_with_wrapper(
334
+ self,
335
+ pages: List[MemoryPage],
336
+ token_budget: Optional[int] = None,
337
+ ) -> PackedContext:
338
+ """
339
+ Pack pages and wrap with VM:CONTEXT tags.
340
+
341
+ This is the complete format for inclusion in a developer message.
342
+ """
343
+ result = self.pack(pages, token_budget)
344
+ result.content = f"<VM:CONTEXT>\n{result.content}\n</VM:CONTEXT>"
345
+ # Add wrapper tokens to estimate
346
+ result.tokens_est += 10
347
+ return result