Files
syndarix/backend/app/services/memory/mcp/tools.py
Felipe Cardoso 192237e69b fix(memory): unify Outcome enum and add ABANDONED support
- Add ABANDONED value to core Outcome enum in types.py
- Replace duplicate OutcomeType class in mcp/tools.py with alias to Outcome
- Simplify mcp/service.py to use outcome directly (no more silent mapping)
- Add migration 0006 to extend PostgreSQL episode_outcome enum
- Add missing constraints to migration 0005 (ix_facts_unique_triple_global)

This fixes the semantic issue where ABANDONED outcomes were silently
converted to FAILURE, losing information about task abandonment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 01:46:48 +01:00

486 lines
14 KiB
Python

# app/services/memory/mcp/tools.py
"""
MCP Tool Definitions for Agent Memory System.
Defines the schema and metadata for memory-related MCP tools.
These tools are invoked by AI agents to interact with the memory system.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
# OutcomeType alias - uses core Outcome enum from types module for consistency
from app.services.memory.types import Outcome as OutcomeType
class MemoryType(str, Enum):
"""Types of memory for storage operations."""
WORKING = "working"
EPISODIC = "episodic"
SEMANTIC = "semantic"
PROCEDURAL = "procedural"
class AnalysisType(str, Enum):
"""Types of pattern analysis for the reflect tool."""
RECENT_PATTERNS = "recent_patterns"
SUCCESS_FACTORS = "success_factors"
FAILURE_PATTERNS = "failure_patterns"
COMMON_PROCEDURES = "common_procedures"
LEARNING_PROGRESS = "learning_progress"
# ============================================================================
# Tool Argument Schemas (Pydantic models for validation)
# ============================================================================
class RememberArgs(BaseModel):
"""Arguments for the 'remember' tool."""
memory_type: MemoryType = Field(
...,
description="Type of memory to store in: working, episodic, semantic, or procedural",
)
content: str = Field(
...,
description="The content to remember. Can be text, facts, or procedure steps.",
min_length=1,
max_length=10000,
)
key: str | None = Field(
None,
description="Optional key for working memory entries. Required for working memory type.",
max_length=256,
)
importance: float = Field(
0.5,
description="Importance score from 0.0 (low) to 1.0 (critical)",
ge=0.0,
le=1.0,
)
ttl_seconds: int | None = Field(
None,
description="Time-to-live in seconds for working memory. None for permanent storage.",
ge=1,
le=86400 * 30, # Max 30 days
)
metadata: dict[str, Any] = Field(
default_factory=dict,
description="Additional metadata to store with the memory",
)
# For semantic memory (facts)
subject: str | None = Field(
None,
description="Subject of the fact (for semantic memory)",
max_length=256,
)
predicate: str | None = Field(
None,
description="Predicate/relationship (for semantic memory)",
max_length=256,
)
object_value: str | None = Field(
None,
description="Object of the fact (for semantic memory)",
max_length=1000,
)
# For procedural memory
trigger: str | None = Field(
None,
description="Trigger condition for the procedure (for procedural memory)",
max_length=500,
)
steps: list[dict[str, Any]] | None = Field(
None,
description="Procedure steps as a list of action dictionaries",
)
class RecallArgs(BaseModel):
"""Arguments for the 'recall' tool."""
query: str = Field(
...,
description="Search query to find relevant memories",
min_length=1,
max_length=1000,
)
memory_types: list[MemoryType] = Field(
default_factory=lambda: [MemoryType.EPISODIC, MemoryType.SEMANTIC],
description="Types of memory to search in",
)
limit: int = Field(
10,
description="Maximum number of results to return",
ge=1,
le=100,
)
min_relevance: float = Field(
0.0,
description="Minimum relevance score (0.0-1.0) for results",
ge=0.0,
le=1.0,
)
filters: dict[str, Any] = Field(
default_factory=dict,
description="Additional filters (e.g., outcome, task_type, date range)",
)
include_context: bool = Field(
True,
description="Whether to include surrounding context in results",
)
class ForgetArgs(BaseModel):
"""Arguments for the 'forget' tool."""
memory_type: MemoryType = Field(
...,
description="Type of memory to remove from",
)
key: str | None = Field(
None,
description="Key to remove (for working memory)",
max_length=256,
)
memory_id: str | None = Field(
None,
description="Specific memory ID to remove (for episodic/semantic/procedural)",
)
pattern: str | None = Field(
None,
description="Pattern to match for bulk removal (use with caution)",
max_length=500,
)
confirm_bulk: bool = Field(
False,
description="Must be True to confirm bulk deletion when using pattern",
)
class ReflectArgs(BaseModel):
"""Arguments for the 'reflect' tool."""
analysis_type: AnalysisType = Field(
...,
description="Type of pattern analysis to perform",
)
scope: str | None = Field(
None,
description="Optional scope to limit analysis (e.g., task_type, time range)",
max_length=500,
)
depth: int = Field(
3,
description="Depth of analysis (1=surface, 5=deep)",
ge=1,
le=5,
)
include_examples: bool = Field(
True,
description="Whether to include example memories in the analysis",
)
max_items: int = Field(
10,
description="Maximum number of patterns/examples to analyze",
ge=1,
le=50,
)
class GetMemoryStatsArgs(BaseModel):
"""Arguments for the 'get_memory_stats' tool."""
include_breakdown: bool = Field(
True,
description="Include breakdown by memory type",
)
include_recent_activity: bool = Field(
True,
description="Include recent memory activity summary",
)
time_range_days: int = Field(
7,
description="Time range for activity analysis in days",
ge=1,
le=90,
)
class SearchProceduresArgs(BaseModel):
"""Arguments for the 'search_procedures' tool."""
trigger: str = Field(
...,
description="Trigger or situation to find procedures for",
min_length=1,
max_length=500,
)
task_type: str | None = Field(
None,
description="Optional task type to filter procedures",
max_length=100,
)
min_success_rate: float = Field(
0.5,
description="Minimum success rate (0.0-1.0) for returned procedures",
ge=0.0,
le=1.0,
)
limit: int = Field(
5,
description="Maximum number of procedures to return",
ge=1,
le=20,
)
include_steps: bool = Field(
True,
description="Whether to include detailed steps in the response",
)
class RecordOutcomeArgs(BaseModel):
"""Arguments for the 'record_outcome' tool."""
task_type: str = Field(
...,
description="Type of task that was executed",
min_length=1,
max_length=100,
)
outcome: OutcomeType = Field(
...,
description="Outcome of the task execution",
)
procedure_id: str | None = Field(
None,
description="ID of the procedure that was followed (if any)",
)
context: dict[str, Any] = Field(
default_factory=dict,
description="Context in which the task was executed",
)
lessons_learned: str | None = Field(
None,
description="What was learned from this execution",
max_length=2000,
)
duration_seconds: float | None = Field(
None,
description="How long the task took to execute",
ge=0.0,
)
error_details: str | None = Field(
None,
description="Details about any errors encountered (for failures)",
max_length=2000,
)
# ============================================================================
# Tool Definition Structure
# ============================================================================
@dataclass
class MemoryToolDefinition:
"""Definition of an MCP tool for the memory system."""
name: str
description: str
args_schema: type[BaseModel]
input_schema: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
"""Generate input schema from Pydantic model."""
if not self.input_schema:
self.input_schema = self.args_schema.model_json_schema()
def to_mcp_format(self) -> dict[str, Any]:
"""Convert to MCP tool format."""
return {
"name": self.name,
"description": self.description,
"inputSchema": self.input_schema,
}
def validate_args(self, args: dict[str, Any]) -> BaseModel:
"""Validate and parse arguments."""
return self.args_schema.model_validate(args)
# ============================================================================
# Tool Definitions
# ============================================================================
REMEMBER_TOOL = MemoryToolDefinition(
name="remember",
description="""Store information in the agent's memory system.
Use this tool to:
- Store temporary data in working memory (key-value with optional TTL)
- Record important events in episodic memory (automatically done on session end)
- Store facts/knowledge in semantic memory (subject-predicate-object triples)
- Save procedures in procedural memory (trigger conditions and steps)
Examples:
- Working memory: {"memory_type": "working", "key": "current_task", "content": "Implementing auth", "ttl_seconds": 3600}
- Semantic fact: {"memory_type": "semantic", "subject": "User", "predicate": "prefers", "object_value": "dark mode", "content": "User preference noted"}
- Procedure: {"memory_type": "procedural", "trigger": "When creating a new file", "steps": [{"action": "check_exists"}, {"action": "create"}], "content": "File creation procedure"}
""",
args_schema=RememberArgs,
)
RECALL_TOOL = MemoryToolDefinition(
name="recall",
description="""Retrieve information from the agent's memory system.
Use this tool to:
- Search for relevant past experiences (episodic)
- Look up known facts and knowledge (semantic)
- Find applicable procedures for current task (procedural)
- Get current session state (working)
The query supports semantic search - describe what you're looking for in natural language.
Examples:
- {"query": "How did I handle authentication errors before?", "memory_types": ["episodic"]}
- {"query": "What are the user's preferences?", "memory_types": ["semantic"], "limit": 5}
- {"query": "database connection", "memory_types": ["episodic", "semantic", "procedural"], "filters": {"outcome": "success"}}
""",
args_schema=RecallArgs,
)
FORGET_TOOL = MemoryToolDefinition(
name="forget",
description="""Remove information from the agent's memory system.
Use this tool to:
- Clear temporary working memory entries
- Remove specific memories by ID
- Bulk remove memories matching a pattern (requires confirmation)
WARNING: Deletion is permanent. Use with caution.
Examples:
- Working memory: {"memory_type": "working", "key": "temp_calculation"}
- Specific memory: {"memory_type": "episodic", "memory_id": "ep-123"}
- Bulk (requires confirm): {"memory_type": "working", "pattern": "cache_*", "confirm_bulk": true}
""",
args_schema=ForgetArgs,
)
REFLECT_TOOL = MemoryToolDefinition(
name="reflect",
description="""Analyze patterns in the agent's memory to gain insights.
Use this tool to:
- Identify patterns in recent work
- Understand what leads to success/failure
- Learn from past experiences
- Track learning progress over time
Analysis types:
- recent_patterns: What patterns appear in recent work
- success_factors: What conditions lead to success
- failure_patterns: What causes failures and how to avoid them
- common_procedures: Most frequently used procedures
- learning_progress: How knowledge has grown over time
Examples:
- {"analysis_type": "success_factors", "scope": "code_review", "depth": 3}
- {"analysis_type": "failure_patterns", "include_examples": true, "max_items": 5}
""",
args_schema=ReflectArgs,
)
GET_MEMORY_STATS_TOOL = MemoryToolDefinition(
name="get_memory_stats",
description="""Get statistics about the agent's memory usage.
Returns information about:
- Total memories stored by type
- Storage utilization
- Recent activity summary
- Memory health indicators
Use this to understand memory capacity and usage patterns.
Examples:
- {"include_breakdown": true, "include_recent_activity": true}
- {"time_range_days": 30, "include_breakdown": true}
""",
args_schema=GetMemoryStatsArgs,
)
SEARCH_PROCEDURES_TOOL = MemoryToolDefinition(
name="search_procedures",
description="""Find relevant procedures for a given situation.
Use this tool when you need to:
- Find the best way to handle a situation
- Look up proven approaches to problems
- Get step-by-step guidance for tasks
Returns procedures ranked by relevance and success rate.
Examples:
- {"trigger": "Deploying to production", "min_success_rate": 0.8}
- {"trigger": "Handling merge conflicts", "task_type": "git_operations", "limit": 3}
""",
args_schema=SearchProceduresArgs,
)
RECORD_OUTCOME_TOOL = MemoryToolDefinition(
name="record_outcome",
description="""Record the outcome of a task execution.
Use this tool after completing a task to:
- Update procedure success/failure rates
- Store lessons learned for future reference
- Improve procedure recommendations
This helps the memory system learn from experience.
Examples:
- {"task_type": "code_review", "outcome": "success", "lessons_learned": "Breaking changes caught early"}
- {"task_type": "deployment", "outcome": "failure", "error_details": "Database migration timeout", "lessons_learned": "Need to test migrations locally first"}
""",
args_schema=RecordOutcomeArgs,
)
# All tool definitions in a dictionary for easy lookup
MEMORY_TOOL_DEFINITIONS: dict[str, MemoryToolDefinition] = {
"remember": REMEMBER_TOOL,
"recall": RECALL_TOOL,
"forget": FORGET_TOOL,
"reflect": REFLECT_TOOL,
"get_memory_stats": GET_MEMORY_STATS_TOOL,
"search_procedures": SEARCH_PROCEDURES_TOOL,
"record_outcome": RECORD_OUTCOME_TOOL,
}
def get_all_tool_schemas() -> list[dict[str, Any]]:
"""Get MCP-formatted schemas for all memory tools."""
return [tool.to_mcp_format() for tool in MEMORY_TOOL_DEFINITIONS.values()]
def get_tool_definition(name: str) -> MemoryToolDefinition | None:
"""Get a specific tool definition by name."""
return MEMORY_TOOL_DEFINITIONS.get(name)