claude-mpm 4.8.0__py3-none-any.whl → 4.8.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/golang_engineer.json +257 -0
- claude_mpm/agents/templates/nextjs_engineer.json +122 -132
- claude_mpm/agents/templates/php-engineer.json +258 -175
- claude_mpm/agents/templates/product_owner.json +335 -0
- claude_mpm/agents/templates/python_engineer.json +150 -80
- claude_mpm/agents/templates/ruby-engineer.json +115 -191
- claude_mpm/agents/templates/rust_engineer.json +257 -0
- claude_mpm/agents/templates/typescript_engineer.json +102 -124
- claude_mpm/hooks/__init__.py +14 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +4 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +23 -2
- claude_mpm/hooks/failure_learning/__init__.py +60 -0
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +235 -0
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +217 -0
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +286 -0
- claude_mpm/services/memory/failure_tracker.py +563 -0
- claude_mpm/services/memory_hook_service.py +76 -0
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/RECORD +24 -16
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.8.0.dist-info → claude_mpm-4.8.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,563 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Failure Tracker Service
|
4
|
+
=======================
|
5
|
+
|
6
|
+
Session-level state manager for tracking failures and their fixes to enable
|
7
|
+
automatic learning extraction.
|
8
|
+
|
9
|
+
WHY: When tasks fail and agents fix them, we need to track these failure-fix
|
10
|
+
pairs to extract learnings. This service provides in-memory session tracking
|
11
|
+
without requiring database or filesystem persistence.
|
12
|
+
|
13
|
+
DESIGN DECISION: Session-scoped tracking keeps the MVP simple. Failures are
|
14
|
+
tracked during a session, matched with fixes, and learnings are extracted
|
15
|
+
before session end. No persistent storage needed for MVP.
|
16
|
+
|
17
|
+
Architecture:
|
18
|
+
- Failure events: Captured from tool outputs (errors, exceptions, test failures)
|
19
|
+
- Fix events: Detected when same task type succeeds after failure
|
20
|
+
- Learning synthesis: Template-based extraction from failure-fix pairs
|
21
|
+
- Memory routing: Direct learnings to appropriate agent memory files
|
22
|
+
|
23
|
+
Example flow:
|
24
|
+
1. Bash tool returns error → FailureEvent created
|
25
|
+
2. User or agent makes changes
|
26
|
+
3. Bash tool succeeds → Fix detected, matched with failure
|
27
|
+
4. Learning extracted and written to agent memory
|
28
|
+
"""
|
29
|
+
|
30
|
+
import logging
|
31
|
+
import re
|
32
|
+
from dataclasses import dataclass, field
|
33
|
+
from datetime import datetime, timezone
|
34
|
+
from typing import Dict, List, Optional, Tuple
|
35
|
+
|
36
|
+
logger = logging.getLogger(__name__)
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class FailureEvent:
|
41
|
+
"""Represents a detected task failure.
|
42
|
+
|
43
|
+
Attributes:
|
44
|
+
task_id: Unique identifier for this failure event
|
45
|
+
task_type: Type of task that failed (bash, test, build, etc.)
|
46
|
+
tool_name: Name of tool that failed (Bash, NotebookEdit, etc.)
|
47
|
+
error_message: The actual error message
|
48
|
+
context: Additional context (agent, session, working_dir, etc.)
|
49
|
+
timestamp: When the failure occurred
|
50
|
+
fixed: Whether this failure has been fixed
|
51
|
+
fix_timestamp: When the fix occurred (if fixed)
|
52
|
+
"""
|
53
|
+
|
54
|
+
task_id: str
|
55
|
+
task_type: str
|
56
|
+
tool_name: str
|
57
|
+
error_message: str
|
58
|
+
context: Dict[str, str] = field(default_factory=dict)
|
59
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
60
|
+
fixed: bool = False
|
61
|
+
fix_timestamp: Optional[datetime] = None
|
62
|
+
|
63
|
+
def mark_fixed(self) -> None:
|
64
|
+
"""Mark this failure as fixed."""
|
65
|
+
self.fixed = True
|
66
|
+
self.fix_timestamp = datetime.now(timezone.utc)
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class FixEvent:
|
71
|
+
"""Represents a detected fix for a previous failure.
|
72
|
+
|
73
|
+
Attributes:
|
74
|
+
task_type: Type of task that succeeded
|
75
|
+
tool_name: Name of tool that succeeded
|
76
|
+
success_message: Output from successful execution
|
77
|
+
context: Additional context
|
78
|
+
timestamp: When the fix occurred
|
79
|
+
matched_failure: The failure event this fix resolves
|
80
|
+
"""
|
81
|
+
|
82
|
+
task_type: str
|
83
|
+
tool_name: str
|
84
|
+
success_message: str
|
85
|
+
context: Dict[str, str] = field(default_factory=dict)
|
86
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
87
|
+
matched_failure: Optional[FailureEvent] = None
|
88
|
+
|
89
|
+
|
90
|
+
@dataclass
|
91
|
+
class Learning:
|
92
|
+
"""Represents an extracted learning from a failure-fix pair.
|
93
|
+
|
94
|
+
Attributes:
|
95
|
+
category: Learning category (error-handling, testing, configuration, etc.)
|
96
|
+
problem: Description of the original problem
|
97
|
+
solution: Description of the solution
|
98
|
+
context: Task context (tool, agent, etc.)
|
99
|
+
target_agent: Which agent should receive this learning
|
100
|
+
failure_event: The original failure
|
101
|
+
fix_event: The fix that resolved it
|
102
|
+
timestamp: When the learning was extracted
|
103
|
+
"""
|
104
|
+
|
105
|
+
category: str
|
106
|
+
problem: str
|
107
|
+
solution: str
|
108
|
+
context: Dict[str, str]
|
109
|
+
target_agent: str
|
110
|
+
failure_event: FailureEvent
|
111
|
+
fix_event: FixEvent
|
112
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
113
|
+
|
114
|
+
def to_markdown(self) -> str:
|
115
|
+
"""Format learning as markdown for memory file.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Markdown-formatted learning entry
|
119
|
+
"""
|
120
|
+
return (
|
121
|
+
f"## {self.category}\n"
|
122
|
+
f"- **Problem**: {self.problem}\n"
|
123
|
+
f"- **Solution**: {self.solution}\n"
|
124
|
+
f"- **Context**: {', '.join(f'{k}: {v}' for k, v in self.context.items())}\n"
|
125
|
+
f"- **Date**: {self.timestamp.strftime('%Y-%m-%d')}\n"
|
126
|
+
)
|
127
|
+
|
128
|
+
|
129
|
+
class FailureTracker:
|
130
|
+
"""Session-level tracker for failures, fixes, and learnings.
|
131
|
+
|
132
|
+
WHY: Provides centralized state management for the failure-learning system.
|
133
|
+
Hooks interact with this tracker to record failures, detect fixes, and
|
134
|
+
extract learnings.
|
135
|
+
|
136
|
+
DESIGN DECISION: In-memory session tracking is sufficient for MVP. Each
|
137
|
+
session maintains its own failure history. When a fix is detected, we
|
138
|
+
search for matching failures and create learning pairs.
|
139
|
+
"""
|
140
|
+
|
141
|
+
# Failure detection patterns (ordered from most specific to least specific)
|
142
|
+
ERROR_PATTERNS = [
|
143
|
+
(r"SyntaxError: (.+)", "syntax-error"),
|
144
|
+
(r"TypeError: (.+)", "type-error"),
|
145
|
+
(r"ImportError: (.+)", "import-error"),
|
146
|
+
(r"ModuleNotFoundError: (.+)", "module-not-found"),
|
147
|
+
(r"FileNotFoundError: (.+)", "file-not-found"),
|
148
|
+
(r"FAILED (.+)", "test-failure"),
|
149
|
+
(r"✗ (.+) failed", "test-failure"),
|
150
|
+
(r"(\d+) failed", "test-failure"),
|
151
|
+
(r"Exception: (.+)", "exception"),
|
152
|
+
(r"Command failed: (.+)", "command-error"),
|
153
|
+
(r"Error: (.+)", "error"), # Generic error - match last
|
154
|
+
]
|
155
|
+
|
156
|
+
# Task type classification patterns
|
157
|
+
TASK_TYPE_PATTERNS = {
|
158
|
+
"test": [r"pytest", r"test", r"\.test\.py", r"tests/"],
|
159
|
+
"build": [r"make", r"build", r"compile", r"setup\.py"],
|
160
|
+
"lint": [r"lint", r"flake8", r"mypy", r"black", r"isort", r"ruff"],
|
161
|
+
"git": [r"git ", r"commit", r"push", r"pull", r"merge"],
|
162
|
+
"install": [r"pip install", r"npm install", r"yarn", r"poetry"],
|
163
|
+
"script": [r"\.sh", r"\.py", r"\.js", r"script"],
|
164
|
+
}
|
165
|
+
|
166
|
+
def __init__(self):
|
167
|
+
"""Initialize the failure tracker."""
|
168
|
+
self.failures: List[FailureEvent] = []
|
169
|
+
self.fixes: List[FixEvent] = []
|
170
|
+
self.learnings: List[Learning] = []
|
171
|
+
self.session_id = datetime.now(timezone.utc).isoformat()
|
172
|
+
|
173
|
+
def detect_failure(
|
174
|
+
self, tool_name: str, tool_output: str, context: Optional[Dict[str, str]] = None
|
175
|
+
) -> Optional[FailureEvent]:
|
176
|
+
"""Detect if tool output contains a failure.
|
177
|
+
|
178
|
+
WHY: Failures can occur in many forms (errors, exceptions, test failures).
|
179
|
+
This method uses regex patterns to identify failures and extract relevant
|
180
|
+
error messages.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
tool_name: Name of the tool that executed
|
184
|
+
tool_output: Output from the tool
|
185
|
+
context: Additional context (agent, session, etc.)
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
FailureEvent if failure detected, None otherwise
|
189
|
+
"""
|
190
|
+
if not tool_output:
|
191
|
+
return None
|
192
|
+
|
193
|
+
context = context or {}
|
194
|
+
|
195
|
+
# Check each error pattern
|
196
|
+
for pattern, error_type in self.ERROR_PATTERNS:
|
197
|
+
match = re.search(pattern, tool_output, re.MULTILINE | re.IGNORECASE)
|
198
|
+
if match:
|
199
|
+
error_message = match.group(1) if match.lastindex else match.group(0)
|
200
|
+
|
201
|
+
# Classify task type
|
202
|
+
task_type = self._classify_task_type(tool_output, context)
|
203
|
+
|
204
|
+
# Create failure event
|
205
|
+
task_id = f"{task_type}_{len(self.failures)}_{int(datetime.now(timezone.utc).timestamp())}"
|
206
|
+
failure = FailureEvent(
|
207
|
+
task_id=task_id,
|
208
|
+
task_type=task_type,
|
209
|
+
tool_name=tool_name,
|
210
|
+
error_message=error_message.strip(),
|
211
|
+
context={
|
212
|
+
**context,
|
213
|
+
"error_type": error_type,
|
214
|
+
"output_preview": tool_output[:200],
|
215
|
+
},
|
216
|
+
)
|
217
|
+
|
218
|
+
self.failures.append(failure)
|
219
|
+
logger.info(f"Detected failure: {task_type} - {error_message[:50]}...")
|
220
|
+
return failure
|
221
|
+
|
222
|
+
return None
|
223
|
+
|
224
|
+
def detect_fix(
|
225
|
+
self,
|
226
|
+
tool_name: str,
|
227
|
+
tool_output: str,
|
228
|
+
exit_code: int = 0,
|
229
|
+
context: Optional[Dict[str, str]] = None,
|
230
|
+
) -> Optional[Tuple[FixEvent, FailureEvent]]:
|
231
|
+
"""Detect if a successful execution fixes a previous failure.
|
232
|
+
|
233
|
+
WHY: When a task succeeds, it might be fixing a previous failure of the
|
234
|
+
same task type. This method matches successful executions with recent
|
235
|
+
failures to detect fixes.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
tool_name: Name of the tool that succeeded
|
239
|
+
tool_output: Output from the tool
|
240
|
+
exit_code: Exit code (0 = success)
|
241
|
+
context: Additional context
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Tuple of (FixEvent, matched FailureEvent) if fix detected, None otherwise
|
245
|
+
"""
|
246
|
+
# Only consider successful executions
|
247
|
+
if exit_code != 0:
|
248
|
+
return None
|
249
|
+
|
250
|
+
context = context or {}
|
251
|
+
task_type = self._classify_task_type(tool_output, context)
|
252
|
+
|
253
|
+
# Find matching unfixed failure
|
254
|
+
matching_failure = self._find_matching_failure(task_type, tool_name)
|
255
|
+
if not matching_failure:
|
256
|
+
return None
|
257
|
+
|
258
|
+
# Create fix event
|
259
|
+
fix = FixEvent(
|
260
|
+
task_type=task_type,
|
261
|
+
tool_name=tool_name,
|
262
|
+
success_message=tool_output[:200] if tool_output else "Success",
|
263
|
+
context=context,
|
264
|
+
matched_failure=matching_failure,
|
265
|
+
)
|
266
|
+
|
267
|
+
# Mark failure as fixed
|
268
|
+
matching_failure.mark_fixed()
|
269
|
+
self.fixes.append(fix)
|
270
|
+
|
271
|
+
logger.info(f"Detected fix for {task_type} failure: {matching_failure.task_id}")
|
272
|
+
return (fix, matching_failure)
|
273
|
+
|
274
|
+
def extract_learning(
|
275
|
+
self,
|
276
|
+
fix_event: FixEvent,
|
277
|
+
failure_event: FailureEvent,
|
278
|
+
target_agent: Optional[str] = None,
|
279
|
+
) -> Learning:
|
280
|
+
"""Extract learning from a failure-fix pair.
|
281
|
+
|
282
|
+
WHY: When we have a failure and its fix, we can synthesize a learning
|
283
|
+
that captures the problem-solution pair for future reference.
|
284
|
+
|
285
|
+
DESIGN DECISION: MVP uses template-based extraction (no AI). The learning
|
286
|
+
format is simple and actionable, ready for agent memory files.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
fix_event: The fix that resolved the failure
|
290
|
+
failure_event: The original failure
|
291
|
+
target_agent: Which agent should receive this learning (auto-detected if None)
|
292
|
+
|
293
|
+
Returns:
|
294
|
+
Learning object with extracted knowledge
|
295
|
+
"""
|
296
|
+
# Auto-detect target agent if not specified
|
297
|
+
if not target_agent:
|
298
|
+
target_agent = self._determine_target_agent(failure_event, fix_event)
|
299
|
+
|
300
|
+
# Categorize the learning
|
301
|
+
category = self._categorize_learning(failure_event)
|
302
|
+
|
303
|
+
# Extract problem and solution descriptions
|
304
|
+
problem = self._extract_problem_description(failure_event)
|
305
|
+
solution = self._extract_solution_description(fix_event, failure_event)
|
306
|
+
|
307
|
+
# Build context
|
308
|
+
learning_context = {
|
309
|
+
"task_type": failure_event.task_type,
|
310
|
+
"tool": failure_event.tool_name,
|
311
|
+
"error_type": failure_event.context.get("error_type", "unknown"),
|
312
|
+
}
|
313
|
+
|
314
|
+
# Add agent context if available
|
315
|
+
if "agent_type" in failure_event.context:
|
316
|
+
learning_context["agent"] = failure_event.context["agent_type"]
|
317
|
+
|
318
|
+
learning = Learning(
|
319
|
+
category=category,
|
320
|
+
problem=problem,
|
321
|
+
solution=solution,
|
322
|
+
context=learning_context,
|
323
|
+
target_agent=target_agent,
|
324
|
+
failure_event=failure_event,
|
325
|
+
fix_event=fix_event,
|
326
|
+
)
|
327
|
+
|
328
|
+
self.learnings.append(learning)
|
329
|
+
logger.info(f"Extracted learning for {target_agent}: {category}")
|
330
|
+
return learning
|
331
|
+
|
332
|
+
def get_unfixed_failures(self) -> List[FailureEvent]:
|
333
|
+
"""Get all failures that haven't been fixed yet.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
List of unfixed failure events
|
337
|
+
"""
|
338
|
+
return [f for f in self.failures if not f.fixed]
|
339
|
+
|
340
|
+
def get_learnings_for_agent(self, agent_id: str) -> List[Learning]:
|
341
|
+
"""Get all learnings targeted for a specific agent.
|
342
|
+
|
343
|
+
Args:
|
344
|
+
agent_id: Agent identifier
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
List of learnings for that agent
|
348
|
+
"""
|
349
|
+
return [l for l in self.learnings if l.target_agent == agent_id]
|
350
|
+
|
351
|
+
def get_session_stats(self) -> Dict[str, int]:
|
352
|
+
"""Get statistics for the current session.
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
Dict with failure/fix/learning counts
|
356
|
+
"""
|
357
|
+
return {
|
358
|
+
"total_failures": len(self.failures),
|
359
|
+
"fixed_failures": len([f for f in self.failures if f.fixed]),
|
360
|
+
"unfixed_failures": len([f for f in self.failures if not f.fixed]),
|
361
|
+
"total_fixes": len(self.fixes),
|
362
|
+
"total_learnings": len(self.learnings),
|
363
|
+
}
|
364
|
+
|
365
|
+
def _classify_task_type(self, output: str, context: Dict[str, str]) -> str:
|
366
|
+
"""Classify the task type based on output and context.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
output: Tool output
|
370
|
+
context: Additional context
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
Task type string (test, build, lint, etc.)
|
374
|
+
"""
|
375
|
+
# Check context first
|
376
|
+
if "command" in context:
|
377
|
+
command = context["command"].lower()
|
378
|
+
for task_type, patterns in self.TASK_TYPE_PATTERNS.items():
|
379
|
+
if any(
|
380
|
+
re.search(pattern, command, re.IGNORECASE) for pattern in patterns
|
381
|
+
):
|
382
|
+
return task_type
|
383
|
+
|
384
|
+
# Check output
|
385
|
+
output_lower = output.lower()
|
386
|
+
for task_type, patterns in self.TASK_TYPE_PATTERNS.items():
|
387
|
+
if any(
|
388
|
+
re.search(pattern, output_lower, re.IGNORECASE) for pattern in patterns
|
389
|
+
):
|
390
|
+
return task_type
|
391
|
+
|
392
|
+
# Default to general execution
|
393
|
+
return "execution"
|
394
|
+
|
395
|
+
def _find_matching_failure(
|
396
|
+
self, task_type: str, tool_name: str
|
397
|
+
) -> Optional[FailureEvent]:
|
398
|
+
"""Find the most recent unfixed failure matching the task type.
|
399
|
+
|
400
|
+
Args:
|
401
|
+
task_type: Type of task
|
402
|
+
tool_name: Tool name
|
403
|
+
|
404
|
+
Returns:
|
405
|
+
Matching FailureEvent or None
|
406
|
+
"""
|
407
|
+
# Search in reverse chronological order
|
408
|
+
for failure in reversed(self.failures):
|
409
|
+
if (
|
410
|
+
not failure.fixed
|
411
|
+
and failure.task_type == task_type
|
412
|
+
and failure.tool_name == tool_name
|
413
|
+
):
|
414
|
+
return failure
|
415
|
+
|
416
|
+
# If no exact match, try matching just tool_name for generic tasks
|
417
|
+
if task_type == "execution":
|
418
|
+
for failure in reversed(self.failures):
|
419
|
+
if not failure.fixed and failure.tool_name == tool_name:
|
420
|
+
return failure
|
421
|
+
|
422
|
+
return None
|
423
|
+
|
424
|
+
def _determine_target_agent(
|
425
|
+
self, failure_event: FailureEvent, fix_event: FixEvent
|
426
|
+
) -> str:
|
427
|
+
"""Determine which agent should receive the learning.
|
428
|
+
|
429
|
+
Args:
|
430
|
+
failure_event: The failure
|
431
|
+
fix_event: The fix
|
432
|
+
|
433
|
+
Returns:
|
434
|
+
Agent identifier (PM, engineer, qa, etc.)
|
435
|
+
"""
|
436
|
+
# Check if agent was involved in the context
|
437
|
+
if "agent_type" in failure_event.context:
|
438
|
+
return failure_event.context["agent_type"]
|
439
|
+
|
440
|
+
if "agent_type" in fix_event.context:
|
441
|
+
return fix_event.context["agent_type"]
|
442
|
+
|
443
|
+
# Route by task type
|
444
|
+
task_type = failure_event.task_type
|
445
|
+
if task_type in ("test", "lint"):
|
446
|
+
return "qa"
|
447
|
+
if task_type in ("build", "install", "script") or task_type == "git":
|
448
|
+
return "engineer"
|
449
|
+
# Default to PM for general learnings
|
450
|
+
return "PM"
|
451
|
+
|
452
|
+
def _categorize_learning(self, failure_event: FailureEvent) -> str:
|
453
|
+
"""Categorize the learning based on failure type.
|
454
|
+
|
455
|
+
Args:
|
456
|
+
failure_event: The failure event
|
457
|
+
|
458
|
+
Returns:
|
459
|
+
Category string
|
460
|
+
"""
|
461
|
+
error_type = failure_event.context.get("error_type", "unknown")
|
462
|
+
task_type = failure_event.task_type
|
463
|
+
|
464
|
+
# Map error types to categories
|
465
|
+
category_map = {
|
466
|
+
"test-failure": "Testing",
|
467
|
+
"syntax-error": "Code Quality",
|
468
|
+
"type-error": "Code Quality",
|
469
|
+
"import-error": "Dependencies",
|
470
|
+
"module-not-found": "Dependencies",
|
471
|
+
"file-not-found": "File Management",
|
472
|
+
"command-error": "Configuration",
|
473
|
+
"error": "Error Handling",
|
474
|
+
"exception": "Error Handling",
|
475
|
+
}
|
476
|
+
|
477
|
+
# Try error type first
|
478
|
+
if error_type in category_map:
|
479
|
+
return category_map[error_type]
|
480
|
+
|
481
|
+
# Try task type
|
482
|
+
task_category_map = {
|
483
|
+
"test": "Testing",
|
484
|
+
"build": "Build Process",
|
485
|
+
"lint": "Code Quality",
|
486
|
+
"git": "Version Control",
|
487
|
+
"install": "Dependencies",
|
488
|
+
}
|
489
|
+
|
490
|
+
if task_type in task_category_map:
|
491
|
+
return task_category_map[task_type]
|
492
|
+
|
493
|
+
return "General"
|
494
|
+
|
495
|
+
def _extract_problem_description(self, failure_event: FailureEvent) -> str:
|
496
|
+
"""Extract a concise problem description from the failure.
|
497
|
+
|
498
|
+
Args:
|
499
|
+
failure_event: The failure event
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
Problem description string
|
503
|
+
"""
|
504
|
+
error_msg = failure_event.error_message
|
505
|
+
task_type = failure_event.task_type
|
506
|
+
|
507
|
+
# Truncate long error messages
|
508
|
+
if len(error_msg) > 100:
|
509
|
+
error_msg = error_msg[:97] + "..."
|
510
|
+
|
511
|
+
return f"{task_type.capitalize()} failed: {error_msg}"
|
512
|
+
|
513
|
+
def _extract_solution_description(
|
514
|
+
self, fix_event: FixEvent, failure_event: FailureEvent
|
515
|
+
) -> str:
|
516
|
+
"""Extract a solution description from the fix.
|
517
|
+
|
518
|
+
WHY: We want to capture what changed between failure and fix.
|
519
|
+
For MVP, we use a simple heuristic based on the time gap.
|
520
|
+
|
521
|
+
Args:
|
522
|
+
fix_event: The fix event
|
523
|
+
failure_event: The failure event
|
524
|
+
|
525
|
+
Returns:
|
526
|
+
Solution description string
|
527
|
+
"""
|
528
|
+
# Calculate time between failure and fix
|
529
|
+
time_delta = fix_event.timestamp - failure_event.timestamp
|
530
|
+
time_str = f"{int(time_delta.total_seconds())}s"
|
531
|
+
|
532
|
+
# Generic solution description for MVP
|
533
|
+
# In future versions, this could analyze git diff, file changes, etc.
|
534
|
+
return f"Fixed after {time_str} - verified with successful {fix_event.task_type} execution"
|
535
|
+
|
536
|
+
|
537
|
+
# Singleton instance for session-level tracking
|
538
|
+
_tracker_instance: Optional[FailureTracker] = None
|
539
|
+
|
540
|
+
|
541
|
+
def get_failure_tracker() -> FailureTracker:
|
542
|
+
"""Get or create the singleton FailureTracker instance.
|
543
|
+
|
544
|
+
WHY: Session-level tracking requires a singleton to maintain state
|
545
|
+
across multiple hook invocations during the same session.
|
546
|
+
|
547
|
+
Returns:
|
548
|
+
The FailureTracker singleton instance
|
549
|
+
"""
|
550
|
+
global _tracker_instance
|
551
|
+
if _tracker_instance is None:
|
552
|
+
_tracker_instance = FailureTracker()
|
553
|
+
return _tracker_instance
|
554
|
+
|
555
|
+
|
556
|
+
def reset_failure_tracker() -> None:
|
557
|
+
"""Reset the failure tracker (for testing or session restart).
|
558
|
+
|
559
|
+
WHY: Tests need to reset state between runs. Also useful for
|
560
|
+
explicitly starting a new tracking session.
|
561
|
+
"""
|
562
|
+
global _tracker_instance
|
563
|
+
_tracker_instance = None
|
@@ -95,6 +95,9 @@ class MemoryHookService(BaseService, MemoryHookInterface):
|
|
95
95
|
# Register kuzu-memory hooks if available
|
96
96
|
self._register_kuzu_memory_hooks()
|
97
97
|
|
98
|
+
# Register failure-learning hooks
|
99
|
+
self._register_failure_learning_hooks()
|
100
|
+
|
98
101
|
except Exception as e:
|
99
102
|
self.logger.warning(f"Failed to register memory hooks: {e}")
|
100
103
|
|
@@ -177,6 +180,79 @@ class MemoryHookService(BaseService, MemoryHookInterface):
|
|
177
180
|
except Exception as e:
|
178
181
|
self.logger.warning(f"Failed to register kuzu-memory hooks: {e}")
|
179
182
|
|
183
|
+
def _register_failure_learning_hooks(self):
|
184
|
+
"""Register failure-learning hooks for automatic learning extraction.
|
185
|
+
|
186
|
+
WHY: When tasks fail and agents fix them, we want to automatically capture
|
187
|
+
this as a learning. The failure-learning system provides:
|
188
|
+
1. Failure detection from tool outputs (errors, exceptions, test failures)
|
189
|
+
2. Fix detection when same task type succeeds after failure
|
190
|
+
3. Learning extraction and persistence to agent memory files
|
191
|
+
|
192
|
+
DESIGN DECISION: These hooks work as a chain with specific priorities:
|
193
|
+
- FailureDetectionHook (priority=85): Detects failures after tool execution
|
194
|
+
- FixDetectionHook (priority=87): Matches fixes with failures
|
195
|
+
- LearningExtractionHook (priority=89): Extracts and persists learnings
|
196
|
+
|
197
|
+
The system is enabled by default but can be disabled via configuration.
|
198
|
+
"""
|
199
|
+
try:
|
200
|
+
# Check if failure-learning is enabled in config
|
201
|
+
from claude_mpm.core.config import Config
|
202
|
+
|
203
|
+
config = Config()
|
204
|
+
failure_learning_config = config.get("memory.failure_learning", {})
|
205
|
+
|
206
|
+
if isinstance(failure_learning_config, dict):
|
207
|
+
enabled = failure_learning_config.get("enabled", True)
|
208
|
+
else:
|
209
|
+
# Default to enabled if config section doesn't exist
|
210
|
+
enabled = True
|
211
|
+
|
212
|
+
if not enabled:
|
213
|
+
self.logger.debug("Failure-learning disabled in configuration")
|
214
|
+
return
|
215
|
+
|
216
|
+
# Import failure-learning hooks
|
217
|
+
from claude_mpm.hooks.failure_learning import (
|
218
|
+
get_failure_detection_hook,
|
219
|
+
get_fix_detection_hook,
|
220
|
+
get_learning_extraction_hook,
|
221
|
+
)
|
222
|
+
|
223
|
+
# Get hook instances
|
224
|
+
failure_hook = get_failure_detection_hook()
|
225
|
+
fix_hook = get_fix_detection_hook()
|
226
|
+
learning_hook = get_learning_extraction_hook()
|
227
|
+
|
228
|
+
# Register hooks in priority order
|
229
|
+
success1 = self.hook_service.register_hook(failure_hook)
|
230
|
+
success2 = self.hook_service.register_hook(fix_hook)
|
231
|
+
success3 = self.hook_service.register_hook(learning_hook)
|
232
|
+
|
233
|
+
if success1:
|
234
|
+
self.registered_hooks.append("failure_detection")
|
235
|
+
self.logger.debug("✅ Failure detection enabled")
|
236
|
+
|
237
|
+
if success2:
|
238
|
+
self.registered_hooks.append("fix_detection")
|
239
|
+
self.logger.debug("✅ Fix detection enabled")
|
240
|
+
|
241
|
+
if success3:
|
242
|
+
self.registered_hooks.append("learning_extraction")
|
243
|
+
self.logger.debug("✅ Learning extraction enabled")
|
244
|
+
|
245
|
+
if success1 and success2 and success3:
|
246
|
+
self.logger.info(
|
247
|
+
"✅ Failure-learning system enabled "
|
248
|
+
"(failures → fixes → learnings → memory)"
|
249
|
+
)
|
250
|
+
|
251
|
+
except ImportError as e:
|
252
|
+
self.logger.debug(f"Failure-learning hooks not available: {e}")
|
253
|
+
except Exception as e:
|
254
|
+
self.logger.warning(f"Failed to register failure-learning hooks: {e}")
|
255
|
+
|
180
256
|
def _load_relevant_memories_hook(self, context):
|
181
257
|
"""Hook function to load relevant memories before Claude interaction.
|
182
258
|
|