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 <noreply@anthropic.com>
This commit is contained in:
ajayr
2026-07-16 08:29:30 +08:00
committed by GitHub
co-authored by Claude Fable 5
parent 1769b2de0d
commit 5c80c07dfe
2 changed files with 41 additions and 1 deletions
@@ -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(
@@ -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)