alma-memory 0.3.0__py3-none-any.whl → 0.5.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 +99 -29
- alma/confidence/__init__.py +47 -0
- alma/confidence/engine.py +540 -0
- alma/confidence/types.py +351 -0
- alma/config/loader.py +3 -2
- alma/consolidation/__init__.py +23 -0
- alma/consolidation/engine.py +678 -0
- alma/consolidation/prompts.py +84 -0
- alma/core.py +15 -15
- alma/domains/__init__.py +6 -6
- alma/domains/factory.py +12 -9
- alma/domains/schemas.py +17 -3
- alma/domains/types.py +8 -4
- alma/events/__init__.py +75 -0
- alma/events/emitter.py +284 -0
- alma/events/storage_mixin.py +246 -0
- alma/events/types.py +126 -0
- alma/events/webhook.py +425 -0
- alma/exceptions.py +49 -0
- alma/extraction/__init__.py +31 -0
- alma/extraction/auto_learner.py +264 -0
- alma/extraction/extractor.py +420 -0
- alma/graph/__init__.py +81 -0
- alma/graph/backends/__init__.py +18 -0
- alma/graph/backends/memory.py +236 -0
- alma/graph/backends/neo4j.py +417 -0
- alma/graph/base.py +159 -0
- alma/graph/extraction.py +198 -0
- alma/graph/store.py +860 -0
- alma/harness/__init__.py +4 -4
- alma/harness/base.py +18 -9
- alma/harness/domains.py +27 -11
- alma/initializer/__init__.py +37 -0
- alma/initializer/initializer.py +418 -0
- alma/initializer/types.py +250 -0
- alma/integration/__init__.py +9 -9
- alma/integration/claude_agents.py +10 -10
- alma/integration/helena.py +32 -22
- alma/integration/victor.py +57 -33
- alma/learning/__init__.py +27 -27
- alma/learning/forgetting.py +198 -148
- alma/learning/heuristic_extractor.py +40 -24
- alma/learning/protocols.py +62 -14
- alma/learning/validation.py +7 -2
- alma/mcp/__init__.py +4 -4
- alma/mcp/__main__.py +2 -1
- alma/mcp/resources.py +17 -16
- alma/mcp/server.py +102 -44
- alma/mcp/tools.py +174 -37
- alma/progress/__init__.py +3 -3
- alma/progress/tracker.py +26 -20
- alma/progress/types.py +8 -12
- alma/py.typed +0 -0
- alma/retrieval/__init__.py +11 -11
- alma/retrieval/cache.py +20 -21
- alma/retrieval/embeddings.py +4 -4
- alma/retrieval/engine.py +114 -35
- alma/retrieval/scoring.py +73 -63
- alma/session/__init__.py +2 -2
- alma/session/manager.py +5 -5
- alma/session/types.py +5 -4
- alma/storage/__init__.py +41 -0
- alma/storage/azure_cosmos.py +107 -31
- alma/storage/base.py +157 -4
- alma/storage/chroma.py +1443 -0
- alma/storage/file_based.py +56 -20
- alma/storage/pinecone.py +1080 -0
- alma/storage/postgresql.py +1452 -0
- alma/storage/qdrant.py +1306 -0
- alma/storage/sqlite_local.py +376 -31
- alma/types.py +62 -14
- alma_memory-0.5.0.dist-info/METADATA +905 -0
- alma_memory-0.5.0.dist-info/RECORD +76 -0
- {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
- alma_memory-0.3.0.dist-info/METADATA +0 -438
- alma_memory-0.3.0.dist-info/RECORD +0 -46
- {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
alma/harness/__init__.py
CHANGED
|
@@ -9,17 +9,17 @@ A structured framework for creating learning agents across any domain:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from alma.harness.base import (
|
|
12
|
-
Setting,
|
|
13
|
-
Context,
|
|
14
12
|
Agent,
|
|
15
|
-
|
|
13
|
+
Context,
|
|
16
14
|
Harness,
|
|
15
|
+
MemorySchema,
|
|
16
|
+
Setting,
|
|
17
17
|
)
|
|
18
18
|
from alma.harness.domains import (
|
|
19
19
|
CodingDomain,
|
|
20
|
-
ResearchDomain,
|
|
21
20
|
ContentDomain,
|
|
22
21
|
OperationsDomain,
|
|
22
|
+
ResearchDomain,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
alma/harness/base.py
CHANGED
|
@@ -17,23 +17,23 @@ Flow:
|
|
|
17
17
|
Repeat -> Agent appears to "learn" without weight changes
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
from abc import ABC, abstractmethod
|
|
21
20
|
from dataclasses import dataclass, field
|
|
22
21
|
from datetime import datetime, timezone
|
|
23
|
-
from typing import Optional, List, Dict, Any, Callable
|
|
24
22
|
from enum import Enum
|
|
23
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
25
24
|
|
|
26
|
-
from alma.types import
|
|
25
|
+
from alma.types import MemoryScope, MemorySlice
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class ToolType(Enum):
|
|
30
29
|
"""Categories of tools available to agents."""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
|
|
31
|
+
SEARCH = "search" # Web search, semantic search
|
|
32
|
+
DATA_ACCESS = "data_access" # APIs, databases
|
|
33
|
+
EXECUTION = "execution" # Code execution, automation
|
|
34
34
|
COMMUNICATION = "communication" # Email, messaging
|
|
35
|
-
ANALYSIS = "analysis"
|
|
36
|
-
CREATION = "creation"
|
|
35
|
+
ANALYSIS = "analysis" # Data processing, synthesis
|
|
36
|
+
CREATION = "creation" # Content generation, design
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@dataclass
|
|
@@ -43,6 +43,7 @@ class Tool:
|
|
|
43
43
|
|
|
44
44
|
Tools are the building blocks agents use to accomplish tasks.
|
|
45
45
|
"""
|
|
46
|
+
|
|
46
47
|
name: str
|
|
47
48
|
description: str
|
|
48
49
|
tool_type: ToolType
|
|
@@ -66,6 +67,7 @@ class Setting:
|
|
|
66
67
|
Includes available tools and immutable constraints that don't change
|
|
67
68
|
between runs. The setting defines WHAT the agent CAN do.
|
|
68
69
|
"""
|
|
70
|
+
|
|
69
71
|
name: str
|
|
70
72
|
description: str
|
|
71
73
|
tools: List[Tool] = field(default_factory=list)
|
|
@@ -100,6 +102,7 @@ class Context:
|
|
|
100
102
|
This is injected fresh each time and contains task-specific information.
|
|
101
103
|
The context defines WHAT the agent should do THIS run.
|
|
102
104
|
"""
|
|
105
|
+
|
|
103
106
|
task: str
|
|
104
107
|
user_id: Optional[str] = None
|
|
105
108
|
project_id: Optional[str] = None
|
|
@@ -137,6 +140,7 @@ class MemorySchema:
|
|
|
137
140
|
This defines WHAT gets remembered and HOW, ensuring relevance
|
|
138
141
|
and preventing scope creep. Each domain has its own schema.
|
|
139
142
|
"""
|
|
143
|
+
|
|
140
144
|
domain: str
|
|
141
145
|
description: str
|
|
142
146
|
|
|
@@ -195,6 +199,7 @@ class Agent:
|
|
|
195
199
|
Agents start "dumb" but get smarter via memory injections.
|
|
196
200
|
They use tools to accomplish tasks and log reflections post-run.
|
|
197
201
|
"""
|
|
202
|
+
|
|
198
203
|
name: str
|
|
199
204
|
role: str
|
|
200
205
|
description: str
|
|
@@ -227,6 +232,7 @@ class Agent:
|
|
|
227
232
|
@dataclass
|
|
228
233
|
class RunResult:
|
|
229
234
|
"""Result of a harness run."""
|
|
235
|
+
|
|
230
236
|
success: bool
|
|
231
237
|
output: Any
|
|
232
238
|
reflections: List[str] = field(default_factory=list)
|
|
@@ -319,7 +325,9 @@ class Harness:
|
|
|
319
325
|
agent=self.agent.name,
|
|
320
326
|
task=context.task,
|
|
321
327
|
outcome="success" if result.success else "failure",
|
|
322
|
-
strategy_used=
|
|
328
|
+
strategy_used=(
|
|
329
|
+
", ".join(result.tools_used) if result.tools_used else "direct"
|
|
330
|
+
),
|
|
323
331
|
duration_ms=result.duration_ms,
|
|
324
332
|
error_message=result.error,
|
|
325
333
|
feedback="; ".join(result.reflections) if result.reflections else None,
|
|
@@ -347,6 +355,7 @@ class Harness:
|
|
|
347
355
|
RunResult with output or prompt
|
|
348
356
|
"""
|
|
349
357
|
import time
|
|
358
|
+
|
|
350
359
|
start_time = time.time()
|
|
351
360
|
|
|
352
361
|
# 1. Pre-run: Get relevant memories
|
alma/harness/domains.py
CHANGED
|
@@ -13,23 +13,22 @@ Each domain includes:
|
|
|
13
13
|
- Agent templates
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from
|
|
17
|
-
from typing import List, Dict, Any, Optional
|
|
16
|
+
from typing import Any
|
|
18
17
|
|
|
19
18
|
from alma.harness.base import (
|
|
19
|
+
Agent,
|
|
20
|
+
Harness,
|
|
21
|
+
MemorySchema,
|
|
20
22
|
Setting,
|
|
21
23
|
Tool,
|
|
22
24
|
ToolType,
|
|
23
|
-
Agent,
|
|
24
|
-
MemorySchema,
|
|
25
|
-
Harness,
|
|
26
25
|
)
|
|
27
26
|
|
|
28
|
-
|
|
29
27
|
# =============================================================================
|
|
30
28
|
# CODING DOMAIN
|
|
31
29
|
# =============================================================================
|
|
32
30
|
|
|
31
|
+
|
|
33
32
|
class CodingDomain:
|
|
34
33
|
"""Pre-built configurations for coding/development agents."""
|
|
35
34
|
|
|
@@ -114,13 +113,19 @@ class CodingDomain:
|
|
|
114
113
|
name="playwright",
|
|
115
114
|
description="Browser automation for UI testing",
|
|
116
115
|
tool_type=ToolType.EXECUTION,
|
|
117
|
-
constraints=[
|
|
116
|
+
constraints=[
|
|
117
|
+
"Use explicit waits, not sleep()",
|
|
118
|
+
"Prefer role-based selectors",
|
|
119
|
+
],
|
|
118
120
|
),
|
|
119
121
|
Tool(
|
|
120
122
|
name="api_client",
|
|
121
123
|
description="HTTP client for API testing",
|
|
122
124
|
tool_type=ToolType.DATA_ACCESS,
|
|
123
|
-
constraints=[
|
|
125
|
+
constraints=[
|
|
126
|
+
"Log all requests/responses",
|
|
127
|
+
"Handle timeouts gracefully",
|
|
128
|
+
],
|
|
124
129
|
),
|
|
125
130
|
Tool(
|
|
126
131
|
name="database_query",
|
|
@@ -201,6 +206,7 @@ class CodingDomain:
|
|
|
201
206
|
# RESEARCH DOMAIN
|
|
202
207
|
# =============================================================================
|
|
203
208
|
|
|
209
|
+
|
|
204
210
|
class ResearchDomain:
|
|
205
211
|
"""Pre-built configurations for research and analysis agents."""
|
|
206
212
|
|
|
@@ -268,7 +274,10 @@ class ResearchDomain:
|
|
|
268
274
|
name="synthesis",
|
|
269
275
|
description="Combine multiple sources into insights",
|
|
270
276
|
tool_type=ToolType.ANALYSIS,
|
|
271
|
-
constraints=[
|
|
277
|
+
constraints=[
|
|
278
|
+
"Note conflicting information",
|
|
279
|
+
"Confidence levels required",
|
|
280
|
+
],
|
|
272
281
|
),
|
|
273
282
|
],
|
|
274
283
|
global_constraints=[
|
|
@@ -314,6 +323,7 @@ class ResearchDomain:
|
|
|
314
323
|
# CONTENT DOMAIN
|
|
315
324
|
# =============================================================================
|
|
316
325
|
|
|
326
|
+
|
|
317
327
|
class ContentDomain:
|
|
318
328
|
"""Pre-built configurations for content creation agents."""
|
|
319
329
|
|
|
@@ -490,6 +500,7 @@ class ContentDomain:
|
|
|
490
500
|
# OPERATIONS DOMAIN
|
|
491
501
|
# =============================================================================
|
|
492
502
|
|
|
503
|
+
|
|
493
504
|
class OperationsDomain:
|
|
494
505
|
"""Pre-built configurations for operations and support agents."""
|
|
495
506
|
|
|
@@ -636,6 +647,7 @@ class OperationsDomain:
|
|
|
636
647
|
# FACTORY FUNCTION
|
|
637
648
|
# =============================================================================
|
|
638
649
|
|
|
650
|
+
|
|
639
651
|
def create_harness(
|
|
640
652
|
domain: str,
|
|
641
653
|
agent_type: str,
|
|
@@ -666,7 +678,9 @@ def create_harness(
|
|
|
666
678
|
"victor": CodingDomain.create_victor,
|
|
667
679
|
},
|
|
668
680
|
"research": {
|
|
669
|
-
"researcher": lambda a: ResearchDomain.create_researcher(
|
|
681
|
+
"researcher": lambda a: ResearchDomain.create_researcher(
|
|
682
|
+
a, kwargs.get("focus", "general")
|
|
683
|
+
),
|
|
670
684
|
},
|
|
671
685
|
"content": {
|
|
672
686
|
"copywriter": ContentDomain.create_copywriter,
|
|
@@ -678,7 +692,9 @@ def create_harness(
|
|
|
678
692
|
}
|
|
679
693
|
|
|
680
694
|
if domain not in factories:
|
|
681
|
-
raise ValueError(
|
|
695
|
+
raise ValueError(
|
|
696
|
+
f"Unknown domain: {domain}. Available: {list(factories.keys())}"
|
|
697
|
+
)
|
|
682
698
|
|
|
683
699
|
if agent_type not in factories[domain]:
|
|
684
700
|
raise ValueError(
|
|
@@ -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.initializer import SessionInitializer
|
|
26
|
+
from alma.initializer.types import (
|
|
27
|
+
CodebaseOrientation,
|
|
28
|
+
InitializationResult,
|
|
29
|
+
RulesOfEngagement,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CodebaseOrientation",
|
|
34
|
+
"InitializationResult",
|
|
35
|
+
"RulesOfEngagement",
|
|
36
|
+
"SessionInitializer",
|
|
37
|
+
]
|
|
@@ -0,0 +1,418 @@
|
|
|
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(
|
|
188
|
+
WorkItem.create(
|
|
189
|
+
project_id="", # Will be set by caller
|
|
190
|
+
title=title,
|
|
191
|
+
description=title,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
# Match numbered items: 1., 2., etc.
|
|
197
|
+
number_match = re.match(r"^\d+\.\s+(.+)$", line)
|
|
198
|
+
if number_match:
|
|
199
|
+
title = number_match.group(1).strip()
|
|
200
|
+
work_items.append(
|
|
201
|
+
WorkItem.create(
|
|
202
|
+
project_id="",
|
|
203
|
+
title=title,
|
|
204
|
+
description=title,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# If no structured items found, create single item from prompt
|
|
210
|
+
if not work_items:
|
|
211
|
+
# Truncate long prompts for title
|
|
212
|
+
title = user_prompt[:100].strip()
|
|
213
|
+
if len(user_prompt) > 100:
|
|
214
|
+
title += "..."
|
|
215
|
+
|
|
216
|
+
work_items.append(
|
|
217
|
+
WorkItem.create(
|
|
218
|
+
project_id="",
|
|
219
|
+
title=title,
|
|
220
|
+
description=user_prompt,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return work_items
|
|
225
|
+
|
|
226
|
+
def orient_to_codebase(
|
|
227
|
+
self,
|
|
228
|
+
project_path: str,
|
|
229
|
+
max_commits: int = 5,
|
|
230
|
+
) -> CodebaseOrientation:
|
|
231
|
+
"""
|
|
232
|
+
Orient to current codebase state.
|
|
233
|
+
|
|
234
|
+
Reads git status, recent commits, and file structure.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
project_path: Path to project root
|
|
238
|
+
max_commits: Max number of recent commits to include
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
CodebaseOrientation with codebase state
|
|
242
|
+
"""
|
|
243
|
+
path = Path(project_path)
|
|
244
|
+
|
|
245
|
+
# Default orientation
|
|
246
|
+
orientation = CodebaseOrientation(
|
|
247
|
+
current_branch="unknown",
|
|
248
|
+
has_uncommitted_changes=False,
|
|
249
|
+
recent_commits=[],
|
|
250
|
+
root_path=str(path),
|
|
251
|
+
key_directories=[],
|
|
252
|
+
config_files=[],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Check if it's a git repo
|
|
256
|
+
git_dir = path / ".git"
|
|
257
|
+
is_git_repo = git_dir.exists()
|
|
258
|
+
|
|
259
|
+
if is_git_repo:
|
|
260
|
+
try:
|
|
261
|
+
# Get current branch
|
|
262
|
+
result = subprocess.run(
|
|
263
|
+
["git", "branch", "--show-current"],
|
|
264
|
+
cwd=path,
|
|
265
|
+
capture_output=True,
|
|
266
|
+
text=True,
|
|
267
|
+
timeout=5,
|
|
268
|
+
)
|
|
269
|
+
if result.returncode == 0:
|
|
270
|
+
orientation.current_branch = (
|
|
271
|
+
result.stdout.strip() or "HEAD detached"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Check for uncommitted changes
|
|
275
|
+
result = subprocess.run(
|
|
276
|
+
["git", "status", "--porcelain"],
|
|
277
|
+
cwd=path,
|
|
278
|
+
capture_output=True,
|
|
279
|
+
text=True,
|
|
280
|
+
timeout=5,
|
|
281
|
+
)
|
|
282
|
+
if result.returncode == 0:
|
|
283
|
+
orientation.has_uncommitted_changes = bool(result.stdout.strip())
|
|
284
|
+
|
|
285
|
+
# Get recent commits
|
|
286
|
+
result = subprocess.run(
|
|
287
|
+
["git", "log", "--oneline", f"-{max_commits}"],
|
|
288
|
+
cwd=path,
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
timeout=5,
|
|
292
|
+
)
|
|
293
|
+
if result.returncode == 0:
|
|
294
|
+
commits = result.stdout.strip().split("\n")
|
|
295
|
+
orientation.recent_commits = [c for c in commits if c]
|
|
296
|
+
|
|
297
|
+
except subprocess.TimeoutExpired:
|
|
298
|
+
logger.warning("Git commands timed out")
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.warning(f"Git orientation failed: {e}")
|
|
301
|
+
|
|
302
|
+
# Find key directories
|
|
303
|
+
key_dirs = ["src", "lib", "tests", "test", "app", "api", "core"]
|
|
304
|
+
orientation.key_directories = [d for d in key_dirs if (path / d).is_dir()]
|
|
305
|
+
|
|
306
|
+
# Find config files
|
|
307
|
+
config_files = [
|
|
308
|
+
"package.json",
|
|
309
|
+
"pyproject.toml",
|
|
310
|
+
"setup.py",
|
|
311
|
+
"Cargo.toml",
|
|
312
|
+
"go.mod",
|
|
313
|
+
"pom.xml",
|
|
314
|
+
"build.gradle",
|
|
315
|
+
"Makefile",
|
|
316
|
+
"CMakeLists.txt",
|
|
317
|
+
]
|
|
318
|
+
orientation.config_files = [f for f in config_files if (path / f).exists()]
|
|
319
|
+
|
|
320
|
+
# Generate summary
|
|
321
|
+
orientation.summary = self._generate_orientation_summary(orientation)
|
|
322
|
+
|
|
323
|
+
return orientation
|
|
324
|
+
|
|
325
|
+
def get_rules_of_engagement(
|
|
326
|
+
self,
|
|
327
|
+
agent: str,
|
|
328
|
+
) -> RulesOfEngagement:
|
|
329
|
+
"""
|
|
330
|
+
Get rules of engagement from agent scope.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
agent: Agent name
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
RulesOfEngagement with scope rules, constraints, quality gates
|
|
337
|
+
"""
|
|
338
|
+
rules = RulesOfEngagement()
|
|
339
|
+
|
|
340
|
+
if not self.alma:
|
|
341
|
+
return rules
|
|
342
|
+
|
|
343
|
+
# Get scope from ALMA
|
|
344
|
+
scope = self.alma.scopes.get(agent)
|
|
345
|
+
if not scope:
|
|
346
|
+
return rules
|
|
347
|
+
|
|
348
|
+
# Convert scope to rules
|
|
349
|
+
if scope.can_learn:
|
|
350
|
+
rules.scope_rules = [f"Learn from: {', '.join(scope.can_learn)}"]
|
|
351
|
+
|
|
352
|
+
if scope.cannot_learn:
|
|
353
|
+
rules.constraints = [f"Do not learn from: {', '.join(scope.cannot_learn)}"]
|
|
354
|
+
|
|
355
|
+
# Default quality gates
|
|
356
|
+
rules.quality_gates = [
|
|
357
|
+
"All tests pass",
|
|
358
|
+
"No regressions introduced",
|
|
359
|
+
"Changes documented if significant",
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
return rules
|
|
363
|
+
|
|
364
|
+
def _summarize_goal(self, prompt: str, work_items: List[Any]) -> str:
|
|
365
|
+
"""Summarize goal from prompt and work items."""
|
|
366
|
+
if len(work_items) == 1:
|
|
367
|
+
return prompt
|
|
368
|
+
|
|
369
|
+
item_titles = [getattr(item, "title", str(item)) for item in work_items]
|
|
370
|
+
return f"{prompt}\n\nBroken down into {len(work_items)} items: {', '.join(item_titles[:3])}{'...' if len(item_titles) > 3 else ''}"
|
|
371
|
+
|
|
372
|
+
def _select_starting_point(self, work_items: List[Any]) -> Optional[Any]:
|
|
373
|
+
"""Select the best starting point from work items."""
|
|
374
|
+
if not work_items:
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
# Find highest priority unblocked item
|
|
378
|
+
actionable = [
|
|
379
|
+
item
|
|
380
|
+
for item in work_items
|
|
381
|
+
if getattr(item, "status", "pending") == "pending"
|
|
382
|
+
and not getattr(item, "blocked_by", [])
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
if actionable:
|
|
386
|
+
# Sort by priority (higher = more important)
|
|
387
|
+
actionable.sort(key=lambda x: getattr(x, "priority", 50), reverse=True)
|
|
388
|
+
return actionable[0]
|
|
389
|
+
|
|
390
|
+
return work_items[0]
|
|
391
|
+
|
|
392
|
+
def _generate_orientation_summary(self, orientation: CodebaseOrientation) -> str:
|
|
393
|
+
"""Generate a one-line summary of codebase orientation."""
|
|
394
|
+
parts = []
|
|
395
|
+
|
|
396
|
+
parts.append(f"Branch: {orientation.current_branch}")
|
|
397
|
+
|
|
398
|
+
if orientation.has_uncommitted_changes:
|
|
399
|
+
parts.append("has uncommitted changes")
|
|
400
|
+
|
|
401
|
+
if orientation.key_directories:
|
|
402
|
+
parts.append(f"key dirs: {', '.join(orientation.key_directories[:3])}")
|
|
403
|
+
|
|
404
|
+
if orientation.config_files:
|
|
405
|
+
# Infer project type from config files
|
|
406
|
+
if "package.json" in orientation.config_files:
|
|
407
|
+
parts.append("Node.js project")
|
|
408
|
+
elif (
|
|
409
|
+
"pyproject.toml" in orientation.config_files
|
|
410
|
+
or "setup.py" in orientation.config_files
|
|
411
|
+
):
|
|
412
|
+
parts.append("Python project")
|
|
413
|
+
elif "Cargo.toml" in orientation.config_files:
|
|
414
|
+
parts.append("Rust project")
|
|
415
|
+
elif "go.mod" in orientation.config_files:
|
|
416
|
+
parts.append("Go project")
|
|
417
|
+
|
|
418
|
+
return "; ".join(parts)
|