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
@@ -14,7 +14,7 @@ This module provides the main SessionManager class which offers:
14
14
  from __future__ import annotations
15
15
  import asyncio
16
16
  import logging
17
- from typing import Any, Dict, List, Optional, Callable, Union
17
+ from typing import Any, Dict, List, Optional, Callable
18
18
  from datetime import datetime
19
19
  import uuid
20
20
 
@@ -22,18 +22,20 @@ from chuk_ai_session_manager.models.session import Session
22
22
  from chuk_ai_session_manager.models.session_event import SessionEvent
23
23
  from chuk_ai_session_manager.models.event_source import EventSource
24
24
  from chuk_ai_session_manager.models.event_type import EventType
25
- from chuk_ai_session_manager.session_storage import get_backend, ChukSessionsStore
25
+ from chuk_ai_session_manager.session_storage import ChukSessionsStore
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
+ DEFAULT_TOKEN_MODEL = "gpt-4o-mini"
30
+
29
31
 
30
32
  class SessionManager:
31
33
  """
32
34
  High-level session manager for AI conversations.
33
-
35
+
34
36
  Provides an easy-to-use interface for tracking conversations, managing
35
37
  system prompts, handling infinite context, and monitoring usage.
36
-
38
+
37
39
  Examples:
38
40
  Basic usage:
39
41
  ```python
@@ -41,22 +43,22 @@ class SessionManager:
41
43
  await sm.user_says("Hello!")
42
44
  await sm.ai_responds("Hi there!", model="gpt-4")
43
45
  ```
44
-
46
+
45
47
  With system prompt:
46
48
  ```python
47
49
  sm = SessionManager(system_prompt="You are a helpful assistant.")
48
50
  await sm.user_says("What can you do?")
49
51
  ```
50
-
52
+
51
53
  Infinite context:
52
54
  ```python
53
55
  sm = SessionManager(infinite_context=True, token_threshold=4000)
54
56
  # Automatically handles long conversations
55
57
  ```
56
58
  """
57
-
59
+
58
60
  def __init__(
59
- self,
61
+ self,
60
62
  session_id: Optional[str] = None,
61
63
  system_prompt: Optional[str] = None,
62
64
  parent_id: Optional[str] = None,
@@ -64,11 +66,12 @@ class SessionManager:
64
66
  store: Optional[ChukSessionsStore] = None,
65
67
  infinite_context: bool = False,
66
68
  token_threshold: int = 4000,
67
- max_turns_per_segment: int = 20
69
+ max_turns_per_segment: int = 20,
70
+ default_model: str = DEFAULT_TOKEN_MODEL,
68
71
  ):
69
72
  """
70
73
  Initialize a SessionManager.
71
-
74
+
72
75
  Args:
73
76
  session_id: Optional session ID. If not provided, a new one will be generated.
74
77
  system_prompt: Optional system prompt to set the context for the AI assistant.
@@ -78,28 +81,31 @@ class SessionManager:
78
81
  infinite_context: Enable automatic infinite context handling.
79
82
  token_threshold: Token limit before creating new session (infinite mode).
80
83
  max_turns_per_segment: Turn limit before creating new session (infinite mode).
84
+ default_model: Model name used for token counting (default: gpt-4o-mini).
81
85
  """
82
86
  # Core session management
83
87
  self._session_id = session_id
84
88
  self._system_prompt = system_prompt
85
89
  self._parent_id = parent_id
86
90
  self._metadata = metadata or {}
87
- self._store = store
91
+ self._store = store or ChukSessionsStore()
88
92
  self._session: Optional[Session] = None
89
93
  self._initialized = False
90
94
  self._lock = asyncio.Lock()
91
95
  self._loaded_from_storage = False # Track if loaded from storage
92
-
96
+ self._default_model = default_model
97
+ self._summary_callback: Optional[Callable] = None
98
+
93
99
  # Infinite context settings
94
100
  self._infinite_context = infinite_context
95
101
  self._token_threshold = token_threshold
96
102
  self._max_turns_per_segment = max_turns_per_segment
97
-
103
+
98
104
  # Infinite context state
