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
alma/progress/types.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress Tracking Types.
|
|
3
|
+
|
|
4
|
+
Data models for tracking work items and progress.
|
|
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
|
+
WorkItemStatus = Literal[
|
|
14
|
+
"pending", # Not started
|
|
15
|
+
"in_progress", # Currently being worked on
|
|
16
|
+
"blocked", # Waiting on something
|
|
17
|
+
"review", # Completed, awaiting review
|
|
18
|
+
"done", # Completed and verified
|
|
19
|
+
"failed", # Could not complete
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class WorkItem:
|
|
25
|
+
"""
|
|
26
|
+
A trackable unit of work.
|
|
27
|
+
|
|
28
|
+
Can represent features, bugs, tasks, research questions,
|
|
29
|
+
or any domain-specific work unit.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
project_id: str
|
|
34
|
+
agent: Optional[str]
|
|
35
|
+
|
|
36
|
+
# Work item details
|
|
37
|
+
title: str
|
|
38
|
+
description: str
|
|
39
|
+
item_type: str # "feature", "bug", "task", "research_question", etc.
|
|
40
|
+
status: WorkItemStatus = "pending"
|
|
41
|
+
priority: int = 50 # 0-100, higher = more important
|
|
42
|
+
|
|
43
|
+
# Progress tracking
|
|
44
|
+
started_at: Optional[datetime] = None
|
|
45
|
+
completed_at: Optional[datetime] = None
|
|
46
|
+
time_spent_ms: int = 0
|
|
47
|
+
attempt_count: int = 0
|
|
48
|
+
|
|
49
|
+
# Relationships
|
|
50
|
+
parent_id: Optional[str] = None
|
|
51
|
+
blocks: List[str] = field(default_factory=list)
|
|
52
|
+
blocked_by: List[str] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
# Validation
|
|
55
|
+
tests: List[str] = field(default_factory=list)
|
|
56
|
+
tests_passing: bool = False
|
|
57
|
+
acceptance_criteria: List[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
# Timestamps
|
|
60
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
61
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
62
|
+
|
|
63
|
+
# Extensible metadata
|
|
64
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def create(
|
|
68
|
+
cls,
|
|
69
|
+
project_id: str,
|
|
70
|
+
title: str,
|
|
71
|
+
description: str,
|
|
72
|
+
item_type: str = "task",
|
|
73
|
+
agent: Optional[str] = None,
|
|
74
|
+
priority: int = 50,
|
|
75
|
+
parent_id: Optional[str] = None,
|
|
76
|
+
**kwargs,
|
|
77
|
+
) -> "WorkItem":
|
|
78
|
+
"""Factory method to create a new work item."""
|
|
79
|
+
return cls(
|
|
80
|
+
id=str(uuid.uuid4()),
|
|
81
|
+
project_id=project_id,
|
|
82
|
+
agent=agent,
|
|
83
|
+
title=title,
|
|
84
|
+
description=description,
|
|
85
|
+
item_type=item_type,
|
|
86
|
+
priority=priority,
|
|
87
|
+
parent_id=parent_id,
|
|
88
|
+
**kwargs,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def start(self) -> None:
|
|
92
|
+
"""Mark work item as started."""
|
|
93
|
+
self.status = "in_progress"
|
|
94
|
+
self.started_at = datetime.now(timezone.utc)
|
|
95
|
+
self.attempt_count += 1
|
|
96
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
97
|
+
|
|
98
|
+
def complete(self, tests_passing: bool = True) -> None:
|
|
99
|
+
"""Mark work item as completed."""
|
|
100
|
+
self.status = "done"
|
|
101
|
+
self.completed_at = datetime.now(timezone.utc)
|
|
102
|
+
self.tests_passing = tests_passing
|
|
103
|
+
if self.started_at:
|
|
104
|
+
self.time_spent_ms += int(
|
|
105
|
+
(self.completed_at - self.started_at).total_seconds() * 1000
|
|
106
|
+
)
|
|
107
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
108
|
+
|
|
109
|
+
def block(self, blocked_by: Optional[str] = None, reason: str = "") -> None:
|
|
110
|
+
"""Mark work item as blocked."""
|
|
111
|
+
self.status = "blocked"
|
|
112
|
+
if blocked_by:
|
|
113
|
+
self.blocked_by.append(blocked_by)
|
|
114
|
+
if reason:
|
|
115
|
+
self.metadata["block_reason"] = reason
|
|
116
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
117
|
+
|
|
118
|
+
def fail(self, reason: str = "") -> None:
|
|
119
|
+
"""Mark work item as failed."""
|
|
120
|
+
self.status = "failed"
|
|
121
|
+
if reason:
|
|
122
|
+
self.metadata["failure_reason"] = reason
|
|
123
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
124
|
+
|
|
125
|
+
def is_actionable(self) -> bool:
|
|
126
|
+
"""Check if work item can be worked on."""
|
|
127
|
+
return (
|
|
128
|
+
self.status in ("pending", "in_progress")
|
|
129
|
+
and len(self.blocked_by) == 0
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class ProgressLog:
|
|
135
|
+
"""
|
|
136
|
+
Session-level progress snapshot.
|
|
137
|
+
|
|
138
|
+
Records the state of progress at a point in time.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
id: str
|
|
142
|
+
project_id: str
|
|
143
|
+
agent: str
|
|
144
|
+
session_id: str
|
|
145
|
+
|
|
146
|
+
# Progress counts
|
|
147
|
+
items_total: int
|
|
148
|
+
items_done: int
|
|
149
|
+
items_in_progress: int
|
|
150
|
+
items_blocked: int
|
|
151
|
+
items_pending: int
|
|
152
|
+
|
|
153
|
+
# Current focus
|
|
154
|
+
current_item_id: Optional[str]
|
|
155
|
+
current_action: str
|
|
156
|
+
|
|
157
|
+
# Session metrics
|
|
158
|
+
session_start: datetime
|
|
159
|
+
actions_taken: int = 0
|
|
160
|
+
outcomes_recorded: int = 0
|
|
161
|
+
|
|
162
|
+
# Timestamp
|
|
163
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def create(
|
|
167
|
+
cls,
|
|
168
|
+
project_id: str,
|
|
169
|
+
agent: str,
|
|
170
|
+
session_id: str,
|
|
171
|
+
items_total: int,
|
|
172
|
+
items_done: int,
|
|
173
|
+
items_in_progress: int,
|
|
174
|
+
items_blocked: int,
|
|
175
|
+
items_pending: int,
|
|
176
|
+
current_item_id: Optional[str] = None,
|
|
177
|
+
current_action: str = "",
|
|
178
|
+
session_start: Optional[datetime] = None,
|
|
179
|
+
) -> "ProgressLog":
|
|
180
|
+
"""Factory method to create progress log."""
|
|
181
|
+
return cls(
|
|
182
|
+
id=str(uuid.uuid4()),
|
|
183
|
+
project_id=project_id,
|
|
184
|
+
agent=agent,
|
|
185
|
+
session_id=session_id,
|
|
186
|
+
items_total=items_total,
|
|
187
|
+
items_done=items_done,
|
|
188
|
+
items_in_progress=items_in_progress,
|
|
189
|
+
items_blocked=items_blocked,
|
|
190
|
+
items_pending=items_pending,
|
|
191
|
+
current_item_id=current_item_id,
|
|
192
|
+
current_action=current_action,
|
|
193
|
+
session_start=session_start or datetime.now(timezone.utc),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class ProgressSummary:
|
|
199
|
+
"""
|
|
200
|
+
Summary of progress for display/reporting.
|
|
201
|
+
|
|
202
|
+
A simplified view of progress state.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
project_id: str
|
|
206
|
+
agent: Optional[str]
|
|
207
|
+
|
|
208
|
+
# Counts
|
|
209
|
+
total: int
|
|
210
|
+
done: int
|
|
211
|
+
in_progress: int
|
|
212
|
+
blocked: int
|
|
213
|
+
pending: int
|
|
214
|
+
failed: int
|
|
215
|
+
|
|
216
|
+
# Percentages
|
|
217
|
+
completion_rate: float # 0-1
|
|
218
|
+
success_rate: float # done / (done + failed)
|
|
219
|
+
|
|
220
|
+
# Current focus
|
|
221
|
+
current_item: Optional[WorkItem]
|
|
222
|
+
next_suggested: Optional[WorkItem]
|
|
223
|
+
|
|
224
|
+
# Blockers
|
|
225
|
+
blockers: List[WorkItem]
|
|
226
|
+
|
|
227
|
+
# Time tracking
|
|
228
|
+
total_time_ms: int
|
|
229
|
+
avg_time_per_item_ms: float
|
|
230
|
+
|
|
231
|
+
# Timestamps
|
|
232
|
+
last_activity: Optional[datetime]
|
|
233
|
+
generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def completion_percentage(self) -> int:
|
|
237
|
+
"""Get completion as percentage."""
|
|
238
|
+
return int(self.completion_rate * 100)
|
|
239
|
+
|
|
240
|
+
def format_summary(self) -> str:
|
|
241
|
+
"""Format as human-readable string."""
|
|
242
|
+
lines = [
|
|
243
|
+
f"Progress: {self.done}/{self.total} ({self.completion_percentage}%)",
|
|
244
|
+
f" In Progress: {self.in_progress}",
|
|
245
|
+
f" Blocked: {self.blocked}",
|
|
246
|
+
f" Pending: {self.pending}",
|
|
247
|
+
]
|
|
248
|
+
if self.current_item:
|
|
249
|
+
lines.append(f" Current: {self.current_item.title}")
|
|
250
|
+
if self.next_suggested:
|
|
251
|
+
lines.append(f" Next: {self.next_suggested.title}")
|
|
252
|
+
if self.blockers:
|
|
253
|
+
lines.append(f" Blockers: {len(self.blockers)} items")
|
|
254
|
+
return "\n".join(lines)
|
alma/session/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Session Management Module.
|
|
3
|
+
|
|
4
|
+
Handles session continuity, handoffs, and quick context reload.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from alma.session.types import (
|
|
8
|
+
SessionHandoff,
|
|
9
|
+
SessionContext,
|
|
10
|
+
SessionOutcome,
|
|
11
|
+
)
|
|
12
|
+
from alma.session.manager import SessionManager
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SessionHandoff",
|
|
16
|
+
"SessionContext",
|
|
17
|
+
"SessionOutcome",
|
|
18
|
+
"SessionManager",
|
|
19
|
+
]
|
alma/session/manager.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Manager.
|
|
3
|
+
|
|
4
|
+
Manages session continuity, handoffs, and quick context reload.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
9
|
+
import uuid
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from alma.session.types import (
|
|
13
|
+
SessionHandoff,
|
|
14
|
+
SessionContext,
|
|
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
|