alma-memory 0.2.0__py3-none-any.whl → 0.4.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.
@@ -0,0 +1,37 @@
1
+ """
2
+ ALMA Initializer Module.
3
+
4
+ Bootstrap pattern that orients the agent before work begins.
5
+
6
+ Usage:
7
+ from alma.initializer import SessionInitializer, InitializationResult
8
+
9
+ initializer = SessionInitializer(alma)
10
+ result = initializer.initialize(
11
+ project_id="my-project",
12
+ agent="Helena",
13
+ user_prompt="Test the login flow",
14
+ project_path="/path/to/project",
15
+ )
16
+
17
+ # Inject into agent prompt
18
+ prompt = f'''
19
+ {result.to_prompt()}
20
+
21
+ Now proceed with the first work item.
22
+ '''
23
+ """
24
+
25
+ from alma.initializer.types import (
26
+ CodebaseOrientation,
27
+ InitializationResult,
28
+ RulesOfEngagement,
29
+ )
30
+ from alma.initializer.initializer import SessionInitializer
31
+
32
+ __all__ = [
33
+ "CodebaseOrientation",
34
+ "InitializationResult",
35
+ "RulesOfEngagement",
36
+ "SessionInitializer",
37
+ ]
@@ -0,0 +1,410 @@
1
+ """
2
+ Session Initializer.
3
+
4
+ Bootstrap pattern that orients the agent before work begins.
5
+ "Stage manager sets the stage, actor performs."
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Any, List, Optional
13
+
14
+ from alma.initializer.types import (
15
+ CodebaseOrientation,
16
+ InitializationResult,
17
+ RulesOfEngagement,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class SessionInitializer:
24
+ """
25
+ Bootstrap domain memory from user prompt.
26
+
27
+ The Initializer Pattern:
28
+ 1. Expand user prompt to structured work items
29
+ 2. Orient to current codebase state (git, files)
30
+ 3. Retrieve relevant memories from past sessions
31
+ 4. Set rules of engagement from agent scope
32
+ 5. Suggest optimal starting point
33
+
34
+ Usage:
35
+ initializer = SessionInitializer(alma)
36
+
37
+ result = initializer.initialize(
38
+ project_id="my-project",
39
+ agent="Helena",
40
+ user_prompt="Test the login flow",
41
+ )
42
+
43
+ # Inject into agent prompt
44
+ prompt = f'''
45
+ {result.to_prompt()}
46
+
47
+ Now proceed with the first work item.
48
+ '''
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ alma: Optional[Any] = None,
54
+ progress_tracker: Optional[Any] = None,
55
+ session_manager: Optional[Any] = None,
56
+ ):
57
+ """
58
+ Initialize the SessionInitializer.
59
+
60
+ Args:
61
+ alma: ALMA instance for memory retrieval
62
+ progress_tracker: ProgressTracker for work item management
63
+ session_manager: SessionManager for session context
64
+ """
65
+ self.alma = alma
66
+ self.progress_tracker = progress_tracker
67
+ self.session_manager = session_manager
68
+
69
+ def initialize(
70
+ self,
71
+ project_id: str,
72
+ agent: str,
73
+ user_prompt: str,
74
+ project_path: Optional[str] = None,
75
+ auto_expand: bool = True,
76
+ memory_top_k: int = 5,
77
+ ) -> InitializationResult:
78
+ """
79
+ Full session initialization.
80
+
81
+ Args:
82
+ project_id: Project identifier
83
+ agent: Agent name (e.g., "Helena", "Victor")
84
+ user_prompt: Raw user prompt/task
85
+ project_path: Optional path to project root (for git orientation)
86
+ auto_expand: Whether to expand prompt to work items
87
+ memory_top_k: How many memories to retrieve
88
+
89
+ Returns:
90
+ InitializationResult with everything agent needs
91
+ """
92
+ logger.info(f"Initializing session for {agent} on {project_id}")
93
+
94
+ # Create result
95
+ result = InitializationResult.create(
96
+ project_id=project_id,
97
+ agent=agent,
98
+ original_prompt=user_prompt,
99
+ )
100
+
101
+ # 1. Expand prompt to work items
102
+ if auto_expand:
103
+ work_items = self.expand_prompt(user_prompt)
104
+ result.work_items = work_items
105
+ if work_items:
106
+ result.goal = self._summarize_goal(user_prompt, work_items)
107
+
108
+ # 2. Orient to codebase
109
+ if project_path:
110
+ result.orientation = self.orient_to_codebase(project_path)
111
+
112
+ # 3. Retrieve relevant memories
113
+ if self.alma:
114
+ try:
115
+ memories = self.alma.retrieve(
116
+ task=user_prompt,
117
+ agent=agent,
118
+ top_k=memory_top_k,
119
+ )
120
+ result.relevant_memories = memories
121
+ except Exception as e:
122
+ logger.warning(f"Failed to retrieve memories: {e}")
123
+
124
+ # 4. Get rules of engagement
125
+ if self.alma:
126
+ result.rules = self.get_rules_of_engagement(agent)
127
+
128
+ # 5. Suggest starting point
129
+ if result.work_items:
130
+ result.recommended_start = self._select_starting_point(result.work_items)
131
+
132
+ # 6. Get recent activity from session manager
133
+ if self.session_manager:
134
+ try:
135
+ context = self.session_manager.start_session(
136
+ agent=agent,
137
+ goal=result.goal,
138
+ )
139
+ result.session_id = context.session_id
140
+ if context.previous_handoff:
141
+ result.recent_activity = context.previous_handoff.next_steps or []
142
+ except Exception as e:
143
+ logger.warning(f"Failed to get session context: {e}")
144
+
145
+ logger.info(
146
+ f"Initialization complete: {len(result.work_items)} work items, "
147
+ f"orientation: {'yes' if result.orientation else 'no'}, "
148
+ f"memories: {'yes' if result.relevant_memories else 'no'}"
149
+ )
150
+
151
+ return result
152
+
153
+ def expand_prompt(
154
+ self,
155
+ user_prompt: str,
156
+ use_ai: bool = False,
157
+ ) -> List[Any]:
158
+ """
159
+ Expand user prompt into structured work items.
160
+
161
+ Simple implementation: extract bullet points and numbered items.
162
+ AI implementation: use LLM to break down complex tasks.
163
+
164
+ Args:
165
+ user_prompt: Raw user prompt
166
+ use_ai: Whether to use AI for expansion (requires LLM)
167
+
168
+ Returns:
169
+ List of WorkItem objects
170
+ """
171
+ from alma.progress import WorkItem
172
+
173
+ work_items = []
174
+
175
+ # Simple extraction: look for bullet points and numbered items
176
+ lines = user_prompt.strip().split('\n')
177
+
178
+ for line in lines:
179
+ line = line.strip()
180
+ if not line:
181
+ continue
182
+
183
+ # Match bullet points: -, *, •
184
+ bullet_match = re.match(r'^[-*•]\s+(.+)$', line)
185
+ if bullet_match:
186
+ title = bullet_match.group(1).strip()
187
+ work_items.append(WorkItem.create(
188
+ project_id="", # Will be set by caller
189
+ title=title,
190
+ description=title,
191
+ ))
192
+ continue
193
+
194
+ # Match numbered items: 1., 2., etc.
195
+ number_match = re.match(r'^\d+\.\s+(.+)$', line)
196
+ if number_match:
197
+ title = number_match.group(1).strip()
198
+ work_items.append(WorkItem.create(
199
+ project_id="",
200
+ title=title,
201
+ description=title,
202
+ ))
203
+ continue
204
+
205
+ # If no structured items found, create single item from prompt
206
+ if not work_items:
207
+ # Truncate long prompts for title
208
+ title = user_prompt[:100].strip()
209
+ if len(user_prompt) > 100:
210
+ title += "..."
211
+
212
+ work_items.append(WorkItem.create(
213
+ project_id="",
214
+ title=title,
215
+ description=user_prompt,
216
+ ))
217
+
218
+ return work_items
219
+
220
+ def orient_to_codebase(
221
+ self,
222
+ project_path: str,
223
+ max_commits: int = 5,
224
+ ) -> CodebaseOrientation:
225
+ """
226
+ Orient to current codebase state.
227
+
228
+ Reads git status, recent commits, and file structure.
229
+
230
+ Args:
231
+ project_path: Path to project root
232
+ max_commits: Max number of recent commits to include
233
+
234
+ Returns:
235
+ CodebaseOrientation with codebase state
236
+ """
237
+ path = Path(project_path)
238
+
239
+ # Default orientation
240
+ orientation = CodebaseOrientation(
241
+ current_branch="unknown",
242
+ has_uncommitted_changes=False,
243
+ recent_commits=[],
244
+ root_path=str(path),
245
+ key_directories=[],
246
+ config_files=[],
247
+ )
248
+
249
+ # Check if it's a git repo
250
+ git_dir = path / ".git"
251
+ is_git_repo = git_dir.exists()
252
+
253
+ if is_git_repo:
254
+ try:
255
+ # Get current branch
256
+ result = subprocess.run(
257
+ ["git", "branch", "--show-current"],
258
+ cwd=path,
259
+ capture_output=True,
260
+ text=True,
261
+ timeout=5,
262
+ )
263
+ if result.returncode == 0:
264
+ orientation.current_branch = result.stdout.strip() or "HEAD detached"
265
+
266
+ # Check for uncommitted changes
267
+ result = subprocess.run(
268
+ ["git", "status", "--porcelain"],
269
+ cwd=path,
270
+ capture_output=True,
271
+ text=True,
272
+ timeout=5,
273
+ )
274
+ if result.returncode == 0:
275
+ orientation.has_uncommitted_changes = bool(result.stdout.strip())
276
+
277
+ # Get recent commits
278
+ result = subprocess.run(
279
+ ["git", "log", "--oneline", f"-{max_commits}"],
280
+ cwd=path,
281
+ capture_output=True,
282
+ text=True,
283
+ timeout=5,
284
+ )
285
+ if result.returncode == 0:
286
+ commits = result.stdout.strip().split('\n')
287
+ orientation.recent_commits = [c for c in commits if c]
288
+
289
+ except subprocess.TimeoutExpired:
290
+ logger.warning("Git commands timed out")
291
+ except Exception as e:
292
+ logger.warning(f"Git orientation failed: {e}")
293
+
294
+ # Find key directories
295
+ key_dirs = ["src", "lib", "tests", "test", "app", "api", "core"]
296
+ orientation.key_directories = [
297
+ d for d in key_dirs if (path / d).is_dir()
298
+ ]
299
+
300
+ # Find config files
301
+ config_files = [
302
+ "package.json", "pyproject.toml", "setup.py", "Cargo.toml",
303
+ "go.mod", "pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
304
+ ]
305
+ orientation.config_files = [
306
+ f for f in config_files if (path / f).exists()
307
+ ]
308
+
309
+ # Generate summary
310
+ orientation.summary = self._generate_orientation_summary(orientation)
311
+
312
+ return orientation
313
+
314
+ def get_rules_of_engagement(
315
+ self,
316
+ agent: str,
317
+ ) -> RulesOfEngagement:
318
+ """
319
+ Get rules of engagement from agent scope.
320
+
321
+ Args:
322
+ agent: Agent name
323
+
324
+ Returns:
325
+ RulesOfEngagement with scope rules, constraints, quality gates
326
+ """
327
+ rules = RulesOfEngagement()
328
+
329
+ if not self.alma:
330
+ return rules
331
+
332
+ # Get scope from ALMA
333
+ scope = self.alma.scopes.get(agent)
334
+ if not scope:
335
+ return rules
336
+
337
+ # Convert scope to rules
338
+ if scope.can_learn:
339
+ rules.scope_rules = [
340
+ f"Learn from: {', '.join(scope.can_learn)}"
341
+ ]
342
+
343
+ if scope.cannot_learn:
344
+ rules.constraints = [
345
+ f"Do not learn from: {', '.join(scope.cannot_learn)}"
346
+ ]
347
+
348
+ # Default quality gates
349
+ rules.quality_gates = [
350
+ "All tests pass",
351
+ "No regressions introduced",
352
+ "Changes documented if significant",
353
+ ]
354
+
355
+ return rules
356
+
357
+ def _summarize_goal(self, prompt: str, work_items: List[Any]) -> str:
358
+ """Summarize goal from prompt and work items."""
359
+ if len(work_items) == 1:
360
+ return prompt
361
+
362
+ item_titles = [getattr(item, 'title', str(item)) for item in work_items]
363
+ return f"{prompt}\n\nBroken down into {len(work_items)} items: {', '.join(item_titles[:3])}{'...' if len(item_titles) > 3 else ''}"
364
+
365
+ def _select_starting_point(self, work_items: List[Any]) -> Optional[Any]:
366
+ """Select the best starting point from work items."""
367
+ if not work_items:
368
+ return None
369
+
370
+ # Find highest priority unblocked item
371
+ actionable = [
372
+ item for item in work_items
373
+ if getattr(item, 'status', 'pending') == 'pending'
374
+ and not getattr(item, 'blocked_by', [])
375
+ ]
376
+
377
+ if actionable:
378
+ # Sort by priority (higher = more important)
379
+ actionable.sort(
380
+ key=lambda x: getattr(x, 'priority', 50),
381
+ reverse=True
382
+ )
383
+ return actionable[0]
384
+
385
+ return work_items[0]
386
+
387
+ def _generate_orientation_summary(self, orientation: CodebaseOrientation) -> str:
388
+ """Generate a one-line summary of codebase orientation."""
389
+ parts = []
390
+
391
+ parts.append(f"Branch: {orientation.current_branch}")
392
+
393
+ if orientation.has_uncommitted_changes:
394
+ parts.append("has uncommitted changes")
395
+
396
+ if orientation.key_directories:
397
+ parts.append(f"key dirs: {', '.join(orientation.key_directories[:3])}")
398
+
399
+ if orientation.config_files:
400
+ # Infer project type from config files
401
+ if "package.json" in orientation.config_files:
402
+ parts.append("Node.js project")
403
+ elif "pyproject.toml" in orientation.config_files or "setup.py" in orientation.config_files:
404
+ parts.append("Python project")
405
+ elif "Cargo.toml" in orientation.config_files:
406
+ parts.append("Rust project")
407
+ elif "go.mod" in orientation.config_files:
408
+ parts.append("Go project")
409
+
410
+ return "; ".join(parts)
@@ -0,0 +1,242 @@
1
+ """
2
+ Initializer Types.
3
+
4
+ Data structures for the Session Initializer pattern.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Dict, List, Optional
10
+ import uuid
11
+
12
+
13
+ @dataclass
14
+ class CodebaseOrientation:
15
+ """Codebase orientation information."""
16
+
17
+ # Git state
18
+ current_branch: str
19
+ has_uncommitted_changes: bool
20
+ recent_commits: List[str] # Last N commit messages
21
+
22
+ # File structure
23
+ root_path: str
24
+ key_directories: List[str] # src/, tests/, etc.
25
+ config_files: List[str] # package.json, pyproject.toml, etc.
26
+
27
+ # Summary
28
+ summary: Optional[str] = None
29
+
30
+ def to_prompt(self) -> str:
31
+ """Format orientation for prompt injection."""
32
+ lines = [
33
+ f"Branch: {self.current_branch}",
34
+ f"Uncommitted changes: {'Yes' if self.has_uncommitted_changes else 'No'}",
35
+ ]
36
+
37
+ if self.recent_commits:
38
+ lines.append("Recent commits:")
39
+ for commit in self.recent_commits[:5]:
40
+ lines.append(f" - {commit}")
41
+
42
+ if self.key_directories:
43
+ lines.append(f"Key directories: {', '.join(self.key_directories)}")
44
+
45
+ if self.summary:
46
+ lines.append(f"Summary: {self.summary}")
47
+
48
+ return "\n".join(lines)
49
+
50
+
51
+ @dataclass
52
+ class RulesOfEngagement:
53
+ """Rules governing agent behavior during session."""
54
+
55
+ # What agent CAN do
56
+ scope_rules: List[str] = field(default_factory=list)
57
+
58
+ # What agent CANNOT do
59
+ constraints: List[str] = field(default_factory=list)
60
+
61
+ # Must pass before marking "done"
62
+ quality_gates: List[str] = field(default_factory=list)
63
+
64
+ def to_prompt(self) -> str:
65
+ """Format rules for prompt injection."""
66
+ lines = []
67
+
68
+ if self.scope_rules:
69
+ lines.append("You CAN:")
70
+ for rule in self.scope_rules:
71
+ lines.append(f" - {rule}")
72
+
73
+ if self.constraints:
74
+ lines.append("You CANNOT:")
75
+ for constraint in self.constraints:
76
+ lines.append(f" - {constraint}")
77
+
78
+ if self.quality_gates:
79
+ lines.append("Before marking DONE, verify:")
80
+ for gate in self.quality_gates:
81
+ lines.append(f" - {gate}")
82
+
83
+ return "\n".join(lines)
84
+
85
+
86
+ @dataclass
87
+ class InitializationResult:
88
+ """
89
+ Result of session initialization.
90
+
91
+ Contains everything an agent needs to start work:
92
+ - Expanded goal and work items
93
+ - Codebase orientation
94
+ - Relevant memories
95
+ - Rules of engagement
96
+ - Recommended starting point
97
+ """
98
+
99
+ id: str
100
+ session_id: str
101
+ project_id: str
102
+ agent: str
103
+
104
+ # Original and expanded goal
105
+ original_prompt: str
106
+ goal: str
107
+
108
+ # Work items extracted from goal
109
+ work_items: List[Any] = field(default_factory=list) # WorkItem objects
110
+
111
+ # Codebase orientation
112
+ orientation: Optional[CodebaseOrientation] = None
113
+
114
+ # Recent activity
115
+ recent_activity: List[str] = field(default_factory=list)
116
+
117
+ # Relevant memories (MemorySlice)
118
+ relevant_memories: Optional[Any] = None
119
+
120
+ # Rules of engagement
121
+ rules: RulesOfEngagement = field(default_factory=RulesOfEngagement)
122
+
123
+ # Suggested first action
124
+ recommended_start: Optional[Any] = None # WorkItem
125
+
126
+ # Metadata
127
+ initialized_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
128
+ metadata: Dict[str, Any] = field(default_factory=dict)
129
+
130
+ @classmethod
131
+ def create(
132
+ cls,
133
+ project_id: str,
134
+ agent: str,
135
+ original_prompt: str,
136
+ goal: Optional[str] = None,
137
+ session_id: Optional[str] = None,
138
+ ) -> "InitializationResult":
139
+ """Create a new initialization result."""
140
+ return cls(
141
+ id=str(uuid.uuid4()),
142
+ session_id=session_id or str(uuid.uuid4()),
143
+ project_id=project_id,
144
+ agent=agent,
145
+ original_prompt=original_prompt,
146
+ goal=goal or original_prompt,
147
+ )
148
+
149
+ def to_prompt(self) -> str:
150
+ """
151
+ Format initialization result for prompt injection.
152
+
153
+ This is the "briefing" that prepares the agent.
154
+ """
155
+ sections = []
156
+
157
+ # Header
158
+ sections.append(f"## Session Initialization for {self.agent}")
159
+ sections.append(f"Project: {self.project_id}")
160
+ sections.append(f"Session: {self.session_id}")
161
+ sections.append("")
162
+
163
+ # Goal
164
+ sections.append("### Goal")
165
+ sections.append(self.goal)
166
+ sections.append("")
167
+
168
+ # Work items
169
+ if self.work_items:
170
+ sections.append("### Work Items")
171
+ for i, item in enumerate(self.work_items, 1):
172
+ title = getattr(item, 'title', str(item))
173
+ status = getattr(item, 'status', 'pending')
174
+ sections.append(f"{i}. [{status}] {title}")
175
+ sections.append("")
176
+
177
+ # Orientation
178
+ if self.orientation:
179
+ sections.append("### Codebase Orientation")
180
+ sections.append(self.orientation.to_prompt())
181
+ sections.append("")
182
+
183
+ # Relevant memories
184
+ if self.relevant_memories:
185
+ sections.append("### Relevant Knowledge from Past Runs")
186
+ if hasattr(self.relevant_memories, 'to_prompt'):
187
+ sections.append(self.relevant_memories.to_prompt())
188
+ else:
189
+ sections.append(str(self.relevant_memories))
190
+ sections.append("")
191
+
192
+ # Rules of engagement
193
+ if self.rules.scope_rules or self.rules.constraints or self.rules.quality_gates:
194
+ sections.append("### Rules of Engagement")
195
+ sections.append(self.rules.to_prompt())
196
+ sections.append("")
197
+
198
+ # Recommended start
199
+ if self.recommended_start:
200
+ sections.append("### Recommended First Action")
201
+ title = getattr(self.recommended_start, 'title', str(self.recommended_start))
202
+ sections.append(f"Start with: {title}")
203
+ sections.append("")
204
+
205
+ return "\n".join(sections)
206
+
207
+ def to_dict(self) -> Dict[str, Any]:
208
+ """Serialize to dictionary."""
209
+ return {
210
+ "id": self.id,
211
+ "session_id": self.session_id,
212
+ "project_id": self.project_id,
213
+ "agent": self.agent,
214
+ "original_prompt": self.original_prompt,
215
+ "goal": self.goal,
216
+ "work_items": [
217
+ item.to_dict() if hasattr(item, 'to_dict') else str(item)
218
+ for item in self.work_items
219
+ ],
220
+ "orientation": {
221
+ "current_branch": self.orientation.current_branch,
222
+ "has_uncommitted_changes": self.orientation.has_uncommitted_changes,
223
+ "recent_commits": self.orientation.recent_commits,
224
+ "root_path": self.orientation.root_path,
225
+ "key_directories": self.orientation.key_directories,
226
+ "config_files": self.orientation.config_files,
227
+ "summary": self.orientation.summary,
228
+ } if self.orientation else None,
229
+ "recent_activity": self.recent_activity,
230
+ "rules": {
231
+ "scope_rules": self.rules.scope_rules,
232
+ "constraints": self.rules.constraints,
233
+ "quality_gates": self.rules.quality_gates,
234
+ },
235
+ "recommended_start": (
236
+ self.recommended_start.to_dict()
237
+ if self.recommended_start and hasattr(self.recommended_start, 'to_dict')
238
+ else str(self.recommended_start) if self.recommended_start else None
239
+ ),
240
+ "initialized_at": self.initialized_at.isoformat(),
241
+ "metadata": self.metadata,
242
+ }
@@ -0,0 +1,21 @@
1
+ """
2
+ ALMA Progress Tracking Module.
3
+
4
+ Track work items, progress, and suggest next actions.
5
+ """
6
+
7
+ from alma.progress.types import (
8
+ WorkItem,
9
+ WorkItemStatus,
10
+ ProgressLog,
11
+ ProgressSummary,
12
+ )
13
+ from alma.progress.tracker import ProgressTracker
14
+
15
+ __all__ = [
16
+ "WorkItem",
17
+ "WorkItemStatus",
18
+ "ProgressLog",
19
+ "ProgressSummary",
20
+ "ProgressTracker",
21
+ ]