99
105
  self._session_chain: List[str] = []
100
106
  self._full_conversation: List[Dict[str, Any]] = []
101
107
  self._total_segments = 1
102
-
108
+
103
109
  @property
104
110
  def session_id(self) -> str:
105
111
  """Get the current session ID."""
@@ -111,45 +117,39 @@ class SessionManager:
111
117
  # Generate a new ID if needed
112
118
  self._session_id = str(uuid.uuid4())
113
119
  return self._session_id
114
-
120
+
115
121
  @property
116
122
  def system_prompt(self) -> Optional[str]:
117
123
  """Get the current system prompt."""
118
124
  return self._system_prompt
119
-
125
+
120
126
  @property
121
127
  def is_infinite(self) -> bool:
122
128
  """Check if infinite context is enabled."""
123
129
  return self._infinite_context
124
-
130
+
125
131
  @property
126
132
  def _is_new(self) -> bool:
127
- """Check if this is a new session (for test compatibility)."""
128
- # If we have a session_id but haven't initialized yet, we don't know
133
+ """Check if this is a new session."""
129
134
  if not self._initialized:
130
135
  return True
131
- # If we loaded from storage, it's not new
132
136
  return not self._loaded_from_storage
133
-
137
+
134
138
  async def _ensure_session(self) -> Optional[Session]:
135
- """Ensure session is initialized (test compatibility alias)."""
136
- # Special handling for test cases expecting errors
137
- if self._session_id and "nonexistent" in self._session_id:
138
- raise ValueError(f"Session {self._session_id} not found")
139
-
139
+ """Ensure session is initialized and return it."""
140
140
  await self._ensure_initialized()
141
141
  return self._session
142
-
142
+
143
143
  async def update_system_prompt(self, prompt: str) -> None:
144
144
  """
145
145
  Update the system prompt for the session.
146
-
146
+
147
147
  Args:
148
148
  prompt: The new system prompt to use.
149
149
  """
150
150
  async with self._lock:
151
151
  self._system_prompt = prompt
152
-
152
+
153
153
  # Store in session metadata
154
154
  if self._session:
155
155
  self._session.metadata.properties["system_prompt"] = prompt
@@ -157,91 +157,85 @@ class SessionManager:
157
157
  else:
158
158
  # Store for when session is initialized
159
159
  self._metadata["system_prompt"] = prompt
160
-
160
+
161
161
  logger.debug(f"Updated system prompt for session {self.session_id}")
162
-
162
+
163
163
  async def _ensure_initialized(self) -> None:
164
164
  """Ensure the session is initialized."""
165
165
  if self._initialized:
166
166
  return
167
-
167
+
168
168
  async with self._lock:
169
169
  if self._initialized: # Double-check after acquiring lock
170
170
  return
171
-
172
- store = self._store or ChukSessionsStore()
173
-
171
+
172
+ store = self._store
173
+
174
174
  if self._session_id:
175
175
  # Try to load existing session
176
176
  try:
177
177
  self._session = await store.get(self._session_id)
178
-
178
+
179
179
  if self._session:
180
180
  # Mark as loaded from storage
181
181
  self._loaded_from_storage = True
182
-
182
+
183
183
  # Load system prompt from session if not already set
184
- if not self._system_prompt and self._session.metadata.properties:
185
- self._system_prompt = self._session.metadata.properties.get("system_prompt")
186
-
184
+ if (
185
+ not self._system_prompt
186
+ and self._session.metadata.properties
187
+ ):
188
+ self._system_prompt = self._session.metadata.properties.get(
189
+ "system_prompt"
190
+ )
191
+
187
192
  # Initialize session chain for infinite context
188
193
  if self._infinite_context:
189
194
  self._session_chain = [self._session_id]
190
- # TODO: Load full chain from session metadata
191
195
  else:
192
- # Session not found - behavior depends on context
193
- # For some tests, we should raise an error
194
- # For others, we should create a new session
195
- # Check if this looks like a test expecting an error
196
- if "nonexistent" in self._session_id or "not-found" in self._session_id:
197
- raise ValueError(f"Session {self._session_id} not found")
198
-
199
- # Otherwise create a new session with the provided ID
196
+ # Session not found - create a new session with the provided ID
200
197
  session_metadata = {}
