From 5c80c07dfee3a8a0ca67878ed23a27d0d5030fc1 Mon Sep 17 00:00:00 2001 From: ajayr Date: Thu, 16 Jul 2026 01:29:30 +0100 Subject: [PATCH] fix(memory): treat explicit null backend_config values as omitted in DeerMemConfig (#4217) config.example.yaml ships backend_config.model: as a bare key whose children are all comments, which YAML parses to None (make config-upgrade then writes an explicit model: null). DeerMemConfig.model is a non-Optional field with a default, so from_backend_config(**{"model": None}) raised a ValidationError and every run failed with "Input should be a valid dictionary or instance of DeerMemModelConfig". Drop None entries in from_backend_config so YAML null / empty keys fall back to field defaults, matching the documented "empty = host default LLM" semantics. Upstream bug (#4122 schema); regression-pinned in test_deermem_self_contained.py. Co-authored-by: Claude Fable 5 --- .../memory/backends/deermem/deermem/config.py | 8 ++++- backend/tests/test_deermem_self_contained.py | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/config.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/config.py index 0001e3b27..54459823a 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/config.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/config.py @@ -203,10 +203,16 @@ class DeerMemConfig(BaseModel): typo (e.g. ``storage_pat`` missing the ``h``) does not silently fall back to the default and write memory to an unintended location -- mirrors the host layer's ``load_memory_config_from_dict`` warning. + + ``None`` values are dropped so they fall back to the field default: + YAML renders an empty key (``model:`` with only commented children, as + shipped in ``config.example.yaml``) as ``None``, which non-Optional + fields like ``model`` would otherwise reject even though omitting the + key entirely is valid. """ if not backend_config: return cls() - known = {k: v for k, v in backend_config.items() if k in cls.model_fields} + known = {k: v for k, v in backend_config.items() if k in cls.model_fields and v is not None} unknown = sorted(k for k in backend_config if k not in cls.model_fields) if unknown: logger.warning( diff --git a/backend/tests/test_deermem_self_contained.py b/backend/tests/test_deermem_self_contained.py index c28285cee..f5da58e25 100644 --- a/backend/tests/test_deermem_self_contained.py +++ b/backend/tests/test_deermem_self_contained.py @@ -396,3 +396,37 @@ def test_from_backend_config_silent_on_known_keys(caplog): with caplog.at_level("WARNING", logger=cfg_logger): DeerMemConfig.from_backend_config({"storage_path": "/tmp/x", "max_facts": 20}) assert not any("Unknown backend_config keys" in r.message for r in caplog.records) + + +def test_from_backend_config_null_values_fall_back_to_defaults(): + """Explicit YAML ``null`` values must behave like omitted keys, not crash. + + ``config.example.yaml`` ships ``backend_config.model:`` as a bare key with + commented children, which YAML parses to ``None`` (and ``make + config-upgrade`` writes it out as an explicit ``model: null``). Non-Optional + fields like ``model: DeerMemModelConfig`` reject an explicit ``None`` even + though the omitted key would use the field default — so the shipped example + config crashed every run with a DeerMemConfig ValidationError.""" + from deerflow.agents.memory.backends.deermem.deermem.config import ( + DeerMemConfig, + DeerMemModelConfig, + ) + + cfg = DeerMemConfig.from_backend_config({"model": None, "debounce_seconds": None, "storage_path": "/tmp/x"}) + + # None entries fall back to field defaults; real values still parse + assert isinstance(cfg.model, DeerMemModelConfig) + assert cfg.model.model is None # default = no extraction LLM configured + assert cfg.debounce_seconds == DeerMemConfig().debounce_seconds + assert cfg.storage_path == "/tmp/x" + + +def test_from_backend_config_null_values_do_not_warn_as_unknown(caplog): + """Dropped ``None`` entries are known keys — they must not trip the + unknown-key typo warning.""" + from deerflow.agents.memory.backends.deermem.deermem.config import DeerMemConfig + + cfg_logger = "deerflow.agents.memory.backends.deermem.deermem.config" + with caplog.at_level("WARNING", logger=cfg_logger): + DeerMemConfig.from_backend_config({"model": None}) + assert not any("Unknown backend_config keys" in r.message for r in caplog.records)