This commit is contained in:
hc 2026-04-13 18:19:45 +08:00
parent 4efc3d267c
commit 5bdc1bee18
5 changed files with 121 additions and 12 deletions

View File

@ -26,6 +26,7 @@
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
- 当前项目支持结构化 memory使用 `/remember` 保存长期知识,使用 `/memory` 查看。
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory不属于项目规则文件本身。
- session 是原始对话历史,不直接拼进 system promptmemory 才作为长期补充进入 prompt。
- 默认语言:中文优先。
- 默认验证:优先做最小可验证检查,不夸大成功状态。

View File

@ -28,6 +28,8 @@
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
- 项目规则和工作方式仍应写在 `AGENTS.md`
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
- 高风险操作会触发确认,优先先读再改,减少无意义审批
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索

View File

@ -51,6 +51,7 @@ def handle_command(
console.print("Memory 命令")
console.print(" /memory 查看当前项目 memory")
console.print(" /remember <text> 追加保存长期信息")
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)

View File

@ -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 "已完成 dreammemory 已更新"
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()

View File

@ -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]