dream v1
This commit is contained in:
parent
4efc3d267c
commit
5bdc1bee18
@ -26,6 +26,7 @@
|
||||
|
||||
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
|
||||
- 当前项目支持结构化 memory:使用 `/remember` 保存长期知识,使用 `/memory` 查看。
|
||||
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory,不属于项目规则文件本身。
|
||||
- session 是原始对话历史,不直接拼进 system prompt;memory 才作为长期补充进入 prompt。
|
||||
- 默认语言:中文优先。
|
||||
- 默认验证:优先做最小可验证检查,不夸大成功状态。
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
|
||||
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
|
||||
- 项目规则和工作方式仍应写在 `AGENTS.md`
|
||||
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
|
||||
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
|
||||
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
||||
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
||||
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user