claude-mpm 4.1.2__py3-none-any.whl → 4.1.4__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.
Files changed (87) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
  3. claude_mpm/agents/MEMORY.md +21 -49
  4. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
  5. claude_mpm/agents/templates/api_qa.json +36 -116
  6. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
  7. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
  8. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
  9. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
  10. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
  11. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
  12. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
  13. claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
  14. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
  15. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
  16. claude_mpm/agents/templates/code_analyzer.json +18 -36
  17. claude_mpm/agents/templates/data_engineer.json +43 -14
  18. claude_mpm/agents/templates/documentation.json +55 -74
  19. claude_mpm/agents/templates/engineer.json +57 -40
  20. claude_mpm/agents/templates/imagemagick.json +7 -2
  21. claude_mpm/agents/templates/memory_manager.json +1 -1
  22. claude_mpm/agents/templates/ops.json +36 -4
  23. claude_mpm/agents/templates/project_organizer.json +23 -71
  24. claude_mpm/agents/templates/qa.json +34 -2
  25. claude_mpm/agents/templates/refactoring_engineer.json +9 -5
  26. claude_mpm/agents/templates/research.json +36 -4
  27. claude_mpm/agents/templates/security.json +29 -2
  28. claude_mpm/agents/templates/ticketing.json +3 -3
  29. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  30. claude_mpm/agents/templates/version_control.json +28 -2
  31. claude_mpm/agents/templates/web_qa.json +38 -151
  32. claude_mpm/agents/templates/web_ui.json +2 -2
  33. claude_mpm/cli/commands/agent_manager.py +221 -1
  34. claude_mpm/cli/commands/agents.py +556 -1009
  35. claude_mpm/cli/commands/memory.py +248 -927
  36. claude_mpm/cli/commands/run.py +139 -484
  37. claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
  38. claude_mpm/cli/startup_logging.py +76 -0
  39. claude_mpm/core/agent_registry.py +6 -10
  40. claude_mpm/core/framework_loader.py +205 -595
  41. claude_mpm/core/log_manager.py +49 -1
  42. claude_mpm/core/logging_config.py +2 -4
  43. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  45. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  47. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  48. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  49. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  53. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  54. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  55. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  56. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  57. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  58. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  59. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  60. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  61. claude_mpm/services/agents/registry/__init__.py +1 -1
  62. claude_mpm/services/cli/__init__.py +18 -0
  63. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  64. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  65. claude_mpm/services/cli/agent_listing_service.py +463 -0
  66. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  67. claude_mpm/services/cli/agent_validation_service.py +589 -0
  68. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  69. claude_mpm/services/cli/memory_crud_service.py +617 -0
  70. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  71. claude_mpm/services/cli/session_manager.py +513 -0
  72. claude_mpm/services/cli/socketio_manager.py +498 -0
  73. claude_mpm/services/cli/startup_checker.py +370 -0
  74. claude_mpm/services/core/cache_manager.py +311 -0
  75. claude_mpm/services/core/memory_manager.py +637 -0
  76. claude_mpm/services/core/path_resolver.py +498 -0
  77. claude_mpm/services/core/service_container.py +520 -0
  78. claude_mpm/services/core/service_interfaces.py +436 -0
  79. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  80. claude_mpm/services/memory/router.py +116 -10
  81. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
  82. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +86 -55
  83. claude_mpm/cli/commands/run_config_checker.py +0 -159
  84. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
  85. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
  86. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
  87. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.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