claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/engineer.json +33 -11
- claude_mpm/cli/commands/agents.py +556 -1009
- claude_mpm/cli/commands/memory.py +248 -927
- claude_mpm/cli/commands/run.py +139 -484
- claude_mpm/cli/startup_logging.py +76 -0
- claude_mpm/core/agent_registry.py +6 -10
- claude_mpm/core/framework_loader.py +114 -595
- claude_mpm/core/logging_config.py +2 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
- claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
- claude_mpm/services/agents/memory/memory_file_service.py +103 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +407 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +589 -0
- claude_mpm/services/cli/dashboard_launcher.py +424 -0
- claude_mpm/services/cli/memory_crud_service.py +617 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/session_manager.py +513 -0
- claude_mpm/services/cli/socketio_manager.py +498 -0
- claude_mpm/services/cli/startup_checker.py +370 -0
- claude_mpm/services/core/cache_manager.py +311 -0
- claude_mpm/services/core/memory_manager.py +637 -0
- claude_mpm/services/core/path_resolver.py +498 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
- claude_mpm/cli/commands/run_config_checker.py +0 -159
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""State management service for Claude hook handler.
|
|
2
|
+
|
|
3
|
+
This service manages:
|
|
4
|
+
- Agent delegation tracking
|
|
5
|
+
- Git branch caching
|
|
6
|
+
- Session state management
|
|
7
|
+
- Cleanup of old entries
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from collections import deque
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
# Import constants for configuration
|
|
18
|
+
try:
|
|
19
|
+
from claude_mpm.core.constants import TimeoutConfig
|
|
20
|
+
except ImportError:
|
|
21
|
+
# Fallback values if constants module not available
|
|
22
|
+
class TimeoutConfig:
|
|
23
|
+
QUICK_TIMEOUT = 2.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Debug mode is enabled by default for better visibility into hook processing
|
|
27
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StateManagerService:
|
|
31
|
+
"""Manages state for the Claude hook handler."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Initialize state management service."""
|
|
35
|
+
# Maximum sizes for tracking
|
|
36
|
+
self.MAX_DELEGATION_TRACKING = 200
|
|
37
|
+
self.MAX_PROMPT_TRACKING = 100
|
|
38
|
+
self.MAX_CACHE_AGE_SECONDS = 300
|
|
39
|
+
self.CLEANUP_INTERVAL_EVENTS = 100
|
|
40
|
+
|
|
41
|
+
# Agent delegation tracking
|
|
42
|
+
# Store recent Task delegations: session_id -> agent_type
|
|
43
|
+
self.active_delegations = {}
|
|
44
|
+
# Use deque to limit memory usage (keep last 100 delegations)
|
|
45
|
+
self.delegation_history = deque(maxlen=100)
|
|
46
|
+
# Store delegation request data for response correlation: session_id -> request_data
|
|
47
|
+
self.delegation_requests = {}
|
|
48
|
+
|
|
49
|
+
# Git branch cache (to avoid repeated subprocess calls)
|
|
50
|
+
self._git_branch_cache = {}
|
|
51
|
+
self._git_branch_cache_time = {}
|
|
52
|
+
|
|
53
|
+
# Store current user prompts for comprehensive response tracking
|
|
54
|
+
self.pending_prompts = {} # session_id -> prompt data
|
|
55
|
+
|
|
56
|
+
# Track events for periodic cleanup
|
|
57
|
+
self.events_processed = 0
|
|
58
|
+
self.last_cleanup = time.time()
|
|
59
|
+
|
|
60
|
+
def track_delegation(
|
|
61
|
+
self, session_id: str, agent_type: str, request_data: Optional[dict] = None
|
|
62
|
+
):
|
|
63
|
+
"""Track a new agent delegation with optional request data for response correlation."""
|
|
64
|
+
if DEBUG:
|
|
65
|
+
import sys
|
|
66
|
+
|
|
67
|
+
print(
|
|
68
|
+
f" - session_id: {session_id[:16] if session_id else 'None'}...",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
print(f" - agent_type: {agent_type}", file=sys.stderr)
|
|
72
|
+
print(f" - request_data provided: {bool(request_data)}", file=sys.stderr)
|
|
73
|
+
print(
|
|
74
|
+
f" - delegation_requests size before: {len(self.delegation_requests)}",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if session_id and agent_type and agent_type != "unknown":
|
|
79
|
+
self.active_delegations[session_id] = agent_type
|
|
80
|
+
key = f"{session_id}:{datetime.now().timestamp()}"
|
|
81
|
+
self.delegation_history.append((key, agent_type))
|
|
82
|
+
|
|
83
|
+
# Store request data for response tracking correlation
|
|
84
|
+
if request_data:
|
|
85
|
+
self.delegation_requests[session_id] = {
|
|
86
|
+
"agent_type": agent_type,
|
|
87
|
+
"request": request_data,
|
|
88
|
+
"timestamp": datetime.now().isoformat(),
|
|
89
|
+
}
|
|
90
|
+
if DEBUG:
|
|
91
|
+
import sys
|
|
92
|
+
|
|
93
|
+
print(
|
|
94
|
+
f" - ✅ Stored in delegation_requests[{session_id[:16]}...]",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
print(
|
|
98
|
+
f" - delegation_requests size after: {len(self.delegation_requests)}",
|
|
99
|
+
file=sys.stderr,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Clean up old delegations (older than 5 minutes)
|
|
103
|
+
cutoff_time = datetime.now().timestamp() - 300
|
|
104
|
+
keys_to_remove = []
|
|
105
|
+
for sid in list(self.active_delegations.keys()):
|
|
106
|
+
# Check if this is an old entry by looking in history
|
|
107
|
+
found_recent = False
|
|
108
|
+
for hist_key, _ in reversed(self.delegation_history):
|
|
109
|
+
if hist_key.startswith(sid):
|
|
110
|
+
_, timestamp = hist_key.split(":", 1)
|
|
111
|
+
if float(timestamp) > cutoff_time:
|
|
112
|
+
found_recent = True
|
|
113
|
+
break
|
|
114
|
+
if not found_recent:
|
|
115
|
+
keys_to_remove.append(sid)
|
|
116
|
+
|
|
117
|
+
for key in keys_to_remove:
|
|
118
|
+
if key in self.active_delegations:
|
|
119
|
+
del self.active_delegations[key]
|
|
120
|
+
if key in self.delegation_requests:
|
|
121
|
+
del self.delegation_requests[key]
|
|
122
|
+
|
|
123
|
+
def get_delegation_agent_type(self, session_id: str) -> str:
|
|
124
|
+
"""Get the agent type for a session's active delegation."""
|
|
125
|
+
# First try exact session match
|
|
126
|
+
if session_id and session_id in self.active_delegations:
|
|
127
|
+
return self.active_delegations[session_id]
|
|
128
|
+
|
|
129
|
+
# Then try to find in recent history
|
|
130
|
+
if session_id:
|
|
131
|
+
for key, agent_type in reversed(self.delegation_history):
|
|
132
|
+
if key.startswith(session_id):
|
|
133
|
+
return agent_type
|
|
134
|
+
|
|
135
|
+
return "unknown"
|
|
136
|
+
|
|
137
|
+
def cleanup_old_entries(self):
|
|
138
|
+
"""Clean up old entries to prevent memory growth."""
|
|
139
|
+
datetime.now().timestamp() - self.MAX_CACHE_AGE_SECONDS
|
|
140
|
+
|
|
141
|
+
# Clean up delegation tracking dictionaries
|
|
142
|
+
for storage in [self.active_delegations, self.delegation_requests]:
|
|
143
|
+
if len(storage) > self.MAX_DELEGATION_TRACKING:
|
|
144
|
+
# Keep only the most recent entries
|
|
145
|
+
sorted_keys = sorted(storage.keys())
|
|
146
|
+
excess = len(storage) - self.MAX_DELEGATION_TRACKING
|
|
147
|
+
for key in sorted_keys[:excess]:
|
|
148
|
+
del storage[key]
|
|
149
|
+
|
|
150
|
+
# Clean up pending prompts
|
|
151
|
+
if len(self.pending_prompts) > self.MAX_PROMPT_TRACKING:
|
|
152
|
+
sorted_keys = sorted(self.pending_prompts.keys())
|
|
153
|
+
excess = len(self.pending_prompts) - self.MAX_PROMPT_TRACKING
|
|
154
|
+
for key in sorted_keys[:excess]:
|
|
155
|
+
del self.pending_prompts[key]
|
|
156
|
+
|
|
157
|
+
# Clean up git branch cache
|
|
158
|
+
expired_keys = [
|
|
159
|
+
key
|
|
160
|
+
for key, cache_time in self._git_branch_cache_time.items()
|
|
161
|
+
if datetime.now().timestamp() - cache_time > self.MAX_CACHE_AGE_SECONDS
|
|
162
|
+
]
|
|
163
|
+
for key in expired_keys:
|
|
164
|
+
self._git_branch_cache.pop(key, None)
|
|
165
|
+
self._git_branch_cache_time.pop(key, None)
|
|
166
|
+
|
|
167
|
+
def get_git_branch(self, working_dir: Optional[str] = None) -> str:
|
|
168
|
+
"""Get git branch for the given directory with caching.
|
|
169
|
+
|
|
170
|
+
WHY caching approach:
|
|
171
|
+
- Avoids repeated subprocess calls which are expensive
|
|
172
|
+
- Caches results for 30 seconds per directory
|
|
173
|
+
- Falls back gracefully if git command fails
|
|
174
|
+
- Returns 'Unknown' for non-git directories
|
|
175
|
+
"""
|
|
176
|
+
# Use current working directory if not specified
|
|
177
|
+
if not working_dir:
|
|
178
|
+
working_dir = os.getcwd()
|
|
179
|
+
|
|
180
|
+
# Check cache first (cache for 30 seconds)
|
|
181
|
+
current_time = datetime.now().timestamp()
|
|
182
|
+
cache_key = working_dir
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
cache_key in self._git_branch_cache
|
|
186
|
+
and cache_key in self._git_branch_cache_time
|
|
187
|
+
and current_time - self._git_branch_cache_time[cache_key] < 30
|
|
188
|
+
):
|
|
189
|
+
return self._git_branch_cache[cache_key]
|
|
190
|
+
|
|
191
|
+
# Try to get git branch
|
|
192
|
+
try:
|
|
193
|
+
# Change to the working directory temporarily
|
|
194
|
+
original_cwd = os.getcwd()
|
|
195
|
+
os.chdir(working_dir)
|
|
196
|
+
|
|
197
|
+
# Run git command to get current branch
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
["git", "branch", "--show-current"],
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
timeout=TimeoutConfig.QUICK_TIMEOUT,
|
|
203
|
+
check=False, # Quick timeout to avoid hanging
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Restore original directory
|
|
207
|
+
os.chdir(original_cwd)
|
|
208
|
+
|
|
209
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
210
|
+
branch = result.stdout.strip()
|
|
211
|
+
# Cache the result
|
|
212
|
+
self._git_branch_cache[cache_key] = branch
|
|
213
|
+
self._git_branch_cache_time[cache_key] = current_time
|
|
214
|
+
return branch
|
|
215
|
+
# Not a git repository or no branch
|
|
216
|
+
self._git_branch_cache[cache_key] = "Unknown"
|
|
217
|
+
self._git_branch_cache_time[cache_key] = current_time
|
|
218
|
+
return "Unknown"
|
|
219
|
+
|
|
220
|
+
except (
|
|
221
|
+
subprocess.TimeoutExpired,
|
|
222
|
+
subprocess.CalledProcessError,
|
|
223
|
+
FileNotFoundError,
|
|
224
|
+
OSError,
|
|
225
|
+
):
|
|
226
|
+
# Git not available or command failed
|
|
227
|
+
self._git_branch_cache[cache_key] = "Unknown"
|
|
228
|
+
self._git_branch_cache_time[cache_key] = current_time
|
|
229
|
+
return "Unknown"
|
|
230
|
+
|
|
231
|
+
def find_matching_request(self, session_id: str) -> Optional[dict]:
|
|
232
|
+
"""Find matching request data for a session, with fuzzy matching fallback."""
|
|
233
|
+
# First try exact match
|
|
234
|
+
request_info = self.delegation_requests.get(session_id)
|
|
235
|
+
|
|
236
|
+
# If exact match fails, try partial matching
|
|
237
|
+
if not request_info and session_id:
|
|
238
|
+
if DEBUG:
|
|
239
|
+
import sys
|
|
240
|
+
|
|
241
|
+
print(
|
|
242
|
+
f" - Trying fuzzy match for session {session_id[:16]}...",
|
|
243
|
+
file=sys.stderr,
|
|
244
|
+
)
|
|
245
|
+
# Try to find a session that matches the first 8-16 characters
|
|
246
|
+
for stored_sid in list(self.delegation_requests.keys()):
|
|
247
|
+
if (
|
|
248
|
+
stored_sid.startswith(session_id[:8])
|
|
249
|
+
or session_id.startswith(stored_sid[:8])
|
|
250
|
+
or (
|
|
251
|
+
len(session_id) >= 16
|
|
252
|
+
and len(stored_sid) >= 16
|
|
253
|
+
and stored_sid[:16] == session_id[:16]
|
|
254
|
+
)
|
|
255
|
+
):
|
|
256
|
+
if DEBUG:
|
|
257
|
+
import sys
|
|
258
|
+
|
|
259
|
+
print(
|
|
260
|
+
f" - ✅ Fuzzy match found: {stored_sid[:16]}...",
|
|
261
|
+
file=sys.stderr,
|
|
262
|
+
)
|
|
263
|
+
request_info = self.delegation_requests.get(stored_sid)
|
|
264
|
+
# Update the key to use the current session_id for consistency
|
|
265
|
+
if request_info:
|
|
266
|
+
self.delegation_requests[session_id] = request_info
|
|
267
|
+
# Optionally remove the old key to avoid duplicates
|
|
268
|
+
if stored_sid != session_id:
|
|
269
|
+
del self.delegation_requests[stored_sid]
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
return request_info
|
|
273
|
+
|
|
274
|
+
def remove_request(self, session_id: str):
|
|
275
|
+
"""Remove request data for a session."""
|
|
276
|
+
if session_id in self.delegation_requests:
|
|
277
|
+
del self.delegation_requests[session_id]
|
|
278
|
+
|
|
279
|
+
def increment_events_processed(self) -> bool:
|
|
280
|
+
"""Increment events processed counter and return True if cleanup is needed."""
|
|
281
|
+
self.events_processed += 1
|
|
282
|
+
return self.events_processed % self.CLEANUP_INTERVAL_EVENTS == 0
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Subagent response processing service for Claude hook handler.
|
|
2
|
+
|
|
3
|
+
This service handles:
|
|
4
|
+
- SubagentStop event processing
|
|
5
|
+
- Structured response extraction
|
|
6
|
+
- Response tracking and correlation
|
|
7
|
+
- Memory field processing
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
# Debug mode is enabled by default for better visibility into hook processing
|
|
18
|
+
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "true").lower() != "false"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SubagentResponseProcessor:
|
|
22
|
+
"""Processes subagent responses and extracts structured data."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, state_manager, response_tracking_manager, connection_manager):
|
|
25
|
+
"""Initialize the subagent response processor.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
state_manager: StateManagerService instance
|
|
29
|
+
response_tracking_manager: ResponseTrackingManager instance
|
|
30
|
+
connection_manager: ConnectionManagerService instance
|
|
31
|
+
"""
|
|
32
|
+
self.state_manager = state_manager
|
|
33
|
+
self.response_tracking_manager = response_tracking_manager
|
|
34
|
+
self.connection_manager = connection_manager
|
|
35
|
+
|
|
36
|
+
def process_subagent_stop(self, event: dict):
|
|
37
|
+
"""Handle subagent stop events with improved agent type detection.
|
|
38
|
+
|
|
39
|
+
WHY comprehensive subagent stop capture:
|
|
40
|
+
- Provides visibility into subagent lifecycle and delegation patterns
|
|
41
|
+
- Captures agent type, ID, reason, and results for analysis
|
|
42
|
+
- Enables tracking of delegation success/failure patterns
|
|
43
|
+
- Useful for understanding subagent performance and reliability
|
|
44
|
+
"""
|
|
45
|
+
# Enhanced debug logging for session correlation
|
|
46
|
+
session_id = event.get("session_id", "")
|
|
47
|
+
if DEBUG:
|
|
48
|
+
print(
|
|
49
|
+
f" - session_id: {session_id[:16] if session_id else 'None'}...",
|
|
50
|
+
file=sys.stderr,
|
|
51
|
+
)
|
|
52
|
+
print(f" - event keys: {list(event.keys())}", file=sys.stderr)
|
|
53
|
+
print(
|
|
54
|
+
f" - delegation_requests size: {len(self.state_manager.delegation_requests)}",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
57
|
+
# Show all stored session IDs for comparison
|
|
58
|
+
all_sessions = list(self.state_manager.delegation_requests.keys())
|
|
59
|
+
if all_sessions:
|
|
60
|
+
print(" - Stored sessions (first 16 chars):", file=sys.stderr)
|
|
61
|
+
for sid in all_sessions[:10]: # Show up to 10
|
|
62
|
+
print(
|
|
63
|
+
f" - {sid[:16]}... (agent: {self.state_manager.delegation_requests[sid].get('agent_type', 'unknown')})",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
print(" - No stored sessions in delegation_requests!", file=sys.stderr)
|
|
68
|
+
|
|
69
|
+
# Get agent type and other basic info
|
|
70
|
+
agent_type, agent_id, reason = self._extract_basic_info(event, session_id)
|
|
71
|
+
|
|
72
|
+
# Always log SubagentStop events for debugging
|
|
73
|
+
if DEBUG or agent_type != "unknown":
|
|
74
|
+
print(
|
|
75
|
+
f"Hook handler: Processing SubagentStop - agent: '{agent_type}', session: '{session_id}', reason: '{reason}'",
|
|
76
|
+
file=sys.stderr,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Get working directory and git branch
|
|
80
|
+
working_dir = event.get("cwd", "")
|
|
81
|
+
git_branch = (
|
|
82
|
+
self.state_manager.get_git_branch(working_dir) if working_dir else "Unknown"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Try to extract structured response from output if available
|
|
86
|
+
output = event.get("output", "")
|
|
87
|
+
structured_response = self._extract_structured_response(output, agent_type)
|
|
88
|
+
|
|
89
|
+
# Track agent response
|
|
90
|
+
self._track_response(
|
|
91
|
+
event,
|
|
92
|
+
session_id,
|
|
93
|
+
agent_type,
|
|
94
|
+
reason,
|
|
95
|
+
working_dir,
|
|
96
|
+
git_branch,
|
|
97
|
+
output,
|
|
98
|
+
structured_response,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Build subagent stop data for event emission
|
|
102
|
+
subagent_stop_data = self._build_subagent_stop_data(
|
|
103
|
+
event,
|
|
104
|
+
session_id,
|
|
105
|
+
agent_type,
|
|
106
|
+
agent_id,
|
|
107
|
+
reason,
|
|
108
|
+
working_dir,
|
|
109
|
+
git_branch,
|
|
110
|
+
structured_response,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Debug log the processed data
|
|
114
|
+
if DEBUG:
|
|
115
|
+
print(
|
|
116
|
+
f"SubagentStop processed data: agent_type='{agent_type}', session_id='{session_id}'",
|
|
117
|
+
file=sys.stderr,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Emit to /hook namespace with high priority
|
|
121
|
+
self.connection_manager.emit_event("/hook", "subagent_stop", subagent_stop_data)
|
|
122
|
+
|
|
123
|
+
def _extract_basic_info(self, event: dict, session_id: str) -> Tuple[str, str, str]:
|
|
124
|
+
"""Extract basic info from the event."""
|
|
125
|
+
# First try to get agent type from our tracking
|
|
126
|
+
agent_type = (
|
|
127
|
+
self.state_manager.get_delegation_agent_type(session_id)
|
|
128
|
+
if session_id
|
|
129
|
+
else "unknown"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Fall back to event data if tracking didn't have it
|
|
133
|
+
if agent_type == "unknown":
|
|
134
|
+
agent_type = event.get("agent_type", event.get("subagent_type", "unknown"))
|
|
135
|
+
|
|
136
|
+
agent_id = event.get("agent_id", event.get("subagent_id", ""))
|
|
137
|
+
reason = event.get("reason", event.get("stop_reason", "unknown"))
|
|
138
|
+
|
|
139
|
+
# Try to infer agent type from other fields if still unknown
|
|
140
|
+
if agent_type == "unknown" and "task" in event:
|
|
141
|
+
task_desc = str(event.get("task", "")).lower()
|
|
142
|
+
if "research" in task_desc:
|
|
143
|
+
agent_type = "research"
|
|
144
|
+
elif "engineer" in task_desc or "code" in task_desc:
|
|
145
|
+
agent_type = "engineer"
|
|
146
|
+
elif "pm" in task_desc or "project" in task_desc:
|
|
147
|
+
agent_type = "pm"
|
|
148
|
+
|
|
149
|
+
return agent_type, agent_id, reason
|
|
150
|
+
|
|
151
|
+
def _extract_structured_response(
|
|
152
|
+
self, output: str, agent_type: str
|
|
153
|
+
) -> Optional[dict]:
|
|
154
|
+
"""Extract structured JSON response from output."""
|
|
155
|
+
if not output:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
json_match = re.search(r"```json\s*(\{.*?\})\s*```", str(output), re.DOTALL)
|
|
160
|
+
if json_match:
|
|
161
|
+
structured_response = json.loads(json_match.group(1))
|
|
162
|
+
if DEBUG:
|
|
163
|
+
print(
|
|
164
|
+
f"Extracted structured response from {agent_type} agent in SubagentStop",
|
|
165
|
+
file=sys.stderr,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Log if MEMORIES field is present
|
|
169
|
+
if structured_response.get("MEMORIES") and DEBUG:
|
|
170
|
+
memories_count = len(structured_response["MEMORIES"])
|
|
171
|
+
print(
|
|
172
|
+
f"Agent {agent_type} returned MEMORIES field with {memories_count} items",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return structured_response
|
|
177
|
+
except (json.JSONDecodeError, AttributeError):
|
|
178
|
+
pass # No structured response, that's okay
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def _track_response(
|
|
183
|
+
self,
|
|
184
|
+
event: dict,
|
|
185
|
+
session_id: str,
|
|
186
|
+
agent_type: str,
|
|
187
|
+
reason: str,
|
|
188
|
+
working_dir: str,
|
|
189
|
+
git_branch: str,
|
|
190
|
+
output: str,
|
|
191
|
+
structured_response: Optional[dict],
|
|
192
|
+
):
|
|
193
|
+
"""Track the agent response if response tracking is enabled."""
|
|
194
|
+
if DEBUG:
|
|
195
|
+
print(
|
|
196
|
+
f" - response_tracking_enabled: {self.response_tracking_manager.response_tracking_enabled}",
|
|
197
|
+
file=sys.stderr,
|
|
198
|
+
)
|
|
199
|
+
print(
|
|
200
|
+
f" - response_tracker exists: {self.response_tracking_manager.response_tracker is not None}",
|
|
201
|
+
file=sys.stderr,
|
|
202
|
+
)
|
|
203
|
+
print(
|
|
204
|
+
f" - session_id: {session_id[:16] if session_id else 'None'}...",
|
|
205
|
+
file=sys.stderr,
|
|
206
|
+
)
|
|
207
|
+
print(f" - agent_type: {agent_type}", file=sys.stderr)
|
|
208
|
+
print(f" - reason: {reason}", file=sys.stderr)
|
|
209
|
+
|
|
210
|
+
if (
|
|
211
|
+
self.response_tracking_manager.response_tracking_enabled
|
|
212
|
+
and self.response_tracking_manager.response_tracker
|
|
213
|
+
):
|
|
214
|
+
try:
|
|
215
|
+
# Get the original request data (with fuzzy matching fallback)
|
|
216
|
+
request_info = self.state_manager.find_matching_request(session_id)
|
|
217
|
+
|
|
218
|
+
if DEBUG:
|
|
219
|
+
print(
|
|
220
|
+
f" - request_info present: {bool(request_info)}",
|
|
221
|
+
file=sys.stderr,
|
|
222
|
+
)
|
|
223
|
+
if request_info:
|
|
224
|
+
print(
|
|
225
|
+
" - ✅ Found request data for response tracking",
|
|
226
|
+
file=sys.stderr,
|
|
227
|
+
)
|
|
228
|
+
print(
|
|
229
|
+
f" - stored agent_type: {request_info.get('agent_type')}",
|
|
230
|
+
file=sys.stderr,
|
|
231
|
+
)
|
|
232
|
+
print(
|
|
233
|
+
f" - request keys: {list(request_info.get('request', {}).keys())}",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
print(
|
|
238
|
+
f" - ❌ No request data found for session {session_id[:16]}...",
|
|
239
|
+
file=sys.stderr,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if request_info:
|
|
243
|
+
# Use the output as the response
|
|
244
|
+
response_text = (
|
|
245
|
+
str(output)
|
|
246
|
+
if output
|
|
247
|
+
else f"Agent {agent_type} completed with reason: {reason}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Get the original request
|
|
251
|
+
original_request = request_info.get("request", {})
|
|
252
|
+
prompt = original_request.get("prompt", "")
|
|
253
|
+
description = original_request.get("description", "")
|
|
254
|
+
|
|
255
|
+
# Combine prompt and description
|
|
256
|
+
full_request = prompt
|
|
257
|
+
if description and description != prompt:
|
|
258
|
+
if full_request:
|
|
259
|
+
full_request += f"\n\nDescription: {description}"
|
|
260
|
+
else:
|
|
261
|
+
full_request = description
|
|
262
|
+
|
|
263
|
+
if not full_request:
|
|
264
|
+
full_request = f"Task delegation to {agent_type} agent"
|
|
265
|
+
|
|
266
|
+
# Prepare metadata
|
|
267
|
+
metadata = {
|
|
268
|
+
"exit_code": event.get("exit_code", 0),
|
|
269
|
+
"success": reason in ["completed", "finished", "done"],
|
|
270
|
+
"has_error": reason
|
|
271
|
+
in ["error", "timeout", "failed", "blocked"],
|
|
272
|
+
"duration_ms": event.get("duration_ms"),
|
|
273
|
+
"working_directory": working_dir,
|
|
274
|
+
"git_branch": git_branch,
|
|
275
|
+
"timestamp": datetime.now().isoformat(),
|
|
276
|
+
"event_type": "subagent_stop",
|
|
277
|
+
"reason": reason,
|
|
278
|
+
"original_request_timestamp": request_info.get("timestamp"),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Add structured response if available
|
|
282
|
+
if structured_response:
|
|
283
|
+
metadata["structured_response"] = structured_response
|
|
284
|
+
metadata["task_completed"] = structured_response.get(
|
|
285
|
+
"task_completed", False
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Check for MEMORIES field and process if present
|
|
289
|
+
if structured_response.get("MEMORIES") and DEBUG:
|
|
290
|
+
memories = structured_response["MEMORIES"]
|
|
291
|
+
print(
|
|
292
|
+
f"Found MEMORIES field in {agent_type} response with {len(memories)} items",
|
|
293
|
+
file=sys.stderr,
|
|
294
|
+
)
|
|
295
|
+
# The memory will be processed by extract_and_update_memory
|
|
296
|
+
# which is called by the memory hook service
|
|
297
|
+
|
|
298
|
+
# Track the response
|
|
299
|
+
file_path = (
|
|
300
|
+
self.response_tracking_manager.response_tracker.track_response(
|
|
301
|
+
agent_name=agent_type,
|
|
302
|
+
request=full_request,
|
|
303
|
+
response=response_text,
|
|
304
|
+
session_id=session_id,
|
|
305
|
+
metadata=metadata,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if file_path and DEBUG:
|
|
310
|
+
print(
|
|
311
|
+
f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}",
|
|
312
|
+
file=sys.stderr,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Clean up the request data
|
|
316
|
+
self.state_manager.remove_request(session_id)
|
|
317
|
+
|
|
318
|
+
elif DEBUG:
|
|
319
|
+
print(
|
|
320
|
+
f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}",
|
|
321
|
+
file=sys.stderr,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
if DEBUG:
|
|
326
|
+
print(
|
|
327
|
+
f"❌ Failed to track response on SubagentStop: {e}",
|
|
328
|
+
file=sys.stderr,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _build_subagent_stop_data(
|
|
332
|
+
self,
|
|
333
|
+
event: dict,
|
|
334
|
+
session_id: str,
|
|
335
|
+
agent_type: str,
|
|
336
|
+
agent_id: str,
|
|
337
|
+
reason: str,
|
|
338
|
+
working_dir: str,
|
|
339
|
+
git_branch: str,
|
|
340
|
+
structured_response: Optional[dict],
|
|
341
|
+
) -> dict:
|
|
342
|
+
"""Build the subagent stop data for event emission."""
|
|
343
|
+
subagent_stop_data = {
|
|
344
|
+
"agent_type": agent_type,
|
|
345
|
+
"agent_id": agent_id,
|
|
346
|
+
"reason": reason,
|
|
347
|
+
"session_id": session_id,
|
|
348
|
+
"working_directory": working_dir,
|
|
349
|
+
"git_branch": git_branch,
|
|
350
|
+
"timestamp": datetime.now().isoformat(),
|
|
351
|
+
"is_successful_completion": reason in ["completed", "finished", "done"],
|
|
352
|
+
"is_error_termination": reason in ["error", "timeout", "failed", "blocked"],
|
|
353
|
+
"is_delegation_related": agent_type
|
|
354
|
+
in ["research", "engineer", "pm", "ops", "qa", "documentation", "security"],
|
|
355
|
+
"has_results": bool(event.get("results") or event.get("output")),
|
|
356
|
+
"duration_context": event.get("duration_ms"),
|
|
357
|
+
"hook_event_name": "SubagentStop", # Explicitly set for dashboard
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Add structured response data if available
|
|
361
|
+
if structured_response:
|
|
362
|
+
subagent_stop_data["structured_response"] = {
|
|
363
|
+
"task_completed": structured_response.get("task_completed", False),
|
|
364
|
+
"instructions": structured_response.get("instructions", ""),
|
|
365
|
+
"results": structured_response.get("results", ""),
|
|
366
|
+
"files_modified": structured_response.get("files_modified", []),
|
|
367
|
+
"tools_used": structured_response.get("tools_used", []),
|
|
368
|
+
"remember": structured_response.get("remember"),
|
|
369
|
+
"MEMORIES": structured_response.get(
|
|
370
|
+
"MEMORIES"
|
|
371
|
+
), # Complete memory replacement
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return subagent_stop_data
|