mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-16 17:46:20 +00:00
* fix(agents): do not hide invalid config with file fallback * test(agents): cover invalid on-disk config fallback * fix(agents): resolve stores off the event loop * fix(agents): distinguish missing nested config from main config * fix(agents): reject missing explicit config path * test(agents): isolate config fallback test * test(agents): isolate router blocking IO coverage * test(agents): pin malformed config.yaml parse-error propagation An unparseable config.yaml used to be swallowed by the broad except Exception and silently downgrade to FileAgentStore. The narrowed except FileNotFoundError already propagates yaml.ParserError/ScannerError; pin that contract with a real on-disk config instead of monkeypatched get_app_config.
107 lines
4.0 KiB
Python
107 lines
4.0 KiB
Python
"""Custom agent definition persistence — abstract store + file/db backends.
|
|
|
|
The public entry point is :func:`get_agent_store`, which the free functions in
|
|
:mod:`deerflow.config.agents_config` dispatch to. ``file`` (default) preserves
|
|
today's on-disk layout; ``db`` shares definitions across nodes via the SQL
|
|
persistence layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
from deerflow.persistence.agents.base import (
|
|
AgentDeleteOutcome,
|
|
AgentExistsError,
|
|
AgentStore,
|
|
parse_agent_config,
|
|
)
|
|
from deerflow.persistence.agents.model import AgentRow
|
|
|
|
if TYPE_CHECKING:
|
|
from deerflow.config.app_config import AppConfig
|
|
|
|
__all__ = [
|
|
"AgentDeleteOutcome",
|
|
"AgentExistsError",
|
|
"AgentRow",
|
|
"AgentStore",
|
|
"get_agent_store",
|
|
"make_agent_store",
|
|
"parse_agent_config",
|
|
]
|
|
|
|
_file_store_singleton: AgentStore | None = None
|
|
|
|
|
|
def make_agent_store(config: AppConfig) -> AgentStore:
|
|
"""Build (or reuse) the store selected by ``config.agent_storage.backend``.
|
|
|
|
``db`` requires ``database.backend`` to be ``sqlite`` or ``postgres``; a
|
|
``memory`` database has no durable URL and is rejected here (the gateway
|
|
also fails fast at startup, but this guard covers the graph-process path).
|
|
"""
|
|
if config.agent_storage.backend == "db":
|
|
db_backend = config.database.backend
|
|
if db_backend not in ("sqlite", "postgres"):
|
|
raise ValueError(
|
|
f"agent_storage.backend='db' requires database.backend to be 'sqlite' or 'postgres', "
|
|
f"but database.backend is '{db_backend}'. A 'memory' database is per-process and cannot "
|
|
"share agent definitions across nodes; set database.backend accordingly or use "
|
|
"agent_storage.backend='file'."
|
|
)
|
|
from deerflow.persistence.agents.sql import SqlAgentStore
|
|
|
|
return SqlAgentStore(config.database.app_sync_sqlalchemy_url)
|
|
|
|
return _file_store()
|
|
|
|
|
|
def get_agent_store() -> AgentStore:
|
|
"""Return the store for the current process's configuration.
|
|
|
|
Defaults to the file backend only when search mode cannot find the main app
|
|
config — the free functions in ``agents_config`` must keep working in
|
|
lightweight contexts (CLI, tests, tools) that never load a full
|
|
``config.yaml``. A missing explicit ``DEER_FLOW_CONFIG_PATH`` or a missing
|
|
nested config is an operator error and propagates instead of falling back.
|
|
Only an explicit ``agent_storage.backend: db`` diverges from the file
|
|
default once configuration resolves successfully.
|
|
|
|
Cross-process invariant (the ``db`` backend's whole point): the per-run
|
|
agent build runs in the **graph subprocess**, a different process from the
|
|
gateway. Its cross-node guarantee holds only because ``get_app_config()``
|
|
resolves ``config.yaml`` there too and returns ``backend: db`` — so the read
|
|
path in the graph process sees the same shared table the gateway wrote. The
|
|
``except`` below is a genuine *no resolvable config* fallback (CLI/tests),
|
|
**not** a mask for a misconfigured graph process: if ``config.yaml`` is
|
|
reachable there (it is, same working tree), ``db`` is honoured, not silently
|
|
downgraded to node-local ``file``. Pinned by
|
|
``test_get_agent_store_resolves_db_backend_from_on_disk_config``.
|
|
"""
|
|
from deerflow.config.app_config import AppConfig, get_app_config
|
|
|
|
try:
|
|
config = get_app_config()
|
|
except FileNotFoundError:
|
|
if os.getenv("DEER_FLOW_CONFIG_PATH"):
|
|
raise
|
|
# ``get_app_config()`` also loads optional nested config files. Only
|
|
# fall back when the main config itself is absent.
|
|
try:
|
|
AppConfig.resolve_config_path()
|
|
except FileNotFoundError:
|
|
return _file_store()
|
|
raise
|
|
return make_agent_store(config)
|
|
|
|
|
|
def _file_store() -> AgentStore:
|
|
global _file_store_singleton
|
|
if _file_store_singleton is None:
|
|
from deerflow.persistence.agents.file import FileAgentStore
|
|
|
|
_file_store_singleton = FileAgentStore()
|
|
return _file_store_singleton
|