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.
- alma/__init__.py +76 -1
- alma/confidence/__init__.py +47 -0
- alma/confidence/engine.py +506 -0
- alma/confidence/types.py +331 -0
- alma/domains/__init__.py +30 -0
- alma/domains/factory.py +356 -0
- alma/domains/schemas.py +434 -0
- alma/domains/types.py +268 -0
- alma/initializer/__init__.py +37 -0
- alma/initializer/initializer.py +410 -0
- alma/initializer/types.py +242 -0
- alma/progress/__init__.py +21 -0
- alma/progress/tracker.py +601 -0
- alma/progress/types.py +254 -0
- alma/session/__init__.py +19 -0
- alma/session/manager.py +399 -0
- alma/session/types.py +287 -0
- alma/storage/azure_cosmos.py +6 -0
- alma/storage/sqlite_local.py +101 -0
- alma_memory-0.4.0.dist-info/METADATA +488 -0
- {alma_memory-0.2.0.dist-info → alma_memory-0.4.0.dist-info}/RECORD +23 -7
- alma_memory-0.2.0.dist-info/METADATA +0 -327
- {alma_memory-0.2.0.dist-info → alma_memory-0.4.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.2.0.dist-info → alma_memory-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
]
|