docs(harness): make the custom memory storage guide followable (#5844)

`deerflow.agents.memory.storage` does not exist (memory/__init__.py says
DeerMem-private symbols are imported from
deerflow.agents.memory.backends.deermem.deermem.core.*), the documented class
shape cannot be built by create_storage() (it instantiates cls(config)) or
called by the memory updater (load/reload/save all pass user_id=, save also
expected_revision=), and storage_class is split on the last dot, so the
`module:Class` value the guide shows never resolves. memory.mdx also promised a
fallback that the code explicitly refuses.
This commit is contained in:
lihongyuan99 2026-09-25 08:58:03 +08:00 committed by GitHub
parent bc262743d7
commit 2a76c9db71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 18 deletions

View File

@ -119,17 +119,25 @@ sandbox:
Replace the file-based memory with any persistent store by implementing `MemoryStorage`:
```python
from deerflow.agents.memory.storage import MemoryStorage
from typing import Any
from deerflow.agents.memory.backends.deermem.deermem.core.storage import MemoryStorage
class RedisMemoryStorage(MemoryStorage):
def load(self, agent_name: str | None = None) -> dict[str, Any]:
# `create_storage()` builds the class as `cls(config)`, so it must take the backend config.
def __init__(self, config) -> None:
...
def reload(self, agent_name: str | None = None) -> dict[str, Any]:
# The memory updater calls these with `user_id=` — and `save()` also with
# `expected_revision=` — so both extra parameters must stay in the signature.
def load(self, agent_name: str | None = None, *, user_id: str | None = None) -> dict[str, Any]:
...
def save(self, memory_data: dict[str, Any], agent_name: str | None = None) -> bool:
def reload(self, agent_name: str | None = None, *, user_id: str | None = None) -> dict[str, Any]:
...
def save(self, memory_data: dict[str, Any], agent_name: str | None = None, *, user_id: str | None = None, expected_revision: int | None = None) -> bool:
...
```
@ -137,9 +145,12 @@ Configure it in `config.yaml`:
```yaml
memory:
storage_class: mypackage.storage:RedisMemoryStorage
backend_config:
storage_class: mypackage.storage.RedisMemoryStorage
```
Unlike `sandbox.use:` above, `storage_class` is resolved with a dotted `module.Class` path — `RedisMemoryStorage` is split off after the last dot, so a `module:Class` value fails to import.
## Custom skills
Skills are the easiest extension point. Create a directory under `skills/custom/your-skill-name/` with a `SKILL.md` file. The skill is discovered automatically on the next `load_skills()` call.

View File

@ -145,14 +145,23 @@ You can change the storage path with the `storage_path` field. Relative paths ar
## Custom storage backend
The `storage_class` field allows you to replace the default file-based storage with a custom implementation. Any class that extends `MemoryStorage` and implements `load()`, `reload()`, and `save()` methods can be used:
The `storage_class` field allows you to replace the default file-based storage with a custom implementation. Any class that extends `MemoryStorage` (`deerflow.agents.memory.backends.deermem.deermem.core.storage`) and implements `load()`, `reload()`, and `save()` methods can be used:
```yaml
memory:
storage_class: mypackage.storage.RedisMemoryStorage
backend_config:
storage_class: mypackage.storage.RedisMemoryStorage
```
If the configured class cannot be loaded, the system falls back to the default `FileMemoryStorage` and logs an error.
The value is a dotted `module.Class` path and the class is constructed as `cls(config)` — see "Custom memory storage" under Customization for the full interface.
If the configured class cannot be imported, is not a `MemoryStorage` subclass, or cannot be constructed, memory does **not** fall back to `FileMemoryStorage`. Building the memory manager raises instead:
```
ValueError: backend_config.storage_class='mypackage.storage.NotThere' failed to load: No module named 'mypackage'. Refusing to silently fall back because memory is persistent state.
```
surfaced as a pydantic `ValidationError` from `DeerMem`'s constructor, because storage is wired in `model_post_init()`.
## Disabling memory

View File

@ -115,17 +115,25 @@ sandbox:
通过实现 `MemoryStorage` 将基于文件的记忆替换为任何持久化存储:
```python
from deerflow.agents.memory.storage import MemoryStorage
from typing import Any
from deerflow.agents.memory.backends.deermem.deermem.core.storage import MemoryStorage
class RedisMemoryStorage(MemoryStorage):
def load(self, agent_name: str | None = None) -> dict[str, Any]:
# `create_storage()` 以 `cls(config)` 构造存储类,因此必须接收 backend config。
def __init__(self, config) -> None:
...
def reload(self, agent_name: str | None = None) -> dict[str, Any]:
# memory updater 调用这些方法时始终带上 `user_id=`,`save()` 还会带上
# `expected_revision=`,所以这两个参数必须保留在签名里。
def load(self, agent_name: str | None = None, *, user_id: str | None = None) -> dict[str, Any]:
...
def save(self, memory_data: dict[str, Any], agent_name: str | None = None) -> bool:
def reload(self, agent_name: str | None = None, *, user_id: str | None = None) -> dict[str, Any]:
...
def save(self, memory_data: dict[str, Any], agent_name: str | None = None, *, user_id: str | None = None, expected_revision: int | None = None) -> bool:
...
```
@ -133,9 +141,12 @@ class RedisMemoryStorage(MemoryStorage):
```yaml
memory:
storage_class: mypackage.storage:RedisMemoryStorage
backend_config:
storage_class: mypackage.storage.RedisMemoryStorage
```
与上面的 `sandbox.use:` 不同,`storage_class` 按 `module.Class` 点号路径解析——类名取自最后一个点号之后,所以写成 `module:Class` 会导入失败。
## 自定义技能
技能是最简单的扩展点。在 `skills/custom/your-skill-name/` 下创建一个目录,并在其中添加 `SKILL.md` 文件。技能将在下次 `load_skills()` 调用时自动被发现。

View File

@ -53,8 +53,9 @@ memory:
# 绝对路径按原样使用,相对路径相对于 base_dir 解析。
storage_path: memory.json
# 存储类(默认:基于文件的 JSON 存储)
storage_class: deerflow.agents.memory.storage.FileMemoryStorage
# 存储类(留空 = FileMemoryStorage,即默认的基于文件的 JSON 存储,不走 importlib;
# 自定义实现填 `module.Class` 点号路径)
storage_class: ""
# 处理排队记忆更新前等待的秒数(防抖)
debounce_seconds: 30
@ -98,14 +99,23 @@ DeerFlow 支持两级记忆:
## 自定义存储后端
`storage_class` 字段允许你用自定义实现替换默认的基于文件的存储。任何继承 `MemoryStorage` 并实现 `load()`、`reload()` 和 `save()` 方法的类都可以使用:
`storage_class` 字段允许你用自定义实现替换默认的基于文件的存储。任何继承 `MemoryStorage`(位于 `deerflow.agents.memory.backends.deermem.deermem.core.storage`)并实现 `load()`、`reload()` 和 `save()` 方法的类都可以使用:
```yaml
memory:
storage_class: mypackage.storage.RedisMemoryStorage
backend_config:
storage_class: mypackage.storage.RedisMemoryStorage
```
如果配置的类无法加载,系统回退到默认的 `FileMemoryStorage` 并记录错误。
该值是 `module.Class` 点号路径,且存储类会按 `cls(config)` 构造——完整接口见「自定义」页面的「自定义记忆存储」。
如果配置的类无法导入、不是 `MemoryStorage` 的子类,或无法构造,系统**不会**回退到 `FileMemoryStorage`,而是在构建记忆管理器时直接抛出:
```
ValueError: backend_config.storage_class='mypackage.storage.NotThere' failed to load: No module named 'mypackage'. Refusing to silently fall back because memory is persistent state.
```
由于 storage 在 `model_post_init()` 中装配,这个错误会以 pydantic 的 `ValidationError` 形式从 `DeerMem` 构造函数冒出来。
## 禁用记忆