dream v1
This commit is contained in:
parent
4efc3d267c
commit
5bdc1bee18
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
|
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
|
||||||
- 当前项目支持结构化 memory:使用 `/remember` 保存长期知识,使用 `/memory` 查看。
|
- 当前项目支持结构化 memory:使用 `/remember` 保存长期知识,使用 `/memory` 查看。
|
||||||
|
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory,不属于项目规则文件本身。
|
||||||
- session 是原始对话历史,不直接拼进 system prompt;memory 才作为长期补充进入 prompt。
|
- session 是原始对话历史,不直接拼进 system prompt;memory 才作为长期补充进入 prompt。
|
||||||
- 默认语言:中文优先。
|
- 默认语言:中文优先。
|
||||||
- 默认验证:优先做最小可验证检查,不夸大成功状态。
|
- 默认验证:优先做最小可验证检查,不夸大成功状态。
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
|
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
|
||||||
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
|
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
|
||||||
- 项目规则和工作方式仍应写在 `AGENTS.md`
|
- 项目规则和工作方式仍应写在 `AGENTS.md`
|
||||||
|
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
|
||||||
|
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
|
||||||
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
||||||
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
||||||
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
|
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
|
||||||
|
|||||||
@ -51,6 +51,7 @@ def handle_command(
|
|||||||
console.print("Memory 命令")
|
console.print("Memory 命令")
|
||||||
console.print(" /memory 查看当前项目 memory")
|
console.print(" /memory 查看当前项目 memory")
|
||||||
console.print(" /remember <text> 追加保存长期信息")
|
console.print(" /remember <text> 追加保存长期信息")
|
||||||
|
console.print(" /dream 从当前 session 整理长期 memory")
|
||||||
console.print("")
|
console.print("")
|
||||||
console.print("模式命令")
|
console.print("模式命令")
|
||||||
console.print(" /mode 查看当前模式")
|
console.print(" /mode 查看当前模式")
|
||||||
@ -111,6 +112,11 @@ def handle_command(
|
|||||||
console.print(content or "当前项目还没有已保存的 memory。")
|
console.print(content or "当前项目还没有已保存的 memory。")
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
|
if name == "/dream":
|
||||||
|
message = agent.dream(memory)
|
||||||
|
console.print(message)
|
||||||
|
return agent
|
||||||
|
|
||||||
if name == "/permissions":
|
if name == "/permissions":
|
||||||
if args == "auto-on":
|
if args == "auto-on":
|
||||||
permissions.set_auto_approve(True)
|
permissions.set_auto_approve(True)
|
||||||
|
|||||||
@ -94,6 +94,7 @@ class Agent:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.tools = {tool.name: tool for tool in tools}
|
self.tools = {tool.name: tool for tool in tools}
|
||||||
|
self.workspace = workspace
|
||||||
self.history: list[dict[str, Any]] = list(history or [])
|
self.history: list[dict[str, Any]] = list(history or [])
|
||||||
self.session_store = session_store
|
self.session_store = session_store
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
@ -111,6 +112,15 @@ class Agent:
|
|||||||
raise RuntimeError(event["message"])
|
raise RuntimeError(event["message"])
|
||||||
return "".join(parts).strip() or "(empty response)"
|
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]]:
|
def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]:
|
||||||
user_message = {"role": "user", "content": user_input}
|
user_message = {"role": "user", "content": user_input}
|
||||||
self.history.append(user_message)
|
self.history.append(user_message)
|
||||||
@ -218,6 +228,53 @@ class Agent:
|
|||||||
return ""
|
return ""
|
||||||
return memory
|
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:
|
def _build_runtime_summary(self, workspace: Path) -> str:
|
||||||
tool_names = ", ".join(self.tools.keys()) or "(none)"
|
tool_names = ", ".join(self.tools.keys()) or "(none)"
|
||||||
shell_name = self._detect_shell()
|
shell_name = self._detect_shell()
|
||||||
|
|||||||
@ -5,6 +5,15 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SECTION_ORDER = [
|
||||||
|
"User Memory",
|
||||||
|
"Project Memory",
|
||||||
|
"Constraints",
|
||||||
|
"Consolidated Facts",
|
||||||
|
"Scratch Notes",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class MemoryStore:
|
class MemoryStore:
|
||||||
def __init__(self, cwd: Path) -> None:
|
def __init__(self, cwd: Path) -> None:
|
||||||
self.cwd = cwd.resolve()
|
self.cwd = cwd.resolve()
|
||||||
@ -26,30 +35,39 @@ class MemoryStore:
|
|||||||
path.write_text(migrated, encoding="utf-8")
|
path.write_text(migrated, encoding="utf-8")
|
||||||
|
|
||||||
def append(self, text: str) -> str:
|
def append(self, text: str) -> str:
|
||||||
content = self.read().rstrip()
|
sections = self.read_sections()
|
||||||
entry = self._format_scratch_entry(text)
|
entry = self._format_scratch_entry(text)
|
||||||
updated = f"{content}\n\n{entry}\n"
|
scratch = sections["Scratch Notes"].strip()
|
||||||
self.path().write_text(updated, encoding="utf-8")
|
sections["Scratch Notes"] = f"{scratch}\n\n{entry}".strip() if scratch else entry
|
||||||
|
self._write_sections(sections)
|
||||||
return "Scratch Notes"
|
return "Scratch Notes"
|
||||||
|
|
||||||
def read(self) -> str:
|
def read(self) -> str:
|
||||||
self.ensure_structure()
|
self.ensure_structure()
|
||||||
return self.path().read_text(encoding="utf-8").strip()
|
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:
|
def path(self) -> Path:
|
||||||
return self.root / "MEMORY.md"
|
return self.root / "MEMORY.md"
|
||||||
|
|
||||||
def _default_template(self, scratch_notes: str = "") -> str:
|
def _default_template(self, scratch_notes: str = "") -> str:
|
||||||
scratch = self._format_scratch_entry(scratch_notes) if scratch_notes.strip() else ""
|
scratch = self._format_scratch_entry(scratch_notes) if scratch_notes.strip() else ""
|
||||||
template = (
|
sections = {name: "" for name in SECTION_ORDER}
|
||||||
"# Memory\n\n"
|
sections["Scratch Notes"] = scratch
|
||||||
"## User Memory\n\n"
|
return self._render_sections(sections)
|
||||||
"## 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"
|
|
||||||
|
|
||||||
def _format_scratch_entry(self, text: str) -> str:
|
def _format_scratch_entry(self, text: str) -> str:
|
||||||
lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()]
|
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)
|
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:
|
def _sanitize_cwd(self, cwd: Path) -> str:
|
||||||
text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd))
|
text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd))
|
||||||
digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8]
|
digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user