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