Tianye Song 8511fa6aa3
fix(memory): consolidated facts inherit expected_valid_days from sources (#4225)
* fix(memory): consolidated facts inherit expected_valid_days from sources

Consolidation (#3996) and per-fact expected_valid_days (#4143) were both
authored by the same contributor but never connected: the consolidated
new_fact carried the newest source's createdAt but no expected_valid_days,
so _effective_fact_staleness_age fell back to the global staleness_age_days.
A merge of several 200-day-old stable facts (each evd=3650) would land with
no evd, read as a 90-day window, and re-enter the staleness candidate set on
the very next cycle - the merge discarded the lifetime signal of the
underlying information and contradicted consolidation's premise (these are
stable, related facts worth synthesising).

Fix: the merged fact inherits expected_valid_days set so it is re-reviewed at
the EARLIEST source review deadline (min(createdAt + expected_valid_days)
across sources, relative to the merged fact's createdAt = the newest source's).
A merge combines details from every source, so a volatile sub-detail (evd=7)
must not inherit a stable source's 3650-day window and escape staleness review
for years - staleness KEEP/REMOVE is the only path that re-validates a merged
fact, so biasing toward the soonest deadline keeps uncertain merges re-checked
sooner. A source already past its deadline yields a minimal positive window
(review next cycle) rather than the global fallback, which would defer an
overdue review. Capped at the creation-time staleness_max_lifetime_multiplier
like any new fact. Omitted when no source carries a valid evd (legacy facts
fall back to the global age at read time, matching pre-feature behaviour).

DRY: extract _read_expected_valid_days(fact) -> int | None, the shared type
rule (int/float, reject bool, coerce to int BEFORE the > 0 guard) previously
inlined in four places - _normalize_memory_update_fact,
_effective_fact_staleness_age, the newFact creation cap, and consolidation
inheritance. All four call the single helper. Coercing before the guard
matters for values in (0, 1): 0.5 passes a raw > 0 check but truncates to 0,
which would violate the helper's "positive int or None" contract; the order
now matches the original _normalize_memory_update_fact rule.

No prompt/schema change: consolidation's prompt does not surface source evd to
the LLM, so asking the model to assign a merged lifetime would be guessing
without signal. Source inheritance is deterministic and always available.

Tests: consolidation evd cases now use time-stable createdAt (relative to now
via a _days_ago helper) covering - earliest-deadline selection, creation-cap
clamp, omit when no source evd, volatile source governs the deadline (and
re-enters staleness next cycle), overdue source clamps to a minimal window,
float coercion. Plus TestReadExpectedValidDays / TestEffectiveFactStalenessAge
regression cases for the (0, 1) coercion-order fix.

* fix(memory): reject non-finite expected_valid_days before int coercion

The shared _read_expected_valid_days helper (introduced when consolidating the
evd type rule across four call sites) coerces with int(raw) before the > 0
guard - reversing the original _normalize_memory_update_fact order so that a
fractional 0.5 does not leak as 0. But int(raw) raises for non-finite floats:
int(nan) raises ValueError and int(inf)/int(-inf) raise OverflowError. Python's
JSON decoder accepts NaN / Infinity as floats by default, so a single malformed
expected_valid_days in a hand-edited memory.json would abort staleness selection
or consolidation instead of falling back to the global lifetime.

On main, _effective_fact_staleness_age checked raw > 0 first, so NaN fell back
safely (nan > 0 is false) - but inf did NOT (inf > 0 is true, so main also
crashed on inf). This helper is now the persisted-fact read path for both
staleness and consolidation, so the regression (and the pre-existing inf crash)
must be closed here.

Fix: require math.isfinite(float(raw)) before the int() coercion, then keep
the existing positivity check and fallback. NaN / +/-inf all return None, so
callers fall back to the global staleness_age_days. Normal int/float values
(including large ones) are unaffected - isfinite is a no-op for them.

Tests:
- TestReadExpectedValidDays.test_rejects_non_finite_values - NaN, inf, -inf
  return None (not raise).
- TestEffectiveFactStalenessAge.test_falls_back_for_non_finite_values - the
  persisted-fact read path returns the global age for each, no raise.
- test_consolidation_with_non_finite_source_evd_does_not_raise - end-to-end:
  a NaN-evd source merged with a stable source does not abort consolidation;
  the NaN source's effective lifetime falls back to the global 90 and its
  deadline participates in the earliest-deadline computation.

* test(memory): hoist _select_stale_candidates import + tidy deadline docstring

Two review nits from the latest pass:

- `_select_stale_candidates` was imported inline inside three test methods;
  hoisted to the module-level import block so the dependency is declared once.
- `test_consolidated_evd_volatile_source_with_equal_created_at_future_deadline`
  had an abandoned calculation in its comment ("3 + 7 = 10 ... minus 3 already
  elapsed = 7? No:") that could mislead future readers into thinking
  elapsed-since-creation factors into the inherited window. Collapsed to a
  single clear line stating the window is relative to the merged createdAt,
  regardless of the source's current age.

* fix(memory): reject huge-int expected_valid_days above timedelta.max.days

_read_expected_valid_days routed every numeric value through float(raw) for the
math.isfinite guard, but Python's JSON decoder parses an integer literal with
no decimal point as an arbitrary-precision int (unlike 1e400, which decodes to
float inf). So a hand-edited memory.json carrying "expected_valid_days": 10**400
raised OverflowError in float(raw) before math.isfinite was ever called -
exactly the malformed-field-aborts-everything scenario the helper's docstring
claims to prevent.

The earlier non-finite fix only closed the float cases (NaN / +/-inf / 1e400).
A huge int below the float limit but above timedelta.max.days (e.g. 10**12)
would pass the helper and raise OverflowError downstream in
timedelta(days=evd) during staleness selection or consolidation - the same
crash fancyboi999 flagged for extend_by_days, just reached via a stored evd.

Fix: branch on type so an int never passes through float() (matching the
reviewer's suggestion), AND cap the returned int at timedelta.max.days
(999999999) so the downstream timedelta(days=evd) call cannot overflow either.
The float branch keeps the isfinite + int() coercion. Both branches share the
0 < evd <= timedelta.max.days positivity/range check.

Normal values are unaffected - any legitimate expected_valid_days is far below
the cap (the config ceiling staleness_max_extension_days tops out at 36500).

Tests (all three layers):
- TestReadExpectedValidDays.test_rejects_huge_int_above_timedelta_max - 10**400,
  10**12, 10**9, timedelta.max.days+1 return None; timedelta.max.days itself
  is accepted.
- TestEffectiveFactStalenessAge.test_falls_back_for_huge_int_above_timedelta_max
  - the persisted-fact read path returns the global age, no raise.
- test_consolidation_with_huge_int_source_evd_does_not_raise[1e400|1e12|1e9] -
  parametrized end-to-end: a huge-int-evd source merged with a stable source
  does not abort consolidation; the bad source falls back to the global 90.

* fix(memory): guard datetime arithmetic, not just timedelta construction

The huge-int fix capped _read_expected_valid_days at timedelta.max.days, but
that only proves timedelta(days=evd) can be constructed - adding it to a real
fact timestamp still overflows datetime.max. @fancyboi999 reproduced it: a
source with expected_valid_days=timedelta.max.days raises
"OverflowError: date value out of range" at dt + timedelta(...) in the new
consolidation deadline calculation. capping at timedelta.max.days was another
patch chasing the next overflow boundary, not a real close.

Root cause: the helper was doing datetime-range validation, but the safe bound
depends on the datetime the evd is added to, not on the evd alone. So the
responsibility moves to the arithmetic site, with try/except as the terminal
guard - no concrete upper bound to be wrong about.

Changes:
- _read_expected_valid_days returns any positive int (huge ints included, not
  routed through float). Its job is type/positivity validation only.
- New _safe_add_days(dt, days) -> datetime | None wraps dt + timedelta(days),
  returning None on OverflowError/ValueError. This is the terminal guard -
  there is no further boundary to overflow because try/except catches any
  datetime-range failure regardless of magnitude.
- _select_stale_candidates uses _safe_add_days(now, -effective_age); a None
  result means the window is unrepresentably large, so the fact cannot yet be
  stale and is skipped (not selected).
- Consolidation computes each source's deadline via _safe_add_days; a source
  whose deadline overflows falls back to the global staleness_age_days deadline
  (same treatment as a legacy no-evd source), so one malformed field cannot
  abort the merge.

Normal values are unaffected - any legitimate expected_valid_days is far below
the overflow boundary (the config ceiling staleness_max_extension_days tops
out at 36500).

Tests:
- TestSafeAddDays: normal/negative shifts; 10**400/10**12/10**9 return None;
  timedelta.max.days (the exact reproduced value) returns None, not raises.
- TestSelectStaleCandidates.test_huge_evd_does_not_abort_selection: a fact with
  a huge evd is skipped, not selected, and selection does not raise.
- test_consolidation_with_huge_int_source_evd_does_not_raise now parametrized
  over [1e400, 1e12, 1e9, timedelta.max.days] - the last is the value that
  constructs a valid timedelta but overflows datetime arithmetic.
- Helper/read-path tests updated to assert huge ints are returned as-is (the
  overflow guard is no longer in the helper).
2026-07-21 09:36:09 +08:00
..
2026-01-14 09:57:52 +08:00
2026-07-20 23:56:37 +08:00

DeerFlow Backend

DeerFlow is a LangGraph-based AI super agent with sandbox execution, persistent memory, and extensible tool integration. The backend enables AI agents to execute code, browse the web, manage files, delegate tasks to subagents, and retain context across conversations - all in isolated, per-thread environments.


Architecture

                        ┌──────────────────────────────────────┐
                        │          Nginx (Port 2026)           │
                        │      Unified reverse proxy           │
                        └───────┬──────────────────┬───────────┘
                                │
            /api/langgraph/*    │    /api/* (other)
            rewritten to /api/* │
                                ▼
               ┌────────────────────────────────────────┐
               │        Gateway API (8001)              │
               │        FastAPI REST + agent runtime    │
               │                                        │
               │ Models, MCP, Skills, Memory, Uploads,  │
               │ Artifacts, Threads, Runs, Streaming    │
               │                                        │
               │ ┌────────────────────────────────────┐ │
               │ │ Lead Agent                         │ │
               │ │ Middleware Chain, Tools, Subagents │ │
               │ └────────────────────────────────────┘ │
               └────────────────────────────────────────┘

Request Routing (via Nginx):

  • /api/langgraph/* → Gateway LangGraph-compatible API - agent interactions, threads, streaming
  • /api/* (other) → Gateway API - models, MCP, skills, memory, artifacts, uploads, thread-local cleanup
  • / (non-API) → Frontend - Next.js web interface

Core Components

Lead Agent

The single LangGraph agent (lead_agent) is the runtime entry point, created via make_lead_agent(config). It combines:

  • Dynamic model selection with thinking and vision support
  • Middleware chain for cross-cutting concerns (9 middlewares)
  • Tool system with sandbox, MCP, community, and built-in tools
  • Subagent delegation for parallel task execution
  • System prompt with skills injection, memory context, and working directory guidance

Middleware Chain

Middlewares execute in strict order, each handling a specific concern:

# Middleware Purpose
1 ThreadDataMiddleware Creates per-thread isolated directories (workspace, uploads, outputs)
2 UploadsMiddleware Injects newly uploaded files into conversation context
3 SandboxMiddleware Acquires sandbox environment for code execution
4 SummarizationMiddleware Reduces context when approaching token limits (optional)
5 TodoListMiddleware Tracks multi-step tasks in plan mode (optional)
6 TitleMiddleware Auto-generates conversation titles after first exchange
7 MemoryMiddleware Queues conversations for async memory extraction
8 ViewImageMiddleware Injects image data for vision-capable models (conditional)
9 ClarificationMiddleware Intercepts clarification requests and interrupts execution (must be last)

Sandbox System

Per-thread isolated execution with virtual path translation:

  • Abstract interface: execute_command, read_file, write_file, list_dir
  • Providers: LocalSandboxProvider (filesystem) and AioSandboxProvider (Docker, in community/). Async runtime paths use async sandbox lifecycle hooks so startup, readiness polling, and release do not block the event loop. AioSandboxProvider validates active-cache and warm-pool containers during acquire/reuse, dropping definitively dead entries so a thread can provision a fresh sandbox after an unexpected container exit while keeping get() as an in-memory lookup. Backend health-check failures are treated as unknown, not dead, and a container that cannot be verified during discovery is simply not adopted (acquire falls through to create instead of failing).
  • Virtual paths: /mnt/user-data/{workspace,uploads,outputs} → thread-specific physical directories
  • Skills path: /mnt/skillsdeer-flow/skills/ directory
  • Skills loading: Recursively discovers nested SKILL.md files under skills/{public,custom} and preserves nested container paths
  • SkillScan: Native offline deterministic scanning runs before the LLM skill scanner on installs and agent-managed skill writes; CRITICAL findings block and warning findings become LLM context
  • File-write safety: str_replace serializes read-modify-write per (sandbox.id, path) so isolated sandboxes keep concurrency even when virtual paths match
  • Tools: bash, ls, read_file, write_file, str_replace (write_file overwrites by default and exposes append for end-of-file writes; bash is disabled by default when using LocalSandboxProvider; use AioSandboxProvider for isolated shell access)

Subagent System

Async task delegation with concurrent execution:

  • Built-in agents: general-purpose (full toolset) and bash (command specialist, exposed only when shell access is available)
  • Concurrency: Max 3 subagents per turn, 15-minute timeout
  • Execution: Background thread pools with status tracking and SSE events
  • Flow: Agent calls task() tool → executor runs subagent in background → polls for completion → returns result

Memory System

LLM-powered persistent context retention across conversations:

  • Automatic extraction: Analyzes conversations for user context, facts, and preferences
  • Structured storage: User context (work, personal, top-of-mind), history, and confidence-scored facts
  • Debounced updates: Batches updates to minimize LLM calls (configurable wait time)
  • System prompt injection: Top facts + context injected into agent prompts
  • Run-level memory identity: GET /api/threads/{thread_id}/runs/{run_id}/events?event_types=context:memory returns the SHA-256 identity of the effective hidden memory block without copying memory text into the event store
  • Storage: JSON file with mtime-based cache invalidation

Tool Ecosystem

Category Tools
Sandbox bash, ls, read_file, write_file, str_replace
Built-in present_files, ask_clarification, view_image, task (subagent)
Community Tavily (web search), Jina AI (web fetch), Crawl4AI (web fetch), Firecrawl (scraping), fastCRW (scraping), DuckDuckGo (image search)
MCP Any Model Context Protocol server (stdio, SSE, HTTP transports)
Skills Domain-specific workflows injected via system prompt

Gateway API

FastAPI application providing REST endpoints for frontend integration:

Route Purpose
GET /api/models List available LLM models
GET/PUT /api/mcp/config Manage MCP server configurations
POST /api/mcp/cache/reset Reset cached MCP tools so they reload on next use
GET/PUT /api/skills List and manage skills
POST /api/skills/install Install skill from .skill archive
GET /api/memory Retrieve memory data
POST /api/memory/reload Force memory reload
GET /api/memory/config Memory configuration
GET /api/memory/status Combined config + data
GET /api/threads/{id}/runs/{run_id}/events Debug/audit events for one run; filter event_types=context:memory for effective memory identity
POST /api/threads/{id}/uploads Upload files (auto-converts PDF/PPT/Excel/Word to Markdown, rejects directory paths, auto-renames duplicate filenames in one request)
GET /api/threads/{id}/uploads/list List uploaded files
DELETE /api/threads/{id} Delete DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail
GET /api/threads/{id}/artifacts/{path} Serve generated artifacts

IM Channels

The IM bridge supports Feishu, Slack, and Telegram. Slack and Telegram still use the final runs.wait() response path, while Feishu now streams through runs.stream(["messages-tuple", "values"]), serializes rapid same-thread turns inside the channel manager, and updates a single in-thread card per source message in place.

For Feishu card updates, DeerFlow stores the running card's message_id per inbound message and patches that same card until the run finishes, preserving the existing OK / DONE reaction flow. When a follow-up arrives inside an existing Feishu topic while another turn is still running, the later message now waits on the mapped DeerFlow thread_id, receives a queued/running card on that exact source message, and keeps a compact source-message blockquote in subsequent patches so rapid consecutive questions remain distinguishable.


Quick Start

Prerequisites

  • Python 3.12+
  • uv package manager
  • API keys for your chosen LLM provider

Installation

cd deer-flow

# Copy configuration files
cp config.example.yaml config.yaml

# Install backend dependencies
cd backend
make install

Configuration

Edit config.yaml in the project root:

models:
  - name: gpt-4o
    display_name: GPT-4o
    use: langchain_openai:ChatOpenAI
    model: gpt-4o
    api_key: $OPENAI_API_KEY
    supports_thinking: false
    supports_vision: true

  - name: gpt-5-responses
    display_name: GPT-5 (Responses API)
    use: langchain_openai:ChatOpenAI
    model: gpt-5
    api_key: $OPENAI_API_KEY
    use_responses_api: true
    output_version: responses/v1
    supports_vision: true

Set your API keys:

export OPENAI_API_KEY="your-api-key-here"

Running

Full Application (from project root):

make dev  # Starts Gateway + Frontend + Nginx

Access at: http://localhost:2026

Backend Only (from backend directory):

# Gateway API + embedded agent runtime
make dev

Direct access: Gateway at http://localhost:8001

Terminal Workbench (TUI) — a terminal-native UI over the embedded harness, no services required:

uv pip install 'deerflow-harness[tui]'   # optional 'textual' dependency
deerflow                                 # launch the TUI
deerflow --print "summarize this repo"   # headless one-shot

Sessions opened in the TUI appear in the Web UI sidebar (it writes the shared threads_meta store under the local default user). See docs/TUI.md.


Project Structure

backend/
├── packages/harness/           # deerflow-harness package (import: deerflow.*)
│   └── deerflow/
│       ├── agents/             # Agent system
│       │   ├── lead_agent/     # Main agent (factory, prompts)
│       │   ├── middlewares/    # Middleware components
│       │   ├── memory/         # Memory extraction & storage
│       │   └── thread_state.py # ThreadState schema
│       ├── sandbox/            # Sandbox execution
│       │   ├── local/          # Local filesystem provider
│       │   ├── sandbox.py      # Abstract interface
│       │   ├── tools.py        # bash, ls, read/write/str_replace
│       │   └── middleware.py   # Sandbox lifecycle
│       ├── subagents/          # Subagent delegation
│       │   ├── builtins/       # general-purpose, bash agents
│       │   ├── executor.py     # Background execution engine
│       │   └── registry.py     # Agent registry
│       ├── tools/builtins/     # Built-in tools
│       ├── mcp/                # MCP protocol integration
│       ├── models/             # Model factory
│       ├── skills/             # Skill discovery & loading
│       ├── config/             # Configuration system
│       ├── runtime/            # Embedded run execution (RunManager, StreamBridge)
│       ├── persistence/        # Checkpointer/store engines & schema migrations
│       ├── guardrails/         # Pre-tool-call authorization providers
│       ├── tracing/            # Tracer factory & trace metadata
│       ├── uploads/            # Uploads manager
│       ├── tui/                # Terminal UI (`deerflow` console script)
│       ├── community/          # Community tools & providers
│       ├── reflection/         # Dynamic module loading
│       └── utils/              # Utilities
├── app/                        # FastAPI Gateway + IM channels (import: app.*)
│   ├── gateway/                # Gateway API
│   │   ├── app.py              # Application setup
│   │   └── routers/            # Route modules
│   └── channels/               # IM channel integrations
├── docs/                       # Documentation
├── tests/                      # Test suite
├── langgraph.json              # LangGraph graph registry for tooling/Studio compatibility
├── pyproject.toml              # Python dependencies
├── Makefile                    # Development commands
└── Dockerfile                  # Container build

langgraph.json is not the default service entrypoint. The scripts and Docker deployments run the Gateway embedded runtime; the file is kept for LangGraph tooling, Studio, or direct LangGraph Server compatibility.


Configuration

Main Configuration (config.yaml)

Place in project root. Config values starting with $ resolve as environment variables.

Key sections:

  • models - LLM configurations with class paths, API keys, thinking/vision flags
  • tools - Tool definitions with module paths and groups
  • tool_groups - Logical tool groupings
  • sandbox - Execution environment provider
  • skills - Skills directory paths
  • title - Auto-title generation settings
  • summarization - Context summarization settings
  • subagents - Subagent system (enabled/disabled)
  • memory - Memory system settings (enabled, storage, debounce, facts limits)

Provider note:

  • models[*].use references provider classes by module path (for example langchain_openai:ChatOpenAI).
  • If a provider module is missing, DeerFlow now returns an actionable error with install guidance (for example uv add langchain-google-genai).

Extensions Configuration (extensions_config.json)

MCP servers and skill states in a single file:

{
  "mcpServers": {
    "github": {
      "enabled": true,
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {"GITHUB_TOKEN": "$GITHUB_TOKEN"}
    },
    "secure-http": {
      "enabled": true,
      "type": "http",
      "url": "https://api.example.com/mcp",
      "oauth": {
        "enabled": true,
        "token_url": "https://auth.example.com/oauth/token",
        "grant_type": "client_credentials",
        "client_id": "$MCP_OAUTH_CLIENT_ID",
        "client_secret": "$MCP_OAUTH_CLIENT_SECRET"
      }
    },
    "postgres": {
      "enabled": false,
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
      "description": "PostgreSQL database access",
      "routing": {
        "mode": "prefer",
        "priority": 50,
        "keywords": ["orders", "users", "SQL", "database", "table"]
      },
      "tools": {
        "query": {
          "routing": {
            "priority": 100,
            "keywords": ["query database", "orders table", "metrics"]
          }
        }
      }
    }
  },
  "skills": {
    "pdf-processing": {"enabled": true}
  }
}

routing adds soft MCP preference hints to the agent prompt. It helps the model prefer a configured MCP tool for matching requests without forbidding other tools. When tool_search.enabled=true defers MCP schemas, matching routing metadata can auto-promote up to tool_search.auto_promote_top_k deferred schemas before the model call.

Environment Variables

  • DEER_FLOW_CONFIG_PATH - Override config.yaml location
  • DEER_FLOW_EXTENSIONS_CONFIG_PATH - Override extensions_config.json location
  • Model API keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, DEEPSEEK_API_KEY, etc.
  • Tool API keys: TAVILY_API_KEY, GITHUB_TOKEN, etc.

LangSmith Tracing

DeerFlow has built-in LangSmith integration for observability. When enabled, all LLM calls, agent runs, tool executions, and middleware processing are traced and visible in the LangSmith dashboard.

Setup:

  1. Sign up at smith.langchain.com and create a project.
  2. Add the following to your .env file in the project root:
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=lsv2_pt_xxxxxxxxxxxxxxxx
LANGSMITH_PROJECT=xxx

Legacy variables: The LANGCHAIN_TRACING_V2, LANGCHAIN_API_KEY, LANGCHAIN_PROJECT, and LANGCHAIN_ENDPOINT variables are also supported for backward compatibility. LANGSMITH_* variables take precedence when both are set.

Langfuse Tracing

DeerFlow also supports Langfuse observability for LangChain-compatible runs.

Add the following to your .env file:

LANGFUSE_TRACING=true
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxxxxxxxxxx
LANGFUSE_BASE_URL=https://cloud.langfuse.com

If you are using a self-hosted Langfuse deployment, set LANGFUSE_BASE_URL to your Langfuse host.

Dual Provider Behavior

If both LangSmith and Langfuse are enabled, DeerFlow initializes and attaches both callbacks so the same run data is reported to both systems.

If a provider is explicitly enabled but required credentials are missing, or the provider callback cannot be initialized, DeerFlow raises an error when tracing is initialized during model creation instead of silently disabling tracing.

Docker: In docker-compose.yaml, tracing is disabled by default (LANGSMITH_TRACING=false). Set LANGSMITH_TRACING=true and/or LANGFUSE_TRACING=true in your .env, together with the required credentials, to enable tracing in containerized deployments.


Development

Commands

make install    # Install dependencies
make dev        # Run Gateway API + embedded agent runtime (port 8001)
make gateway    # Run Gateway API without reload (port 8001)
make lint       # Run linter (ruff)
make format     # Format code (ruff)
make detect-blocking-io  # Inventory blocking IO that may block the backend event loop
make migrate-rev MSG="..."  # Autogenerate a new alembic revision against the live ORM models

Schema Migrations

DeerFlow's application tables (runs, threads_meta, feedback, users, run_events, and the channel_* tables) are owned by alembic. The Gateway runs alembic upgrade head automatically on startup via bootstrap_schema(engine, backend=...), so operators do not run alembic manually in production. Bootstrap is concurrency-safe (Postgres advisory lock across processes; per-engine asyncio.Lock inside one SQLite process) and idempotent against pre-existing schemas (empty / legacy / versioned).

When you add or change an ORM model, ship the change as a new revision under packages/harness/deerflow/persistence/migrations/versions/:

make migrate-rev MSG="add foo column to runs"

The target invokes scripts/_autogen_revision.py, which builds a fresh temp SQLite at head and diffs the live models against it — so a clean checkout does not need a pre-existing ./data/deerflow.db. Review the generated file and switch raw op.add_column / op.drop_column calls to the idempotent helpers in migrations/_helpers.py before committing. There is no make migrate / make migrate-stamp target on purpose — Gateway startup is the only execution path, which keeps operational mistakes off the table. See backend/CLAUDE.md (Schema Migrations) for the full design.

Code Style

  • Linter/Formatter: ruff
  • Line length: 240 characters
  • Python: 3.12+ with type hints
  • Quotes: Double quotes
  • Indentation: 4 spaces

Testing

uv run pytest

make detect-blocking-io statically scans backend business code for blocking IO that may run on the backend event loop and is not test-coverage-bound. It prints a concise summary for human review and writes complete JSON findings to .deer-flow/blocking-io-findings.json at the repository root (regardless of whether the target is invoked from the repo root or from backend/). JSON findings include both broad IO category and review-oriented fields such as priority, location, blocking_call, event_loop_exposure, reason, and code. priority is a deterministic review ordering from the operation type, not proof of a bug. Bare-name same-file calls are resolved by function name, so duplicate helper names in one file can conservatively over-report async reachability.


Technology Stack

  • LangGraph (1.0.6+) - Agent framework and multi-agent orchestration
  • LangChain (1.2.3+) - LLM abstractions and tool system
  • FastAPI (0.115.0+) - Gateway REST API
  • langchain-mcp-adapters - Model Context Protocol support
  • agent-sandbox - Sandboxed code execution
  • markitdown - Multi-format document conversion
  • tavily-python / firecrawl-py - Web search and scraping

Documentation


License

See the LICENSE file in the project root.

Contributing

See CONTRIBUTING.md for contribution guidelines.