201
198
  if self._metadata:
202
199
  session_metadata.update(self._metadata)
203
200
  if self._system_prompt:
204
201
  session_metadata["system_prompt"] = self._system_prompt
205
-
202
+
206
203
  self._session = await Session.create(
207
204
  session_id=self._session_id,
208
205
  parent_id=self._parent_id,
209
- metadata=session_metadata
206
+ metadata=session_metadata,
210
207
  )
211
-
208
+
212
209
  # Ensure metadata properties are set
213
210
  if session_metadata:
214
211
  self._session.metadata.properties.update(session_metadata)
215
-
212
+
216
213
  await store.save(self._session)
217
214
  self._loaded_from_storage = False
218
-
215
+
219
216
  if self._infinite_context:
220
217
  self._session_chain = [self._session_id]
221
- except ValueError:
222
- # Re-raise ValueError for tests expecting it
223
- raise
224
218
  except Exception as e:
225
- # For other errors, create new session
219
+ # For errors, create new session
226
220
  logger.debug(f"Error loading session {self._session_id}: {e}")
227
221
  session_metadata = {}
228
222
  if self._metadata:
229
223
  session_metadata.update(self._metadata)
230
224
  if self._system_prompt:
231
225
  session_metadata["system_prompt"] = self._system_prompt
232
-
226
+
233
227
  self._session = await Session.create(
234
228
  session_id=self._session_id,
235
229
  parent_id=self._parent_id,
236
- metadata=session_metadata
230
+ metadata=session_metadata,
237
231
  )
238
-
232
+
239
233
  if session_metadata:
240
234
  self._session.metadata.properties.update(session_metadata)
241
-
235
+
242
236
  await store.save(self._session)
243
237
  self._loaded_from_storage = False
244
-
238
+
245
239
  if self._infinite_context:
246
240
  self._session_chain = [self._session_id]
247
241
  else:
@@ -251,67 +245,69 @@ class SessionManager:
251
245
  session_metadata.update(self._metadata)
252
246
  if self._system_prompt:
253
247
  session_metadata["system_prompt"] = self._system_prompt
254
-
248
+
255
249
  self._session = await Session.create(
256
- parent_id=self._parent_id,
257
- metadata=session_metadata
250
+ parent_id=self._parent_id, metadata=session_metadata
258
251
  )
259
252
  self._session_id = self._session.id
260
-
253
+
261
254
  if session_metadata:
262
255
  self._session.metadata.properties.update(session_metadata)
263
-
256
+
264
257
  await store.save(self._session)
265
258
  self._loaded_from_storage = False
266
-
259
+
267
260
  if self._infinite_context:
268
261
  self._session_chain = [self._session_id]
269
-
262
+
270
263
  self._initialized = True
271
-
264
+
272
265
  async def _save_session(self) -> None:
273
266
  """Save the current session."""
274
267
  if self._session:
275
- store = self._store or ChukSessionsStore()
276
- await store.save(self._session)
277
-
268
+ await self._store.save(self._session)
269
+
278
270
  async def _should_create_new_segment(self) -> bool:
279
271
  """Check if we should create a new session segment."""
280
272
  if not self._infinite_context:
281
273
  return False
282
-
274
+
283
275
  await self._ensure_initialized()
284
-
276
+
285
277
  # Check token threshold
286
278
  if self._session.total_tokens >= self._token_threshold:
287
279
  return True
288
-
280
+
289
281
  # Check turn threshold
290
- message_events = [e for e in self._session.events if e.type == EventType.MESSAGE]
282
+ message_events = [
283
+ e for e in self._session.events if e.type == EventType.MESSAGE
284
+ ]
291
285
  if len(message_events) >= self._max_turns_per_segment:
292
286
  return True
293
-
287
+
294
288
  return False
295
-
289
+
296
290
  async def _create_summary(self, llm_callback: Optional[Callable] = None) -> str:
