feat(evolution): flag self-evolution bubbles in UI and relax MEMORY.md writes

This commit is contained in:
zhayujie
2026-06-07 21:00:03 +08:00
parent 8dd21ddb83
commit 26300a8d43
3 changed files with 31 additions and 7 deletions

View File

@@ -79,13 +79,14 @@ them. When their signal is clear, act; do not be shy here.
Act ONLY on something the main assistant clearly MISSED that does not belong Act ONLY on something the main assistant clearly MISSED that does not belong
in any skill. in any skill.
- MEMORY.md is the curated long-term index, auto-loaded into EVERY future - MEMORY.md is the curated long-term index, auto-loaded into EVERY future
conversation. Treat it as precious: writing here is RARE and reserved for conversation. Treat it as precious: edit it in place to CORRECT a wrong
CORRECTING a wrong fact already in MEMORY.md (edit that line in place). fact, or append a new durable preference/decision/lesson — but do so
Do NOT append new entries to MEMORY.md — that is the nightly pass's job. SPARINGLY (a lasting fact, not a passing detail; the nightly pass handles
- For a genuinely important NEW durable fact the chat missed, append ONE routine consolidation).
short bullet to today's `memory/YYYY-MM-DD.md` (not MEMORY.md). When unsure, - For a NEW fact that is important but not yet clearly lasting, append ONE
the daily file is the safe place — but first ask whether this really short bullet to today's `memory/YYYY-MM-DD.md` instead. When unsure, the
belongs in a skill instead. daily file is the safe place — but first ask whether this really belongs
in a skill.
- Keep it to ONE short bullet. Never write paragraphs, never re-summarize the - Keep it to ONE short bullet. Never write paragraphs, never re-summarize the
conversation, never copy what the main assistant already recorded. conversation, never copy what the main assistant already recorded.
- If it is already captured anywhere (check MEMORY.md AND the daily file - If it is already captured anywhere (check MEMORY.md AND the daily file

View File

@@ -124,6 +124,11 @@ def _is_internal_user_marker(text: str) -> bool:
return any(t.startswith(m) for m in _SCHEDULED_DISPLAY_MARKERS) return any(t.startswith(m) for m in _SCHEDULED_DISPLAY_MARKERS)
def _is_evolution_text(text: str) -> bool:
"""True if assistant text is a self-evolution summary (before cleaning)."""
return (text or "").lstrip().startswith(_EVOLUTION_DISPLAY_MARKER)
def _clean_display_text(text: str) -> str: def _clean_display_text(text: str) -> str:
"""Strip internal markers from assistant text for user-facing display. """Strip internal markers from assistant text for user-facing display.
@@ -306,6 +311,10 @@ def _group_into_display_turns(
step["result"] = tr.get("result", "") step["result"] = tr.get("result", "")
step["is_error"] = tr.get("is_error", False) step["is_error"] = tr.get("is_error", False)
# Detect a self-evolution bubble BEFORE cleaning the marker away, so the
# UI can flag it even though the visible text stays clean.
is_evolution = _is_evolution_text(final_text)
# Clean internal markers from the user-facing assistant text. Applies to # Clean internal markers from the user-facing assistant text. Applies to
# both the final content and the mirrored content step so the rendered # both the final content and the mirrored content step so the rendered
# bubble shows clean text while the stored message keeps the markers. # bubble shows clean text while the stored message keeps the markers.
@@ -321,6 +330,8 @@ def _group_into_display_turns(
"steps": steps, "steps": steps,
"created_at": final_ts or (user_row[1] if user_row else 0), "created_at": final_ts or (user_row[1] if user_row else 0),
} }
if is_evolution:
turn["kind"] = "evolution"
if merged_extras: if merged_extras:
turn["extras"] = merged_extras turn["extras"] = merged_extras
turns.append(turn) turns.append(turn)

View File

@@ -124,6 +124,7 @@ const I18N = {
config_max_steps: '最大执行步数', config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数', config_max_steps: '最大执行步数', config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数',
config_enable_thinking: '深度思考', config_enable_thinking_hint: '是否启用深度思考模式', config_enable_thinking: '深度思考', config_enable_thinking_hint: '是否启用深度思考模式',
config_self_evolution: '自主进化', config_self_evolution_hint: '会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项', config_self_evolution: '自主进化', config_self_evolution_hint: '会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项',
evolution_badge: '自主学习',
config_channel_type: '通道类型', config_channel_type: '通道类型',
config_provider: '模型厂商', config_model_name: '模型', config_provider: '模型厂商', config_model_name: '模型',
config_custom_model_hint: '输入自定义模型名称', config_custom_model_hint: '输入自定义模型名称',
@@ -327,6 +328,7 @@ const I18N = {
config_max_steps: 'Max Steps', config_max_steps_hint: 'Max tool calls the Agent can make in a single conversation', config_max_steps: 'Max Steps', config_max_steps_hint: 'Max tool calls the Agent can make in a single conversation',
config_enable_thinking: 'Deep Thinking', config_enable_thinking_hint: 'Enable deep thinking mode', config_enable_thinking: 'Deep Thinking', config_enable_thinking_hint: 'Enable deep thinking mode',
config_self_evolution: 'Self-Evolution', config_self_evolution_hint: 'Auto-review idle conversations to consolidate memory, improve skills, and follow up on unfinished tasks', config_self_evolution: 'Self-Evolution', config_self_evolution_hint: 'Auto-review idle conversations to consolidate memory, improve skills, and follow up on unfinished tasks',
evolution_badge: 'Self-learned',
config_channel_type: 'Channel Type', config_channel_type: 'Channel Type',
config_provider: 'Provider', config_model_name: 'Model', config_provider: 'Provider', config_model_name: 'Model',
config_custom_model_hint: 'Enter custom model name', config_custom_model_hint: 'Enter custom model name',
@@ -2886,10 +2888,20 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
stepsHtml = renderThinkingHtml(reasoning) + renderToolCallsHtml(toolCalls); stepsHtml = renderThinkingHtml(reasoning) + renderToolCallsHtml(toolCalls);
} }
// Self-evolution bubbles get a small badge so the user can feel the agent
// learned something on its own (text itself stays clean).
const evolutionBadge = (msg && msg.kind === 'evolution')
? `<div class="flex items-center gap-1 mb-1.5 text-xs text-slate-400 dark:text-slate-500">
<i class="fas fa-seedling text-[11px]"></i>
<span>${t('evolution_badge')}</span>
</div>`
: '';
el.innerHTML = ` el.innerHTML = `
<img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0"> <img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
<div class="min-w-0 flex-1 max-w-[85%]"> <div class="min-w-0 flex-1 max-w-[85%]">
<div class="bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-2xl px-4 py-3 text-sm leading-relaxed msg-content text-slate-700 dark:text-slate-200"> <div class="bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-2xl px-4 py-3 text-sm leading-relaxed msg-content text-slate-700 dark:text-slate-200">
${evolutionBadge}
${stepsHtml ? `<div class="agent-steps">${stepsHtml}</div>` : ''} ${stepsHtml ? `<div class="agent-steps">${stepsHtml}</div>` : ''}
<div class="answer-content">${renderMarkdown(displayContent)}</div> <div class="answer-content">${renderMarkdown(displayContent)}</div>
<div class="bot-audio-slot"></div> <div class="bot-audio-slot"></div>