hermes-agent/self_evolution/evolution_executor.py
玉冰 3cd384dc43 feat: add self-evolution plugin — agent self-optimization system
Add a comprehensive self-evolution system that enables Hermes Agent
to continuously improve through automated analysis and optimization:

Core components:
- reflection_engine: Nightly session analysis (1:00 AM)
- evolution_proposer: Generate improvement proposals from insights
- quality_scorer: Multi-dimensional session quality evaluation
- strategy_injector: Inject learned strategies into new sessions
- strategy_compressor: Strategy optimization and deduplication
- git_analyzer: Code change pattern analysis
- rule_engine: Pattern-based rule generation
- feishu_notifier: Feishu card notifications for evolution events

Storage:
- db.py: SQLite telemetry storage
- strategy_store: Persistent strategy storage
- models.py: Data models

Plugin integration:
- plugin.yaml, hooks.py, __init__.py for plugin system
- cron_jobs.py for scheduled tasks

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:40:13 +08:00

325 lines
12 KiB
Python

"""
Self Evolution Plugin — Evolution Executor
============================================
Executes approved evolution proposals with rollback support.
Design reference: Claude Code plugins/ralph-wiggum/
- Self-referential feedback loop: execute → verify → rollback if needed
- Each change has a "completion promise" (verification criteria)
- Iteration > Perfection
"""
from __future__ import annotations
import json
import logging
import time
import uuid
from pathlib import Path
from typing import Optional
from self_evolution import db
from self_evolution.models import Proposal, ImprovementUnit
logger = logging.getLogger(__name__)
from self_evolution.paths import DATA_DIR as STRATEGIES_DIR, STRATEGIES_FILE, ARCHIVE_DIR
from self_evolution.paths import SKILLS_DIR, MEMORIES_DIR
class EvolutionExecutor:
"""Execute approved evolution proposals.
Supported proposal types:
- skill: create a new skill via skill_manager_tool
- strategy: update strategy rules
- memory: update PERFORMANCE.md via memory_tool
- tool_preference: update tool preference config
"""
def execute(self, proposal: Proposal):
"""Execute an approved proposal."""
logger.info("Executing proposal: %s (%s)", proposal.id, proposal.proposal_type)
try:
match proposal.proposal_type:
case "skill":
self._create_skill(proposal)
case "strategy":
self._update_strategy(proposal)
case "memory":
self._update_memory(proposal)
case "tool_preference":
self._update_tool_preference(proposal)
case "code_improvement":
self._save_optimization_request(proposal)
# Mark as executed
db.update(
"evolution_proposals",
{"status": "executed", "resolved_at": time.time()},
where="id = ?",
where_params=(proposal.id,),
)
# Create improvement tracking unit
self._create_tracking_unit(proposal)
logger.info("Proposal %s executed successfully", proposal.id)
except Exception as exc:
logger.exception("Failed to execute proposal %s: %s", proposal.id, exc)
db.update(
"evolution_proposals",
{"status": "execution_failed", "resolved_at": time.time()},
where="id = ?",
where_params=(proposal.id,),
)
def check_and_rollback(self):
"""Check active improvement units and rollback if needed.
Called during dream consolidation to verify previous changes.
"""
units = db.fetch_all("improvement_units", where="status = 'active'")
for unit_data in units:
unit = ImprovementUnit(
id=unit_data["id"],
proposal_id=unit_data["proposal_id"],
change_type=unit_data["change_type"],
version=unit_data.get("version", 0),
baseline_score=unit_data.get("baseline_score", 0),
current_score=unit_data.get("current_score", 0),
sessions_sampled=unit_data.get("sessions_sampled", 0),
min_sessions=unit_data.get("min_sessions", 10),
min_improvement=unit_data.get("min_improvement", 0.05),
max_regression=unit_data.get("max_regression", 0.10),
)
# Update current score from recent sessions
self._update_unit_score(unit)
if unit.should_revert:
self._revert(unit)
logger.warning("Rolled back improvement unit %s", unit.id)
elif unit.should_promote:
self._promote(unit)
logger.info("Promoted improvement unit %s", unit.id)
# ── Proposal Type Handlers ────────────────────────────────────────────
def _create_skill(self, proposal: Proposal):
"""Create a new skill via the skill_manager_tool."""
from self_evolution.strategy_store import StrategyStore
store = StrategyStore()
skill_dir = SKILLS_DIR / proposal.id
skill_dir.mkdir(parents=True, exist_ok=True)
skill_content = (
f"---\n"
f"name: {proposal.id}\n"
f"description: {proposal.title}\n"
f"---\n\n"
f"{proposal.description}\n"
)
(skill_dir / "SKILL.md").write_text(skill_content, encoding="utf-8")
logger.info("Created learned skill: %s", skill_dir)
def _update_strategy(self, proposal: Proposal):
"""Update strategy rules file with version tracking."""
from self_evolution.strategy_store import StrategyStore
store = StrategyStore()
current = store.load()
# Check for duplicate strategies by title similarity
rules = current.get("rules", [])
existing_titles = {r.get("name", "").strip().lower() for r in rules}
if proposal.title.strip().lower() in existing_titles:
logger.warning("Skipping duplicate strategy: %s", proposal.title)
return
# Archive current version
version = current.get("version", 0) + 1
store.archive(version - 1)
# Parse new strategy from proposal description
new_strategy = {
"id": proposal.id,
"name": proposal.title,
"type": "learned",
"description": proposal.description,
"hint_text": proposal.description,
"conditions": [],
"severity": "medium",
"created_at": time.time(),
}
# Add to strategies
rules.append(new_strategy)
current["rules"] = rules
current["version"] = version
store.save(current)
logger.info("Updated strategies to version %d", version)
# Invalidate injector cache so new strategy takes effect immediately
from self_evolution.strategy_injector import invalidate_cache
invalidate_cache()
def _update_memory(self, proposal: Proposal):
"""Update PERFORMANCE.md via the memory system."""
perf_path = MEMORIES_DIR / "PERFORMANCE.md"
perf_path.parent.mkdir(parents=True, exist_ok=True)
existing = ""
if perf_path.exists():
existing = perf_path.read_text(encoding="utf-8")
# Append new entry
timestamp = time.strftime("%Y-%m-%d %H:%M", time.localtime())
entry = f"\n## [{timestamp}] 自动学习\n{proposal.description}\n"
# Keep file under reasonable size (last 50 entries)
entries = (existing + entry).split("\n## ")
if len(entries) > 50:
entries = entries[-50:]
perf_path.write_text("\n## ".join(entries), encoding="utf-8")
logger.info("Updated PERFORMANCE.md")
def _update_tool_preference(self, proposal: Proposal):
"""Update tool preference config."""
prefs_path = STRATEGIES_DIR / "tool_preferences.json"
prefs = {}
if prefs_path.exists():
prefs = json.loads(prefs_path.read_text(encoding="utf-8"))
prefs[proposal.id] = {
"description": proposal.description,
"expected_impact": proposal.expected_impact,
"created_at": time.time(),
}
prefs_path.write_text(
json.dumps(prefs, ensure_ascii=False, indent=2),
encoding="utf-8",
)
logger.info("Updated tool preferences: %s", proposal.id)
# ── Tracking & Verification ───────────────────────────────────────────
def _create_tracking_unit(self, proposal: Proposal):
"""Create an improvement tracking unit after execution.
Inspired by Ralph Wiggum's completion_promise pattern.
"""
# Get baseline score from recent sessions
recent = db.fetch_all(
"session_scores",
order_by="created_at DESC",
limit=10,
)
baseline = (
sum(s.get("composite_score", 0) for s in recent) / len(recent)
if recent else 0
)
unit = ImprovementUnit(
id=f"unit-{uuid.uuid4().hex[:8]}",
proposal_id=proposal.id,
change_type=proposal.proposal_type,
baseline_score=baseline,
min_sessions=10,
min_improvement=0.05,
max_regression=0.10,
)
db.insert("improvement_units", unit.to_db_row())
logger.info("Created tracking unit: %s (baseline=%.3f)", unit.id, baseline)
def _update_unit_score(self, unit: ImprovementUnit):
"""Update the current score for an improvement unit."""
# Count sessions since this unit was created
unit_data = db.fetch_one("improvement_units", where="id = ?", params=(unit.id,))
if not unit_data:
return
created_at = unit_data.get("created_at", 0)
recent = db.fetch_all(
"session_scores",
where="created_at >= ?",
params=(created_at,),
order_by="created_at DESC",
)
if recent:
current_score = sum(s.get("composite_score", 0) for s in recent) / len(recent)
sessions_sampled = len(recent)
db.update(
"improvement_units",
{
"current_score": current_score,
"sessions_sampled": sessions_sampled,
},
where="id = ?",
where_params=(unit.id,),
)
unit.current_score = current_score
unit.sessions_sampled = sessions_sampled
def _revert(self, unit: ImprovementUnit):
"""Revert a change by restoring the previous version."""
from self_evolution.strategy_store import StrategyStore
store = StrategyStore()
if unit.version > 0:
old = store.load_archive(unit.version - 1)
if old:
store.save(old)
db.update(
"improvement_units",
{"status": "reverted", "resolved_at": time.time()},
where="id = ?",
where_params=(unit.id,),
)
def _promote(self, unit: ImprovementUnit):
"""Promote an improvement unit from active to permanent."""
db.update(
"improvement_units",
{"status": "promoted", "resolved_at": time.time()},
where="id = ?",
where_params=(unit.id,),
)
# ── Code Improvement (save request document) ────────────────────────────
def _save_optimization_request(self, proposal: Proposal):
"""Save a code improvement request as a document.
Does NOT auto-modify code. The user reviews the request and decides
whether to implement changes manually or via Claude Code.
"""
req_dir = DATA_DIR / "optimization_requests"
req_dir.mkdir(parents=True, exist_ok=True)
doc_path = req_dir / f"{proposal.id}.md"
doc_content = (
f"# 程序优化需求\n\n"
f"**标题**: {proposal.title}\n"
f"**预期影响**: {proposal.expected_impact}\n"
f"**风险评估**: {proposal.risk_assessment}\n"
f"**回滚方案**: {proposal.rollback_plan}\n"
f"**创建时间**: {time.strftime('%Y-%m-%d %H:%M', time.localtime())}\n\n"
f"---\n\n"
f"{proposal.description}\n"
)
doc_path.write_text(doc_content, encoding="utf-8")
logger.info("Saved optimization request: %s", doc_path)