297
291
  """
298
292
  Create a summary of the current session.
299
-
293
+
300
294
  Args:
301
295
  llm_callback: Optional async function to generate summary using an LLM.
302
296
  Should accept List[Dict] messages and return str summary.
303
297
  """
304
298
  await self._ensure_initialized()
305
- message_events = [e for e in self._session.events if e.type == EventType.MESSAGE]
306
-
299
+ message_events = [
300
+ e for e in self._session.events if e.type == EventType.MESSAGE
301
+ ]
302
+
307
303
  # Use LLM callback if provided
308
304
  if llm_callback:
309
305
  messages = await self.get_messages_for_llm(include_system=False)
310
306
  return await llm_callback(messages)
311
-
307
+
312
308
  # Simple summary generation
313
309
  user_messages = [e for e in message_events if e.source == EventSource.USER]
314
-
310
+
315
311
  topics = []
316
312
  for event in user_messages:
317
313
  content = str(event.message)
@@ -319,127 +315,131 @@ class SessionManager:
319
315
  question = content.split("?")[0].strip()
320
316
  if len(question) > 10:
321
317
  topics.append(question[:50])
322
-
318
+
323
319
  if topics:
324
320
  summary = f"User discussed: {'; '.join(topics[:3])}"
325
321
  if len(topics) > 3:
326
322
  summary += f" and {len(topics) - 3} other topics"
327
323
  else:
328
324
  summary = f"Conversation with {len(user_messages)} user messages and {len(message_events) - len(user_messages)} responses"
329
-
325
+
330
326
  return summary
331
-
327
+
332
328
  async def _create_new_segment(self, llm_callback: Optional[Callable] = None) -> str:
333
329
  """
334
330
  Create a new session segment with summary.
335
-
331
+
336
332
  Args:
337
333
  llm_callback: Optional async function to generate summary using an LLM.
338
-
334
+
339
335
  Returns:
340
336
  The new session ID.
341
337
  """
338
+ # Use the instance callback if no explicit callback provided
339
+ callback = llm_callback or self._summary_callback
340
+
342
341
  # Create summary of current session
343
- summary = await self._create_summary(llm_callback)
344
-
342
+ summary = await self._create_summary(callback)
343
+
345
344
  # Add summary to current session
346
345
  summary_event = SessionEvent(
347
- message=summary,
348
- source=EventSource.SYSTEM,
349
- type=EventType.SUMMARY
346
+ message=summary, source=EventSource.SYSTEM, type=EventType.SUMMARY
350
347
  )
351
348
  await self._ensure_initialized()
352
349
  await self._session.add_event_and_save(summary_event)
353
-
350
+
354
351
  # Create new session with current as parent
355
352
  new_session = await Session.create(parent_id=self._session_id)
356
-
353
+
357
354
  # Copy system prompt to new session
358
355
  if self._system_prompt:
359
356
  new_session.metadata.properties["system_prompt"] = self._system_prompt
360
-
357
+
361
358
  # Save new session
362
- store = self._store or ChukSessionsStore()
363
- await store.save(new_session)
364
-
359
+ await self._store.save(new_session)
360
+
365
361
  # Update our state
366
362
  old_session_id = self._session_id
367
363
  self._session_id = new_session.id
368
364
  self._session = new_session
369
365
  self._session_chain.append(self._session_id)
370
366
  self._total_segments += 1
371
-
372
- logger.info(f"Created new session segment: {old_session_id} -> {self._session_id}")
367
+
368
+ logger.info(
369
+ f"Created new session segment: {old_session_id} -> {self._session_id}"
370
+ )
373
371
  return self._session_id
374
-
372
+
375
373
  async def user_says(self, message: str, **metadata) -> str:
376
374
  """
377
375
  Track a user message.
378
-
376
+
379
377
  Args:
380
378
  message: What the user said.
381
379
  **metadata: Optional metadata to attach to the event.
382
-
380
+
383
381
  Returns:
384
382
  The current session ID (may change in infinite mode).
385
383
  """
386
384
  # Check for segmentation before adding message
387
385
  if await self._should_create_new_segment():
388
386
  await self._create_new_segment()
389
-
387
+
390
388
  await self._ensure_initialized()
