# Phase 7 — SqliteStore 持久化实现方案 - **文档编号**:14 - **标题**:Phase 7 — SqliteStore 持久化实现方案 - **日期**:2026-07-05 - **状态**:已定稿 - **涉及模块**:memory/store - **关联文档**:roadmap.md, 6-memory-system.md --- ## 背景与目标 Phase 7 的核心任务是完成 MemoryStore trait 的 SQLite 后端实现,使 Agent 进程重启后记忆数据不丢失。这是 v0.2.0 从"内存玩具"走向"可用工具"的关键门槛,也是后续 Phase 8(MVP 出口)和 Phase 10(ContextSlot)的前置依赖。 **成功标准**: - SqliteStore 完整实现 MemoryStore trait(4 个方法:save/get/delete/list) - 进程关闭后重新打开同一数据库文件,数据完整可读 - 与现有 InMemoryStore 通过 MemoryStore trait 可互换,消费者零改动 - 所有现有测试保持通过,clippy 0 警告 ### Scope & Non-goals | 范围 | 内容 | |------|------| | 包含 | 单表 CRUD + prefix/since/offset/limit 查询 + WAL 并发 + Mutex 串行化 + 错误映射 | | 不包含(Phase 7) | 淘汰策略(EvictionPolicy,仅 InMemoryStore 持有,需 v0.3 纳入 SqliteStore) | | 不包含(Phase 7) | Schema 迁移框架(PRAGMA user_version 足矣,不引入 refinery/sea-query) | | 不包含(Phase 7) | 批量写入 / 事务 API(N+1 clear 延迟可接受,优化后置) | | 不包含(Phase 7) | 跨进程共享同一数据库文件(Mutex 为单进程设计) | --- ## 当前状态分析 ### 现有实现 - MemoryStore trait 已在 v0.1 Phase 3 就绪,定义 4 个异步方法 - InMemoryStore 实现稳定运行,使用 `Mutex` 作为后端 - 全量测试 191 个通过,clippy 0 警告 - 项目当前无 SQLite 或其他数据库依赖 ### 现有消费者 通过 `crate::memory::store::MemoryStore` 路径引用的模块: | 模块 | 文件 | 使用方式 | |------|------|----------| | Agent Builder | `agent/builder.rs` | RuntimeBundle 中引用 MemoryStore | | Session Memory | `agent/session_memory.rs` | SessionMemory 实现 | | Agent Runtime | `agent/runtime.rs` | 类型标注 | | Agent Session | `agent/session.rs` | 默认 InMemoryStore 兜底 | | Conversation | `memory/conversation.rs` | ConversationMemory 测试 | | Knowledge | `memory/knowledge.rs` | KnowledgeStore 测试 | | Retriever | `memory/retriever.rs` | MemoryRetriever 测试 | 所有消费者均通过 `MemoryStore` trait 访问,不依赖具体实现类型,因此新增 SqliteStore 不会产生编译或运行时影响。 ### 目录结构现状 ``` src/memory/ ├── mod.rs ├── store.rs ← 包含 MemoryStore trait + InMemoryStore + EvictionPolicy ├── conversation.rs ├── knowledge.rs ├── retriever.rs └── vector.rs ``` `store.rs` 目前是一个单体文件,同时承载 trait 定义和 InMemoryStore 实现。 --- ## 调研发现 ### MemoryStore trait 定义 ```rust #[async_trait] pub trait MemoryStore: Send + Sync { async fn save(&self, item: MemoryItem) -> Result<(), MemoryError>; async fn get(&self, id: &str) -> Result, MemoryError>; async fn delete(&self, id: &str) -> Result<(), MemoryError>; async fn list(&self, filter: &MemoryFilter) -> Result, MemoryError>; } ``` ### 关键类型 | 类型 | 定义 | |------|------| | `MemoryItem` | `{ id: String, content: String, metadata: Value, created_at: OffsetDateTime }` | | `MemoryFilter` | `{ prefix: Option, since: Option, offset: Option, limit: Option }` | | `MemoryError` | 变体:`NotFound` / `Storage` / `Serialization` / `InvalidInput` / `RetrievalError` | | `EvictionPolicy` | `None` / `Ttl { ttl_secs }` / `Capacity { max_items }` | | `EvictionConfig` | `{ policy, check_interval }` | ### 并发模型参考 InMemoryStore 当前使用 `Mutex` 实现 `Send + Sync`。SqliteStore 将遵循相同模式,使用 `Arc>` + `spawn_blocking` 满足异步 trait 约束。 ### Schema 设计考虑 - `created_at` 使用 TEXT(ISO 8601) 存储——`.to_string()` 零转换,字典序与时间序一致(前提:所有时间戳归一化到 UTC;`OffsetDateTime::to_string()` 在 UTC 下输出 `"2026-07-05T12:00:00Z"` 格式,字典序与时间序严格对应) - 初始 schema 即创建 `created_at` 索引,避免后续大数据量全表排序 - Schema 版本管理通过 `PRAGMA user_version` 实现,零外部依赖,后续加字段只需追加 `if version < N { ALTER TABLE }` --- ## 可选方案 ### A. rusqlite + Mutex\(推荐) | 维度 | 评估 | |------|------| | 新增依赖 | 1 个(rusqlite 0.32 + bundled features) | | 实现量 | ~200 行 | | SQL 支持 | 原生支持 prefix LIKE 过滤 + ORDER BY 排序 | | 性能 | 有索引时查询 O(log n),写入串行化 | | 并发 | WAL 模式 + Mutex 串行化写入,适合单进程 Agent | | 事务支持 | 完整 ACID | | 崩溃安全 | WAL 模式,崩溃恢复有保障 | **适用场景**:单进程 Agent 本地持久化、嵌入式场景、需要关系查询能力的通用存储。 **外部依赖评估**: - rusqlite 0.32 — 最新稳定版(2025-12 发布),维护活跃(月均 2+ 次提交),Apache-2.0 许可证 - `bundled` feature 编译 SQLite 源码(Public Domain)进二进制,无系统级 SQLite 依赖,零外部 C 库安装步骤 - 供应链风险:bundled 模式依赖 crate 发布节奏同步 SQLite 安全更新;SQLite 安全公告频率极低(年均 <3 例),此风险可接受 ### B. JSONL 文件 | 维度 | 评估 | |------|------| | 新增依赖 | 0 | | 实现量 | ~150 行 | | get() 复杂度 | O(n) 全量扫描 | | delete() 复杂度 | O(n) 全量重写 | | 并发 | 需文件锁(flock) | | 事务支持 | 无 | | 崩溃安全 | 无保障,写入中断可能丢失或损坏数据 | **否决理由**:核心的 get() 查询场景不可接受 O(n) 性能;在 Agent 运行时频繁读写记忆的场景下,全量扫描的成本会随着数据积累线性增长,不符合可用性要求。 ### C. sled 嵌入式 KV | 维度 | 评估 | |------|------| | 新增依赖 | 1 个(纯 Rust) | | 实现量 | ~150 行 | | prefix scan | 原生支持 | | 排序 | 需手动实现 | | 关系模型 | 不如 SQL 匹配当前查询模式 | | 社区成熟度 | 较新,API 仍在演进 | **否决理由**:当前查询模式(prefix 过滤 + 按 created_at 排序)在关系模型中用一条 SQL 即可表达,引入 KV 存储反而需要手动处理排序逻辑。非必要不引入新存储范式。 --- ## 推荐方案 ### 总体方向:方案 A(rusqlite + Mutex\) 选择理由: 1. **最少依赖,最高匹配**:1 个新增依赖即可完整支持 MemoryFilter 的所有查询维度(prefix LIKE、created_at 范围、offset/limit) 2. **生产就绪**:rusqlite 是 SQLite 的 Rust 绑定事实标准,bundled 模式免去系统 SQLite 依赖 3. **Schema 演进简单**:PRAGMA user_version + 逐版本迁移,零外部迁移工具依赖 4. **与 InMemoryStore 语义一致**:Mutex 串行化 + spawn_blocking 适配 async trait,与现有并发模型同构 ### 关键设计决策 | 决策 | 选择 | 理由 | |------|------|------| | Schema 版本管理 | PRAGMA user_version | 零外部依赖,~20 行,后续 ALTER TABLE 即可 | | created_at 存储格式 | TEXT(ISO 8601) + UTC 归一化 | 零转换代码;UTC 下输出 `"2026-07-05T12:00:00Z"`,字典序与时间序严格一致 | | Upsert SQL 策略 | `INSERT ... ON CONFLICT(id) DO UPDATE SET ...` | 保留调用方传入的 `created_at`,避免被 `DEFAULT` 覆盖 | | 性能索引 | 初始 schema 加 created_at 索引 | 避免大数据量全表排序 | | 配置参数 | 仅 `path`、`busy_timeout=5s` | 其余内置默认值;5s 超时避免 `SQLITE_BUSY` 快速失败 | | Mutex 中毒恢复 | `.lock().unwrap_or_else(\|e\| e.into_inner())` | 不 panic,恢复执行 | | 批量操作 | 不加 | N+1 clear ~250ms(N=50),可接受,优化后置 | | spawn_blocking 取消安全性 | 短事务模式(auto-commit) | 每个操作独立事务,取消时后台 task 自然完成/panic,不 Cross 操作持有 Mutex | **我们放弃了什么**(集中 Trade-off 记录): - **写入串行化**:`Mutex` 确保 SQLite 写入安全,代价是同一时刻只能有一个写入者。Agent 场景下写入频率低(每次 LLM 调用触发 1-2 次),串行化不构成瓶颈 - **单进程锁**:无法跨进程共享同一数据库文件。多进程场景需要网络后端(PostgreSQL/Redis) - **无横向扩展**:单文件 SQLite 无分片能力。需扩展时切换到分布式后端 ### Schema 定义(初始版本) ```sql CREATE TABLE IF NOT EXISTS memory_items ( id TEXT PRIMARY KEY, content TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_memory_items_created_at ON memory_items(created_at); ``` ### 数据完整性防御 | 异常场景 | 防御措施 | 错误映射 | |---------|---------|---------| | 数据库文件损坏 | `migrate()` 中执行 `PRAGMA quick_check`;失败时 `open()` 返回 `MemoryError::Storage` | `Storage` | | created_at 解析失败 | `get()`/`list()` 中 `OffsetDateTime::parse` 失败不 panic,返回 `MemoryError::Serialization` | `Serialization` | | content / metadata 为 NULL | `get()` 中检测 SQLite 返回值,NULL 时返回 `MemoryError::Storage` | `Storage` | | 约束冲突(PRIMARY KEY / NOT NULL) | 映射为 `MemoryError::InvalidInput` | `InvalidInput` | | 序列化/反序列化失败 | `serde_json::to_string`/`from_str` 错误映射为 `MemoryError::Serialization` | `Serialization` | ### 性能预算(目标延迟,单条操作) | 操作 | 目标延迟 | 说明 | |------|---------|------| | `save(1KB item)` | < 5ms | 含 serde_json 序列化 + spawn_blocking + SQLite INSERT | | `get(1KB item)` | < 3ms | 含 SQLite SELECT + 反序列化 | | `list(prefix 匹配 100 行)` | < 20ms | 含索引 B-tree 遍历 + ORDER BY + LIMIT | | 并发 10 writer | p99 < 50ms | Mutex 串行化排队,每 writer 等待 9×5ms 内 | 实施后通过 Step C 测试验证以上预算。未达标时不阻塞发布,但记录为可观测告警阈值。 --- ## 实施建议 ### 实施计划 #### Step A — 目录重构(纯搬移,零行为变化) 目标:将单体 `store.rs` 拆分为模块目录架构,为新增 SqliteStore 做准备。 ``` src/memory/ ├── store.rs ← 模块根:MemoryStore trait + EvictionPolicy/EvictionConfig │ + pub mod in_memory; │ + pub mod sqlite_store; │ + pub use in_memory::InMemoryStore; ├── store/ │ ├── in_memory.rs ← InMemoryStore 提取至此(struct + impl + 6 个内联测试) │ └── sqlite_store.rs ← 新增 SqliteStore ``` 模式参考:`llm/provider.rs` → `llm/provider/{openai,anthropic,ollama}.rs` **外部消费者的导入路径不变**(`crate::memory::store::MemoryStore`),零改动风险。 重构步骤: 1. 创建 `src/memory/store/` 目录 2. 创建 `src/memory/store/in_memory.rs`,从原 `store.rs` 提取 InMemoryStore 全部代码(struct + impl + Default + 6 个测试) 3. 修改 `src/memory/store.rs`:保留 MemoryStore trait + EvictionPolicy/EvictionConfig,加 `pub mod in_memory;` + `pub use in_memory::InMemoryStore;` 4. 验证:`cargo test --all-targets` 全绿,测试数量不变(191 pass) #### Step B — SqliteStore 实现 1. `Cargo.toml` 添加 `rusqlite = { version = "0.32", features = ["bundled"] }` 2. 创建 `src/memory/store/sqlite_store.rs`,实现: - `SqliteStore` 结构体:`{ conn: Arc> }` - `SqliteStore::open(path)` 构造函数,支持 `":memory:"` - `lock_conn()` 辅助方法(Mutex 中毒恢复) - `migrate()` Schema 初始化 + 版本管理 - `MemoryStore` trait 的 4 个方法 - `From for MemoryError` 3. 修改 `src/memory/store.rs`:加 `pub mod sqlite_store;` + `pub use sqlite_store::SqliteStore;` 4. 修改 `src/memory.rs`:加 `pub use store::SqliteStore;` 5. 编写测试覆盖: - CRUD 基本操作 - Upsert(同 id 重复 save 覆盖) - prefix 过滤 - 由于/until 时间范围过滤 - 并发 10 个 writer × 10 次操作 - 持久化恢复(write → drop → reopen → read) 6. 验证:`cargo test --all-targets` 全绿 + `cargo clippy --all-targets -- -D warnings` 0 警告 #### Step C — 验证确认 1. 确认现有 memory 模块内测试全部通过 2. 确认 agent/llm/tools/prompt 模块不受影响 3. 确认 SqliteStore 与 InMemoryStore 通过 MemoryStore trait 可互换 4. 确认 clippy 无新增警告 ### Commit 安排 | 顺序 | 类型 | Scope | 描述 | |------|------|-------|------| | 1 | refactor | memory | 将 store.rs 拆分为模块目录,仅结构搬移 | | 2 | feat | memory | 实现 SqliteStore 持久化 | ### 风险与缓解 | 风险 | 严重度 | 缓解措施 | |------|--------|----------| | Mutex 中毒导致后续操作全部失败 | 中 | `lock_conn()` 使用 `.lock().unwrap_or_else(\|e\| e.into_inner())` 恢复模式,不 panic | | spawn_blocking 取消后连接状态不一致 | 中 | 每个操作使用短事务(auto-commit),不跨操作持有 Mutex;取消时遗留 task 自然完成或 panic,Mutex 通过 `.into_inner()` 恢复 | | WAL 文件无限增长 | 低 | 内置 auto-checkpoint 阈值 + 启动时执行 `PRAGMA wal_checkpoint(TRUNCATE)` | | list 无索引导致全表扫描 | 中(大数据量) | 初始 schema 即创建 `idx_memory_items_created_at` 索引 | | 父目录不存在导致 open 失败 | 低 | `open()` 内部调用 `fs::create_dir_all()` 确保目录存在 | | clear() N+1 删除性能 | 低 | 不走 trait 接口的逐条删除,可后续优化为直接 `DELETE FROM memory_items` | | 数据库文件损坏 | 低 | `migrate()` 中执行 `PRAGMA quick_check`;失败时返回 `MemoryError::Storage`,调用方可切换 InMemoryStore | ### 可观测性(实施时落实) - 所有 MemoryStore 方法通过 `tracing::instrument` 记录延迟和结果(`info!` 正常完成,`warn!` 超过性能预算阈值,`error!` 操作失败) - `list()` 返回行数通过 `tracing::debug` 记录(调优参考) - WAL 文件大小在 `migrate()` 后检查一次,超过 100MB 时记录 `warn!` - 操作计数(读写次数、错误率)暂不暴露为独立 metrics,v0.3 按需添加 --- ## 已知假设 | 假设 | 验证状态 | Fallback | |------|---------|----------| | 单进程独享 SQLite 文件,无跨进程竞争 | ✅ 设计前提(Mutex 为单进程设计) | 多进程场景使用网络后端(PostgreSQL/Redis,v0.3+) | | ISO 8601 TEXT 字典序等价于时间序 | ✅ 条件成立(需 UTC 归一化) | 若时区异常,切换 INTEGER(unix_timestamp) 存储后重建索引 | | SqliteStore 初始化失败可 fallback 到 InMemoryStore | ✅ 调用方自行控制 | `open()` 返回 `MemoryError`,消费者 catch 后改用 `InMemoryStore::new()` | | N+1 clear ~250ms(N=50) 可接受 | 🟡 未实测(基于 N×5ms 推算) | 若成为瓶颈,SqliteStore 内部加 `delete_by_prefix()` 方法(不走 trait 接口) | | rusqlite bundled SQLite 版本足够新 | ✅ 0.32 版内置 SQLite 3.46 | 如需特定版本,切换 `bundled` 为指定版本或使用系统 SQLite | | busy_timeout=5s 覆盖所有竞争场景 | 🟡 未实测(WAL 下写写冲突概率低) | 若观测到 `SQLITE_BUSY`,增大超时或在重试逻辑中处理 | | spawn_blocking 线程池不会被耗尽 | ✅ 默认 512 线程,Agent 场景占用 ≤10 | 若观测到阻塞任务排队,启动时 `tokio::task::spawn_blocking` 已有兜底排队机制 | --- ## 参考来源 - [rusqlite crate](https://crates.io/crates/rusqlite) — 官方文档 - [SQLite PRAGMA user_version](https://www.sqlite.org/pragma.html#pragma_user_version) — Schema 版本管理机制 - [SQLite WAL mode](https://www.sqlite.org/wal.html) — 并发读写性能优化 - `docs/6-memory-system.md` — Phase 3 MemoryStore trait 原始设计 - `docs/roadmap.md` — 项目里程碑规划(Phase 7/8/10 依赖关系) - `src/llm/provider.rs` → `src/llm/provider/` — 目录重构模式参考 --- ## 实施计划 ### 任务总览 3 个阶段、8 个任务单元、2 个 Commit。 ### 阶段一:目录重构 #### Task A1 — 创建 store/ 目录并提取 InMemoryStore | 项目 | 内容 | |------|------| | 任务描述 | 创建 `src/memory/store/` 目录,新建 `store/in_memory.rs`,从 `store.rs` 完整提取 InMemoryStore 结构体、impl MemoryStore、impl Default、6 个内联测试 | | 涉及文件 | `src/memory/store.rs` → 分割到 `src/memory/store/in_memory.rs`(新增) | | 前置依赖 | 无 | | 预估工作量 | S(< 1h) | | 风险等级 | 低 — 纯搬移,编译器可验证 | | 验收条件 | `cargo build` 通过(此时 store.rs 尚未修改,store/in_memory.rs 应被 crate 忽略) | 注意:需要先在 store.rs 顶部添加 `pub mod in_memory;` 声明,否则子模块不会被编译。或者可以先创建目录和文件,等 Task A2 再统一加声明路径。 实际做法:先创建文件但不声明,A2 统一声明。这样 A1 和 A2 之间可以有一个无编译的中间状态。 #### Task A2 — 修改 store.rs 模块根 | 项目 | 内容 | |------|------| | 任务描述 | 修改 `store.rs` 为纯模块根:保留 `MemoryStore` trait、`EvictionPolicy`、`EvictionConfig`;添加 `pub mod in_memory;` + `pub use in_memory::InMemoryStore;`;删除已提取到 in_memory.rs 中的代码 | | 涉及文件 | `src/memory/store.rs`(修改) | | 前置依赖 | Task A1(文件已存在) | | 预估工作量 | S(< 1h) | | 风险等级 | 低 — 保留部分不变,提取部分在子模块中 | | 验收条件 | `cargo test --all-targets` 全绿,测试数量不变(191 pass),clippy 0 warning | #### Task A3 — 验证阶段一 | 项目 | 内容 | |------|------| | 任务描述 | 运行全量测试链确认目录重构零行为变化 | | 涉及文件 | 全量 | | 前置依赖 | Task A2 | | 预估工作量 | XS(验证) | | 风险等级 | 低 | | 验收条件 | `cargo test --all-targets` 191 pass、`cargo clippy --all-targets -- -D warnings` 0 警告、`cargo build` 通过 | ### 阶段二:SqliteStore 实现 #### Task B1 — 添加 rusqlite 及 dev-dependencies | 项目 | 内容 | |------|------| | 任务描述 | 在 `Cargo.toml` 中添加依赖:`[dependencies]` 加 `rusqlite = { version = "0.32", features = ["bundled"] }`,`[dev-dependencies]` 加 `tempfile = "3"`(用于测试隔离);运行 `cargo build` 确认编译通过,`cargo test --no-run` 验证 dev-dependencies 可用 | | 涉及文件 | `Cargo.toml`(修改)、`Cargo.lock`(自动更新) | | 前置依赖 | 无(可与阶段一并行) | | 预估工作量 | XS(< 15min) | | 风险等级 | 低 — 标准依赖添加 | | 验收条件 | `cargo build` 成功,`cargo test --no-run` 成功,Cargo.lock 中生成 rusqlite 和 tempfile 条目 | #### Task B2 — 实现 SqliteStore 核心 | 项目 | 内容 | |------|------| | 任务描述 | 创建 `src/memory/store/sqlite_store.rs`,实现以下 8 个子模块: | | | (1)`SqliteStore` 结构体 `{ conn: Arc> }` | | | (2)`SqliteStore::open(path)` — 支持 `":memory:"`,内部调用 `fs::create_dir_all` 确保父目录存在 | | | (3)`lock_conn()` — 内部辅助方法,`.lock().unwrap_or_else(\|e\| e.into_inner())` 处理 Mutex 中毒 | | | (4)`migrate()` — 按版本递增执行迁移:`PRAGMA user_version` 检查(初始版本号=1)→ 建表 `memory_items` + 索引 `idx_memory_items_created_at` + 设置 WAL 模式 + `busy_timeout=5s` + `PRAGMA synchronous = NORMAL` + `PRAGMA wal_autocheckpoint=1000` + `PRAGMA quick_check`(检测数据库损坏)+ `PRAGMA wal_checkpoint(TRUNCATE)` | | | (5)`MemoryStore` trait 的 4 个方法实现(save/get/delete/list),全部使用 spawn_blocking 包裹;**生命周期注意**:`spawn_blocking` 闭包前先 `.conn.clone()` 取 `Arc`,参数调 `.clone()` 取 owned 值,再传入 `spawn_blocking(move \|{ ... })` | | | — `save`: `INSERT INTO memory_items (id, content, metadata, created_at) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(id) DO UPDATE SET content=excluded.content, metadata=excluded.metadata, created_at=excluded.created_at`(全字段覆盖 upsert,与 InMemoryStore 行为一致) | | | — `get`: `SELECT content, metadata, created_at FROM memory_items WHERE id = ?1` → Ok(None) 当无结果 | | | — `delete`: `DELETE FROM memory_items WHERE id = ?1`(幂等,不返回 NotFound) | | | — `list`: 根据 filter 字段(prefix/since/offset/limit)组合动态构造 WHERE 子句 + `ORDER BY created_at ASC` + `LIMIT ? OFFSET ?`,全部使用参数化查询防注入 | | | (6)序列化转换层:`time::OffsetDateTime` 存为 TEXT(ISO 8601),通过 `.to_string()` 绑定 `String` 参数;`serde_json::Value` 存为 TEXT,通过 `serde_json::to_string()` 绑定 `String` 参数;读取时通过 `OffsetDateTime::parse` 和 `serde_json::from_str` 反序列化。在 SqliteStore 内部实现 `to_sql_params()` / `from_sql_row()` 辅助方法集中处理 | | | (7)错误映射:不在 blanket impl From 中处理所有 rusqlite Error,而是在每个方法内部按数据完整性防御表的映射规则逐类处理: | | | — 数据库文件损坏 / IO 错误 → `MemoryError::Storage` | | | — created_at 解析失败 → `MemoryError::Serialization`(不 panic) | | | — content/metadata 为 NULL → `MemoryError::Storage` | | | — serde_json 序列化/反序列化失败 → `MemoryError::Serialization` | | | — 约束冲突 → `MemoryError::InvalidInput` | | | (8)可观测性:在 4 个 trait 方法和 `open()` 上添加 `#[tracing::instrument(skip(self))]`;正常完成记录 `trace!`,超过性能预算阈值记录 `warn!`,操作失败记录 `error!` | | 涉及文件 | `src/memory/store/sqlite_store.rs`(新增) | | 前置依赖 | Task B1(rusqlite + tempfile 依赖)、Task A1(store/ 目录存在) | | 预估工作量 | M(1-4h) | | 风险等级 | 高 — 3 个技术点需留意:`time::OffsetDateTime` 无 `rusqlite::ToSql` 实现,需显式处理 String 绑定;`spawn_blocking` + `&self` 生命周期需 clone 后才能传闭包;错误映射需精细匹配 `rusqlite::Error` 嵌套变体(`SqliteFailure` 内含 `ErrorCode`) | | 验收条件 | 单元测试通过(见 Task B4)、`cargo build` 通过 | #### Task B3 — 注册模块并重导出 | 项目 | 内容 | |------|------| | 任务描述 | 在 `store.rs` 添加 `pub mod sqlite_store;` + `pub use sqlite_store::SqliteStore;`;在 `memory.rs` 添加 `pub use store::SqliteStore;` | | 涉及文件 | `src/memory/store.rs`(修改)、`src/memory.rs`(修改) | | 前置依赖 | Task B2(sqlite_store.rs 文件存在) | | 预估工作量 | XS(< 15min) | | 风险等级 | 低 | | 验收条件 | `cargo build` 通过,`SqliteStore` 可从 `agcore::memory::SqliteStore` 路径访问 | #### Task B4 — 编写测试 | 项目 | 内容 | |------|------| | 任务描述 | 在 `sqlite_store.rs` 中编写 `#[cfg(test)] mod tests`,覆盖: | | | (1)CRUD 基本操作(save → get → list → delete → get None) | | | (2)Upsert 语义(同 id 重复 save 覆盖内容,created_at 保持调用方传入值) | | | (3)prefix 过滤(MemoryFilter.prefix) | | | (4)时间范围过滤(MemoryFilter.since) | | | (5)offset/limit 分页 | | | (6)并发 10 个 writer × 10 次写入,验证无数据丢失 | | | (7)持久化恢复(write → drop store → reopen 同一文件 → read) | | | (8)错误路径:`open("/nonexistent_dir/ag.db")` 返回 Storage 错误 | | | 辅助函数:`make_item(id)` 创建 MemoryItem,使用 `tempfile::TempDir`(或自定义 tmp 路径)隔离测试数据库文件 | | 涉及文件 | `src/memory/store/sqlite_store.rs`(修改追加 test mod) | | 前置依赖 | Task B2(实现完成) | | 预估工作量 | M(1-4h) | | 风险等级 | 中 — 并发测试的时序控制、持久化恢复测试的 TempDir 管理 | | 验收条件 | 全量测试通过,新增测试 ≥ 8 个 | #### Task B5 — 验证阶段二 | 项目 | 内容 | |------|------| | 任务描述 | 运行全量测试链确认 SqliteStore 实现正确,不影响已有模块 | | 涉及文件 | 全量 | | 前置依赖 | Task B3、Task B4 | | 预估工作量 | S(< 1h) | | 风险等级 | 低 | | 验收条件 | `cargo test --all-targets` 全绿(191 + 新增测试)、`cargo clippy --all-targets -- -D warnings` 0 警告、`cargo build` 通过 | ### 阶段三:验证收尾 #### Task C1 — 跨模块兼容性验证 | 项目 | 内容 | |------|------| | 任务描述 | (1)确认 `ConversationMemory` / `KnowledgeStore` / `MemoryRetriever` / `AgentSession` / `SessionMemory` 以 `Arc` 接受 SqliteStore 时编译通过且测试全绿 | | | (2)确认 SqliteStore 与 InMemoryStore 可互换——修改一个现有测试将后端从 InMemoryStore 换为 SqliteStore(使用 `":memory:"`),测试全绿 | | | (3)验证性能预算:在测试环境下测量 save(1KB)/get(1KB)/list(100行) 的单次延迟,确认 < 5ms / < 3ms / < 20ms | | 涉及文件 | 测试文件(memory/ 模块内各 test mod) | | 前置依赖 | Task B5 | | 预估工作量 | S(< 1h) | | 风险等级 | 低 | | 验收条件 | 全量测试通过 + 互换测试通过 + 性能预算大致满足(未达标不阻塞发布) | ### 依赖关系图 ``` 阶段一(目录重构) 阶段二(SqliteStore 实现) ┌──────────────┐ ┌──────────────┐ │ Task A1 │ │ Task B1 │ ← 无依赖,可与 A 并行 │ 创建目录+提取 │ │ Cargo.toml │ └──────┬───────┘ └──────┬───────┘ ↓ ↓ ┌──────────────┐ ┌──────────────┐ │ Task A2 │ │ Task B2 │ │ 修改store.rs │← A1 ───→│ 核心实现 │← B1 + A1 └──────┬───────┘ └──────┬───────┘ ↓ ↓ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Task A3 │ │ Task B3 │← B2 ──→│ Task B4 │ │ 验证阶段一 │ │ 注册+重导出 │ │ 编写测试 │ └──────────────┘ └──────┬───────┘ └──────┬───────┘ ↓ ↓ ┌──────────────┐────────────────┘ │ Task B5 │← B3 + B4 │ 验证阶段二 │ └──────┬───────┘ ↓ 阶段三(验证收尾) ┌──────────────┐ │ Task C1 │ │ 跨模块兼容性 │ └──────────────┘ ``` ### Commit 安排 | 顺序 | Commit 类型 | Scope | 描述 | 包含 Task | |------|------------|-------|------|-----------| | 1 | refactor | memory | 将 store.rs 拆分为模块目录,仅结构搬移 | A1 → A2 → A3 | | 2 | feat | memory | 实现 SqliteStore 持久化(含错误映射、测试、WAL 模式) | B1 → B2 → B3 → B4 → B5 → C1 | 注意:Task B1 与阶段一无依赖,可以在 Commit 1 合并进行或在 Commit 2 开头。建议在 Commit 2 开头,因为 Cargo.toml 变更属于功能变更而非重构。 ### 验证全链 实施完毕后整体认证链路: 1. `cargo test --all-targets` — 全量测试通过 2. `cargo clippy --all-targets -- -D warnings` — 0 警告 3. `cargo build --release` — release 构建通过 4. 确认 `cargo doc --no-deps` 无 warning(新增公开类型文档注释) 5. 确认测试数量:191 + (8 个 new sqlite_store tests) = 199+ pass