diff --git a/frontend/src/content/en/harness/customization.mdx b/frontend/src/content/en/harness/customization.mdx index f15033a50a..f4485afeaa 100644 --- a/frontend/src/content/en/harness/customization.mdx +++ b/frontend/src/content/en/harness/customization.mdx @@ -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. diff --git a/frontend/src/content/en/harness/memory.mdx b/frontend/src/content/en/harness/memory.mdx index e0f4ea906c..c3fcd5149b 100644 --- a/frontend/src/content/en/harness/memory.mdx +++ b/frontend/src/content/en/harness/memory.mdx @@ -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 diff --git a/frontend/src/content/zh/harness/customization.mdx b/frontend/src/content/zh/harness/customization.mdx index 93b77909af..1916e64a91 100644 --- a/frontend/src/content/zh/harness/customization.mdx +++ b/frontend/src/content/zh/harness/customization.mdx @@ -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()` 调用时自动被发现。 diff --git a/frontend/src/content/zh/harness/memory.mdx b/frontend/src/content/zh/harness/memory.mdx index 4e107d43d4..20c9a9b885 100644 --- a/frontend/src/content/zh/harness/memory.mdx +++ b/frontend/src/content/zh/harness/memory.mdx @@ -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` 构造函数冒出来。 ## 禁用记忆