Files
lijiaoqiao/agent/instinct.py
Your Name c169d35c72 feat: add reviewer tool, lazy loading, manifest profiles, instinct learning, and hook profiles
Features:
- reviewer_tool: Multi-language code review (Python, JS/TS, Go, Rust, Java, C++, C#, PHP)
- lazy_loader: Context-triggered skill loading with 3-tier budget system
- manifest: Profile-based skill installation (coding, debugging, planning, shipping, research)
- instinct: Learning system that tracks patterns and predicts skill loading
- hook_profile: Security hook profiles (minimal, standard, strict, developer, rust-dev)
- hook_tool + hooks: Post-write hook execution system

Code Review Tool:
- Supports 9 languages with lint, typecheck, format, test, security checks
- Auto-detects language from file extensions
- Configurable tools per language (e.g., ruff, eslint, golangci-lint)

Lazy Loading:
- CONTEXT_TO_SKILLS mapping for 20+ context triggers
- Budget-aware loading (Tier 0: core, Tier 1: context, Tier 2: rare)
- Emergency mode at >90% context usage

Integration:
- Registered in model_tools.py and toolsets.py
- 8 language reviewers mapped in lazy_loader (python, go, rust, js, java, cpp, csharp, php)
2026-04-14 22:40:56 +08:00

509 lines
17 KiB
Python

"""
Instinct Learning System
Learns from successful interactions to improve tool/skill loading.
Tracks patterns of what tools and skills are used together,
and uses these patterns to predict future loading needs.
Inspired by ECC's experience tracking.
Usage:
from agent.instinct import InstinctLearner, get_instinct_learner
# Get the learner singleton
learner = get_instinct_learner()
# Record a successful interaction
learner.record_interaction(context="debug python code", tools=["terminal", "read_file"], skills=["systematic-debugging"])
# Get predicted skills for context
predictions = learner.predict_for_context("my python is broken")
"""
import json
import logging
import time
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Any
from dataclasses import dataclass, field, asdict
from collections import defaultdict
from functools import lru_cache
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__)
# =============================================================================
# Data Structures
# =============================================================================
@dataclass
class InteractionRecord:
"""Record of a successful interaction."""
timestamp: float
context: str
context_hash: str # For quick lookup
tools: List[str]
skills: List[str]
success: bool = True
feedback: Optional[str] = None # Optional user feedback
@classmethod
def create(cls, context: str, tools: List[str], skills: List[str],
feedback: Optional[str] = None) -> "InteractionRecord":
import hashlib
context_hash = hashlib.md5(context.lower().encode()).hexdigest()[:12]
return cls(
timestamp=time.time(),
context=context,
context_hash=context_hash,
tools=sorted(tools),
skills=sorted(skills),
feedback=feedback,
)
@dataclass
class PatternStats:
"""Statistics for a context pattern."""
count: int = 0
total_success: int = 0
avg_tools: float = 0.0
avg_skills: float = 0.0
last_used: float = 0.0
# Tool/skill co-occurrence counts
tool_counts: Dict[str, int] = field(default_factory=dict)
skill_counts: Dict[str, int] = field(default_factory=dict)
@property
def success_rate(self) -> float:
if self.count == 0:
return 0.0
return self.total_success / self.count
def to_dict(self) -> Dict:
return {
"count": self.count,
"total_success": self.total_success,
"avg_tools": self.avg_tools,
"avg_skills": self.avg_skills,
"last_used": self.last_used,
"tool_counts": self.tool_counts,
"skill_counts": self.skill_counts,
}
@classmethod
def from_dict(cls, data: Dict) -> "PatternStats":
return cls(
count=data.get("count", 0),
total_success=data.get("total_success", 0),
avg_tools=data.get("avg_tools", 0.0),
avg_skills=data.get("avg_skills", 0.0),
last_used=data.get("last_used", 0.0),
tool_counts=data.get("tool_counts", {}),
skill_counts=data.get("skill_counts", {}),
)
@dataclass
class InstinctProfile:
"""
Learned instincts for a context pattern.
Created by aggregating InteractionRecords.
"""
pattern: str # The context pattern/key
tools: List[str] # Most commonly used tools
skills: List[str] # Most commonly used skills
confidence: float # 0-1 based on sample size
sample_size: int
last_used: float
def to_dict(self) -> Dict:
return {
"pattern": self.pattern,
"tools": self.tools,
"skills": self.skills,
"confidence": self.confidence,
"sample_size": self.sample_size,
"last_used": self.last_used,
}
# =============================================================================
# Instinct Learner
# =============================================================================
class InstinctLearner:
"""
Learns from interactions to predict tool/skill loading.
Tracks patterns and builds instincts that can be used
to preload tools and skills before they're explicitly needed.
"""
def __init__(self, storage_path: Optional[Path] = None):
"""
Initialize the instinct learner.
Args:
storage_path: Path to store interaction data.
Defaults to ~/.hermes/instinct/
"""
if storage_path is None:
storage_path = get_hermes_home() / "instinct"
self.storage_path = storage_path
self.storage_path.mkdir(parents=True, exist_ok=True)
self.interactions_file = storage_path / "interactions.jsonl"
self.patterns_file = storage_path / "patterns.json"
self.instincts_file = storage_path / "instincts.json"
# In-memory cache
self._patterns: Dict[str, PatternStats] = {}
self._instincts: Dict[str, InstinctProfile] = {}
self._context_hints: Dict[str, List[str]] = {} # hint -> [pattern]
# Load existing data
self._load()
def _load(self) -> None:
"""Load patterns and instincts from disk."""
# Load patterns
if self.patterns_file.exists():
try:
data = json.loads(self.patterns_file.read_text())
self._patterns = {
k: PatternStats.from_dict(v) for k, v in data.items()
}
logger.info(f"Loaded {len(self._patterns)} patterns")
except Exception as e:
logger.warning(f"Failed to load patterns: {e}")
# Load instincts
if self.instincts_file.exists():
try:
data = json.loads(self.instincts_file.read_text())
self._instincts = {
k: InstinctProfile(**v) for k, v in data.items()
}
logger.info(f"Loaded {len(self._instincts)} instincts")
except Exception as e:
logger.warning(f"Failed to load instincts: {e}")
def _save(self) -> None:
"""Save patterns and instincts to disk."""
try:
# Save patterns
patterns_data = {k: v.to_dict() for k, v in self._patterns.items()}
self.patterns_file.write_text(json.dumps(patterns_data, indent=2))
# Save instincts
instincts_data = {k: v.to_dict() for k, v in self._instincts.items()}
self.instincts_file.write_text(json.dumps(instincts_data, indent=2))
logger.debug(f"Saved {len(self._patterns)} patterns, {len(self._instincts)} instincts")
except Exception as e:
logger.error(f"Failed to save instinct data: {e}")
def record_interaction(
self,
context: str,
tools: List[str],
skills: List[str],
success: bool = True,
feedback: Optional[str] = None,
) -> None:
"""
Record a successful interaction.
Args:
context: The conversation context
tools: Tools that were used
skills: Skills that were loaded
success: Whether the interaction was successful
feedback: Optional user feedback
"""
import re
# Create interaction record
record = InteractionRecord.create(context, tools, skills, feedback)
# Append to interactions log
with open(self.interactions_file, "a") as f:
f.write(json.dumps(asdict(record)) + "\n")
# Extract context hints (keywords)
hints = self._extract_hints(context)
# Update patterns for each hint
for hint in hints:
if hint not in self._patterns:
self._patterns[hint] = PatternStats()
stats = self._patterns[hint]
stats.count += 1
if success:
stats.total_success += 1
# Update running averages
stats.avg_tools = (stats.avg_tools * (stats.count - 1) + len(tools)) / stats.count
stats.avg_skills = (stats.avg_skills * (stats.count - 1) + len(skills)) / stats.count
stats.last_used = record.timestamp
# Update co-occurrence counts
for tool in tools:
stats.tool_counts[tool] = stats.tool_counts.get(tool, 0) + 1
for skill in skills:
stats.skill_counts[skill] = stats.skill_counts.get(skill, 0) + 1
# Update hint index
for hint in hints:
if hint not in self._context_hints:
self._context_hints[hint] = []
if record.context_hash not in self._context_hints[hint]:
self._context_hints[hint].append(record.context_hash)
# Rebuild instincts for affected patterns
self._rebuild_instincts(hints)
# Save periodically (every 10 interactions)
if sum(p.count for p in self._patterns.values()) % 10 == 0:
self._save()
def _extract_hints(self, context: str) -> List[str]:
"""Extract meaningful hints from context."""
import re
context_lower = context.lower()
# Extract words (3+ chars)
words = re.findall(r'\b\w{3,}\b', context_lower)
# Filter out common stop words
stop_words = {
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him',
'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two',
'who', 'boy', 'did', 'she', 'use', 'way', 'will', 'with', 'have',
'this', 'that', 'from', 'they', 'what', 'were', 'been', 'when',
'would', 'there', 'could', 'their', 'which', 'about', 'after',
'before', 'between', 'into', 'over', 'under', 'again', 'more',
'some', 'such', 'only', 'just', 'also', 'very', 'than', 'then',
'them', 'these', 'those', 'here', 'where', 'why', 'how', 'your',
}
hints = [w for w in words if w not in stop_words]
# Add bigrams for common patterns
bigrams = []
for i in range(len(words) - 1):
bigram = f"{words[i]}_{words[i+1]}"
bigrams.append(bigram)
hints.extend(bigrams)
return hints[:20] # Limit to top 20 hints
def _rebuild_instincts(self, hints: List[str]) -> None:
"""
Rebuild instincts for the given hints.
An instinct is a prediction for what tools/skills to load
based on learned patterns.
"""
for hint in hints:
stats = self._patterns.get(hint)
if not stats or stats.count < 2:
continue
# Calculate confidence based on sample size
confidence = min(1.0, stats.count / 10) # Max confidence at 10 samples
# Get top tools (appearing in >50% of interactions)
threshold = stats.count * 0.5
top_tools = [t for t, c in stats.tool_counts.items() if c >= threshold]
# Get top skills
top_skills = [s for s, c in stats.skill_counts.items() if c >= threshold]
if top_tools or top_skills:
self._instincts[hint] = InstinctProfile(
pattern=hint,
tools=top_tools[:5], # Max 5 tools
skills=top_skills[:5], # Max 5 skills
confidence=confidence,
sample_size=stats.count,
last_used=stats.last_used,
)
def predict_for_context(self, context: str, limit: int = 10) -> List[str]:
"""
Predict what to load for a context.
Args:
context: The conversation context
limit: Maximum number of predictions
Returns:
List of tool/skill paths to preload
"""
hints = self._extract_hints(context)
# Aggregate predictions from matching hints
tool_scores: Dict[str, float] = defaultdict(float)
skill_scores: Dict[str, float] = defaultdict(float)
for hint in hints:
instinct = self._instincts.get(hint)
if not instinct:
continue
# Weight by confidence
weight = instinct.confidence
for tool in instinct.tools:
tool_scores[tool] += weight
for skill in instinct.skills:
skill_scores[skill] += weight
# Combine and sort
predictions = []
for tool, score in sorted(tool_scores.items(), key=lambda x: -x[1])[:limit]:
predictions.append(tool)
for skill, score in sorted(skill_scores.items(), key=lambda x: -x[1])[:limit]:
if skill not in predictions:
predictions.append(skill)
return predictions[:limit]
def get_instinct_for_hint(self, hint: str) -> Optional[InstinctProfile]:
"""Get the instinct for a specific hint."""
return self._instincts.get(hint)
def get_stats(self) -> Dict[str, Any]:
"""Get learning statistics."""
total_interactions = sum(p.count for p in self._patterns.values())
return {
"total_patterns": len(self._patterns),
"total_instincts": len(self._instincts),
"total_interactions": total_interactions,
"avg_tools_per_interaction": sum(p.avg_tools for p in self._patterns.values()) / max(1, len(self._patterns)),
"avg_skills_per_interaction": sum(p.avg_skills for p in self._patterns.values()) / max(1, len(self._patterns)),
}
def clear_old_data(self, days: int = 30) -> int:
"""
Clear pattern data older than specified days.
Args:
days: Number of days to retain
Returns:
Number of patterns removed
"""
import time
cutoff = time.time() - (days * 86400)
removed = 0
for pattern in list(self._patterns.keys()):
if self._patterns[pattern].last_used < cutoff:
del self._patterns[pattern]
if pattern in self._instincts:
del self._instincts[pattern]
removed += 1
if removed > 0:
self._save()
return removed
def export_instincts(self) -> Dict[str, Any]:
"""Export all instincts for sharing/backup."""
return {
"version": "1.0.0",
"exported_at": time.time(),
"instincts": {k: v.to_dict() for k, v in self._instincts.items()},
"patterns": {k: v.to_dict() for k, v in self._patterns.items()},
}
def import_instincts(self, data: Dict) -> int:
"""
Import instincts from backup.
Args:
data: Exported instinct data
Returns:
Number of instincts imported
"""
count = 0
for k, v in data.get("instincts", {}).items():
try:
self._instincts[k] = InstinctProfile(**v)
count += 1
except Exception as e:
logger.warning(f"Failed to import instinct {k}: {e}")
for k, v in data.get("patterns", {}).items():
try:
self._patterns[k] = PatternStats.from_dict(v)
except Exception as e:
logger.warning(f"Failed to import pattern {k}: {e}")
if count > 0:
self._save()
return count
# =============================================================================
# Singleton
# =============================================================================
_instinct_learner: Optional[InstinctLearner] = None
def get_instinct_learner() -> InstinctLearner:
"""Get the singleton InstinctLearner instance."""
global _instinct_learner
if _instinct_learner is None:
_instinct_learner = InstinctLearner()
return _instinct_learner
def record_success(
context: str,
tools: List[str],
skills: List[str],
feedback: Optional[str] = None,
) -> None:
"""
Convenience function to record a successful interaction.
Args:
context: The conversation context
tools: Tools that were used
skills: Skills that were loaded
feedback: Optional user feedback
"""
get_instinct_learner().record_interaction(context, tools, skills, success=True, feedback=feedback)
def predict_loading(context: str, limit: int = 10) -> List[str]:
"""
Predict what to load for a context.
Args:
context: The conversation context
limit: Maximum number of predictions
Returns:
List of tool/skill paths to preload
"""
return get_instinct_learner().predict_for_context(context, limit)