391
-
389
+
392
390
  # Create and add the event
393
391
  event = await SessionEvent.create_with_tokens(
394
392
  message=message,
395
393
  prompt=message,
396
- model="gpt-4o-mini", # Default model for token counting
394
+ model=self._default_model,
397
395
  source=EventSource.USER,
398
- type=EventType.MESSAGE
396
+ type=EventType.MESSAGE,
399
397
  )
400
-
398
+
401
399
  # Add metadata
402
400
  for key, value in metadata.items():
403
401
  await event.set_metadata(key, value)
404
-
402
+
405
403
  await self._session.add_event_and_save(event)
406
-
404
+
407
405
  # Track in full conversation for infinite context
408
406
  if self._infinite_context:
409
- self._full_conversation.append({
410
- "role": "user",
411
- "content": message,
412
- "timestamp": event.timestamp.isoformat(),
413
- "session_id": self._session_id
414
- })
415
-
407
+ self._full_conversation.append(
408
+ {
409
+ "role": "user",
410
+ "content": message,
411
+ "timestamp": event.timestamp.isoformat(),
412
+ "session_id": self._session_id,
413
+ }
414
+ )
415
+
416
416
  return self._session_id
417
-
417
+
418
418
  async def ai_responds(
419
- self,
419
+ self,
420
420
  response: str,
421
421
  model: str = "unknown",
422
422
  provider: str = "unknown",
423
- **metadata
423
+ **metadata,
424
424
  ) -> str:
425
425
  """
426
426
  Track an AI response.
427
-
427
+
428
428
  Args:
429
429
  response: The AI's response.
430
430
  model: Model name used.
431
431
  provider: Provider name (openai, anthropic, etc).
432
432
  **metadata: Optional metadata to attach.
433
-
433
+
434
434
  Returns:
435
435
  The current session ID (may change in infinite mode).
436
436
  """
437
437
  # Check for segmentation before adding message
438
438
  if await self._should_create_new_segment():
439
439
  await self._create_new_segment()
440
-
440
+
441
441
  await self._ensure_initialized()
442
-
442
+
443
443
  # Create and add the event
444
444
  event = await SessionEvent.create_with_tokens(
445
445
  message=response,
@@ -447,135 +447,132 @@ class SessionManager:
447
447
  completion=response,
448
448
  model=model,
449
449
  source=EventSource.LLM,
450
- type=EventType.MESSAGE
450
+ type=EventType.MESSAGE,
451
451
  )
452
-
452
+
453
453
  # Add metadata
454
454
  full_metadata = {
455
455
  "model": model,
456
456
  "provider": provider,
457
457
  "timestamp": datetime.now().isoformat(),
458
- **metadata
458
+ **metadata,
459
459
  }
460
-
460
+
461
461
  for key, value in full_metadata.items():
462
462
  await event.set_metadata(key, value)
463
-
463
+
464
464
  await self._session.add_event_and_save(event)
465
-
465
+
466
466
  # Track in full conversation for infinite context
467
467
  if self._infinite_context:
468
- self._full_conversation.append({
469
- "role": "assistant",
470
- "content": response,
471
- "timestamp": event.timestamp.isoformat(),
472
- "session_id": self._session_id,
473
- "model": model,
474
- "provider": provider
475
- })
476
-
468
+ self._full_conversation.append(
469
+ {
470
+ "role": "assistant",
471
+ "content": response,
472
+ "timestamp": event.timestamp.isoformat(),
473
+ "session_id": self._session_id,
474
+ "model": model,
475
+ "provider": provider,
476
+ }
477
+ )
478
+
477
479
  return self._session_id
478
-
480
+
479
481
  async def tool_used(
480
482
  self,
481
483
  tool_name: str,
482
484
  arguments: Dict[str, Any],
483
485
  result: Any,
484
486
  error: Optional[str] = None,
485
- **metadata
487
+ **metadata,
486
488
  ) -> str:
