Files
chatgpt-on-wechat/agent/evolution/record.py
zhayujie ba777ed706 feat(evolution): add self-evolution subsystem
Add a self-evolution subsystem that reviews idle conversations in an
isolated agent and durably learns from them — patching/creating skills,
finishing unfinished tasks, and backfilling missed memory.

- Trigger: background idle scan, fires when a session is idle >= N min AND
  (>= N turns OR context usage > 80%). In-memory cursor reviews only new
  messages so a session never re-learns old content.
- Isolated review agent: same model, restricted toolset, hard write-guard
  confining edits to the workspace (built-in skills are protected).
- Safety: file-level backup before edits + evolution_undo tool; notify the
  user ONLY when a workspace file actually changed (no-nag rule); capped
  concurrency.
- Records to memory/evolution/<date>.md, surfaced in the memory UI's
  renamed "Self-Evolution" tab (merged with dream diaries).
- Hide internal [SCHEDULED]/[EVOLUTION]/backup_id markers from chat history
  display (also fixes scheduler marker leakage) while keeping them in stored
  content for undo.
- Flat config: self_evolution_enabled (default off until release),
  self_evolution_idle_minutes (15), self_evolution_min_turns (6).
- Tests: tests/test_evolution.py (stub + real model modes, 7 scenarios).
2026-06-07 18:55:33 +08:00

56 lines
2.0 KiB
Python

"""Self-evolution record log.
Session-level evolutions are appended to their OWN per-day file under
``memory/evolution/YYYY-MM-DD.md`` (separate from the nightly Deep Dream diary
in ``memory/dreams/``). Each day's file accumulates one short section per
evolution pass — tagged with a timestamp and a backup id for undo — so the
memory UI can surface "what the agent learned/changed today" on one timeline
without ever mixing into the dream diary or the main conversation memory.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Optional
from common.log import logger
def _evolution_dir(workspace_dir: Path, user_id: Optional[str] = None) -> Path:
base = Path(workspace_dir) / "memory"
if user_id:
return base / "users" / user_id / "evolution"
return base / "evolution"
def append_session_evolution(
workspace_dir: Path,
summary: str,
backup_id: Optional[str] = None,
user_id: Optional[str] = None,
) -> None:
"""Append a session-evolution entry to today's evolution log."""
if not summary or not summary.strip():
return
try:
evo_dir = _evolution_dir(workspace_dir, user_id)
evo_dir.mkdir(parents=True, exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
log_file = evo_dir / f"{today}.md"
ts = datetime.now().strftime("%H:%M")
header = f"## {ts}"
body = summary.strip()
if backup_id:
body += f"\n\n_backup_id: {backup_id}_"
# Create with a title if the file is new, otherwise append a section.
if not log_file.exists():
log_file.write_text(f"# Self-Evolution: {today}\n\n", encoding="utf-8")
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"\n{header}\n\n{body}\n")
logger.info(f"[Evolution] Recorded session evolution to {log_file.name}")
except Exception as e:
logger.warning(f"[Evolution] Failed to record session evolution: {e}")