bytedance/deer-flow · error · ValueError

fact must be an object

Error message

fact must be an object

What it means

The fact normalization entrypoint (_normalize_fact) requires its fact argument to be a dict (JSON object). Passing a list, string, number, or None raises ValueError('fact must be an object') before any field is read. It is the outermost type gate for every fact written to memory.

Source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:186

    if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
        raise ValueError(f"fact.{field} must be a list of strings")
    fact[field] = value


def _normalize_fact(
    fact: dict[str, Any],
    *,
    scope: dict[str, str | None],
    existing: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Validate one fact and derive its per-item revision.

    The shared JSON revision protects the multi-file transaction.  The fact's
    own revision protects one Markdown object when a disjoint transaction is
    safely rebased after that shared revision changed.
    """
    if not isinstance(fact, dict):
        raise ValueError("fact must be an object")
    normalized = copy.deepcopy(fact)
    normalized["id"] = str(normalized.get("id") or f"fact_{uuid.uuid4().hex}")
    # Validate the id through the canonical path builder's public contract.
    if not normalized["id"] or any(character not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" for character in normalized["id"]):
        raise ValueError("fact.id may contain only letters, numbers, '_' and '-'")
    normalized["schemaVersion"] = 2
    if not isinstance(normalized.get("content"), str):
        raise ValueError("fact.content must be a string")
    normalized["content"] = normalized["content"].strip()
    if not normalized["content"]:
        raise ValueError("fact.content must not be empty")
    _normalize_category(normalized)
    confidence = normalized.get("confidence", 0.5)
    if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
        raise ValueError("fact.confidence must be a number between 0 and 1")
    normalized["confidence"] = float(confidence)
    status = normalized.get("status", "active")
    if status != "active":

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Parse before saving: fact = json.loads(raw) if isinstance(raw, str) else raw, and check isinstance(fact, dict).
  2. Skip or log-and-continue when the extraction step yields None or a non-object, instead of forwarding it to storage.
  3. If you meant to save many facts, loop over the list and save each dict element individually.

Example fix

# before
memory.save_fact(raw_fact_json)  # raw_fact_json is a str
# after
memory.save_fact(json.loads(raw_fact_json))
Defensive patterns

Strategy: type-guard

Validate before calling

import json
if isinstance(fact, (bytes, str)):
    fact = json.loads(fact)
if not isinstance(fact, dict):
    return  # nothing to save

Type guard

from typing import Any

def is_fact_object(value: Any) -> bool:
    return isinstance(value, dict)

Try / catch

try:
    store.save(fact)
except ValueError as exc:
    if "must be an object" in str(exc):
        return  # extraction produced nothing; not an error condition
    raise

Prevention

When it happens

Trigger: Calling save/upsert with a JSON string instead of a parsed dict (e.g. json.dumps applied twice), a list of facts where one is expected, or None from an upstream extraction step that found nothing.

Common situations: LLM extraction pipelines returning null on 'no memories found' and the caller forwarding it; double serialization bugs; passing the whole response envelope instead of response['fact'].

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/e86bf0eeb4cfbda0. Report an issue: GitHub.