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,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
|