487
489
  """
488
490
  Track a tool call.
489
-
491
+
490
492
  Args:
491
493
  tool_name: Name of the tool called.
492
494
  arguments: Arguments passed to the tool.
493
495
  result: Result returned by the tool.
494
496
  error: Optional error message if tool failed.
495
497
  **metadata: Optional metadata to attach.
496
-
498
+
497
499
  Returns:
498
500
  The current session ID.
499
501
  """
500
502
  await self._ensure_initialized()
501
-
503
+
502
504
  tool_message = {
503
505
  "tool": tool_name,
504
506
  "arguments": arguments,
505
507
  "result": result,
506
508
  "error": error,
507
- "success": error is None
509
+ "success": error is None,
508
510
  }
509
-
511
+
510
512
  # Create event with explicit type TOOL_CALL
511
513
  event = SessionEvent(
512
514
  message=tool_message,
513
515
  source=EventSource.SYSTEM,
514
- type=EventType.TOOL_CALL # This is correct
516
+ type=EventType.TOOL_CALL,
515
517
  )
516
-
518
+
517
519
  for key, value in metadata.items():
518
520
  await event.set_metadata(key, value)
519
-
520
- # This should add the event to the session
521
+
521
522
  await self._session.add_event_and_save(event)
522
-
523
- # Verify the event was added (debug)
523
+
524
524
  tool_events = [e for e in self._session.events if e.type == EventType.TOOL_CALL]
525
525
  logger.debug(f"Tool events after adding: {len(tool_events)}")
526
-
526
+
527
527
  return self._session_id
528
528
 
529
- async def get_messages_for_llm(self, include_system: bool = True) -> List[Dict[str, str]]:
529
+ async def get_messages_for_llm(
530
+ self, include_system: bool = True
531
+ ) -> List[Dict[str, str]]:
530
532
  """
531
533
  Get messages formatted for LLM consumption, optionally including system prompt.
532
-
534
+
533
535
  Args:
534
536
  include_system: Whether to include the system prompt as the first message.
535
-
537
+
536
538
  Returns:
537
539
  List of message dictionaries with 'role' and 'content' keys.
538
540
  """
539
541
  await self._ensure_initialized()
540
-
542
+
541
543
  messages = []
542
-
544
+
543
545
  # Add system prompt if available and requested (and not empty)
544
546
  if include_system and self._system_prompt and self._system_prompt.strip():
545
- messages.append({
546
- "role": "system",
547
- "content": self._system_prompt
548
- })
549
-
547
+ messages.append({"role": "system", "content": self._system_prompt})
548
+
550
549
  # Add conversation messages
551
550
  for event in self._session.events:
552
551
  if event.type == EventType.MESSAGE:
553
552
  if event.source == EventSource.USER:
554
- messages.append({
555
- "role": "user",
556
- "content": str(event.message)
557
- })
553
+ messages.append({"role": "user", "content": str(event.message)})
558
554
  elif event.source == EventSource.LLM:
559
- messages.append({
560
- "role": "assistant",
561
- "content": str(event.message)
562
- })
563
-
555
+ messages.append(
556
+ {"role": "assistant", "content": str(event.message)}
557
+ )
558
+
564
559
  return messages
565
-
566
- async def get_conversation(self, include_all_segments: bool = None) -> List[Dict[str, Any]]:
560
+
561
+ async def get_conversation(
562
+ self, include_all_segments: Optional[bool] = None
563
+ ) -> List[Dict[str, Any]]:
567
564
  """
568
565
  Get conversation history.
569
-
566
+
570
567
  Args:
571
568
  include_all_segments: Include all segments (defaults to infinite_context setting).
572
-
569
+
573
570
  Returns:
574
571
  List of conversation turns.
575
572
  """
576
573
  if include_all_segments is None:
577
574
  include_all_segments = self._infinite_context
578
-
575
+
579
576
  if self._infinite_context and include_all_segments:
580
577
  # Return full conversation across all segments
581
578
  return self._full_conversation.copy()
@@ -586,28 +583,32 @@ class SessionManager:
586
583
  for event in self._session.events:
587
584
  if event.type == EventType.MESSAGE:
