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).
This commit is contained in:
zhayujie
2026-06-07 18:55:33 +08:00
parent 0e4da1d1c5
commit ba777ed706
19 changed files with 1856 additions and 20 deletions

View File

@@ -140,7 +140,7 @@ const I18N = {
skills_section_title: '技能', skill_enable: '启用', skill_disable: '禁用',
skill_toggle_error: '操作失败,请稍后再试',
memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容',
memory_tab_files: '记忆文件', memory_tab_dreams: '梦境日记',
memory_tab_files: '记忆文件', memory_tab_dreams: '自主进化',
memory_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处',
memory_back: '返回列表',
memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间',
@@ -342,7 +342,7 @@ const I18N = {
skills_section_title: 'Skills', skill_enable: 'Enable', skill_disable: 'Disable',
skill_toggle_error: 'Operation failed, please try again',
memory_title: 'Memory', memory_desc: 'View agent memory files and contents',
memory_tab_files: 'Memory Files', memory_tab_dreams: 'Dream Diary',
memory_tab_files: 'Memory Files', memory_tab_dreams: 'Self-Evolution',
memory_loading: 'Loading memory files...', memory_loading_desc: 'Memory files will be displayed here',
memory_back: 'Back to list',
memory_col_name: 'Filename', memory_col_type: 'Type', memory_col_size: 'Size', memory_col_updated: 'Updated',
@@ -4304,13 +4304,14 @@ function toggleSkill(name, currentlyEnabled) {
// Memory View
// =====================================================================
let memoryPage = 1;
let memoryCategory = 'memory'; // 'memory' | 'dream'
let memoryCategory = 'memory'; // 'memory' | 'evolution'
const memoryPageSize = 10;
function switchMemoryTab(tab) {
document.querySelectorAll('.memory-tab').forEach(el => el.classList.remove('active'));
document.getElementById('memory-tab-' + tab).classList.add('active');
memoryCategory = tab === 'dreams' ? 'dream' : 'memory';
// The "dreams" tab now surfaces self-evolution logs (merged with dream diaries).
memoryCategory = tab === 'dreams' ? 'evolution' : 'memory';
loadMemoryView(1);
}
@@ -4327,9 +4328,9 @@ function loadMemoryView(page) {
if (total === 0) {
const emptyIcon = emptyEl.querySelector('i');
const emptyTitle = emptyEl.querySelector('p');
if (memoryCategory === 'dream') {
emptyIcon.className = 'fas fa-moon text-purple-400 text-xl';
emptyTitle.textContent = currentLang === 'zh' ? '暂无梦境日记' : 'No dream diaries yet';
if (memoryCategory === 'evolution') {
emptyIcon.className = 'fas fa-seedling text-emerald-400 text-xl';
emptyTitle.textContent = currentLang === 'zh' ? '暂无进化记录' : 'No evolution records yet';
} else {
emptyIcon.className = 'fas fa-brain text-purple-400 text-xl';
emptyTitle.textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
@@ -4346,10 +4347,15 @@ function loadMemoryView(page) {
files.forEach(f => {
const tr = document.createElement('tr');
tr.className = 'border-b border-slate-100 dark:border-white/5 hover:bg-slate-50 dark:hover:bg-white/5 cursor-pointer transition-colors';
tr.onclick = () => openMemoryFile(f.filename, memoryCategory);
// In the merged evolution tab, resolve each file by its own origin
// (evolution logs vs dream diaries live in different dirs).
const fileCategory = (f.type === 'dream' || f.type === 'evolution') ? f.type : memoryCategory;
tr.onclick = () => openMemoryFile(f.filename, fileCategory);
let typeLabel;
if (f.type === 'global') {
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400">Global</span>';
} else if (f.type === 'evolution') {
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400">Evolution</span>';
} else if (f.type === 'dream') {
typeLabel = '<span class="px-2 py-0.5 rounded-full text-xs bg-violet-50 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">Dream</span>';
} else {