From 5bdc1bee18f84d594bf295fc6914de5483cdd626 Mon Sep 17 00:00:00 2001 From: hc Date: Mon, 13 Apr 2026 18:19:45 +0800 Subject: [PATCH] dream v1 --- AGENTS.md | 1 + SKILLS/cli-core.md | 2 ++ src/cc_slim/commands.py | 6 ++++ src/cc_slim/engine.py | 57 +++++++++++++++++++++++++++++++++++ src/cc_slim/memory.py | 67 +++++++++++++++++++++++++++++++++-------- 5 files changed, 121 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 95d2396..8ab883e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ - `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。 - 当前项目支持结构化 memory:使用 `/remember` 保存长期知识,使用 `/memory` 查看。 +- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory,不属于项目规则文件本身。 - session 是原始对话历史,不直接拼进 system prompt;memory 才作为长期补充进入 prompt。 - 默认语言:中文优先。 - 默认验证:优先做最小可验证检查,不夸大成功状态。 diff --git a/SKILLS/cli-core.md b/SKILLS/cli-core.md index 9c257c5..7a0acd1 100644 --- a/SKILLS/cli-core.md +++ b/SKILLS/cli-core.md @@ -28,6 +28,8 @@ - 优先通过 `/help` 查看当前命令,而不是猜测命令格式 - 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存 - 项目规则和工作方式仍应写在 `AGENTS.md` +- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理 +- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session - 高风险操作会触发确认,优先先读再改,减少无意义审批 - 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行 - workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索 diff --git a/src/cc_slim/commands.py b/src/cc_slim/commands.py index ce40685..1b70a55 100644 --- a/src/cc_slim/commands.py +++ b/src/cc_slim/commands.py @@ -51,6 +51,7 @@ def handle_command( console.print("Memory 命令") console.print(" /memory 查看当前项目 memory") console.print(" /remember 追加保存长期信息") + console.print(" /dream 从当前 session 整理长期 memory") console.print("") console.print("模式命令") console.print(" /mode 查看当前模式") @@ -111,6 +112,11 @@ def handle_command( console.print(content or "当前项目还没有已保存的 memory。") return agent + if name == "/dream": + message = agent.dream(memory) + console.print(message) + return agent + if name == "/permissions": if args == "auto-on": permissions.set_auto_approve(True) diff --git a/src/cc_slim/engine.py b/src/cc_slim/engine.py index 0f7fc02..2bed5f5 100644 --- a/src/cc_slim/engine.py +++ b/src/cc_slim/engine.py @@ -94,6 +94,7 @@ class Agent: ) -> None: self.config = config self.tools = {tool.name: tool for tool in tools} + self.workspace = workspace self.history: list[dict[str, Any]] = list(history or []) self.session_store = session_store self.session_id = session_id @@ -111,6 +112,15 @@ class Agent: raise RuntimeError(event["message"]) return "".join(parts).strip() or "(empty response)" + def dream(self, memory_store: MemoryStore) -> str: + if not self.history: + return "当前会话没有可整理的内容。" + + dream_markdown = self._run_dream_model(memory_store.read(), self._serialize_history_for_dream()) + memory_store.apply_dream(dream_markdown) + self.system_prompt = self._build_system_prompt(self.workspace) + return "已完成 dream,memory 已更新" + def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]: user_message = {"role": "user", "content": user_input} self.history.append(user_message) @@ -218,6 +228,53 @@ class Agent: return "" return memory + def _serialize_history_for_dream(self) -> str: + lines: list[str] = [] + for item in self.history: + role = item.get("role", "unknown") + if role == "assistant" and item.get("tool_calls"): + content = item.get("content", "") + else: + content = item.get("content", "") + if isinstance(content, list): + content = json.dumps(content, ensure_ascii=False) + lines.append(f"[{role}] {content}") + return "\n".join(lines) + + def _dream_prompt(self, current_memory: str, session_text: str) -> str: + return ( + "你不是在做聊天总结,而是在从当前 session 中提炼长期有价值的信息。\n" + "只保留未来仍值得记住的内容,忽略一次性任务细节、临时状态和短期噪声。\n" + "请结合当前 memory 与当前 session,输出可直接写回 memory 的 Markdown。\n" + "只输出以下四个 sections,不要输出代码块、解释或其他标题:\n\n" + "## User Memory\n" + "## Project Memory\n" + "## Constraints\n" + "## Consolidated Facts\n\n" + f"当前 memory:\n{current_memory}\n\n" + f"当前 session:\n{session_text}" + ) + + def _run_dream_model(self, current_memory: str, session_text: str) -> str: + prompt = self._dream_prompt(current_memory, session_text) + if self.config.provider == "openai": + response = self.client.chat.completions.create( + model=self.config.model, + messages=[ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt}, + ], + ) + return response.choices[0].message.content or "" + + response = self.client.messages.create( + model=self.config.model, + system=self.system_prompt, + max_tokens=2048, + messages=[{"role": "user", "content": prompt}], + ) + return "\n".join(block.text for block in response.content if block.type == "text") + def _build_runtime_summary(self, workspace: Path) -> str: tool_names = ", ".join(self.tools.keys()) or "(none)" shell_name = self._detect_shell() diff --git a/src/cc_slim/memory.py b/src/cc_slim/memory.py index fe13376..d19b0a3 100644 --- a/src/cc_slim/memory.py +++ b/src/cc_slim/memory.py @@ -5,6 +5,15 @@ import re from pathlib import Path +SECTION_ORDER = [ + "User Memory", + "Project Memory", + "Constraints", + "Consolidated Facts", + "Scratch Notes", +] + + class MemoryStore: def __init__(self, cwd: Path) -> None: self.cwd = cwd.resolve() @@ -26,30 +35,39 @@ class MemoryStore: path.write_text(migrated, encoding="utf-8") def append(self, text: str) -> str: - content = self.read().rstrip() + sections = self.read_sections() entry = self._format_scratch_entry(text) - updated = f"{content}\n\n{entry}\n" - self.path().write_text(updated, encoding="utf-8") + scratch = sections["Scratch Notes"].strip() + sections["Scratch Notes"] = f"{scratch}\n\n{entry}".strip() if scratch else entry + self._write_sections(sections) return "Scratch Notes" def read(self) -> str: self.ensure_structure() return self.path().read_text(encoding="utf-8").strip() + def read_sections(self) -> dict[str, str]: + self.ensure_structure() + return self._parse_sections(self.read()) + + def apply_dream(self, dream_markdown: str) -> None: + current = self.read_sections() + updated = self._parse_sections(dream_markdown) + for name in SECTION_ORDER: + if name == "Scratch Notes": + continue + if updated.get(name, "").strip(): + current[name] = updated[name].strip() + self._write_sections(current) + def path(self) -> Path: return self.root / "MEMORY.md" def _default_template(self, scratch_notes: str = "") -> str: scratch = self._format_scratch_entry(scratch_notes) if scratch_notes.strip() else "" - template = ( - "# Memory\n\n" - "## User Memory\n\n" - "## Project Memory\n\n" - "## Constraints\n\n" - "## Consolidated Facts\n\n" - "## Scratch Notes\n" - ) - return f"{template}\n{scratch}\n" if scratch else f"{template}\n" + sections = {name: "" for name in SECTION_ORDER} + sections["Scratch Notes"] = scratch + return self._render_sections(sections) def _format_scratch_entry(self, text: str) -> str: lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()] @@ -71,6 +89,31 @@ class MemoryStore: ] return all(section in text for section in required_sections) + def _parse_sections(self, text: str) -> dict[str, str]: + sections = {name: "" for name in SECTION_ORDER} + current: str | None = None + for line in text.splitlines(): + if line.startswith("## "): + name = line[3:].strip() + current = name if name in sections else None + continue + if line.startswith("# Memory"): + continue + if current is not None: + sections[current] = f"{sections[current]}\n{line}".strip() + return sections + + def _render_sections(self, sections: dict[str, str]) -> str: + parts = ["# Memory", ""] + for name in SECTION_ORDER: + parts.append(f"## {name}") + parts.append(sections.get(name, "").strip()) + parts.append("") + return "\n".join(parts).rstrip() + "\n" + + def _write_sections(self, sections: dict[str, str]) -> None: + self.path().write_text(self._render_sections(sections), encoding="utf-8") + def _sanitize_cwd(self, cwd: Path) -> str: text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd)) digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8]