style(memory): apply ruff formatting and linting fixes

Auto-fixed linting errors and formatting issues:
- Removed unused imports (F401): pytest, Any, AnalysisType, MemoryType, OutcomeType
- Removed unused variable (F841): hooks variable in test
- Applied consistent formatting across memory service and test files

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 14:07:48 +01:00
parent e3fe0439fd
commit cf6291ac8e
17 changed files with 236 additions and 185 deletions

View File

@@ -344,7 +344,12 @@ class BudgetAllocator:
Rebalanced budget
"""
if prioritize is None:
prioritize = [ContextType.KNOWLEDGE, ContextType.MEMORY, ContextType.TASK, ContextType.SYSTEM]
prioritize = [
ContextType.KNOWLEDGE,
ContextType.MEMORY,
ContextType.TASK,
ContextType.SYSTEM,
]
# Calculate unused tokens per type
unused: dict[str, int] = {}

View File

@@ -50,7 +50,9 @@ class CacheStats:
"embedding_cache": self.embedding_cache,
"retrieval_cache": self.retrieval_cache,
"overall_hit_rate": self.overall_hit_rate,
"last_cleanup": self.last_cleanup.isoformat() if self.last_cleanup else None,
"last_cleanup": self.last_cleanup.isoformat()
if self.last_cleanup
else None,
"cleanup_count": self.cleanup_count,
}
@@ -104,7 +106,8 @@ class CacheManager:
else:
self._embedding_cache = create_embedding_cache(
max_size=self._settings.cache_max_items,
default_ttl_seconds=self._settings.cache_ttl_seconds * 12, # 1hr for embeddings
default_ttl_seconds=self._settings.cache_ttl_seconds
* 12, # 1hr for embeddings
redis=redis,
)
@@ -271,7 +274,9 @@ class CacheManager:
# Invalidate retrieval cache
if self._retrieval_cache:
uuid_id = UUID(str(memory_id)) if not isinstance(memory_id, UUID) else memory_id
uuid_id = (
UUID(str(memory_id)) if not isinstance(memory_id, UUID) else memory_id
)
count += self._retrieval_cache.invalidate_by_memory(uuid_id)
logger.debug(f"Invalidated {count} cache entries for {memory_type}:{memory_id}")

View File

@@ -405,9 +405,7 @@ class EmbeddingCache:
count = 0
with self._lock:
keys_to_remove = [
k for k, v in self._cache.items() if v.model == model
]
keys_to_remove = [k for k, v in self._cache.items() if v.model == model]
for key in keys_to_remove:
del self._cache[key]
count += 1
@@ -454,9 +452,7 @@ class EmbeddingCache:
Number of entries removed
"""
with self._lock:
keys_to_remove = [
k for k, v in self._cache.items() if v.is_expired()
]
keys_to_remove = [k for k, v in self._cache.items() if v.is_expired()]
for key in keys_to_remove:
del self._cache[key]
self._stats.expirations += 1

View File

