Reframe digest as the abstract memory layer (details stay in the daily/
resource material; digest holds principles, patterns, precedents reachable
via derived_from provenance edges). Replaces the old digester with a
2-phase ReAct workflow + a daily-tick wrapper:
- Phase 1 (Dreamer extract): clusters material into orthogonal memory
sub-units; each sub-unit maps 1:1 to a digest node (no inner atom
enumeration). Biases toward fewer / richer sub-units.
- Phase 2 (Dreamer integrate per sub-unit): cross-bucket recall +
exactly one write decision (CREATE / UPDATE / SKIP); UPDATE shapes
surfaced explicitly (corroborate / refine / correct).
- CronDreamer: scans <daily_dir>/<today>.md + <daily_dir>/<today>/**
+ <resource_dir>/<today>/** and runs dream_one per file.
Write tools are proper subclasses of the canonical file_io WriteStep /
EditStep with only path-shape + bucket + E-1 edge-conservation rules
layered on top:
- DigestWriteStep(WriteStep): path = <digest_dir>/<bucket>/<slug>.md,
must-not-exist, schema mirrors `write` (path / name / description /
content) so frontmatter lands automatically.
- DigestEditStep(EditStep): body-only find-and-replace + must-exist +
E-1 conservation preflight (refuses if any outbound wikilink would
be dropped).
Configuration:
- Bucket vocabulary structured in code (tuple[{name, description}]);
prompt renders the heuristic block at runtime via {buckets}.
- digest_dir / daily_dir / resource_dir come from app config (not tool
params); prompts use {digest_dir} placeholder.
- BaseStep walks class MRO when loading prompts, so subclasses inherit
parent yaml without duplication.
Tooling: agentscope register_tool_function schemas now wrap in the
proper {"type":"function","function":{...}} envelope. OpenAIAsLLM
routes base_url through client_kwargs so non-default endpoints work.
Smoke: tests4/smoke/{_dreamer_fixture.py,test_dreamer_inproc.py,
test_dreamer_cli.sh} drive the end-to-end pipeline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
auto-dream 设计(桶 / 节点 / 边 / 演化)
本文档:digest 沉淀层的桶(物理布局)/ 节点(原子单元)/ 边(wikilink)/ 演化(dream create_or_update;split 归 maintain)。
配套阅读:
structure.md§1.2(数据视角)/ §2(三层存储)/ §3.5(digest 动作)auto_memory_design.md:daily 实时事件 = dream 的入流之一auto_maintain_design.md:M split / D 检测 / CAS 写入协议(dream 模型的运行时实现)auto_link_design.md:dream 写完后的后置增强(背景实体识别 + wikilink 写回)核心:digest = 浅桶(shallow bucket)+ flat .md + 一张图(节点 + 边);dream 定义模型与主流程(create_or_update),maintain 负责 split / 写入运行时。
关键收敛:digest 不分"逻辑层"。所有 .md 文件都是同一种节点,内容决定它扮演什么角色(主题概览 / 概念定义 / 方法描述 / 实体记录 ...)。"主题"从图中涌现,不是结构性宣告。
0. 问题陈述
digest 是 agent 长期记忆的"组织化沉淀"层,与三层架构的另两层职责互补:
| 层 | 组织主轴 | 形态 |
|---|---|---|
| resource/ | 时间(<date>/<name>) |
外部原始资料,不可变 |
| daily/ | 时间 + 任务(<date>/<event-slug>/) |
agent 任务过程,半可变 |
| digest/ | 语义 | 跨任务知识,可重组 |
dream 设计回答四个问题:桶怎么布局 / 节点长什么样 / 边怎么连 / 演化谁负责怎么做。
1. 桶(物理布局)
| 维度 | 决策 |
|---|---|
| 物理几何 | digest/<bucket>/<slug>.md;浅桶一层(顶多两层),桶内 flat |
| bucket 角色 | 仅承担物理归档 + OS-level 浏览锚点;不承担语义本体角色 —— 主题由图中节点表达 |
| bucket 集合 | 固定预定义(vault.yaml 顶层 digest.buckets:),不由 dreamer / maintainer 动态生成 |
| 集合视图 | 自动生成 digest/_buckets.md 作为人/LLM 可读视图;dream 时读后者作 prompt context |
| 初始化 | opinionated default(6 桶,按"答什么问"划分):concept(答"X 是什么")/ procedure(答"怎么做 X")/ entity(答"X 是谁/哪个")/ observation(答"发生了什么")/ preference(答"X 喜欢怎样 / 别做什么";用户记忆主战场)/ 必带 unknown —— 消费层可改桶名,但 unknown 不可删 |
| bucket 主页 | 不强制存在;split 累积出层级时 parent 节点天然成为浏览主页(中心性涌现,非架构必需) |
| 新节点归属 | dream 只能从已有桶集合挑选;LLM 不能造新桶 |
| 未归类节点 | 统一落入兜底桶 digest/unknown/(详下) |
| 跨桶 move | F-1 已禁止;若必须做(人工介入修错桶),走一次 wikilink_handler.retarget_links(old, new) |
unknown 兜底桶:
| 维度 | 内容 |
|---|---|
| 语义 | "分类未定 / 暂无专属归属" —— 合法常态,非故障状态(LLM 没找到合适专属桶时的诚实表达,不是写入失败) |
| 路径 | digest/unknown/<slug>.md,与其它 bucket 完全等同;节点演化与其它桶一致 |
| 错桶后续 | 不主动跨桶 move;若严重,人工 mv + retarget_links(old, new) |
为什么是 unknown 而不是 general:general 听起来像在断言"这个概念真的属于通用类",实际上只是 LLM 没找到合适专属桶;unknown 诚实表达"分类未定",不强加伪类目。但 unknown 不是"待清理状态" —— 它是合法常态,节点在此演化(被 update / 被 wikilink 引 / 中心性增长)与其它桶完全等同;不会随时间被自动清空。
为什么是浅桶而不是深树:
- 物理浏览有"主题轮廓"(打开
digest/auth/能看到这一族节点),不像纯 flat 那样毫无锚点 - 节点不被深路径绑死("属 auth/jwt 还是 auth/session"这种归属焦虑被消解 —— 一个节点可以同时被多个主题通过 wikilink 引用)
- F-1(0 文件移动)+ 平铺后,深树的核心收益(子树重组)消失,只剩深路径维护负担
- 固定集合的关键意义:LLM 在 dream 桶决定时只做"分类",不做"造类" —— 决策面坍缩,跨任务跨时间稳定;不会出现 "auth-stuff" / "auth" / "authentication" 三个语义重叠的桶共存
已排除:动态扩桶 / 拒绝写入(候选丢失)/ 强行选最近似专属桶(本体污染)。
2. 节点
| 维度 | 决策 |
|---|---|
| 粒度 | atomic;一个 .md 文件 = 一个原子单元(概念 / 方法 / 实体 / 案例 / 原则 / 主题概览) |
| 节点角色 | 由 body 内容决定,不由 frontmatter 类型标记;同一节点扮演"主题概览"还是"具体方法",看它的 body 写了什么 |
| 身份(ID) | vault-relative 路径(含 .md)即节点身份 —— digest/auth/jwt-rotation.md |
name frontmatter |
文件名 basename(不含扩展名),与文件名同步 —— 检索 hint / 人读标签,不当 ID 用 |
| frontmatter 保留字段 | 只有 name + description(reme 核心保留) |
可选 kind 字段 |
例:concept / procedure / entity / observation / preference / ...;消费层 schema 提示,reme 核心透明,不读它做结构决策 |
| 文件名冲突 | 同 bucket 内文件名冲突 → 文件系统层断言(写入即拒);不需要独立检测信号 |
| rename | 一次 wikilink_handler.retarget_links(old_path, new_path)(机制现成);无 alias 表,无透明展开 |
为什么 atomic + 路径即 ID:
- 节点粒度 = retrieve 精度上限 —— semantic 检索召回 "一个原子单元" 远比召回 "一个 5000 字的主题文档" 信噪比高
- wikilink 在 atomic 粒度才真有意义 ——
[[digest/auth/jwt-rotation.md]]指向"一个具体方法"比指向"auth 主题文档"精确一个数量级 - F-1 + 平铺 + 下层 immutable 后,slug abstraction 的核心价值(移动鲁棒性)蒸发;路径作 ID 与
wikilink_handler.py默认形态完全对齐(Recommended form: full path relative to the vault with extension) - provenance wikilink 反指 daily/resource 本来就用路径,统一后整个 vault 一种 wikilink 形态
"主题概览节点"靠内容识别,不靠前缀 / kind:hub__ / topic__ 前缀不存在;文件名自然命名(auth-fundamentals.md / jwt-rotation.md)。主题概览身份是图位置(中心性 / split parent)+ body 形态共同涌现。
3. 边
参考实现:reme4/utils/wikilink_handler.py + reme4/schema/file_link.py。
| 形态 | 写法 | 说明 |
|---|---|---|
| 基础 | [[<vault-path>.md]] |
literal,不隐含 .md,不自动短链补全 |
| alias | [[path.md|display-text]] |
rewrite 时 alias 保持 |
| image | ![[image.png]] |
资源引用,不是知识边 |
| 可选谓词 | predicate:: [[path.md]](行级)/ [predicate:: [[path.md]]](内联) |
Dataview 风格;谓词在 [[]] 外,[[]] 内只保留纯目标 |
| 谓词标识符 | [A-Za-z][A-Za-z0-9_]*(is_a / extends / causes / references ...) |
词表开放,任意标识符 |
| 未类型化合法 | 绝大多数 wikilink 不加 predicate;predicate=None 是默认 / 常态 |
|
| 边唯一性键 | (target_path, predicate) 二元组 |
同源同标不同 predicate = 不同边 |
| 不引入 anchor | digest 设计层不使用 [[path.md#section]] |
FileLink.target_anchor schema 保留(供其它消费层),digest 层永远写 None |
reme 核心对 predicate 的"透明"边界(关键):
- 横向 link/ retrieve 中心性 —— 都聚合所有 predicate 算,不分桶
- 只有 edge 唯一性 / 反向索引会用到 predicate(否则
[[A]]和is_a:: [[A]]会被当作同一条边互相覆盖) - 消费层若要按 predicate 做更精细的推理(如"taxonomic 路径只走
is_a边"),自己读FileLink.predicate即可
与 kind 一致的立场(与 reme4_schema_layering 对齐):reme 核心只有节点 + 边两种结构类型;kind / predicate 都是内容标签,绝不参与"hub / topic / leaf"这类结构角色判断。
为什么不引入 anchor:LLM 想"指向具体子主题"时,正确做法是让那个子主题升级为独立节点(必要时通过 split),不在过载 parent 内部用 anchor 凑合。anchor 在 digest 层无语义;prompt 必须明确告知 LLM 写 wikilink 时不带 #section。
4. 演化
4.1 演化只做两件事
| op | 谁 | 何时 | 改什么 |
|---|---|---|---|
| dream(create_or_update) | dreamer(本文档 §4.2) | 入流(新材料进入) | 创建新节点 / update 已有节点 body(语义守恒重写) |
| M split | maintainer(auto_maintain_design.md §1) |
节点过载(token / 主题离散度超阈值) | 把 parent body 拆成 parent overview + N children;parent 文件原地 |
关键观察:"主题概览节点"不是一种 kind,也不是 maintainer 主动涌现的产物 —— 它是 split 的副产品(parent 节点天然成为该 cluster 的 overview,中心性自然高)。
显式排除:
- ❌ merge / dissolve / re-edge / unify —— 跨节点重组不做(同概念二次进入靠 dream update;错桶节点不主动 move)
- ❌ 完美归簇 —— F-5 留白,不确定就不动
- ❌ 实时一致 —— 异步 / eventual
4.2 dream(create_or_update)流程
dream = dreamer 入流唯一改 body 的操作,且只改 subject node。
material 进入(daily / resource 选定 scope)
│
▼
LLM 抽取原子单元 → N 个候选
│
▼ 对每个候选:
SearchStep 召回相似候选节点
(reme4/steps/index/search.py;vector + keyword 并发 → RRF 融合
→ expand_links 邻接展开;scope `digest/`;默认 limit 5~10)
│
▼
LLM 终判:候选池里有"同概念节点"吗?
├─ 有 → update 路径
│ (a) 把新内容融入已有 body(语义守恒重写)
│ (b) 加 provenance 反指
│ (c) 必要时加 / 改 wikilink
│
└─ 无 → create 路径
(a) 挑 bucket(固定集合;无合适专属桶 → `unknown`)
(b) 写文件名(同 bucket 唯一,fs 层断言)
(c) 写 body + provenance + 横向 link
│
▼
写入前 outbound diff 守恒校验(update 走 E-1;create 无 old outbound)
│
▼
CAS 写入(`auto_maintain_design.md` §5)
│
▼
写完 inline 触发 D3 检测(`auto_maintain_design.md` §4)
关键边界:
- dream update 必须语义守恒 —— LLM 重写 body 时只能"融入"新内容,不能删除已有信息(只增不删 / 不改原意;冲突标注
> 注:不同来源记载...,不擅自仲裁);写入前机械校验出边强守恒(E-1,详 §4.4) - dream 不改其它节点正文(F-2) —— 只动 subject
- 0 出边节点合法(没识别到合适邻居),后续 dream 进入时其它节点可以反向链回来 —— 不强求 LLM 一次性给全
- dream 漏判去重(同概念建成新节点)→ 不主动兜底,接受重复;若 vault 累积明显重复,由 auto-link L4 离线 audit 工具产报告(
auto_link_design.md§1.3) - 召回不做 bucket 粗筛 —— LLM 拥有完整跨桶视野,可识别"概念错分到 unknown"或"跨桶同概念"
provenance 写出:
- 行文中自然带:"... 该模式最早出现在 daily/2026/05/15.md 的实践中"
- 可选 predicate:
derived_from:: [[daily/2026/05/15.md]],不强制 - prompt 必须要求"出处用
[[...]]形式表达"(纯散文会被守恒校验视为丢边) - 首版可先用 append 起步(出边集合天然 ⊇,守恒校验自动通过);成熟后切到重写
4.3 F-invariants(演化的硬约束)
| # | 约束 | 含义 |
|---|---|---|
| F-1 | 0 文件移动 | dream / split 都不移动现有文件;split 创建的是新文件,parent 原地 |
| F-2 | 改正文限定 subject | dream update 改 subject body;M split 改 parent body + 创建 children body;没有任何操作改"其它节点正文" |
| F-3 | maintainer 只做 split | 没有 summarize / merge / re-edge / link / unify / dissolve |
| F-4 | 一次一个候选 | M split 一次拆一个;dream 一次处理一个原子单元(N 候选 = N 次 dream) |
| F-5 | 不确定时不动 | dream 拿不准 create 还是 update → 倾向 create;split 拿不准 cluster → 不拆 |
| F-7 | 多归属合法 | 一个节点可被多个引用,也可指向多个;没有"单父"约束 |
| F-10 | inbound 目标节点不动 | 所有 inbound 是裸链 [[<parent-path>.md]](digest 不引入 anchor);split 时全部保持,parent 路径未变即天然有效 |
| F-11 | wikilink 是 body 的一部分 | 不存在"独立的边";reme 核心机械算子只感知字符层,语义责任在 LLM(prompt)+ 守恒校验(outbound diff) |
4.4 边守恒(E-1 / E-2 / E-3)
前提:wikilink 是 body 的一部分(F-11)。"边"不是独立抽象 —— body 一变,边就跟着变。reme 核心没有"修边"算子;边的所有变化都是 body 文本编辑的副作用。但语义守恒不能放任 LLM:守恒责任在 prompt + 机械校验。
| # | 类别 | 规则 | 谁负责 |
|---|---|---|---|
| E-1 | dream update 节点出边(subject 自身) | 强守恒:新 body 出边 ⊇ 原 body 出边((target, predicate) 二元组,predicate 一并守住) |
LLM(prompt)+ 机械(outbound diff) |
| E-2 | split parent 出边(parent 拆解) | (parent_new ∪ ∪children_outbound) ⊇ parent_old |
LLM(split prompt)+ 机械 |
| E-3 | inbound wikilink [[<parent-path>.md]] |
split 时不动 —— 仍指 parent;后续 dream 进入若 LLM 觉得 child 粒度更合适,直接加新边到 child(F-10) | 不动 |
机械 outbound diff 校验(E-1 / E-2)伪码:
write_subject_body(subject, new_body):
old = extract_links(old_body)
new = extract_links(new_body)
if old - new:
new_body = llm_retry_with_missing(old - new)
if old - extract_links(new_body):
raise ConservationViolation(...) # 拒写 + audit
write(subject, new_body)
强守恒(集合包含)而非等价:new ⊇ old = 允许加新边(新关联),不允许减边(老内容不能丢);new == old 会拒绝任何新出边 → update 失去意义。
predicate 守住 —— [[A]] ↔ is_a:: [[A]] 视为不同 key,升降级走显式 audit 路径,不走默认。重排 / 改 alias / 加新边都不被拦下(集合相同或只增)。
provenance 不单列 —— 节点反指上游 daily/resource 的 wikilink 是 body 正文的一部分,跟其它 wikilink 走同一套 E-1 / E-2;reme 核心没有 provenance 专用算子。
inbound anchor 这一类不存在 —— digest 不引入 anchor,所有 inbound 都是裸链,走 E-3 即可,无需机械 retarget 子流程。
5. 与其它层
| 上下游 | 关系 |
|---|---|
| ← auto-memory(daily) | dream 读 daily 作为入流;daily 写完即对 dream 可见 |
| ← resource | dream 读 resource 作为入流(只读,不写) |
| → auto-maintain | dream 写完触发 D3 inline 检测;D3 过载 → enqueue split job(maintain 异步消费);写入并发由 CAS 协议(auto_maintain_design.md §5)保护 |
| → auto-link | dream 写完 enqueue auto-link L1(背景实体识别 + wikilink 写回);走同一 CAS 队列(auto_link_design.md §1.2) |
关键边界:dream 不写 daily / resource(I-2 / I-3);只写 digest 节点 body(自身 subject)。
6. 下一步
本文档覆盖 dream 模型(桶 / 节点 / 边 / 演化)。组织端实现清单(M split / D 检测 / CAS 框架)见 auto_maintain_design.md §10。
- dream step 实现 —— scope → 抽取 → SearchStep 召回 → LLM 终判 → CAS 写入 + E-1 守恒校验
- rename 路径封装 ——
wikilink_handler.retarget_links(old, new)已就绪;封装为单步 step,无 alias 表,无透明展开 - bucket 集合配置 ——
vault.yamlschema / 默认桶模板 /unknown兜底 /_buckets.md视图生成 - 边守恒校验工具 ——
extract_links已就绪;新增 outbound diff 比较器 + LLM 重试编排 + ConservationViolation audit 事件 - provenance prompt 规范 —— dream 引导 LLM 写
[[daily/...]]/[[resource/...]]
实现进入 reme4/steps/jobs/ 与 reme4/file_graph/ 时,本文档与 auto_memory_design.md / auto_maintain_design.md / auto_link_design.md 共同作为契约依据。