81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""双层记忆: `workspace/users/<user_id>/.memory/` (§3.7 / §7.4)。
|
|
|
|
core.md —— 注 system prompt,每次都看到。装稳定事实
|
|
(用户偏好 / 常用命令 / 项目约定 / 模型 quirk 备忘等)
|
|
extended/<x>.md —— 索引(标题+路径)注 prompt,内容 agent 用 `read` 按需拉。
|
|
装少数任务才用的专题资料(某 API 速查 / 某历史事件等)
|
|
|
|
为什么这样切:
|
|
core 一直挂在上下文里,token 成本固定 ⇒ 只放跨任务高频用的精炼内容
|
|
extended 索引只占几行,内容按需付费 ⇒ 适合大量低频专题
|
|
|
|
memory 是 per-user(同一 workspace 内按 user_id 隔离),同 user 的所有 task 共享。
|
|
**dotfile `.memory/` 命名**:跟用户起的项目目录(同样落 `<uid>/` 下)区分,避免
|
|
项目名取 `memory` 时撞名;`.` 起头也被 `validate_task_name` 拒,双向防呆。
|
|
user_id 由 web auth 入口(JWT `sub`)透传到 build_agent。SaaS 化时 `<storage_root>`
|
|
替换 `workspace`,布局不变(§7.0)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
from uuid import UUID
|
|
|
|
|
|
def _memory_dir(workspace_dir: Path, user_id: UUID) -> Path:
|
|
return workspace_dir / "users" / str(user_id) / ".memory"
|
|
|
|
|
|
def _read_first_title(p: Path) -> str:
|
|
"""取文件第一个非空 h1/h2 行作为标题;没有就用文件名 stem。"""
|
|
try:
|
|
for raw in p.read_text(encoding="utf-8").splitlines():
|
|
line = raw.strip()
|
|
if line.startswith("#"):
|
|
return line.lstrip("#").strip()
|
|
if line:
|
|
return line[:60]
|
|
except (OSError, UnicodeDecodeError):
|
|
pass
|
|
return p.stem
|
|
|
|
|
|
def _load_core(workspace_dir: Path, user_id: UUID) -> str:
|
|
p = _memory_dir(workspace_dir, user_id) / "core.md"
|
|
if not p.is_file():
|
|
return ""
|
|
try:
|
|
return p.read_text(encoding="utf-8").strip()
|
|
except (OSError, UnicodeDecodeError):
|
|
return ""
|
|
|
|
|
|
def _extended_index(workspace_dir: Path, user_id: UUID) -> List[Tuple[str, Path]]:
|
|
"""返回 [(title, abs_path), ...],按文件名排序。"""
|
|
ext_dir = _memory_dir(workspace_dir, user_id) / "extended"
|
|
if not ext_dir.is_dir():
|
|
return []
|
|
items: List[Tuple[str, Path]] = []
|
|
for p in sorted(ext_dir.glob("*.md")):
|
|
if p.is_file():
|
|
items.append((_read_first_title(p), p.resolve()))
|
|
return items
|
|
|
|
|
|
def memory_block(workspace_dir: Path, user_id: UUID) -> str:
|
|
"""构造注入 system prompt 的记忆段;两块都空就返回空串。"""
|
|
core = _load_core(workspace_dir, user_id)
|
|
ext = _extended_index(workspace_dir, user_id)
|
|
if not core and not ext:
|
|
return ""
|
|
|
|
parts = ["\n\n## 记忆 (user 级,跨 task 共享)"]
|
|
if core:
|
|
parts.append("\n### Core (常驻 prompt)\n")
|
|
parts.append(core)
|
|
if ext:
|
|
parts.append("\n\n### Extended (按需用 `read` 加载)\n")
|
|
for title, path in ext:
|
|
parts.append(f"- `{path}` — {title}\n")
|
|
return "".join(parts)
|