alma-memory 0.2.0__py3-none-any.whl → 0.3.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.
alma/session/types.py ADDED
@@ -0,0 +1,287 @@
1
+ """
2
+ Session Management Types.
3
+
4
+ Data models for session continuity and handoffs.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import Optional, List, Dict, Any, Literal
10
+ import uuid
11
+
12
+
13
+ SessionOutcome = Literal["success", "failure", "interrupted", "unknown"]
14
+
15
+
16
+ @dataclass
17
+ class SessionHandoff:
18
+ """
19
+ Compressed context for session continuity.
20
+
21
+ Captures the essential state at session end so the next session
22
+ can quickly resume without full context reconstruction.
23
+ """
24
+
25
+ id: str
26
+ project_id: str
27
+ agent: str
28
+ session_id: str
29
+
30
+ # Where we left off
31
+ last_action: str
32
+ last_outcome: SessionOutcome
33
+ current_goal: str
34
+
35
+ # Quick context (not full history)
36
+ key_decisions: List[str] = field(default_factory=list) # Max 10 most important
37
+ active_files: List[str] = field(default_factory=list) # Files being worked on
38
+ blockers: List[str] = field(default_factory=list) # Current blockers
39
+ next_steps: List[str] = field(default_factory=list) # Planned next actions
40
+
41
+ # Test/validation state
42
+ test_status: Dict[str, bool] = field(default_factory=dict) # test_name -> passing
43
+
44
+ # Confidence signals
45
+ confidence_level: float = 0.5 # 0-1, how well is this going
46
+ risk_flags: List[str] = field(default_factory=list) # Concerns noted
47
+
48
+ # Timing
49
+ session_start: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
50
+ session_end: Optional[datetime] = None
51
+ duration_ms: int = 0
52
+
53
+ # For semantic retrieval
54
+ embedding: Optional[List[float]] = None
55
+
56
+ # Timestamps
57
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
58
+
59
+ # Extensible metadata
60
+ metadata: Dict[str, Any] = field(default_factory=dict)
61
+
62
+ @classmethod
63
+ def create(
64
+ cls,
65
+ project_id: str,
66
+ agent: str,
67
+ session_id: str,
68
+ last_action: str,
69
+ current_goal: str,
70
+ last_outcome: SessionOutcome = "unknown",
71
+ session_start: Optional[datetime] = None,
72
+ **kwargs,
73
+ ) -> "SessionHandoff":
74
+ """Factory method to create a new session handoff."""
75
+ return cls(
76
+ id=str(uuid.uuid4()),
77
+ project_id=project_id,
78
+ agent=agent,
79
+ session_id=session_id,
80
+ last_action=last_action,
81
+ last_outcome=last_outcome,
82
+ current_goal=current_goal,
83
+ session_start=session_start or datetime.now(timezone.utc),
84
+ **kwargs,
85
+ )
86
+
87
+ def finalize(
88
+ self,
89
+ last_action: str,
90
+ last_outcome: SessionOutcome,
91
+ next_steps: Optional[List[str]] = None,
92
+ ) -> None:
93
+ """Finalize the handoff at session end."""
94
+ self.last_action = last_action
95
+ self.last_outcome = last_outcome
96
+ self.session_end = datetime.now(timezone.utc)
97
+ if self.session_start:
98
+ self.duration_ms = int(
99
+ (self.session_end - self.session_start).total_seconds() * 1000
100
+ )
101
+ if next_steps:
102
+ self.next_steps = next_steps[:10] # Cap at 10
103
+
104
+ def add_decision(self, decision: str) -> None:
105
+ """Record a key decision (max 10)."""
106
+ if len(self.key_decisions) >= 10:
107
+ self.key_decisions.pop(0) # Remove oldest
108
+ self.key_decisions.append(decision)
109
+
110
+ def add_blocker(self, blocker: str) -> None:
111
+ """Record a blocker."""
112
+ if blocker not in self.blockers:
113
+ self.blockers.append(blocker)
114
+
115
+ def remove_blocker(self, blocker: str) -> None:
116
+ """Remove a resolved blocker."""
117
+ if blocker in self.blockers:
118
+ self.blockers.remove(blocker)
119
+
120
+ def set_test_status(self, test_name: str, passing: bool) -> None:
121
+ """Update test status."""
122
+ self.test_status[test_name] = passing
123
+
124
+ def format_quick_reload(self) -> str:
125
+ """
126
+ Format handoff as a quick reload string.
127
+
128
+ Returns a compact string that can be parsed by the next session
129
+ for rapid context restoration.
130
+ """
131
+ lines = [
132
+ f"## Session Handoff: {self.session_id}",
133
+ f"Agent: {self.agent}",
134
+ f"Goal: {self.current_goal}",
135
+ f"Last Action: {self.last_action} ({self.last_outcome})",
136
+ f"Confidence: {int(self.confidence_level * 100)}%",
137
+ ]
138
+
139
+ if self.next_steps:
140
+ lines.append("\n### Next Steps:")
141
+ for step in self.next_steps[:5]:
142
+ lines.append(f"- {step}")
143
+
144
+ if self.blockers:
145
+ lines.append("\n### Blockers:")
146
+ for blocker in self.blockers:
147
+ lines.append(f"- {blocker}")
148
+
149
+ if self.key_decisions:
150
+ lines.append("\n### Key Decisions:")
151
+ for decision in self.key_decisions[-5:]: # Last 5 decisions
152
+ lines.append(f"- {decision}")
153
+
154
+ if self.active_files:
155
+ lines.append("\n### Active Files:")
156
+ for f in self.active_files[:5]:
157
+ lines.append(f"- {f}")
158
+
159
+ if self.risk_flags:
160
+ lines.append("\n### Risks:")
161
+ for risk in self.risk_flags:
162
+ lines.append(f"- {risk}")
163
+
164
+ # Test summary
165
+ if self.test_status:
166
+ passing = sum(1 for v in self.test_status.values() if v)
167
+ total = len(self.test_status)
168
+ lines.append(f"\n### Tests: {passing}/{total} passing")
169
+
170
+ return "\n".join(lines)
171
+
172
+
173
+ @dataclass
174
+ class SessionContext:
175
+ """
176
+ Full context for starting/resuming a session.
177
+
178
+ Provides everything an agent needs to orient itself at session start.
179
+ """
180
+
181
+ project_id: str
182
+ agent: str
183
+ session_id: str
184
+
185
+ # Handoff from previous session
186
+ previous_handoff: Optional[SessionHandoff] = None
187
+
188
+ # Current progress state (from ProgressSummary)
189
+ progress: Optional[Any] = None # Will be ProgressSummary when integrated
190
+
191
+ # Recent outcomes (from ALMA)
192
+ recent_outcomes: List[Any] = field(default_factory=list)
193
+
194
+ # Relevant heuristics (from ALMA)
195
+ relevant_heuristics: List[Any] = field(default_factory=list)
196
+
197
+ # Environment orientation
198
+ codebase_state: Optional[Dict[str, Any]] = None # git status, recent commits
199
+ environment_state: Optional[Dict[str, Any]] = None # running services, etc.
200
+
201
+ # Suggested focus
202
+ suggested_focus: Optional[Any] = None # WorkItem when integrated
203
+
204
+ # Rules of engagement
205
+ rules_of_engagement: List[str] = field(default_factory=list)
206
+
207
+ # Timestamps
208
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
209
+
210
+ @classmethod
211
+ def create(
212
+ cls,
213
+ project_id: str,
214
+ agent: str,
215
+ session_id: Optional[str] = None,
216
+ **kwargs,
217
+ ) -> "SessionContext":
218
+ """Factory method to create new session context."""
219
+ return cls(
220
+ project_id=project_id,
221
+ agent=agent,
222
+ session_id=session_id or str(uuid.uuid4()),
223
+ **kwargs,
224
+ )
225
+
226
+ def format_orientation(self) -> str:
227
+ """
228
+ Format context as an orientation briefing.
229
+
230
+ Returns a structured string for quick agent orientation.
231
+ """
232
+ lines = [
233
+ "# Session Orientation",
234
+ f"Project: {self.project_id}",
235
+ f"Agent: {self.agent}",
236
+ f"Session: {self.session_id}",
237
+ ]
238
+
239
+ # Previous session summary
240
+ if self.previous_handoff:
241
+ lines.append("\n## Previous Session")
242
+ lines.append(f"Outcome: {self.previous_handoff.last_outcome}")
243
+ lines.append(f"Last Action: {self.previous_handoff.last_action}")
244
+ lines.append(f"Goal: {self.previous_handoff.current_goal}")
245
+ if self.previous_handoff.next_steps:
246
+ lines.append("Next Steps from Last Session:")
247
+ for step in self.previous_handoff.next_steps[:3]:
248
+ lines.append(f" - {step}")
249
+
250
+ # Current progress
251
+ if self.progress:
252
+ lines.append("\n## Progress")
253
+ # Assuming progress has a format_summary method
254
+ if hasattr(self.progress, "format_summary"):
255
+ lines.append(self.progress.format_summary())
256
+ else:
257
+ lines.append(str(self.progress))
258
+
259
+ # Blockers from previous session
260
+ if self.previous_handoff and self.previous_handoff.blockers:
261
+ lines.append("\n## Outstanding Blockers")
262
+ for blocker in self.previous_handoff.blockers:
263
+ lines.append(f"- {blocker}")
264
+
265
+ # Suggested focus
266
+ if self.suggested_focus:
267
+ lines.append("\n## Suggested Focus")
268
+ if hasattr(self.suggested_focus, "title"):
269
+ lines.append(f"Task: {self.suggested_focus.title}")
270
+ else:
271
+ lines.append(str(self.suggested_focus))
272
+
273
+ # Environment state
274
+ if self.codebase_state:
275
+ lines.append("\n## Codebase State")
276
+ if "branch" in self.codebase_state:
277
+ lines.append(f"Branch: {self.codebase_state['branch']}")
278
+ if "uncommitted" in self.codebase_state:
279
+ lines.append(f"Uncommitted Changes: {self.codebase_state['uncommitted']}")
280
+
281
+ # Rules
282
+ if self.rules_of_engagement:
283
+ lines.append("\n## Rules of Engagement")
284
+ for rule in self.rules_of_engagement:
285
+ lines.append(f"- {rule}")
286
+
287
+ return "\n".join(lines)
@@ -910,3 +910,104 @@ class SQLiteStorage(StorageBackend):
910
910
  or datetime.now(timezone.utc),
911
911
  metadata=json.loads(row["metadata"]) if row["metadata"] else {},
912
912
  )
913
+
914
+ # ===== Additional abstract method implementations =====
915
+
916
+ def update_heuristic_confidence(
917
+ self,
918
+ heuristic_id: str,
919
+ new_confidence: float,
920
+ ) -> bool:
921
+ """Update confidence score for a heuristic."""
922
+ with self._get_connection() as conn:
923
+ cursor = conn.execute(
924
+ "UPDATE heuristics SET confidence = ? WHERE id = ?",
925
+ (new_confidence, heuristic_id),
926
+ )
927
+ return cursor.rowcount > 0
928
+
929
+ def update_knowledge_confidence(
930
+ self,
931
+ knowledge_id: str,
932
+ new_confidence: float,
933
+ ) -> bool:
934
+ """Update confidence score for domain knowledge."""
935
+ with self._get_connection() as conn:
936
+ cursor = conn.execute(
937
+ "UPDATE domain_knowledge SET confidence = ? WHERE id = ?",
938
+ (new_confidence, knowledge_id),
939
+ )
940
+ return cursor.rowcount > 0
941
+
942
+ def delete_heuristic(self, heuristic_id: str) -> bool:
943
+ """Delete a heuristic by ID."""
944
+ with self._get_connection() as conn:
945
+ # Also remove from embedding index
946
+ conn.execute(
947
+ "DELETE FROM embeddings WHERE memory_type = 'heuristic' AND memory_id = ?",
948
+ (heuristic_id,),
949
+ )
950
+ cursor = conn.execute(
951
+ "DELETE FROM heuristics WHERE id = ?",
952
+ (heuristic_id,),
953
+ )
954
+ if cursor.rowcount > 0:
955
+ # Rebuild index if we had one
956
+ if "heuristic" in self._indices:
957
+ self._load_faiss_indices()
958
+ return True
959
+ return False
960
+
961
+ def delete_outcome(self, outcome_id: str) -> bool:
962
+ """Delete an outcome by ID."""
963
+ with self._get_connection() as conn:
964
+ # Also remove from embedding index
965
+ conn.execute(
966
+ "DELETE FROM embeddings WHERE memory_type = 'outcome' AND memory_id = ?",
967
+ (outcome_id,),
968
+ )
969
+ cursor = conn.execute(
970
+ "DELETE FROM outcomes WHERE id = ?",
971
+ (outcome_id,),
972
+ )
973
+ if cursor.rowcount > 0:
974
+ if "outcome" in self._indices:
975
+ self._load_faiss_indices()
976
+ return True
977
+ return False
978
+
979
+ def delete_domain_knowledge(self, knowledge_id: str) -> bool:
980
+ """Delete domain knowledge by ID."""
981
+ with self._get_connection() as conn:
982
+ # Also remove from embedding index
983
+ conn.execute(
984
+ "DELETE FROM embeddings WHERE memory_type = 'domain_knowledge' AND memory_id = ?",
985
+ (knowledge_id,),
986
+ )
987
+ cursor = conn.execute(
988
+ "DELETE FROM domain_knowledge WHERE id = ?",
989
+ (knowledge_id,),
990
+ )
991
+ if cursor.rowcount > 0:
992
+ if "domain_knowledge" in self._indices:
993
+ self._load_faiss_indices()
994
+ return True
995
+ return False
996
+
997
+ def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
998
+ """Delete an anti-pattern by ID."""
999
+ with self._get_connection() as conn:
1000
+ # Also remove from embedding index
1001
+ conn.execute(
1002
+ "DELETE FROM embeddings WHERE memory_type = 'anti_pattern' AND memory_id = ?",
1003
+ (anti_pattern_id,),
1004
+ )
1005
+ cursor = conn.execute(
1006
+ "DELETE FROM anti_patterns WHERE id = ?",
1007
+ (anti_pattern_id,),
1008
+ )
1009
+ if cursor.rowcount > 0:
1010
+ if "anti_pattern" in self._indices:
1011
+ self._load_faiss_indices()
1012
+ return True
1013
+ return False