* feat(memory): honcho backend config parsing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(memory): honcho v3 http client Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(memory): honcho memory manager (workspace-per-user, fail-closed identity, async offload) - HonchoMemoryManager implements the MemoryManager contract (add/get_context/ search/get_memory/shutdown_flush + aadd/aget_context/asearch offloaded via asyncio.to_thread), signatures verified against manager.py's tier-1/tier-2/ async abstracts. - Workspace resolution: workspace_overrides[user_id] else workspace_prefix + sanitize_id(user_id); missing/empty user_id fails closed (no-op write, empty read) rather than falling back to a shared workspace. User peer: user_peer_overrides[user_id] else sanitize_id(user_id). - get_context self-truncates to max_injection_chars and raises MemoryManagerError only under failure_policy.read=fail_closed; default is log-and-return "". - Restore backends/honcho/__init__.py to the noop direct-import convention (MANAGER_CLASS = HonchoMemoryManager) now that honcho_manager.py exists, replacing Task 10's temporary lazy __getattr__ scaffold. - Fix Task 10 deferred docstring minor: sanitize_id docstring now states the grammar allows up to 100 chars while this helper caps at 64. - 19 new tests appended to test_honcho_memory_backend.py (write/read/async/ lifecycle/factory-discovery); 27/27 pass. Verified end-to-end that manager.py's drop-in backend scanner resolves "honcho" with no core edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory): collision-resistant identity derivation, exception containment, passive-writes flag Task review findings (2 Critical + 1 Important), all fixed in the same worktree: - CRITICAL (cross-user bleed): sanitize_id is lossy -- "user.name@example.com" and "user-name@example.com" both sanitized to the same string, merging two users' memory into one workspace/peer. Add _stable_id() (sanitize_id output + 8-hex-char SHA-256 suffix of the raw id) and use it on the default (non-override) path in _workspace/_user_peer; workspace_overrides / user_peer_overrides still match on the raw key, unchanged. The hash suffix also guarantees a non-empty result for a raw id that sanitizes to "" (e.g. "!!!"), so _user_peer can no longer return "". Documented in the manager's isolation docstring. - CRITICAL (exception containment): client.py's _post() called response.json() outside the try block, so a 200 with a non-JSON body raised a bare JSONDecodeError that would escape add() with no upstream handler. Wrap the parse and raise HonchoRequestError (mirrors Mem0Client._request). Broadened the manager's four boundary excepts from `except HonchoRequestError` to `except Exception` (mirrors openviking_manager.py's broad-guard precedent), with `except MemoryManagerError: raise` first so a contract error is never swallowed or double-wrapped. - IMPORTANT: added requires_passive_writes_in_tool_mode: ClassVar[bool] = True -- Honcho's only write path is passive add() (no fact CRUD hooks), so tool mode must keep MemoryMiddleware writes flowing to the deriver. Mirrors mem0_manager.py's identical flag/rationale. Minors addressed: get_memory(user_id=None) empty-shape-with-no-calls test; empty-string user_id tests for add()/get_context(); dedicated collision test proving two colliding raw ids resolve to different workspaces/peers. 10 new tests (37/37 total pass); RED verified by stashing only the implementation files (tests import the not-yet-existing _stable_id, so the whole module fails to collect) before restoring the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(memory): blocking-io anchor for honcho backend; docs + config example - Adds test_honcho_memory_backend.py in tests/blocking_io/ with fake-client blocking IO - Mirrors openviking anchor structure and conftest conventions - Updates backends/README.md with honcho row and config keys section - Updates config.example.yaml with honcho commented block - Updates backend/AGENTS.md with honcho memory backend bullet - Documents workspace resolution (prefix + collision-resistant sanitized id) - Documents tool mode passive write retention via MemoryMiddleware - Documents async entrypoint offloading via asyncio.to_thread - Documents fail_closed vs fail_open recall failure policy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory): wire close() to shutdown hook; correct honcho README defaults and tool-mode note - HonchoMemoryManager.close() releases the HTTP client, mirroring mem0_manager.py's pattern and the base MemoryManager.close() shutdown hook. - README: fix workspace_prefix (deerflow-u-), message_char_limit (8000), max_injection_chars (6000), and base_url (default http://localhost:8000, not required) against backends/honcho/config.py; add missing timeout_seconds/connect_timeout_seconds rows; replace the "middleware mode only" claim with wording matching reality (tool mode supported, search implemented, passive writes retained via MemoryMiddleware). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory): honor failure_policy.read on all honcho recall paths; review nits Addresses PR #4730 review feedback: - search() and get_memory() now route through a _read_or_fallback policy gate (mem0's pattern), so failure_policy.read: fail_closed raises MemoryManagerError on every recall path as documented; get_context() uses the same helper, preventing future drift. - Session ids use the collision-resistant _stable_id derivation; bare sanitize_id would merge threads like "t.1"/"t-1" into one session. - HonchoClient accepts a transport kwarg (Mem0Client precedent) so tests inject httpx.MockTransport through the constructor. - Config: empty/null workspace/peer override values fail fast at parse time instead of silently falling through to the default derivation. - _UTC_NOW_FIELDS 1-tuple replaced by a plain _UTC_NOW_FORMAT constant. - README: user_peer_overrides row described the wrong target (it overrides the user's own peer, not assistant_peer); document the non-empty constraint on override values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(memory): qualify honcho isolation claim for shared workspace_overrides The module docstring claimed users cannot see each other's memory by construction, unconditionally. That holds for the default one-workspace-per-user derivation, but a workspace_overrides entry mapping several users to one workspace shares that workspace's search index: search() uses Honcho's workspace-scoped /search (no peer filter), while get_context()/get_memory() stay peer-scoped via working_representation. State the asymmetry in the docstring, the README Workspace Resolution section, and the workspace_overrides table row. Docs-only; no behavior change (review follow-up on #4730). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Memory Backends
Each subfolder under agents/memory/backends/ is a pluggable memory backend. Swap the active one by changing one line in config.yaml - no deer-flow core changes required.
deermem/- the default backend (deer-flow's own: structured facts + JSON storage).noop/- an empty backend and the template to copy when adding a new one.openviking/- optional remote backend using the officiallangchain-openvikingpackage (single-user middleware mode).honcho/- optional remote Honcho backend over HTTP: user-model memory (representations built by Honcho's server-side deriver; no local LLM calls). Workspace-per-user isolation with per-user overrides.
This guide tells you which files to touch when you change, swap, or add a memory system. Paths are relative to backend/ unless noted.
Table of Contents
- Add a New Backend
- Switch the Active Backend
- Backend Contract
- Do Not Modify
- Common Pitfalls
- Reference
Add a New Backend
Copy noop/ to backends/<yourname>/ and edit three files in this folder plus two outside it.
| File | What to change |
|---|---|
backends/<yourname>/config.py |
Declare your config fields + from_backend_config (parse backend_config; read storage_path from it - do not import deer-flow path helpers) |
backends/<yourname>/<yourname>_manager.py |
Rename the class; parse config in model_post_init; implement from_config + the tier-1 abstracts (add/get_context); override tier-2/3 methods as needed (see Backend Contract) |
backends/<yourname>/__init__.py |
MANAGER_CLASS = YourManager (relative import) |
config.yaml (repo root, parent of backend/) |
memory.manager_class: <yourname> + your knobs under memory.backend_config |
packages/harness/pyproject.toml |
Only if the backend needs external libs: declare the dependency; add [tool.uv.sources] for vendored source. Otherwise uv sync purges it (see Common Pitfalls) |
See the docstring at the top of noop/noop_manager.py for the full 6-step walkthrough.
Switch the Active Backend
Edit config.yaml (repo root) only:
memory:
manager_class: <name> # deermem / noop / <yourname>
backend_config: { ... } # that backend's private config
Then restart deer-flow - the memory manager is a process-level singleton; a running process does not hot-reload config or backend code.
Backend Contract
1. The three-tier contract
MemoryManager is a pydantic BaseModel (not a bare ABC). Methods are tiered:
- Tier 1 (abstract) --
add+get_context: every backend MUST implement (write + read-inject are the backend's fundamental duties; missing one is caught at instantiation). - Tier 2 (management, with defaults) --
add_nowait(delegates toadd),search/get_memory/clear_memory/import_memory/export_memory/delete_memory(defaultraise NotImplementedError),shutdown_flush(defaultTrue). Override the ones your backend supports. - Tier 3 (optional hooks, with defaults) --
warm(defaultTrue),reload_memory/create_fact/delete_fact/update_fact(default raise),on_pre_compress/on_turn_start(default no-op).
A new backend implements from_config + add + get_context and overrides only what it supports; the rest inherits defaults. Signatures must match (parameter names, keyword-only args). noop is the minimal reference.
2. Return shape (critical, easy to get wrong)
get_memory / export_memory / clear_memory / import_memory return a dict that the gateway casts to the DeerMem shape (MemoryResponse: version / lastUpdated / user / history / facts[]). Your backend must return a dict this shape accepts, or:
- the data is silently dropped (pydantic ignores unknown fields);
- the frontend gets empty defaults and
lastUpdated=""crashes the date formatter.
A non-DeerMem backend maps its native records (e.g. {"results": [...]}) into this shape via a small adapter helper.
3. Tier-3 hooks (contracted, no hasattr probing)
create_fact / delete_fact / update_fact / reload_memory / warm are tier-3 hooks ON the base contract (with defaults). Callers (gateway / client / tools) invoke them directly and catch NotImplementedError for unsupported backends -- no more hasattr probing.
create_fact/delete_fact/update_fact- the frontend's add/delete/edit-fact buttons. Default raises (caller returns 501).reload_memory- the frontend's reload button (caller falls back toget_memoryonNotImplementedError).warm- one-time warm-up at gateway startup (defaultTrue= nothing to warm).
Implement the ones your backend supports; the rest inherit the default raise.
4. Portability (the golden rule)
Important
A backend talks to the host through exactly two channels: (1) the ABC method arguments (
manager.py), and (2) thebackend_configdict. The onlyfrom deerflowimport allowed anywhere in your backend folder is the ABC contract line in<name>_manager.py:
from deerflow.agents.memory.manager import MemoryManager
Change that one line (and only that line) to port the backend to another agent. Do not import deer-flow path helpers, config singletons, or models - get storage_path and everything else from backend_config.
5. What the host provides
The factory (manager.py::get_memory_manager) resolves the backend class, injects storage_path into backend_config, then calls cls.from_config(backend_config, mode=cfg.mode, **host_hooks). The host hooks (passed as from_config kwargs, NOT in backend_config):
backend_config["storage_path"](str) - a writable state dir (the host'sruntime_homeby default, or whateverconfig.yamlsets). Use this as your storage root.callbacks(MemoryCallbacks| None) - observability;on_memory_llm_callmerges trace metadata before your LLM call (langfuse). Pass it to your LLM path; ignore if you don't trace.should_keep_hidden_message/trace_context_manager/host_llm_factory- other host hooks; consume infrom_configif relevant.- Plus whatever the user puts under
config.yaml::memory.backend_config(your backend's own knobs).
Each backend's from_config consumes the hooks it needs (DeerMem does; noop ignores them).
Do Not Modify
These are backend-agnostic. Don't touch them when swapping backends (unless you're changing the shared contract, which affects every backend):
| File | Role |
|---|---|
packages/harness/deerflow/agents/memory/manager.py |
ABC + factory + scanner |
packages/harness/deerflow/agents/middlewares/memory_middleware.py |
after_agent -> manager.add |
packages/harness/deerflow/agents/memory/summarization_hook.py |
summarization -> manager.add_nowait |
packages/harness/deerflow/agents/lead_agent/prompt.py |
_get_memory_context -> manager.get_context |
app/gateway/routers/memory.py |
HTTP endpoints -> manager.* (direct call + try/except NotImplementedError) |
packages/harness/deerflow/config/memory_config.py |
shared 4 fields (enabled / injection_enabled / manager_class / backend_config) |
frontend/src/components/workspace/settings/memory-settings-page.tsx |
frontend memory page (assumes DeerMem shape) |
Note
The gateway and frontend are currently hard-coded to the DeerMem shape - that's why backends must return DeerMem-shape data (contract #2). Making them fully backend-agnostic is a larger refactor.
Common Pitfalls
Lessons from integrating external backends:
- External deps must be declared in
pyproject.toml. A bareuv pip installis purged on the nextuv sync/langgraph dev. Declare the dep (and[tool.uv.sources]for vendored source). - Return the DeerMem shape. Otherwise the frontend crashes with
Invalid time valueand your data is silently dropped. Build a small adapter helper to map your native records into it. - Fact CRUD returns 501 if not implemented. The frontend's delete-fact button reports
Operation 'delete fact' not supported. Implementdelete_fact(and friends) to fix it. - Don't import
runtime_home. Readstorage_pathfrombackend_config. (Thenooptemplate shows the correct pattern; importing deer-flow path helpers breaks portability - contract #4.) - Restart deer-flow after changes. The manager is a process-level singleton; a running process does not hot-reload config or backend code.
- Cap
get_contextlength yourself. The host applies no token budget; the backend must truncate (DeerMem hasmax_injection_tokens; noop does not).
Honcho Backend
The optional honcho/ backend is a remote-only HTTP adapter for user-model memory over Honcho (self-hosted or via api.honcho.dev). Middleware mode is the default; tool mode is also supported (search is implemented) and retains passive writes via MemoryMiddleware (requires_passive_writes_in_tool_mode = True) so Honcho's server-side deriver keeps building representations from every turn (no local LLM calls).
Configuration (under memory.backend_config):
| Key | Type | Default | Description |
|---|---|---|---|
base_url |
str | http://localhost:8000 |
Honcho instance URL (e.g., http://localhost:8000 or https://api.honcho.dev) |
api_key |
str | optional | API key for hosted Honcho; required if base_url is api.honcho.dev. Can use $HONCHO_API_KEY env var syntax. Requires allow_insecure_http: true when using plain HTTP |
allow_insecure_http |
bool | false | Allow HTTP (non-HTTPS) connections; needed for localhost development with api_key |
timeout_seconds |
float | 10.0 |
HTTP client timeout (seconds) for calls to Honcho — read/write/pool; see connect_timeout_seconds for the connect phase |
connect_timeout_seconds |
float | 3.0 |
HTTP connect timeout (seconds) for establishing the connection to Honcho |
workspace_prefix |
str | deerflow-u- |
Prefix for isolated workspaces; each user gets one workspace named {prefix}{sanitized_id} |
workspace_overrides |
dict | {} |
Map specific user ids to custom workspace names; overrides the prefix-based derivation. Values must be non-empty (parse error otherwise). Mapping several users to one workspace shares its search index across them (see Workspace Resolution) |
user_peer_overrides |
dict | {} |
Map specific user ids to custom names for the user's own peer; overrides the stable-id derivation. Values must be non-empty (parse error otherwise) |
assistant_peer |
str | deerflow |
Default peer name for the assistant when storing messages |
message_char_limit |
int | 8000 |
Character limit per message; longer messages are truncated |
max_injection_chars |
int | 6000 |
Character limit for injected memory into the system prompt |
failure_policy.read |
str | fail_open |
Recall failure handling: fail_open (log and return empty) or fail_closed (rethrow) |
Workspace Resolution: Each DeerFlow user maps to one Honcho workspace. The workspace name is derived as: workspace_overrides[user_id] (if present) else workspace_prefix + sanitized_id, where sanitized_id is a collision-resistant hash suffix (sanitize[:48]-sha256[:8]). Missing user fails closed to no memory. The default derivation is isolated per user; a workspace_overrides entry that maps several users to one workspace deliberately shares that workspace's search index across them (search uses Honcho's workspace-scoped /search, which has no peer filter), while get_context / get_memory remain peer-scoped.
Tool Mode: While tool-mode memory tools are not fully supported, the backend implements requires_passive_writes_in_tool_mode = True to retain passive writes via MemoryMiddleware while also enabling memory search through the memory_search tool.
Reference
- Template:
noop/- minimal implementation with full docstrings; copy and go. - Contract + factory:
packages/harness/deerflow/agents/memory/manager.py(MemoryManagerbase,MemoryCallbacks,get_memory_managerfactory).