588
585
  turn = {
589
- "role": "user" if event.source == EventSource.USER else "assistant",
586
+ "role": "user"
587
+ if event.source == EventSource.USER
588
+ else "assistant",
590
589
  "content": str(event.message),
591
- "timestamp": event.timestamp.isoformat()
590
+ "timestamp": event.timestamp.isoformat(),
592
591
  }
593
592
  conversation.append(turn)
594
-
593
+
595
594
  return conversation
596
-
595
+
597
596
  async def get_session_chain(self) -> List[str]:
598
597
  """Get the chain of session IDs (infinite context only)."""
599
598
  if self._infinite_context:
600
599
  return self._session_chain.copy()
601
600
  else:
602
601
  return [self.session_id]
603
-
604
- async def get_stats(self, include_all_segments: bool = None) -> Dict[str, Any]:
602
+
603
+ async def get_stats(
604
+ self, include_all_segments: Optional[bool] = None
605
+ ) -> Dict[str, Any]:
605
606
  """
606
607
  Get conversation statistics.
607
-
608
+
608
609
  Args:
609
610
  include_all_segments: Include all segments (defaults to infinite_context setting).
610
-
611
+
611
612
  Returns:
612
613
  Dictionary with conversation stats including:
613
614
  - session_id: Current session ID
@@ -624,17 +625,17 @@ class SessionManager:
624
625
  """
625
626
  if include_all_segments is None:
626
627
  include_all_segments = self._infinite_context
627
-
628
+
628
629
  await self._ensure_initialized()
629
-
630
+
630
631
  if self._infinite_context and include_all_segments:
631
632
  # For infinite context, build the complete chain if needed
632
633
  if len(self._session_chain) < self._total_segments:
633
634
  # Need to reconstruct the chain
634
- store = self._store or ChukSessionsStore()
635
+ store = self._store
635
636
  chain = []
636
637
  current_id = self._session_id
637
-
638
+
638
639
  # Walk backwards to find all segments
639
640
  while current_id:
640
641
  chain.insert(0, current_id)
@@ -643,32 +644,45 @@ class SessionManager:
643
644
  current_id = session.parent_id
644
645
  else:
645
646
  break
646
-
647
+
647
648
  self._session_chain = chain
648
649
  self._total_segments = len(chain)
649
-
650
+
650
651
  # Calculate stats across all segments
651
- user_messages = len([t for t in self._full_conversation if t["role"] == "user"])
652
- ai_messages = len([t for t in self._full_conversation if t["role"] == "assistant"])
653
-
652
+ user_messages = len(
653
+ [t for t in self._full_conversation if t["role"] == "user"]
654
+ )
655
+ ai_messages = len(
656
+ [t for t in self._full_conversation if t["role"] == "assistant"]
657
+ )
658
+
654
659
  # Get token/cost stats by loading all sessions in chain
655
660
  total_tokens = 0
656
661
  total_cost = 0.0
657
662
  total_events = 0
658
-
659
- store = self._store or ChukSessionsStore()
660
-
663
+ tool_calls = 0
664
+
665
+ store = self._store
666
+
661
667
  for session_id in self._session_chain:
662
668
  try:
663
- sess = await store.get(session_id)
669
+ # For the current session, use self._session directly
670
+ # to ensure we have the latest in-memory state
671
+ if session_id == self._session_id:
672
+ sess = self._session
673
+ else:
674
+ sess = await store.get(session_id)
675
+
664
676
  if sess:
665
677
  total_tokens += sess.total_tokens
666
678
  total_cost += sess.total_cost
667
679
  total_events += len(sess.events)
668
- except Exception:
669
- # Skip if can't load session
670
- pass
671
-
680
+ tool_calls += sum(
681
+ 1 for e in sess.events if e.type == EventType.TOOL_CALL
682
+ )
683
+ except Exception as e:
684
+ logger.warning(f"Failed to load session {session_id} in chain: {e}")
685
+
672
686
  return {
673
687
  "session_id": self._session_id,
674
688
  "session_segments": self._total_segments,
@@ -677,21 +691,29 @@ class SessionManager:
677
691
  "total_events": total_events,
678
692
  "user_messages": user_messages,
679
693
  "ai_messages": ai_messages,
680
- "tool_calls": 0, # TODO: Track tools in full conversation
694
+ "tool_calls": tool_calls,
681
695
  "total_tokens": total_tokens,
682
696
  "estimated_cost": total_cost,
683
697
  "created_at": self._session.metadata.created_at.isoformat(),
684
698
  "last_update": self._session.last_update_time.isoformat(),
685
- "infinite_context": True
699
+ "infinite_context": True,
686
700
  }