@@ -384,9 +384,7 @@ class HotMemoryCache[T]:
Number of entries removed
"""
with self._lock:
keys_to_remove = [
k for k, v in self._cache.items() if v.is_expired()
]
keys_to_remove = [k for k, v in self._cache.items() if v.is_expired()]
for key in keys_to_remove:
del self._cache[key]
self._stats.expirations += 1

View File

@@ -321,10 +321,7 @@ class MemoryContextSource:
min_confidence=min_relevance,
)
return [
MemoryContext.from_semantic_memory(fact, query=query)
for fact in facts
]
return [MemoryContext.from_semantic_memory(fact, query=query) for fact in facts]
async def _fetch_procedural(
self,

View File

@@ -287,7 +287,9 @@ class AgentLifecycleManager:
# Get all current state
all_keys = await working.list_keys()
# Filter out checkpoint keys
state_keys = [k for k in all_keys if not k.startswith(self.CHECKPOINT_PREFIX)]
state_keys = [
k for k in all_keys if not k.startswith(self.CHECKPOINT_PREFIX)
]
state: dict[str, Any] = {}
for key in state_keys:
@@ -483,7 +485,9 @@ class AgentLifecycleManager:
# Gather session state for consolidation
all_keys = await working.list_keys()
state_keys = [k for k in all_keys if not k.startswith(self.CHECKPOINT_PREFIX)]
state_keys = [
k for k in all_keys if not k.startswith(self.CHECKPOINT_PREFIX)
]
session_state: dict[str, Any] = {}
for key in state_keys:
@@ -597,14 +601,16 @@ class AgentLifecycleManager:
for key in all_keys:
if key.startswith(self.CHECKPOINT_PREFIX):
checkpoint_id = key[len(self.CHECKPOINT_PREFIX):]
checkpoint_id = key[len(self.CHECKPOINT_PREFIX) :]
checkpoint = await working.get(key)
if checkpoint:
checkpoints.append({
"checkpoint_id": checkpoint_id,
"timestamp": checkpoint.get("timestamp"),
"keys_count": checkpoint.get("keys_count", 0),
})
checkpoints.append(
{
"checkpoint_id": checkpoint_id,
"timestamp": checkpoint.get("timestamp"),
"keys_count": checkpoint.get("keys_count", 0),
}
)
# Sort by timestamp (newest first)
checkpoints.sort(

View File

@@ -414,12 +414,14 @@ class MemoryToolService:
if args.query.lower() in key.lower():
value = await working.get(key)
if value is not None:
results.append({
"type": "working",
"key": key,
"content": str(value),
"relevance": 1.0,
})
results.append(
{
"type": "working",
"key": key,
"content": str(value),
"relevance": 1.0,
}
)
elif memory_type == MemoryType.EPISODIC:
episodic = await self._get_episodic()
@@ -430,14 +432,18 @@ class MemoryToolService:
agent_instance_id=context.agent_instance_id,
)
for episode in episodes:
results.append({
"type": "episodic",
"id": str(episode.id),
"summary": episode.task_description,
"outcome": episode.outcome.value if episode.outcome else None,
"occurred_at": episode.occurred_at.isoformat(),
"relevance": episode.importance_score,
})
results.append(
{
"type": "episodic",
"id": str(episode.id),
"summary": episode.task_description,
"outcome": episode.outcome.value
if episode.outcome
else None,
"occurred_at": episode.occurred_at.isoformat(),
"relevance": episode.importance_score,
}
)
elif memory_type == MemoryType.SEMANTIC:
semantic = await self._get_semantic()
@@ -448,15 +454,17 @@ class MemoryToolService:
min_confidence=args.min_relevance,
)
for fact in facts:
results.append({
"type": "semantic",
"id": str(fact.id),
"subject": fact.subject,
"predicate": fact.predicate,
"object": fact.object,
"confidence": fact.confidence,
"relevance": fact.confidence,
})
results.append(
{
"type": "semantic",
"id": str(fact.id),
"subject": fact.subject,
"predicate": fact.predicate,
"object": fact.object,
"confidence": fact.confidence,
"relevance": fact.confidence,
}
)
elif memory_type == MemoryType.PROCEDURAL:
procedural = await self._get_procedural()
@@ -467,15 +475,17 @@ class MemoryToolService:
limit=args.limit,
)
for proc in procedures:
results.append({
"type": "procedural",
"id": str(proc.id),
"name": proc.name,
"trigger": proc.trigger_pattern,
"success_rate": proc.success_rate,
"steps_count": len(proc.steps) if proc.steps else 0,
"relevance": proc.success_rate,
})
results.append(
{
"type": "procedural",
"id": str(proc.id),
"name": proc.name,
"trigger": proc.trigger_pattern,
"success_rate": proc.success_rate,
"steps_count": len(proc.steps) if proc.steps else 0,
"relevance": proc.success_rate,
}
)
# Sort by relevance and limit
results.sort(key=lambda x: x.get("relevance", 0), reverse=True)
@@ -601,7 +611,11 @@ class MemoryToolService:
if ep.task_type:
task_types[ep.task_type] = task_types.get(ep.task_type, 0) + 1
if ep.outcome:
outcome_val = ep.outcome.value if hasattr(ep.outcome, "value") else str(ep.outcome)
outcome_val = (
ep.outcome.value
if hasattr(ep.outcome, "value")
else str(ep.outcome)
)
outcomes[outcome_val] = outcomes.get(outcome_val, 0) + 1
# Sort by frequency
@@ -613,11 +627,13 @@ class MemoryToolService:
examples = []
if args.include_examples:
for ep in episodes[: min(3, args.max_items)]:
examples.append({
"summary": ep.task_description,
"task_type": ep.task_type,
"outcome": ep.outcome.value if ep.outcome else None,
})
examples.append(
{
"summary": ep.task_description,
"task_type": ep.task_type,
"outcome": ep.outcome.value if ep.outcome else None,
}
)
return {
"analysis_type": "recent_patterns",
@@ -661,11 +677,13 @@ class MemoryToolService:
examples = []
if args.include_examples:
for ep in successful[: min(3, args.max_items)]:
examples.append({
"summary": ep.task_description,
"task_type": ep.task_type,
"lessons": ep.lessons_learned,
})
examples.append(
{
"summary": ep.task_description,
"task_type": ep.task_type,
"lessons": ep.lessons_learned,
}
)
return {
"analysis_type": "success_factors",
@@ -694,9 +712,7 @@ class MemoryToolService:
failure_by_task[task].append(ep)
# Most common failure types
failure_counts = {
task: len(eps) for task, eps in failure_by_task.items()
}
failure_counts = {task: len(eps) for task, eps in failure_by_task.items()}
top_failures = sorted(failure_counts.items(), key=lambda x: x[1], reverse=True)[
: args.max_items
]
@@ -704,12 +720,14 @@ class MemoryToolService:
examples = []
if args.include_examples:
for ep in failed[: min(3, args.max_items)]:
examples.append({
"summary": ep.task_description,
"task_type": ep.task_type,
"lessons": ep.lessons_learned,
"error": ep.outcome_details,
})
examples.append(
{
"summary": ep.task_description,
"task_type": ep.task_type,
"lessons": ep.lessons_learned,
"error": ep.outcome_details,
}
)
return {
"analysis_type": "failure_patterns",
@@ -794,15 +812,21 @@ class MemoryToolService:
insights = []
if top_tasks:
insights.append(f"Most common task type: {top_tasks[0][0]} ({top_tasks[0][1]} occurrences)")
insights.append(
f"Most common task type: {top_tasks[0][0]} ({top_tasks[0][1]} occurrences)"
)
total = sum(outcome_dist.values())
if total > 0:
success_rate = outcome_dist.get("success", 0) / total
if success_rate > 0.8:
insights.append("High success rate observed - current approach is working well")
insights.append(
"High success rate observed - current approach is working well"
)
elif success_rate < 0.5:
insights.append("Success rate below 50% - consider reviewing procedures")
insights.append(
"Success rate below 50% - consider reviewing procedures"
)
return insights
@@ -839,9 +863,13 @@ class MemoryToolService:
if top_failures:
worst_task, count = top_failures[0]
tips.append(f"'{worst_task}' has most failures ({count}) - needs procedure review")
tips.append(
f"'{worst_task}' has most failures ({count}) - needs procedure review"
)
tips.append("Review lessons_learned from past failures before attempting similar tasks")
tips.append(
"Review lessons_learned from past failures before attempting similar tasks"
)
return tips
@@ -912,7 +940,11 @@ class MemoryToolService:
outcomes = {"success": 0, "failure": 0, "partial": 0, "abandoned": 0}
for ep in recent_episodes:
if ep.outcome:
key = ep.outcome.value if hasattr(ep.outcome, "value") else str(ep.outcome)
key = (
ep.outcome.value
if hasattr(ep.outcome, "value")
else str(ep.outcome)
)
if key in outcomes:
outcomes[key] += 1
@@ -942,7 +974,8 @@ class MemoryToolService:
# Filter by minimum success rate if specified
procedures = [
p for p in all_procedures
p
for p in all_procedures
if args.min_success_rate is None or p.success_rate >= args.min_success_rate
][: args.limit]

View File

@@ -441,9 +441,7 @@ class MemoryMetrics:
# Get hits/misses by cache type
for labels_str, hits in self._counters["memory_cache_hits_total"].items():
cache_type = self._parse_labels(labels_str).get(
"cache_type", "unknown"
)
cache_type = self._parse_labels(labels_str).get("cache_type", "unknown")
if cache_type not in stats:
stats[cache_type] = {"hits": 0, "misses": 0}
stats[cache_type]["hits"] = hits
@@ -451,9 +449,7 @@ class MemoryMetrics:
for labels_str, misses in self._counters[
"memory_cache_misses_total"
].items():
cache_type = self._parse_labels(labels_str).get(
"cache_type", "unknown"
)
cache_type = self._parse_labels(labels_str).get("cache_type", "unknown")
if cache_type not in stats:
stats[cache_type] = {"hits": 0, "misses": 0}
stats[cache_type]["misses"] = misses

View File

@@ -149,8 +149,7 @@ class MemoryReflection:
# Filter to time range
episodes = [
e for e in episodes
if time_range.start <= e.occurred_at <= time_range.end
e for e in episodes if time_range.start <= e.occurred_at <= time_range.end
]
if not episodes:
@@ -313,7 +312,9 @@ class MemoryReflection:
f"Task type '{task_type}': {success_rate:.0%} success rate, "
f"avg {avg_duration:.1f}s duration, {avg_tokens:.0f} tokens"
),
confidence=min(1.0, stats["total"] / 10), # Higher sample = higher confidence
confidence=min(
1.0, stats["total"] / 10
), # Higher sample = higher confidence
occurrence_count=stats["total"],
episode_ids=[e.id for e in stats["episodes"]],
first_seen=min(e.occurred_at for e in stats["episodes"]),
@@ -397,7 +398,9 @@ class MemoryReflection:
failed = [e for e in episodes if e.outcome == Outcome.FAILURE]
if len(successful) >= 3 and len(failed) >= 3:
avg_success_duration = statistics.mean(e.duration_seconds for e in successful)
avg_success_duration = statistics.mean(
e.duration_seconds for e in successful
)
avg_failure_duration = statistics.mean(e.duration_seconds for e in failed)
if avg_failure_duration > avg_success_duration * 1.5:
@@ -409,7 +412,7 @@ class MemoryReflection:
description=(
f"Failed tasks average {avg_failure_duration:.1f}s vs "
f"{avg_success_duration:.1f}s for successful tasks "
f"({avg_failure_duration/avg_success_duration:.1f}x longer)"
f"({avg_failure_duration / avg_success_duration:.1f}x longer)"
),
confidence=0.8,
occurrence_count=len(successful) + len(failed),
@@ -427,9 +430,15 @@ class MemoryReflection:
# Analyze token efficiency
if len(successful) >= 3:
avg_tokens = statistics.mean(e.tokens_used for e in successful)
std_tokens = statistics.stdev(e.tokens_used for e in successful) if len(successful) > 1 else 0
std_tokens = (
statistics.stdev(e.tokens_used for e in successful)
if len(successful) > 1
else 0
)
efficient = [e for e in successful if e.tokens_used < avg_tokens - std_tokens]
efficient = [
e for e in successful if e.tokens_used < avg_tokens - std_tokens
]
if len(efficient) >= self._config.min_pattern_occurrences:
patterns.append(
Pattern(
@@ -508,8 +517,7 @@ class MemoryReflection:
# Filter to time range
episodes = [
e for e in episodes
if time_range.start <= e.occurred_at <= time_range.end
e for e in episodes if time_range.start <= e.occurred_at <= time_range.end
]
if len(episodes) < self._config.min_sample_size_for_factor:
@@ -652,9 +660,7 @@ class MemoryReflection:
avg_success_duration = statistics.mean(
e.duration_seconds for e in successful
)
avg_failure_duration = statistics.mean(
e.duration_seconds for e in failed
)
avg_failure_duration = statistics.mean(e.duration_seconds for e in failed)
if avg_success_duration > 0:
duration_ratio = avg_failure_duration / avg_success_duration
@@ -837,7 +843,9 @@ class MemoryReflection:
baseline_durations = [e.duration_seconds for e in baseline]
baseline_mean = statistics.mean(baseline_durations)
baseline_std = statistics.stdev(baseline_durations) if len(baseline_durations) > 1 else 0
baseline_std = (
statistics.stdev(baseline_durations) if len(baseline_durations) > 1 else 0
)
if baseline_std == 0:
return anomalies
@@ -997,7 +1005,10 @@ class MemoryReflection:
) / len(recent)
# Detect significant failure rate increase
if recent_failure_rate > baseline_failure_rate * 1.5 and recent_failure_rate > 0.3:
if (
recent_failure_rate > baseline_failure_rate * 1.5
and recent_failure_rate > 0.3
):
rate_increase = recent_failure_rate / max(baseline_failure_rate, 0.01)
anomalies.append(
@@ -1074,14 +1085,11 @@ class MemoryReflection:
insights.extend(self._insights_from_anomalies(anomalies))
# Generate cross-cutting insights
insights.extend(
self._generate_cross_insights(patterns, factors, anomalies)
)
insights.extend(self._generate_cross_insights(patterns, factors, anomalies))
# Filter by confidence and sort by priority
insights = [
i for i in insights
if i.confidence >= self._config.min_insight_confidence
i for i in insights if i.confidence >= self._config.min_insight_confidence
]
insights.sort(key=lambda i: -i.priority)
@@ -1182,9 +1190,7 @@ class MemoryReflection:
source_patterns=[],
source_factors=[f.id for f in top_positive],
source_anomalies=[],
recommended_actions=[
f"Reinforce: {f.name}" for f in top_positive
],
recommended_actions=[f"Reinforce: {f.name}" for f in top_positive],
generated_at=_utcnow(),
metadata={
"factors": [f.to_dict() for f in top_positive],
@@ -1200,17 +1206,16 @@ class MemoryReflection:
insight_type=InsightType.WARNING,
title="Factors correlating with failure",
description=(
"Risky factors: "
+ ", ".join(f.name for f in top_negative)
"Risky factors: " + ", ".join(f.name for f in top_negative)
),
priority=0.75,
confidence=statistics.mean(abs(f.correlation) for f in top_negative),
confidence=statistics.mean(
abs(f.correlation) for f in top_negative
),
source_patterns=[],
source_factors=[f.id for f in top_negative],
source_anomalies=[],
recommended_actions=[
f"Mitigate: {f.name}" for f in top_negative
],
recommended_actions=[f"Mitigate: {f.name}" for f in top_negative],
generated_at=_utcnow(),
metadata={
"factors": [f.to_dict() for f in top_negative],
@@ -1254,8 +1259,7 @@ class MemoryReflection:
)
failure_rate_anomalies = [
a for a in anomalies
if a.anomaly_type == AnomalyType.UNUSUAL_FAILURE_RATE
a for a in anomalies if a.anomaly_type == AnomalyType.UNUSUAL_FAILURE_RATE
]
if failure_rate_anomalies:
for anomaly in failure_rate_anomalies:
@@ -1295,7 +1299,13 @@ class MemoryReflection:
total_items = len(patterns) + len(factors) + len(anomalies)
if total_items > 0:
warning_count = (
len([p for p in patterns if p.pattern_type == PatternType.RECURRING_FAILURE])
len(
[
p
for p in patterns
if p.pattern_type == PatternType.RECURRING_FAILURE
]
)
+ len([a for a in anomalies if a.is_critical])
+ len([f for f in factors if f.correlation < -0.3])
)
@@ -1312,13 +1322,19 @@ class MemoryReflection:
f"Found {warning_count} warning indicators."
),
priority=0.6,
confidence=min(1.0, total_items / 20), # Higher sample = higher confidence
confidence=min(
1.0, total_items / 20
), # Higher sample = higher confidence
source_patterns=[p.id for p in patterns[:5]],
source_factors=[f.id for f in factors[:5]],
source_anomalies=[a.id for a in anomalies[:5]],
recommended_actions=(
["Continue current practices"] if health_score > 0.7
else ["Review warnings and address issues", "Focus on improvement areas"]
["Continue current practices"]
if health_score > 0.7
else [
"Review warnings and address issues",
"Focus on improvement areas",
]
),
generated_at=_utcnow(),
metadata={
@@ -1374,8 +1390,7 @@ class MemoryReflection:
agent_instance_id=agent_instance_id,
)
episodes_in_range = [
e for e in episodes
if time_range.start <= e.occurred_at <= time_range.end
e for e in episodes if time_range.start <= e.occurred_at <= time_range.end
]
# Run all analyses

View File

@@ -70,8 +70,7 @@ class TimeRange:
"""Create time range for last N hours."""
end = _utcnow()
start = datetime(
end.year, end.month, end.day, end.hour, end.minute, end.second,
tzinfo=UTC
end.year, end.month, end.day, end.hour, end.minute, end.second, tzinfo=UTC
) - __import__("datetime").timedelta(hours=hours)
return cls(start=start, end=end)

View File

@@ -5,8 +5,6 @@ from datetime import UTC, datetime
from unittest.mock import MagicMock
from uuid import uuid4
import pytest
from app.services.context.types import ContextType
from app.services.context.types.memory import MemoryContext, MemorySubtype

View File

@@ -133,9 +133,7 @@ class TestMemoryContextSource:
)
assert result.by_type["working"] == 2
assert all(
c.memory_subtype == MemorySubtype.WORKING for c in result.contexts
)
assert all(c.memory_subtype == MemorySubtype.WORKING for c in result.contexts)
@patch("app.services.memory.integration.context_source.EpisodicMemory")
async def test_fetch_episodic_memory(
@@ -252,11 +250,10 @@ class TestMemoryContextSource:
context_source: MemoryContextSource,
) -> None:
"""Results should be sorted by relevance score."""
with patch.object(
context_source, "_fetch_episodic"
) as mock_ep, patch.object(
context_source, "_fetch_semantic"
) as mock_sem:
with (
patch.object(context_source, "_fetch_episodic") as mock_ep,
patch.object(context_source, "_fetch_semantic") as mock_sem,
):
# Create contexts with different relevance scores
from app.services.context.types.memory import MemoryContext

View File

@@ -105,6 +105,7 @@ class TestLifecycleHooks:
def test_register_spawn_hook(self, lifecycle_hooks: LifecycleHooks) -> None:
"""Should register spawn hook."""
async def my_hook(event: LifecycleEvent) -> None:
pass
@@ -115,7 +116,7 @@ class TestLifecycleHooks:
def test_register_all_hooks(self, lifecycle_hooks: LifecycleHooks) -> None:
"""Should register hooks for all event types."""
hooks = [
[
lifecycle_hooks.on_spawn(AsyncMock()),
lifecycle_hooks.on_pause(AsyncMock()),
lifecycle_hooks.on_resume(AsyncMock()),

View File

@@ -2,7 +2,6 @@
"""Tests for MemoryToolService."""
from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import UUID, uuid4
@@ -14,11 +13,6 @@ from app.services.memory.mcp.service import (
ToolResult,
get_memory_tool_service,
)
from app.services.memory.mcp.tools import (
AnalysisType,
MemoryType,
OutcomeType,
)
from app.services.memory.types import Outcome
pytestmark = pytest.mark.asyncio(loop_scope="function")
@@ -192,7 +186,9 @@ class TestMemoryToolService:
context: ToolContext,
) -> None:
"""Remember should store in episodic memory."""
with patch("app.services.memory.mcp.service.EpisodicMemory") as mock_episodic_cls:
with patch(
"app.services.memory.mcp.service.EpisodicMemory"
) as mock_episodic_cls:
# Setup mock
mock_episode = MagicMock()
mock_episode.id = uuid4()
@@ -260,7 +256,9 @@ class TestMemoryToolService:
context: ToolContext,
) -> None:
"""Remember should store facts in semantic memory."""
with patch("app.services.memory.mcp.service.SemanticMemory") as mock_semantic_cls:
with patch(
"app.services.memory.mcp.service.SemanticMemory"
) as mock_semantic_cls:
mock_fact = MagicMock()
mock_fact.id = uuid4()
@@ -311,7 +309,9 @@ class TestMemoryToolService:
context: ToolContext,
) -> None:
"""Remember should store procedures in procedural memory."""
with patch("app.services.memory.mcp.service.ProceduralMemory") as mock_procedural_cls:
with patch(
"app.services.memory.mcp.service.ProceduralMemory"
) as mock_procedural_cls:
mock_procedure = MagicMock()
mock_procedure.id = uuid4()
@@ -530,15 +530,21 @@ class TestMemoryToolService:
mock_working_cls.for_session = AsyncMock(return_value=mock_working)
mock_episodic = AsyncMock()
mock_episodic.get_recent = AsyncMock(return_value=[MagicMock() for _ in range(10)])
mock_episodic.get_recent = AsyncMock(
return_value=[MagicMock() for _ in range(10)]
)
mock_episodic_cls.create = AsyncMock(return_value=mock_episodic)
mock_semantic = AsyncMock()
mock_semantic.search_facts = AsyncMock(return_value=[MagicMock() for _ in range(5)])
mock_semantic.search_facts = AsyncMock(
return_value=[MagicMock() for _ in range(5)]
)
mock_semantic_cls.create = AsyncMock(return_value=mock_semantic)
mock_procedural = AsyncMock()
mock_procedural.find_matching = AsyncMock(return_value=[MagicMock() for _ in range(3)])
mock_procedural.find_matching = AsyncMock(
return_value=[MagicMock() for _ in range(3)]
)
mock_procedural_cls.create = AsyncMock(return_value=mock_procedural)
result = await service.execute_tool(
@@ -603,8 +609,12 @@ class TestMemoryToolService:
) -> None:
"""Record outcome should store outcome and update procedure."""
with (
patch("app.services.memory.mcp.service.EpisodicMemory") as mock_episodic_cls,
patch("app.services.memory.mcp.service.ProceduralMemory") as mock_procedural_cls,
patch(
"app.services.memory.mcp.service.EpisodicMemory"
) as mock_episodic_cls,
patch(
"app.services.memory.mcp.service.ProceduralMemory"
) as mock_procedural_cls,
):
mock_episode = MagicMock()
mock_episode.id = uuid4()

View File

@@ -358,10 +358,12 @@ class TestMemoryToolDefinition:
)
# Valid args
validated = tool.validate_args({
"memory_type": "working",
"content": "Test content",
})
validated = tool.validate_args(
{
"memory_type": "working",
"content": "Test content",
}
)
assert isinstance(validated, RememberArgs)
# Invalid args
@@ -417,4 +419,6 @@ class TestToolDefinitions:
"""All tool schemas should have properties defined."""
for name, tool in MEMORY_TOOL_DEFINITIONS.items():
schema = tool.to_mcp_format()
assert "properties" in schema["inputSchema"], f"Tool {name} missing properties"
assert "properties" in schema["inputSchema"], (
f"Tool {name} missing properties"
)

View File

@@ -134,9 +134,7 @@ class TestMemoryMetrics:
await metrics.set_memory_items_count("semantic", "project", 50)
all_metrics = await metrics.get_all_metrics()
gauge_metrics = [
m for m in all_metrics if m.name == "memory_items_count"
]
gauge_metrics = [m for m in all_metrics if m.name == "memory_items_count"]
assert len(gauge_metrics) == 2
@@ -181,7 +179,11 @@ class TestMemoryMetrics:
all_metrics = await metrics.get_all_metrics()
count_metric = next(
(m for m in all_metrics if m.name == "memory_working_latency_seconds_count"),
(
m
for m in all_metrics
if m.name == "memory_working_latency_seconds_count"
),
None,
)
sum_metric = next(
@@ -204,9 +206,7 @@ class TestMemoryMetrics:
assert summary["avg_retrieval_latency_ms"] == pytest.approx(62.5, rel=0.01)
@pytest.mark.asyncio
async def test_observe_consolidation_duration(
self, metrics: MemoryMetrics
) -> None:
async def test_observe_consolidation_duration(self, metrics: MemoryMetrics) -> None:
"""Should record consolidation duration histogram."""
await metrics.observe_consolidation_duration(5.0)
await metrics.observe_consolidation_duration(10.0)
@@ -236,7 +236,9 @@ class TestMemoryMetrics:
assert len(all_metrics) >= 3
# Check we have different metric types
counter_metrics = [m for m in all_metrics if m.metric_type == MetricType.COUNTER]
counter_metrics = [
m for m in all_metrics if m.metric_type == MetricType.COUNTER
]
gauge_metrics = [m for m in all_metrics if m.metric_type == MetricType.GAUGE]
assert len(counter_metrics) >= 1

View File

@@ -153,8 +153,7 @@ class TestPatternDetection:
# Should find recurring success pattern for 'build' task
success_patterns = [
p for p in patterns
if p.pattern_type == PatternType.RECURRING_SUCCESS
p for p in patterns if p.pattern_type == PatternType.RECURRING_SUCCESS
]
assert len(success_patterns) >= 1
assert any(p.name.find("build") >= 0 for p in success_patterns)
@@ -193,8 +192,7 @@ class TestPatternDetection:
patterns = await reflection.analyze_patterns(project_id, time_range)
failure_patterns = [
p for p in patterns
if p.pattern_type == PatternType.RECURRING_FAILURE
p for p in patterns if p.pattern_type == PatternType.RECURRING_FAILURE
]
assert len(failure_patterns) >= 1
@@ -229,8 +227,7 @@ class TestPatternDetection:
patterns = await reflection.analyze_patterns(project_id, time_range)
action_patterns = [
p for p in patterns
if p.pattern_type == PatternType.ACTION_SEQUENCE
p for p in patterns if p.pattern_type == PatternType.ACTION_SEQUENCE
]
assert len(action_patterns) >= 1
@@ -438,8 +435,7 @@ class TestAnomalyDetection:
anomalies = await reflection.detect_anomalies(project_id, baseline_days=30)
duration_anomalies = [
a for a in anomalies
if a.anomaly_type == AnomalyType.UNUSUAL_DURATION
a for a in anomalies if a.anomaly_type == AnomalyType.UNUSUAL_DURATION
]
assert len(duration_anomalies) >= 1
@@ -475,8 +471,7 @@ class TestAnomalyDetection:
anomalies = await reflection.detect_anomalies(project_id, baseline_days=30)
outcome_anomalies = [
a for a in anomalies
if a.anomaly_type == AnomalyType.UNEXPECTED_OUTCOME
a for a in anomalies if a.anomaly_type == AnomalyType.UNEXPECTED_OUTCOME
]
assert len(outcome_anomalies) >= 1
@@ -510,8 +505,7 @@ class TestAnomalyDetection:
anomalies = await reflection.detect_anomalies(project_id, baseline_days=30)
token_anomalies = [
a for a in anomalies
if a.anomaly_type == AnomalyType.UNUSUAL_TOKEN_USAGE
a for a in anomalies if a.anomaly_type == AnomalyType.UNUSUAL_TOKEN_USAGE
]
assert len(token_anomalies) >= 1
@@ -650,9 +644,7 @@ class TestInsightGeneration:
insights = await reflection.generate_insights(project_id)
trend_insights = [
i for i in insights if i.insight_type == InsightType.TREND
]
trend_insights = [i for i in insights if i.insight_type == InsightType.TREND]
assert len(trend_insights) >= 1
async def test_insights_sorted_by_priority(
@@ -662,10 +654,7 @@ class TestInsightGeneration:
"""Should sort insights by priority."""
project_id = uuid4()
episodes = [
create_mock_episode(outcome=Outcome.SUCCESS)
for _ in range(10)
]
episodes = [create_mock_episode(outcome=Outcome.SUCCESS) for _ in range(10)]
mock_episodic = MagicMock()
mock_episodic.get_recent = AsyncMock(return_value=episodes)