alma-memory 0.5.0__py3-none-any.whl → 0.7.0__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 (111) hide show
  1. alma/__init__.py +296 -194
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -322
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -264
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -81
  26. alma/graph/backends/__init__.py +32 -18
  27. alma/graph/backends/kuzu.py +624 -0
  28. alma/graph/backends/memgraph.py +432 -0
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -432
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -511
  55. alma/observability/__init__.py +91 -0
  56. alma/observability/config.py +302 -0
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -0
  59. alma/observability/metrics.py +583 -0
  60. alma/observability/tracing.py +440 -0
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -366
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -61
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1048
  80. alma/storage/base.py +1083 -525
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -0
  83. alma/storage/file_based.py +614 -619
  84. alma/storage/migrations/__init__.py +21 -0
  85. alma/storage/migrations/base.py +321 -0
  86. alma/storage/migrations/runner.py +323 -0
  87. alma/storage/migrations/version_stores.py +337 -0
  88. alma/storage/migrations/versions/__init__.py +11 -0
  89. alma/storage/migrations/versions/v1_0_0.py +373 -0
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1452
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1358
  95. alma/testing/__init__.py +46 -0
  96. alma/testing/factories.py +301 -0
  97. alma/testing/mocks.py +389 -0
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.0.dist-info/RECORD +0 -76
  110. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