687
701
  else:
688
702
  # Current session stats only
689
- user_messages = sum(1 for e in self._session.events
690
- if e.type == EventType.MESSAGE and e.source == EventSource.USER)
691
- ai_messages = sum(1 for e in self._session.events
692
- if e.type == EventType.MESSAGE and e.source == EventSource.LLM)
693
- tool_calls = sum(1 for e in self._session.events if e.type == EventType.TOOL_CALL)
694
-
703
+ user_messages = sum(
704
+ 1
705
+ for e in self._session.events
706
+ if e.type == EventType.MESSAGE and e.source == EventSource.USER
707
+ )
708
+ ai_messages = sum(
709
+ 1
710
+ for e in self._session.events
711
+ if e.type == EventType.MESSAGE and e.source == EventSource.LLM
712
+ )
713
+ tool_calls = sum(
714
+ 1 for e in self._session.events if e.type == EventType.TOOL_CALL
715
+ )
716
+
695
717
  return {
696
718
  "session_id": self._session.id,
697
719
  "session_segments": 1,
@@ -704,57 +726,62 @@ class SessionManager:
704
726
  "estimated_cost": self._session.total_cost,
705
727
  "created_at": self._session.metadata.created_at.isoformat(),
706
728
  "last_update": self._session.last_update_time.isoformat(),
707
- "infinite_context": self._infinite_context
729
+ "infinite_context": self._infinite_context,
708
730
  }
709
-
731
+
710
732
  async def set_summary_callback(self, callback: Callable[[List[Dict]], str]) -> None:
711
733
  """
712
734
  Set a custom callback for generating summaries in infinite context mode.
713
-
735
+
714
736
  Args:
715
737
  callback: Async function that takes messages and returns a summary string.
716
738
  """
717
739
  self._summary_callback = callback
718
-
740
+
719
741
  async def load_session_chain(self) -> None:
720
742
  """
721
743
  Load the full session chain for infinite context sessions.
722
-
744
+
723
745
  This reconstructs the conversation history from all linked sessions.
724
746
  """
725
747
  if not self._infinite_context:
726
748
  return
727
-
749
+
728
750
  await self._ensure_initialized()
729
- store = self._store or ChukSessionsStore()
730
-
751
+ store = self._store
752
+
731
753
  # Start from current session and work backwards
732
754
  current_id = self._session_id
733
755
  chain = [current_id]
734
756
  conversation = []
735
-
757
+
736
758
  while current_id:
737
759
  session = await store.get(current_id)
738
760
  if not session:
739
761
  break
740
-
762
+
741
763
  # Extract messages from this session
742
764
  for event in reversed(session.events):
743
765
  if event.type == EventType.MESSAGE:
744
- conversation.insert(0, {
745
- "role": "user" if event.source == EventSource.USER else "assistant",
746
- "content": str(event.message),
747
- "timestamp": event.timestamp.isoformat(),
748
- "session_id": current_id
749
- })
750
-
766
+ conversation.insert(
767
+ 0,
768
+ {
769
+ "role": "user"
770
+ if event.source == EventSource.USER
771
+ else "assistant",
772
+ "content": str(event.message),
773
+ "timestamp": event.timestamp.isoformat(),
774
+ "session_id": current_id,
775
+ },
776
+ )
777
+
751
778
  # Move to parent
752
779
  if session.parent_id:
753
780
  chain.insert(0, session.parent_id)
754
781
  current_id = session.parent_id
755
782
  else:
756
783
  break
757
-
784
+
758
785
  self._session_chain = chain
759
786
  self._full_conversation = conversation
760
- self._total_segments = len(chain)
787
+ self._total_segments = len(chain)