mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* feat: manage and scope subagents * fix: address subagent review feedback * fix: address managed subagent review feedback * fix: harden subagent settings semantics * fix: harden managed subagent cache invalidation * fix: reuse assembled lead agent inputs * fix: migrate managed subagent definitions --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
"""File-backed managed subagent store."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
import threading
|
|
from collections.abc import Hashable
|
|
from pathlib import Path
|
|
|
|
from deerflow.config.paths import get_paths
|
|
from deerflow.persistence.managed_subagents.base import (
|
|
ManagedSubagentDefinition,
|
|
ManagedSubagentExistsError,
|
|
ManagedSubagentStore,
|
|
normalize_managed_subagent_name,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
_write_lock = threading.RLock()
|
|
|
|
|
|
def _normalized_name(name: str) -> str:
|
|
return normalize_managed_subagent_name(name)
|
|
|
|
|
|
class FileManagedSubagentStore(ManagedSubagentStore):
|
|
def cache_identity(self) -> Hashable:
|
|
return ("file", str(get_paths().managed_subagents_dir))
|
|
|
|
def get(self, name: str) -> ManagedSubagentDefinition:
|
|
path = get_paths().managed_subagent_file(_normalized_name(name))
|
|
if not path.is_file():
|
|
raise FileNotFoundError(f"Managed subagent not found: {name}")
|
|
return ManagedSubagentDefinition.model_validate_json(path.read_text(encoding="utf-8"))
|
|
|
|
def list(self) -> list[ManagedSubagentDefinition]:
|
|
root = get_paths().managed_subagents_dir
|
|
if not root.exists():
|
|
return []
|
|
definitions: list[ManagedSubagentDefinition] = []
|
|
for path in sorted(root.glob("*.json")):
|
|
try:
|
|
definitions.append(ManagedSubagentDefinition.model_validate_json(path.read_text(encoding="utf-8")))
|
|
except Exception: # noqa: BLE001 — one corrupt definition must not hide the catalog
|
|
logger.warning("Skipping invalid managed subagent definition %s", path, exc_info=True)
|
|
return sorted(definitions, key=lambda item: item.name)
|
|
|
|
def create(self, definition: ManagedSubagentDefinition) -> None:
|
|
path = get_paths().managed_subagent_file(definition.name)
|
|
with _write_lock:
|
|
if path.exists():
|
|
raise ManagedSubagentExistsError(f"Managed subagent '{definition.name}' already exists")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._atomic_write(path, definition)
|
|
|
|
def update(self, definition: ManagedSubagentDefinition) -> None:
|
|
path = get_paths().managed_subagent_file(definition.name)
|
|
with _write_lock:
|
|
if not path.is_file():
|
|
raise FileNotFoundError(f"Managed subagent not found: {definition.name}")
|
|
self._atomic_write(path, definition)
|
|
|
|
def delete(self, name: str) -> bool:
|
|
path = get_paths().managed_subagent_file(_normalized_name(name))
|
|
with _write_lock:
|
|
if not path.is_file():
|
|
return False
|
|
path.unlink()
|
|
return True
|
|
|
|
def signature(self) -> Hashable:
|
|
root = get_paths().managed_subagents_dir
|
|
if not root.exists():
|
|
return ()
|
|
signature: list[tuple[str, int, int]] = []
|
|
for path in sorted(root.glob("*.json")):
|
|
try:
|
|
stat = path.stat()
|
|
except OSError:
|
|
continue
|
|
signature.append((path.name, stat.st_mtime_ns, stat.st_size))
|
|
return tuple(signature)
|
|
|
|
@staticmethod
|
|
def _atomic_write(path: Path, definition: ManagedSubagentDefinition) -> None:
|
|
payload = definition.model_dump_json(indent=2) + "\n"
|
|
tmp_path: Path | None = None
|
|
try:
|
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, suffix=".tmp", delete=False) as handle:
|
|
handle.write(payload)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
tmp_path = Path(handle.name)
|
|
os.replace(tmp_path, path)
|
|
tmp_path = None
|
|
finally:
|
|
if tmp_path is not None:
|
|
tmp_path.unlink(missing_ok=True)
|