alma/session/manager.py CHANGED
@@ -1,399 +1,442 @@
1
- """
2
- Session Manager.
3
-
4
- Manages session continuity, handoffs, and quick context reload.
5
- """
6
-
7
- import logging
8
- import uuid
9
- from datetime import datetime, timezone
10
- from typing import Any, Callable, Dict, List, Optional
11
-
12
- from alma.session.types import (
13
- SessionContext,
14
- SessionHandoff,
15
- SessionOutcome,
16
- )
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class SessionManager:
22
- """
23
- Manage session continuity for AI agents.
24
-
25
- Provides:
26
- - Session start/end handling
27
- - Handoff creation and retrieval
28
- - Quick reload formatting
29
- - Integration with progress tracking
30
- """
31
-
32
- def __init__(
33
- self,
34
- project_id: str,
35
- storage: Optional[Any] = None, # Will be StorageBackend when integrated
36
- progress_tracker: Optional[Any] = None, # Will be ProgressTracker
37
- max_handoffs: int = 50,
38
- ):
39
- """
40
- Initialize session manager.
41
-
42
- Args:
43
- project_id: Project identifier
44
- storage: Optional storage backend for persistence
45
- progress_tracker: Optional progress tracker integration
46
- max_handoffs: Maximum handoffs to keep in memory
47
- """
48
- self.project_id = project_id
49
- self.storage = storage
50
- self.progress_tracker = progress_tracker
51
- self.max_handoffs = max_handoffs
52
-
53
- # In-memory handoff storage (keyed by agent)
54
- self._handoffs: Dict[str, List[SessionHandoff]] = {}
55
-
56
- # Active sessions
57
- self._active_sessions: Dict[str, SessionHandoff] = {}
58
-
59
- # Context enrichers (callables that add context to sessions)
60
- self._context_enrichers: List[Callable[[SessionContext], SessionContext]] = []
61
-
62
- def register_enricher(
63
- self,
64
- enricher: Callable[[SessionContext], SessionContext],
65
- ) -> None:
66
- """
67
- Register a context enricher.
68
-
69
- Enrichers are called during session start to add context
70
- (e.g., git status, running services, etc.)
71
- """
72
- self._context_enrichers.append(enricher)
73
-
74
- def start_session(
75
- self,
76
- agent: str,
77
- goal: Optional[str] = None,
78
- session_id: Optional[str] = None,
79
- ) -> SessionContext:
80
- """
81
- Start a new session, loading previous context.
82
-
83
- Args:
84
- agent: Agent identifier
85
- goal: Current session goal
86
- session_id: Optional session ID (generated if not provided)
87
-
88
- Returns:
89
- SessionContext with all relevant orientation data
90
- """
91
- session_id = session_id or str(uuid.uuid4())
92
-
93
- # Get previous handoff
94
- previous = self.get_latest_handoff(agent)
95
-
96
- # Create session context
97
- context = SessionContext.create(
98
- project_id=self.project_id,
99
- agent=agent,
100
- session_id=session_id,
101
- previous_handoff=previous,
102
- )
103
-
104
- # Get progress if tracker available
105
- if self.progress_tracker:
106
- try:
107
- context.progress = self.progress_tracker.get_progress_summary(agent)
108
- except Exception as e:
109
- logger.warning(f"Could not get progress: {e}")
110
-
111
- # Apply enrichers
112
- for enricher in self._context_enrichers:
113
- try:
114
- context = enricher(context)
115
- except Exception as e:
116
- logger.warning(f"Context enricher failed: {e}")
117
-
118
- # Create active session handoff for this session
119
- current_goal = goal or (previous.current_goal if previous else "Unknown")
120
- active_handoff = SessionHandoff.create(
121
- project_id=self.project_id,
122
- agent=agent,
123
- session_id=session_id,
124
- last_action="session_start",
125
- current_goal=current_goal,
126
- last_outcome="unknown",
127
- )
128
-
129
- # Carry over blockers from previous session
130
- if previous and previous.blockers:
131
- active_handoff.blockers = previous.blockers.copy()
132
-
133
- self._active_sessions[f"{agent}:{session_id}"] = active_handoff
134
-
135
- logger.info(
136
- f"Started session {session_id} for agent {agent} "
137
- f"(previous: {'yes' if previous else 'no'})"
138
- )
139
-
140
- return context
141
-
142
- def get_active_handoff(
143
- self,
144
- agent: str,
145
- session_id: str,
146
- ) -> Optional[SessionHandoff]:
147
- """Get the active handoff for current session."""
148
- return self._active_sessions.get(f"{agent}:{session_id}")
149
-
150
- def update_session(
151
- self,
152
- agent: str,
153
- session_id: str,
154
- action: Optional[str] = None,
155
- outcome: Optional[SessionOutcome] = None,
156
- decision: Optional[str] = None,
157
- blocker: Optional[str] = None,
158
- resolved_blocker: Optional[str] = None,
159
- active_file: Optional[str] = None,
160
- test_result: Optional[Dict[str, bool]] = None,
161
- confidence: Optional[float] = None,
162
- risk: Optional[str] = None,
163
- ) -> Optional[SessionHandoff]:
164
- """
165
- Update the active session with new information.
166
-
167
- This should be called periodically during a session to track state.
168
-
169
- Args:
170
- agent: Agent identifier
171
- session_id: Session identifier
172
- action: Latest action taken
173
- outcome: Outcome of latest action
174
- decision: Key decision made
175
- blocker: New blocker encountered
176
- resolved_blocker: Blocker that was resolved
177
- active_file: File currently being worked on
178
- test_result: Test name -> passing status
179
- confidence: Updated confidence level
180
- risk: New risk identified
181
-
182
- Returns:
183
- Updated SessionHandoff or None if session not found
184
- """
185
- key = f"{agent}:{session_id}"
186
- handoff = self._active_sessions.get(key)
187
-
188
- if not handoff:
189
- logger.warning(f"No active session found for {key}")
190
- return None
191
-
192
- if action:
193
- handoff.last_action = action
194
- if outcome:
195
- handoff.last_outcome = outcome
196
- if decision:
197
- handoff.add_decision(decision)
198
- if blocker:
199
- handoff.add_blocker(blocker)
200
- if resolved_blocker:
201
- handoff.remove_blocker(resolved_blocker)
202
- if active_file and active_file not in handoff.active_files:
203
- handoff.active_files.append(active_file)
204
- if test_result:
205
- for test_name, passing in test_result.items():
206
- handoff.set_test_status(test_name, passing)
207
- if confidence is not None:
208
- handoff.confidence_level = max(0.0, min(1.0, confidence))
209
- if risk and risk not in handoff.risk_flags:
210
- handoff.risk_flags.append(risk)
211
-
212
- return handoff
213
-
214
- def create_handoff(
215
- self,
216
- agent: str,
217
- session_id: str,
218
- last_action: str,
219
- last_outcome: SessionOutcome,
220
- next_steps: Optional[List[str]] = None,
221
- **context,
222
- ) -> SessionHandoff:
223
- """
224
- Create handoff at session end.
225
-
226
- This finalizes the session and stores the handoff for the next session.
227
-
228
- Args:
229
- agent: Agent identifier
230
- session_id: Session identifier
231
- last_action: Final action taken
232
- last_outcome: Outcome of the session
233
- next_steps: Planned next actions
234
- **context: Additional context to store
235
-
236
- Returns:
237
- Finalized SessionHandoff
238
- """
239
- key = f"{agent}:{session_id}"
240
- handoff = self._active_sessions.get(key)
241
-
242
- if handoff:
243
- # Finalize existing handoff
244
- handoff.finalize(last_action, last_outcome, next_steps)
245
- # Add any additional context
246
- handoff.metadata.update(context)
247
- else:
248
- # Create new handoff (session started without start_session call)
249
- handoff = SessionHandoff.create(
250
- project_id=self.project_id,
251
- agent=agent,
252
- session_id=session_id,
253
- last_action=last_action,
254
- current_goal=context.get("goal", "Unknown"),
255
- last_outcome=last_outcome,
256
- next_steps=next_steps or [],
257
- )
258
- handoff.session_end = datetime.now(timezone.utc)
259
- handoff.metadata.update(context)
260
-
261
- # Store handoff
262
- self._store_handoff(agent, handoff)
263
-
264
- # Clear active session
265
- if key in self._active_sessions:
266
- del self._active_sessions[key]
267
-
268
- logger.info(
269
- f"Created handoff for session {session_id}, "
270
- f"outcome: {last_outcome}, next_steps: {len(next_steps or [])}"
271
- )
272
-
273
- return handoff
274
-
275
- def _store_handoff(self, agent: str, handoff: SessionHandoff) -> None:
276
- """Store a handoff internally and optionally to persistent storage."""
277
- if agent not in self._handoffs:
278
- self._handoffs[agent] = []
279
-
280
- self._handoffs[agent].append(handoff)
281
-
282
- # Trim to max
283
- if len(self._handoffs[agent]) > self.max_handoffs:
284
- self._handoffs[agent] = self._handoffs[agent][-self.max_handoffs :]
285
-
286
- # TODO: Persist to storage backend when integrated
287
-
288
- def get_latest_handoff(self, agent: str) -> Optional[SessionHandoff]:
289
- """Get the most recent handoff for an agent."""
290
- handoffs = self._handoffs.get(agent, [])
291
- return handoffs[-1] if handoffs else None
292
-
293
- def get_previous_sessions(
294
- self,
295
- agent: str,
296
- limit: int = 5,
297
- ) -> List[SessionHandoff]:
298
- """
299
- Get recent session handoffs for an agent.
300
-
301
- Args:
302
- agent: Agent identifier
303
- limit: Maximum number of handoffs to return
304
-
305
- Returns:
306
- List of SessionHandoff, most recent first
307
- """
308
- handoffs = self._handoffs.get(agent, [])
309
- return list(reversed(handoffs[-limit:]))
310
-
311
- def get_quick_reload(
312
- self,
313
- agent: str,
314
- ) -> str:
315
- """
316
- Get compressed context string for quick reload.
317
-
318
- This is a formatted string that can be quickly parsed by an agent
319
- for rapid context restoration.
320
-
321
- Args:
322
- agent: Agent identifier
323
-
324
- Returns:
325
- Formatted quick reload string
326
- """
327
- handoff = self.get_latest_handoff(agent)
328
- if not handoff:
329
- return f"No previous session found for agent {agent}"
330
-
331
- return handoff.format_quick_reload()
332
-
333
- def get_all_agents(self) -> List[str]:
334
- """Get list of all agents with session history."""
335
- return list(self._handoffs.keys())
336
-
337
- def get_agent_stats(self, agent: str) -> Dict[str, Any]:
338
- """
339
- Get session statistics for an agent.
340
-
341
- Returns summary of session history including:
342
- - Total sessions
343
- - Success rate
344
- - Average duration
345
- - Common blockers
346
- """
347
- handoffs = self._handoffs.get(agent, [])
348
- if not handoffs:
349
- return {
350
- "agent": agent,
351
- "total_sessions": 0,
352
- "success_rate": 0.0,
353
- "avg_duration_ms": 0,
354
- "common_blockers": [],
355
- }
356
-
357
- # Calculate stats
358
- total = len(handoffs)
359
- successes = sum(1 for h in handoffs if h.last_outcome == "success")
360
- success_rate = successes / total if total > 0 else 0.0
361
-
362
- durations = [h.duration_ms for h in handoffs if h.duration_ms > 0]
363
- avg_duration = sum(durations) / len(durations) if durations else 0
364
-
365
- # Count blockers
366
- blocker_counts: Dict[str, int] = {}
367
- for h in handoffs:
368
- for blocker in h.blockers:
369
- blocker_counts[blocker] = blocker_counts.get(blocker, 0) + 1
370
- common_blockers = sorted(
371
- blocker_counts.items(), key=lambda x: x[1], reverse=True
372
- )[:5]
373
-
374
- return {
375
- "agent": agent,
376
- "total_sessions": total,
377
- "success_rate": success_rate,
378
- "avg_duration_ms": avg_duration,
379
- "common_blockers": [b[0] for b in common_blockers],
380
- }
381
-
382
- def clear_history(self, agent: Optional[str] = None) -> int:
383
- """
384
- Clear session history.
385
-
386
- Args:
387
- agent: If provided, only clear history for this agent
388
-
389
- Returns:
390
- Number of handoffs cleared
391
- """
392
- if agent:
393
- count = len(self._handoffs.get(agent, []))
394
- self._handoffs[agent] = []
395
- return count
396
- else:
397
- count = sum(len(h) for h in self._handoffs.values())
398
- self._handoffs.clear()
399
- return count
1
+ """
2
+ Session Manager.
3
+
4
+ Manages session continuity, handoffs, and quick context reload.
5
+ """
6
+
7
+ import logging
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Callable, Dict, List, Optional
11
+
12
+ from alma.session.types import (
13
+ SessionContext,
14
+ SessionHandoff,
15
+ SessionOutcome,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SessionManager:
22
+ """
23
+ Manage session continuity for AI agents.
24
+
25
+ Provides:
26
+ - Session start/end handling
27
+ - Handoff creation and retrieval
28
+ - Quick reload formatting
29
+ - Integration with progress tracking
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ project_id: str,
35
+ storage: Optional[Any] = None, # Will be StorageBackend when integrated
36
+ progress_tracker: Optional[Any] = None, # Will be ProgressTracker
37
+ max_handoffs: int = 50,
38
+ ):
39
+ """
40
+ Initialize session manager.
41
+
42
+ Args:
43
+ project_id: Project identifier
44
+ storage: Optional storage backend for persistence
45
+ progress_tracker: Optional progress tracker integration
46
+ max_handoffs: Maximum handoffs to keep in memory
47
+ """
48
+ self.project_id = project_id
49
+ self.storage = storage
50
+ self.progress_tracker = progress_tracker
51
+ self.max_handoffs = max_handoffs
52
+
53
+ # In-memory handoff storage (keyed by agent)
54
+ self._handoffs: Dict[str, List[SessionHandoff]] = {}
55
+
56
+ # Active sessions
57
+ self._active_sessions: Dict[str, SessionHandoff] = {}
58
+
59
+ # Context enrichers (callables that add context to sessions)
60
+ self._context_enrichers: List[Callable[[SessionContext], SessionContext]] = []
61
+
62
+ def register_enricher(
63
+ self,
64
+ enricher: Callable[[SessionContext], SessionContext],
65
+ ) -> None:
66
+ """
67
+ Register a context enricher.
68
+
69
+ Enrichers are called during session start to add context
70
+ (e.g., git status, running services, etc.)
71
+ """
72
+ self._context_enrichers.append(enricher)
73
+
74
+ def start_session(
75
+ self,
76
+ agent: str,
77
+ goal: Optional[str] = None,
78
+ session_id: Optional[str] = None,
79
+ ) -> SessionContext:
80
+ """
81
+ Start a new session, loading previous context.
82
+
83
+ Args:
84
+ agent: Agent identifier
85
+ goal: Current session goal
86
+ session_id: Optional session ID (generated if not provided)
87
+
88
+ Returns:
89
+ SessionContext with all relevant orientation data
90
+ """
91
+ session_id = session_id or str(uuid.uuid4())
92
+
93
+ # Get previous handoff
94
+ previous = self.get_latest_handoff(agent)
95
+
96
+ # Create session context
97
+ context = SessionContext.create(
98
+ project_id=self.project_id,
99
+ agent=agent,
100
+ session_id=session_id,
101
+ previous_handoff=previous,
102
+ )
103
+
104
+ # Get progress if tracker available
105
+ if self.progress_tracker:
106
+ try:
107
+ context.progress = self.progress_tracker.get_progress_summary(agent)
108
+ except Exception as e:
109
+ logger.warning(f"Could not get progress: {e}")
110
+
111
+ # Apply enrichers
112
+ for enricher in self._context_enrichers:
113
+ try:
114
+ context = enricher(context)
115
+ except Exception as e:
116
+ logger.warning(f"Context enricher failed: {e}")
117
+
118
+ # Create active session handoff for this session
119
+ current_goal = goal or (previous.current_goal if previous else "Unknown")
120
+ active_handoff = SessionHandoff.create(
121
+ project_id=self.project_id,
122
+ agent=agent,
123
+ session_id=session_id,
124
+ last_action="session_start",
125
+ current_goal=current_goal,
126
+ last_outcome="unknown",
127
+ )
128
+
129
+ # Carry over blockers from previous session
130
+ if previous and previous.blockers:
131
+ active_handoff.blockers = previous.blockers.copy()
132
+
133
+ self._active_sessions[f"{agent}:{session_id}"] = active_handoff
134
+
135
+ logger.info(
136
+ f"Started session {session_id} for agent {agent} "
137
+ f"(previous: {'yes' if previous else 'no'})"
138
+ )
139
+
140
+ return context
141
+
142
+ def get_active_handoff(
143
+ self,
144
+ agent: str,
145
+ session_id: str,
146
+ ) -> Optional[SessionHandoff]:
147
+ """Get the active handoff for current session."""
148
+ return self._active_sessions.get(f"{agent}:{session_id}")
149
+
150
+ def update_session(
151
+ self,
152
+ agent: str,
153
+ session_id: str,
154
+ action: Optional[str] = None,
155
+ outcome: Optional[SessionOutcome] = None,
156
+ decision: Optional[str] = None,
157
+ blocker: Optional[str] = None,
158
+ resolved_blocker: Optional[str] = None,
159
+ active_file: Optional[str] = None,
160
+ test_result: Optional[Dict[str, bool]] = None,
161
+ confidence: Optional[float] = None,
162
+ risk: Optional[str] = None,
163
+ ) -> Optional[SessionHandoff]:
164
+ """
165
+ Update the active session with new information.
166
+
167
+ This should be called periodically during a session to track state.
168
+
169
+ Args:
170
+ agent: Agent identifier
171
+ session_id: Session identifier
172
+ action: Latest action taken
173
+ outcome: Outcome of latest action
174
+ decision: Key decision made
175
+ blocker: New blocker encountered
176
+ resolved_blocker: Blocker that was resolved
177
+ active_file: File currently being worked on
178
+ test_result: Test name -> passing status
179
+ confidence: Updated confidence level
180
+ risk: New risk identified
181
+
182
+ Returns:
183
+ Updated SessionHandoff or None if session not found
184
+ """
185
+ key = f"{agent}:{session_id}"
186
+ handoff = self._active_sessions.get(key)
187
+
188
+ if not handoff:
189
+ logger.warning(f"No active session found for {key}")
190
+ return None
191
+
192
+ if action:
193
+ handoff.last_action = action
194
+ if outcome:
195
+ handoff.last_outcome = outcome
196
+ if decision:
197
+ handoff.add_decision(decision)
198
+ if blocker:
199
+ handoff.add_blocker(blocker)
200
+ if resolved_blocker:
201
+ handoff.remove_blocker(resolved_blocker)
202
+ if active_file and active_file not in handoff.active_files:
203
+ handoff.active_files.append(active_file)
204
+ if test_result:
205
+ for test_name, passing in test_result.items():
206
+ handoff.set_test_status(test_name, passing)
207
+ if confidence is not None:
208
+ handoff.confidence_level = max(0.0, min(1.0, confidence))
209
+ if risk and risk not in handoff.risk_flags:
210
+ handoff.risk_flags.append(risk)
211
+
212
+ return handoff
213
+
214
+ def create_handoff(
215
+ self,
216
+ agent: str,
217
+ session_id: str,
218
+ last_action: str,
219
+ last_outcome: SessionOutcome,
220
+ next_steps: Optional[List[str]] = None,
221
+ **context,
222
+ ) -> SessionHandoff:
223
+ """
224
+ Create handoff at session end.
225
+
226
+ This finalizes the session and stores the handoff for the next session.
227
+
228
+ Args:
229
+ agent: Agent identifier
230
+ session_id: Session identifier
231
+ last_action: Final action taken
232
+ last_outcome: Outcome of the session
233
+ next_steps: Planned next actions
234
+ **context: Additional context to store
235
+
236
+ Returns:
237
+ Finalized SessionHandoff
238
+ """
239
+ key = f"{agent}:{session_id}"
240
+ handoff = self._active_sessions.get(key)
241
+
242
+ if handoff:
243
+ # Finalize existing handoff
244
+ handoff.finalize(last_action, last_outcome, next_steps)
245
+ # Add any additional context
246
+ handoff.metadata.update(context)
247
+ else:
248
+ # Create new handoff (session started without start_session call)
249
+ handoff = SessionHandoff.create(
250
+ project_id=self.project_id,
251
+ agent=agent,
252
+ session_id=session_id,
253
+ last_action=last_action,
254
+ current_goal=context.get("goal", "Unknown"),
255
+ last_outcome=last_outcome,
256
+ next_steps=next_steps or [],
257
+ )
258
+ handoff.session_end = datetime.now(timezone.utc)
259
+ handoff.metadata.update(context)
260
+
261
+ # Store handoff
262
+ self._store_handoff(agent, handoff)
263
+
264
+ # Clear active session
265
+ if key in self._active_sessions:
266
+ del self._active_sessions[key]
267
+
268
+ logger.info(
269
+ f"Created handoff for session {session_id}, "
270
+ f"outcome: {last_outcome}, next_steps: {len(next_steps or [])}"
271
+ )
272
+
273
+ return handoff
274
+
275
+ def _store_handoff(self, agent: str, handoff: SessionHandoff) -> None:
276
+ """Store a handoff internally and to persistent storage."""
277
+ if agent not in self._handoffs:
278
+ self._handoffs[agent] = []
279
+
280
+ self._handoffs[agent].append(handoff)
281
+
282
+ # Trim to max
283
+ if len(self._handoffs[agent]) > self.max_handoffs:
284
+ self._handoffs[agent] = self._handoffs[agent][-self.max_handoffs :]
285
+
286
+ # Persist to storage backend if available
287
+ if self.storage is not None:
288
+ try:
289
+ self.storage.save_session_handoff(handoff)
290
+ logger.debug(f"Persisted session handoff {handoff.id} to storage")
291
+ except Exception as e:
292
+ logger.warning(f"Failed to persist session handoff: {e}")
293
+
294
+ def _load_handoffs_from_storage(self, agent: str) -> None:
295
+ """Load handoffs from storage into memory cache if not already loaded."""
296
+ if agent in self._handoffs:
297
+ return # Already loaded
298
+
299
+ if self.storage is None:
300
+ self._handoffs[agent] = []
301
+ return
302
+
303
+ try:
304
+ handoffs = self.storage.get_session_handoffs(
305
+ self.project_id, agent, limit=self.max_handoffs
306
+ )
307
+ # Storage returns most recent first, we want oldest first in our list
308
+ self._handoffs[agent] = list(reversed(handoffs))
309
+ logger.debug(f"Loaded {len(handoffs)} handoffs from storage for {agent}")
310
+ except Exception as e:
311
+ logger.warning(f"Failed to load handoffs from storage: {e}")
312
+ self._handoffs[agent] = []
313
+
314
+ def get_latest_handoff(self, agent: str) -> Optional[SessionHandoff]:
315
+ """Get the most recent handoff for an agent."""
316
+ # Ensure handoffs are loaded from storage
317
+ self._load_handoffs_from_storage(agent)
318
+ handoffs = self._handoffs.get(agent, [])
319
+ return handoffs[-1] if handoffs else None
320
+
321
+ def get_previous_sessions(
322
+ self,
323
+ agent: str,
324
+ limit: int = 5,
325
+ ) -> List[SessionHandoff]:
326
+ """
327
+ Get recent session handoffs for an agent.
328
+
329
+ Args:
330
+ agent: Agent identifier
331
+ limit: Maximum number of handoffs to return
332
+
333
+ Returns:
334
+ List of SessionHandoff, most recent first
335
+ """
336
+ # Ensure handoffs are loaded from storage
337
+ self._load_handoffs_from_storage(agent)
338
+ handoffs = self._handoffs.get(agent, [])
339
+ return list(reversed(handoffs[-limit:]))
340
+
341
+ def get_quick_reload(
342
+ self,
343
+ agent: str,
344
+ ) -> str:
345
+ """
346
+ Get compressed context string for quick reload.
347
+
348
+ This is a formatted string that can be quickly parsed by an agent
349
+ for rapid context restoration.
350
+
351
+ Args:
352
+ agent: Agent identifier
353
+
354
+ Returns:
355
+ Formatted quick reload string
356
+ """
357
+ handoff = self.get_latest_handoff(agent)
358
+ if not handoff:
359
+ return f"No previous session found for agent {agent}"
360
+
361
+ return handoff.format_quick_reload()
362
+
363
+ def get_all_agents(self) -> List[str]:
364
+ """Get list of all agents with session history."""
365
+ return list(self._handoffs.keys())
366
+
367
+ def get_agent_stats(self, agent: str) -> Dict[str, Any]:
368
+ """
369
+ Get session statistics for an agent.
370
+
371
+ Returns summary of session history including:
372
+ - Total sessions
373
+ - Success rate
374
+ - Average duration
375
+ - Common blockers
376
+ """
377
+ handoffs = self._handoffs.get(agent, [])
378
+ if not handoffs:
379
+ return {
380
+ "agent": agent,
381
+ "total_sessions": 0,
382
+ "success_rate": 0.0,
383
+ "avg_duration_ms": 0,
384
+ "common_blockers": [],
385
+ }
386
+
387
+ # Calculate stats
388
+ total = len(handoffs)
389
+ successes = sum(1 for h in handoffs if h.last_outcome == "success")
390
+ success_rate = successes / total if total > 0 else 0.0
391
+
392
+ durations = [h.duration_ms for h in handoffs if h.duration_ms > 0]
393
+ avg_duration = sum(durations) / len(durations) if durations else 0
394
+
395
+ # Count blockers
396
+ blocker_counts: Dict[str, int] = {}
397
+ for h in handoffs:
398
+ for blocker in h.blockers:
399
+ blocker_counts[blocker] = blocker_counts.get(blocker, 0) + 1
400
+ common_blockers = sorted(
401
+ blocker_counts.items(), key=lambda x: x[1], reverse=True
402
+ )[:5]
403
+
404
+ return {
405
+ "agent": agent,
406
+ "total_sessions": total,
407
+ "success_rate": success_rate,
408
+ "avg_duration_ms": avg_duration,
409
+ "common_blockers": [b[0] for b in common_blockers],
410
+ }
411
+
412
+ def clear_history(self, agent: Optional[str] = None) -> int:
413
+ """
414
+ Clear session history.
415
+
416
+ Args:
417
+ agent: If provided, only clear history for this agent
418
+
419
+ Returns:
420
+ Number of handoffs cleared
421
+ """
422
+ # Clear from memory
423
+ if agent:
424
+ count = len(self._handoffs.get(agent, []))
425
+ self._handoffs[agent] = []
426
+ else:
427
+ count = sum(len(h) for h in self._handoffs.values())
428
+ self._handoffs.clear()
429
+
430
+ # Clear from storage if available
431
+ if self.storage is not None:
432
+ try:
433
+ storage_count = self.storage.delete_session_handoffs(
434
+ self.project_id, agent
435
+ )
436
+ logger.debug(f"Cleared {storage_count} handoffs from storage")
437
+ # Use storage count if larger (in case memory wasn't fully loaded)
438
+ count = max(count, storage_count)
439
+ except Exception as e:
440
+ logger.warning(f"Failed to clear handoffs from storage: {e}")
441
+
442
+ return count