Files
deerflow-code/offline-backend-20260512/backend/CLAUDE.md
T
AdministratorandClaude Opus 4.8 5c2379577b fix(auth): token 登录将令牌放入 Authorization: Bearer 头
token 换登录调上游 getTokenInfo 时参数传错:原来发静态头
Authorization: Bearer admin + 令牌放 access_token cookie。上游实际
从 Authorization: Bearer <token> 头读取令牌。

- 鉴权头改为 "Bearer "+前端传入 token,删除 access_token cookie
- service_authorization 配置项弃用(保留兼容,不再参与请求)
- 新增回归测试 test_token_login_upstream_call.py
- 同步更新 AUTH_LOGIN.md / CLAUDE.md / config.yaml 注释 / login_config 文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:35:38 +08:00

58 KiB

CLAUDE.md

测试 This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments.

Architecture:

  • Gateway API (port 8001): REST API plus embedded LangGraph-compatible agent runtime
  • Frontend (port 3000): Next.js web interface
  • Nginx (port 2026): Unified reverse proxy entry point
  • Provisioner (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode

Runtime:

  • make dev, Docker dev, and production all run the agent runtime in Gateway via RunManager + run_agent() + StreamBridge (packages/harness/deerflow/runtime/). Nginx exposes that runtime at /api/langgraph/* and rewrites it to Gateway's native /api/* routers.

Project Structure:

deer-flow/
├── Makefile                    # Root commands (check, install, dev, stop)
├── config.yaml                 # Main application configuration
├── extensions_config.json      # MCP servers and skills configuration
├── backend/                    # Backend application (this directory)
│   ├── Makefile               # Backend-only commands (dev, gateway, lint)
│   ├── langgraph.json         # LangGraph Studio graph configuration
│   ├── packages/
│   │   └── harness/           # deerflow-harness package (import: deerflow.*)
│   │       ├── pyproject.toml
│   │       └── deerflow/
│   │           ├── agents/            # LangGraph agent system
│   │           │   ├── lead_agent/    # Main agent (factory + system prompt)
│   │           │   ├── middlewares/   # 10 middleware components
│   │           │   ├── memory/        # Memory V2: provider/manager/builtin/hindsight
│   │           │   └── thread_state.py # ThreadState schema
│   │           ├── sandbox/           # Sandbox execution system
│   │           │   ├── local/         # Local filesystem provider
│   │           │   ├── sandbox.py     # Abstract Sandbox interface
│   │           │   ├── tools.py       # bash, ls, read/write/str_replace
│   │           │   └── middleware.py  # Sandbox lifecycle management
│   │           ├── subagents/         # Subagent delegation system
│   │           │   ├── builtins/      # general-purpose, bash agents
│   │           │   ├── executor.py    # Background execution engine
│   │           │   └── registry.py    # Agent registry
│   │           ├── tools/builtins/    # Built-in tools (present_files, ask_clarification, view_image)
│   │           ├── mcp/               # MCP integration (tools, cache, client)
│   │           ├── models/            # Model factory with thinking/vision support
│   │           ├── skills/            # Skills discovery, loading, parsing
│   │           ├── config/            # Configuration system (app, model, sandbox, tool, etc.)
│   │           ├── community/         # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox)
│   │           ├── reflection/        # Dynamic module loading (resolve_variable, resolve_class)
│   │           ├── utils/             # Utilities (network, readability)
│   │           └── client.py          # Embedded Python client (DeerFlowClient)
│   ├── app/                   # Application layer (import: app.*)
│   │   ├── gateway/           # FastAPI Gateway API
│   │   │   ├── app.py         # FastAPI application
│   │   │   └── routers/       # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels)
│   │   └── channels/          # IM platform integrations
│   ├── tests/                 # Test suite
│   └── docs/                  # Documentation
├── frontend/                   # Next.js frontend application
└── skills/                     # Agent skills directory
    ├── public/                # Public skills (committed)
    └── custom/                # Custom skills (gitignored)

Important Development Guidelines

Documentation Update Policy

CRITICAL: Always update README.md and CLAUDE.md after every code change

When making code changes, you MUST update the relevant documentation:

  • Update README.md for user-facing changes (features, setup, usage instructions)
  • Update CLAUDE.md for development changes (architecture, commands, workflows, internal systems)
  • Keep documentation synchronized with the codebase at all times
  • Ensure accuracy and timeliness of all documentation

Commands

Root directory (for full application):

make check      # Check system requirements
make install    # Install all dependencies (frontend + backend)
make dev        # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight
make start      # Start production services locally
make stop       # Stop all services

Backend directory (for backend development only):

make install    # Install backend dependencies
make dev        # Run Gateway API with reload (port 8001)
make gateway    # Run Gateway API only (port 8001)
make test       # Run all backend tests
make lint       # Lint with ruff
make format     # Format code with ruff

Regression tests related to Docker/provisioner behavior:

  • tests/test_docker_sandbox_mode_detection.py (mode detection from config.yaml)
  • tests/test_provisioner_kubeconfig.py (kubeconfig file/directory handling)

Boundary check (harness → app import firewall):

  • tests/test_harness_boundary.py — ensures packages/harness/deerflow/ never imports from app.*

CI runs these regression tests for every pull request via .github/workflows/backend-unit-tests.yml.

Architecture

Harness / App Split

The backend is split into two layers with a strict dependency direction:

  • Harness (packages/harness/deerflow/): Publishable agent framework package (deerflow-harness). Import prefix: deerflow.*. Contains agent orchestration, tools, sandbox, models, MCP, skills, config — everything needed to build and run agents.
  • App (app/): Unpublished application code. Import prefix: app.*. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk).

Dependency rule: App imports deerflow, but deerflow never imports app. This boundary is enforced by tests/test_harness_boundary.py which runs in CI.

Import conventions:

# Harness internal
from deerflow.agents import make_lead_agent
from deerflow.models import create_chat_model

# App internal
from app.gateway.app import app
from app.channels.service import start_channel_service

# App → Harness (allowed)
from deerflow.config import get_app_config

# Harness → App (FORBIDDEN — enforced by test_harness_boundary.py)
# from app.gateway.routers.uploads import ...  # ← will fail CI

Agent System

Lead Agent (packages/harness/deerflow/agents/lead_agent/agent.py):

  • Entry point: make_lead_agent(config: RunnableConfig) registered in langgraph.json
  • Dynamic model selection via create_chat_model() with thinking/vision support
  • Tools loaded via get_available_tools() - combines sandbox, built-in, MCP, community, and subagent tools
  • System prompt generated by apply_prompt_template() with skills, memory, and subagent instructions

ThreadState (packages/harness/deerflow/agents/thread_state.py):

  • Extends AgentState with: sandbox, thread_data, title, artifacts, todos, uploaded_files, viewed_images
  • Uses custom reducers: merge_artifacts (deduplicate), merge_viewed_images (merge/clear)

Runtime Configuration (via config.configurable):

  • thinking_enabled - Enable model's extended thinking
  • model_name - Select specific LLM model
  • is_plan_mode - Enable TodoList middleware
  • subagent_enabled - Enable task delegation tool

Middleware Chain

Lead-agent middlewares are assembled in strict append order across packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py (build_lead_runtime_middlewares) and packages/harness/deerflow/agents/lead_agent/agent.py (_build_middlewares):

  1. ThreadDataMiddleware - Creates per-thread directories under the user's isolation scope (backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}); resolves user_id via get_effective_user_id() (falls back to "default" in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory
  2. UploadsMiddleware - Tracks and injects newly uploaded files into conversation
  3. SandboxMiddleware - Acquires sandbox, stores sandbox_id in state
  4. DanglingToolCallMiddleware - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in additional_kwargs["tool_calls"]
  5. LLMErrorHandlingMiddleware - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run
  6. GuardrailMiddleware - Pre-tool-call authorization via pluggable GuardrailProvider protocol (optional, if guardrails.enabled in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in AllowlistProvider (zero deps), OAP policy providers (e.g. aport-agent-guardrails), or custom providers. See docs/GUARDRAILS.md for setup, usage, and how to implement a provider.
  7. SandboxAuditMiddleware - Audits sandboxed shell/file operations for security logging before tool execution continues
  8. ToolErrorHandlingMiddleware - Converts tool exceptions into error ToolMessages so the run can continue instead of aborting
  9. SummarizationMiddleware - Context reduction when approaching token limits (optional, if enabled)
  10. TodoListMiddleware - Task tracking with write_todos tool (optional, if plan_mode)
  11. TokenUsageMiddleware - Records token usage metrics when token tracking is enabled (optional)
  12. TitleMiddleware - Auto-generates thread title after first complete exchange and normalizes structured message content before prompting the title model
  13. MemoryMiddleware - Memory V2: wrap_model_call injects Hindsight recall transiently; after_agent auto-retains the turn to Hindsight
  14. ViewImageMiddleware - Injects base64 image data before LLM call (conditional on vision support)
  15. DeferredToolFilterMiddleware - Hides deferred tool schemas from the bound model until tool search is enabled (optional)
  16. SubagentLimitMiddleware - Truncates excess task tool calls from model response to enforce MAX_CONCURRENT_SUBAGENTS limit (optional, if subagent_enabled)
  17. LoopDetectionMiddleware - Detects repeated tool-call loops; hard-stop responses clear both structured tool_calls and raw provider tool-call metadata before forcing a final text answer
  18. ClarificationMiddleware - Intercepts ask_clarification tool calls, interrupts via Command(goto=END) (must be last) 8a. ToolMetricsMiddleware - Appended immediately after ToolErrorHandlingMiddleware in _build_runtime_middlewares, so it sits deepest in the wrap_tool_call chain. Records one tool_call_metrics row per tool/skill/MCP invocation — timing duration and bucketing failures into a coarse error_category (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and self-handled errors returned as error ToolMessages (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into tool_call_metrics.skill_name: the lead agent invokes a skill by read_file-ing its /mnt/skills/<category>/<name>/SKILL.md path (per the <skill_system> prompt block — there is no dedicated "use skill" tool), so skill_name is recovered by scanning every tool call's string arguments for the skills/{public,custom}/{name}/ path shape (_SKILL_PATH_RE), plus the explicit name argument of skill_view/skill_manage/skill_archive. skill_view-specific success detection handles its swallowed failures (Skill 'x' not found. returned as a plain string rather than raised). Regression coverage: tests/test_tool_metrics_middleware.py. Writes are best-effort fire-and-forget. Per-run skill merging: SqlToolMetricsStore.record upserts skill rows by (run_id, skill_name) instead of inserting one row per call — a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one success); a skill that only ever failed yields one error row whose error_message accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no run_id, keep one row each. The read→merge→write is guarded by a per-event-loop asyncio.Lock. Surfaced by the admin GET /api/admin/tool-metrics router (which has a kind=skill filter and a per-skill /summary breakdown).
  19. PromptPrefixMiddleware - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's additional_kwargs['prompt_prefix'] by the Gateway (app.gateway.services) and prepended to content here at call time, so the persisted/displayed message stays prefix-free
  20. SummarizationMiddleware - Context reduction when approaching token limits (optional, if enabled)
  21. TodoListMiddleware - Task tracking with write_todos tool (optional, if plan_mode)
  22. TokenUsageMiddleware - Records token usage metrics when token tracking is enabled (optional)
  23. TitleMiddleware - Auto-generates thread title after first complete exchange and normalizes structured message content before prompting the title model
  24. MemoryMiddleware - Queues conversations for async memory update (filters to user + final AI responses)
  25. ViewImageMiddleware - Injects base64 image data before LLM call (conditional on vision support)
  26. DeferredToolFilterMiddleware - Hides deferred tool schemas from the bound model until tool search is enabled (optional)
  27. SubagentLimitMiddleware - Truncates excess task tool calls from model response to enforce MAX_CONCURRENT_SUBAGENTS limit (optional, if subagent_enabled)
  28. LoopDetectionMiddleware - Detects repeated tool-call loops; hard-stop responses clear both structured tool_calls and raw provider tool-call metadata before forcing a final text answer
  29. ClarificationMiddleware - Intercepts ask_clarification tool calls, interrupts via Command(goto=END) (must be last)

Configuration System

Main Configuration (config.yaml):

Setup: Copy config.example.yaml to config.yaml in the project root directory.

Config Versioning: config.example.yaml has a config_version field. On startup, AppConfig.from_file() compares user version vs example version and emits a warning if outdated. Missing config_version = version 0. Run make config-upgrade to auto-merge missing fields. When changing the config schema, bump config_version in config.example.yaml.

Config Caching: get_app_config() caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with config.yaml edits without requiring a manual process restart.

Configuration priority:

  1. Explicit config_path argument
  2. DEER_FLOW_CONFIG_PATH environment variable
  3. config.yaml in current directory (backend/)
  4. config.yaml in parent directory (project root - recommended location)

Config values starting with $ are resolved as environment variables (e.g., $OPENAI_API_KEY). ModelConfig also declares use_responses_api and output_version so OpenAI /v1/responses can be enabled explicitly while still using langchain_openai:ChatOpenAI.

Extensions Configuration (extensions_config.json):

MCP servers and skills are configured together in extensions_config.json in project root:

Configuration priority:

  1. Explicit config_path argument
  2. DEER_FLOW_EXTENSIONS_CONFIG_PATH environment variable
  3. extensions_config.json in current directory (backend/)
  4. extensions_config.json in parent directory (project root - recommended location)

Gateway API (app/gateway/)

FastAPI application on port 8001 with health check at GET /health. Set GATEWAY_ENABLE_DOCS=false to disable /docs, /redoc, and /openapi.json in production (default: enabled).

Routers:

Router Endpoints
Models (/api/models) GET / - list models; GET /{name} - model details
MCP (/api/mcp) GET /config - get config; PUT /config - update config (saves to extensions_config.json)
Skills (/api/skills) GET / - list skills; GET /{name} - details; PUT /{name} - update enabled; POST /install - install from .skill archive (accepts standard optional frontmatter like version, author, compatibility)
Memory V2 (/api/memory/v2) GET / - builtin data (USER.md/MEMORY.md); POST/PUT/DELETE /entries - add/replace/remove entry; GET/PATCH/DELETE /config - effective config + per-user overrides; POST /search - Hindsight recall/reflect proxy
Agents (/api/agents) GET / - list built-in, owned, and published agents; POST / - create user-owned agent; GET/PUT/DELETE /{id} - read/update/delete by stable agent id; GET /check - validate non-empty display name (duplicates allowed)
Skills (/api/skills) GET / - list skills (optional search fuzzy-name + tag_id filters; each item carries tags); GET /{name} - details; PUT /{name} - update enabled; POST /install - install from .skill archive (accepts standard optional frontmatter like version, author, compatibility)
Skills (/api/skills) GET / - list skills (optional search fuzzy-name + tag_id filters; each item carries tags + owner-authored detail); GET /{name} - details; PUT /{name} - update enabled; PUT /{name}/detail - set the long-form detail text (custom skill: owner or admin; built-in/public: admin only — materializes a skills row on demand); POST /install - install from .skill archive (accepts standard optional frontmatter like version, author, compatibility)
Skills (/api/skills) GET / - list skills (optional search fuzzy-name + tag_id filters; each item carries tags + owner-authored detail); GET /{name} - details; PUT /{name} - update enabled; PUT /{name}/detail - set the long-form detail text (custom skill: owner or admin; built-in/public: admin only — materializes a skills row on demand); GET /{name}/content - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; POST /install - install from .skill archive (accepts standard optional frontmatter like version, author, compatibility)
Skills (/api/skills) GET / - list skills (optional search fuzzy-name + repeatable tag_ids multi-select filter, OR-matched; each item carries tags + owner-authored detail); GET /{name} - details; PUT /{name} - update enabled; PUT /{name}/detail - set the long-form detail text (custom skill: owner or admin; built-in/public: admin only — materializes a skills row on demand); GET /{name}/content - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; POST /install - install from .skill archive (accepts standard optional frontmatter like version, author, compatibility)
Memory (/api/memory) GET / - memory data; POST /reload - force reload; GET /config - config; GET /status - config + data
Agents (/api/agents) GET / - list built-in, owned, and published agents (optional search + repeatable tag_ids multi-select filter, OR-matched; each item carries tags and featured_order); POST / - create user-owned agent; GET/PUT/DELETE /{id} - read/update/delete by stable agent id; GET /check - validate non-empty display name (duplicates allowed); GET /selector - homepage agent picker (single admin-curated list, visibility-filtered, sorted by featured_order asc); PUT /{id}/featured - admin sets/clears the homepage featured_order (must be published=true to feature; passing order: null clears unconditionally); PUT /featured/order - admin bulk-reorders the homepage list (body {agent_ids: [...]} → indices become 0..N-1)
Tags (/api/tags) GET / - list tags (optional tag_type filter; open to all); POST / - create tag (admin); PUT /{id} / DELETE /{id} - update/delete tag (admin); GET /assignments/{target_type}/{target_id} - tags on a resource; PUT /assignments/{target_type}/{target_id} - replace a resource's tags (admin). Tags are typed agent/skill/scheduled_task and may only tag resources of their own type.
Uploads (/api/threads/{id}/uploads) POST / - upload files (auto-converts PDF/PPT/Excel/Word); GET /list - list; DELETE /{filename} - delete
Threads (/api/threads/{id}) DELETE / - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail
Artifacts (/api/threads/{id}/artifacts) GET /{path} - serve artifacts; active content types (text/html, application/xhtml+xml, image/svg+xml) are always forced as download attachments to reduce XSS risk; ?download=true still forces download for other file types
Suggestions (/api/threads/{id}/suggestions) POST / - generate follow-up questions; rich list/block model content is normalized before JSON parsing
Thread Runs (/api/threads/{id}/runs) POST / - create background run; POST /stream - create + SSE stream; POST /wait - create + block; GET / - list runs; GET /{rid} - run details; POST /{rid}/cancel - cancel; GET /{rid}/join - join SSE; GET /{rid}/messages - paginated messages {data, has_more}; GET /{rid}/events - full event stream; GET /../messages - thread messages with feedback; GET /../token-usage - aggregate tokens
Feedback (/api/threads/{id}/runs/{rid}/feedback) PUT / - upsert feedback; DELETE / - delete user feedback; POST / - create feedback; GET / - list feedback; GET /stats - aggregate stats; DELETE /{fid} - delete specific
Runs (/api/runs) POST /stream - stateless run + SSE; POST /wait - stateless run + block; GET /{rid}/messages - paginated messages by run_id {data, has_more} (cursor: after_seq/before_seq); GET /{rid}/feedback - list feedback by run_id
Thread Shares (/api/thread-shares) Per-conversation sharing with two modes (thread_shares.mode). import: POST / {thread_id, mode} - create (or reuse) a revocable share code; GET /{code}/preview + POST /{code}/import - copy that conversation into the caller's account (fresh thread_id via app/gateway/thread_copy.py + new threads_meta row + user-data copy; dedup on metadata.shared_origin). view: a public, read-only static page — see Public Shares below. Both modes: GET / - list my codes; DELETE /{code} - revoke.
Public Shares (/api/public/shares) Unauthenticated (the /api/public/ prefix is whitelisted in auth_middleware._PUBLIC_PATH_PREFIXES). GET /{code} - read-only conversation snapshot (title + serialized messages + artifacts) for a view-mode share; messages are read straight from the latest checkpoint. GET /{code}/files/{path} - serves a file referenced by that conversation (images, Markdown, text, PDF, …) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (text/html, application/xhtml+xml, image/svg+xml) is forced to download (_public_share_disposition); everything else is served inline. Missing/revoked/non-view codes all return 404.
Roundtable Drafts (/api/roundtable-drafts) Per-user persistence for the roundtable-planning page (replaces the old browser localStorage). GET / - list drafts (lightweight meta); POST / - create; GET/PUT/DELETE /{id} - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in PortableLongText. Recommendation history bound to a draft: GET /{id}/recommendations - list newest-first; POST /{id}/recommendations - append one run (objective/status/rationale/picks/candidates). Backed by deerflow.persistence.roundtable_drafts (tables roundtable_drafts, roundtable_recommend_history); store wired in deps.py as app.state.roundtable_draft_store.
Scheduled Tasks (/api/scheduled-tasks) Per-user cron-like agent tasks. GET/POST / - list/create owned tasks; PATCH/DELETE /{id} - update/delete (owner); POST /{id}/{pause,resume,trigger}; GET /{id}/runs, GET /runs/{rid}, run-file preview/edit/download; GET /runs/{rid}/messages - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via _load_run); DELETE /runs/{rid} - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under /{id}/{publish,unpublish,subscribe} and GET /published. Admin (cross-user): GET /admin/all - every task; PATCH/DELETE /admin/{id} - edit/delete any task; POST /admin/{id}/trigger; GET /admin/{id}/runs. Admin routes require system_role == "admin".

Proxied through nginx: /api/langgraph/* → LangGraph, all other /api/* → Gateway.

Scheduled Task Runtime

ScheduledTaskService (deerflow/runtime/scheduler/service.py) polls due tasks every poll_interval_seconds and executes each in its own throwaway thread, serialized by a shared _agent_run_lock.

Stuck-run recovery: each execution attempt is bounded by _RUN_ATTEMPT_TIMEOUT_SECONDS (30 min) via asyncio.wait_for. On timeout the run is retried up to _MAX_RUN_RETRIES (3) times, then marked failed. A watchdog (_reconcile_stuck_runs, run every poll) recovers runs left in running with no in-process owner — e.g. orphaned by a crash or restart — re-driving them through the same retry budget so a task can never stay perpetually "executing". The scheduled_task_runs.retry_count column tracks consumed retries.

Unique task names: task names are unique per user (uq_scheduled_tasks_user_name). create_task/update_task/admin_update_task proactively check via _assert_name_available() and raise DuplicateTaskNameError on a collision; the router translates that into HTTP 409 instead of letting the raw DB IntegrityError surface as a 500.

Custom Agent Persistence

Custom agent metadata lives in the agents ORM table (deerflow.persistence.agents). agents.id is the stable runtime id and the filesystem directory name under .deer-flow/agents/{id}/; agents.name is display-only, can contain Chinese characters, and is not unique. user_id IS NULL marks built-in agents, user-owned agents are visible only to their owner unless published = true, and update/delete operations require ownership. The runtime accepts agent_id in context/configurable, and non-default assistant_id values are treated as agent ids after visibility checks.

Homepage selector (agent picker on the new-chat page) — single admin-curated list backed by agents.featured_order (nullable BIGINT). NULL = not featured. Only published = true agents can be featured (router enforces on writes); any published agent is eligible regardless of ownership. Cleared with order: null. The list rendered to end users is WHERE featured_order IS NOT NULL + visibility-filter, sorted by featured_order ASC. Admins reorder by sending the desired ids to PUT /api/agents/featured/order (assigns indices 0..N-1). BIGINT is used rather than INT so the management UI can seed initial values from Date.now() (overflows INT on MySQL).

Portable Column Types (deerflow/persistence/types.py)

Two TypeDecorators keep ORM columns correct across the sqlite / postgres / mysql backends. All ORM timestamp/JSON columns must use these, not the raw SQLAlchemy types.

  • PortableJSON — native JSON on SQLite/Postgres; TEXT (with manual json.dumps/loads) on MySQL, where older servers lack JSON support.
  • PortableLongText — a large text column: maps to MySQL LONGTEXT (4 GB) and plain TEXT on SQLite/Postgres (already unbounded). Use for big JSON/markdown blobs that would overflow MySQL's 64 KB TEXT cap — e.g. ai_writing_sessions.transcript.
  • BeijingDateTime — stores timestamps as Beijing (UTC+8) wall-clock time. The app records time with datetime.now(UTC), but MySQL DATETIME / SQLite have no timezone and persist whatever wall clock the driver serializes — so a raw DateTime column would store UTC and read 8 hours behind Beijing. BeijingDateTime converts UTC→Beijing on bind and re-attaches +08:00 on result, so the database holds Beijing time while Python still gets timezone-aware datetimes (instant comparisons stay correct, including in WHERE clauses). PostgreSQL timestamptz stores the instant unambiguously, so values pass through unchanged there. Uses a fixed UTC+8 offset (China has no DST), so no tzdata dependency.

Sandbox System (packages/harness/deerflow/sandbox/)

Interface: Abstract Sandbox with execute_command, read_file, write_file, list_dir Provider Pattern: SandboxProvider with acquire, get, release lifecycle Implementations:

  • LocalSandboxProvider - Singleton local filesystem execution with path mappings
  • AioSandboxProvider (packages/harness/deerflow/community/) - Docker-based isolation

Virtual Path System:

  • Agent sees: /mnt/user-data/{workspace,uploads,outputs}, /mnt/skills
  • Physical: backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/..., deer-flow/skills/
  • Translation: replace_virtual_path() / replace_virtual_paths_in_command()
  • Detection: is_local_sandbox() checks sandbox_id == "local"

Sandbox Tools (in packages/harness/deerflow/sandbox/tools.py):

  • bash - Execute commands with path translation and error handling
  • ls - Directory listing (tree format, max 2 levels)
  • read_file - Read file contents with optional line range
  • write_file - Write/append to files, creates directories
  • str_replace - Substring replacement (single or all occurrences); same-path serialization is scoped to (sandbox.id, path) so isolated sandboxes do not contend on identical virtual paths inside one process

Subagent System (packages/harness/deerflow/subagents/)

Built-in Agents: general-purpose (all tools except task) and bash (command specialist) Execution: Dual thread pool - _scheduler_pool (3 workers) + _execution_pool (3 workers) Concurrency: MAX_CONCURRENT_SUBAGENTS = 3 enforced by SubagentLimitMiddleware (truncates excess tool calls in after_model), 15-minute timeout Flow: task() tool → SubagentExecutor → background thread → poll 5s → SSE events → result Events: task_started, task_running, task_completed/task_failed/task_timed_out

Tool System (packages/harness/deerflow/tools/)

get_available_tools(groups, include_mcp, model_name, subagent_enabled) assembles:

  1. Config-defined tools - Resolved from config.yaml via resolve_variable()
  2. MCP tools - From enabled MCP servers (lazy initialized, cached with mtime invalidation)
  3. Built-in tools:
    • present_files - Make output files visible to user (only /mnt/user-data/outputs)
    • ask_clarification - Request clarification (intercepted by ClarificationMiddleware → interrupts)
    • view_image - Read image as base64 (added only if model supports vision)
  4. Subagent tool (if enabled):
    • task - Delegate to subagent (description, prompt, subagent_type, max_turns)

Community tools (packages/harness/deerflow/community/):

  • tavily/ - Web search (5 results default) and web fetch (4KB limit)
  • jina_ai/ - Web fetch via Jina reader API with readability extraction
  • firecrawl/ - Web scraping via Firecrawl API

ACP agent tools:

  • invoke_acp_agent - Invokes external ACP-compatible agents from config.yaml
  • ACP launchers must be real ACP adapters. The standard codex CLI is not ACP-compatible by itself; configure a wrapper such as npx -y @zed-industries/codex-acp or an installed codex-acp binary
  • Missing ACP executables now return an actionable error message instead of a raw [Errno 2]
  • Each ACP agent uses a per-thread workspace at {base_dir}/users/{user_id}/threads/{thread_id}/acp-workspace/. The workspace is accessible to the lead agent via the virtual path /mnt/acp-workspace/ (read-only). In docker sandbox mode, the directory is volume-mounted into the container at /mnt/acp-workspace (read-only); in local sandbox mode, path translation is handled by tools.py
  • image_search/ - Image search via DuckDuckGo

MCP System (packages/harness/deerflow/mcp/)

  • Uses langchain-mcp-adapters MultiServerMCPClient for multi-server management
  • Lazy initialization: Tools loaded on first use via get_cached_mcp_tools()
  • Cache invalidation: Detects config file changes via mtime comparison
  • Transports: stdio (command-based), SSE, HTTP
  • OAuth (HTTP/SSE): Supports token endpoint flows (client_credentials, refresh_token) with automatic token refresh + Authorization header injection
  • Runtime updates: Gateway API saves to extensions_config.json; LangGraph detects via mtime

Skills System (packages/harness/deerflow/skills/)

  • Location: deer-flow/skills/{public,custom}/
  • Format: Directory with SKILL.md (YAML frontmatter: name, description, license, allowed-tools)
  • Loading: load_skills() recursively scans skills/{public,custom} for SKILL.md, parses metadata, and reads enabled state from extensions_config.json
  • Injection: Enabled skills listed in agent system prompt with container paths
  • Installation: POST /api/skills/install extracts .skill ZIP archive to custom/ directory

Skill Curator (packages/harness/deerflow/skills/curator.py)

Background skill lifecycle manager. The Gateway lifespan starts a resident scheduler task (_curator_scheduler_loop in app/gateway/app.py) that — after a 5-minute startup delay — calls maybe_run_curator() every hour. A cross-process file lock (.curator_scheduler.lock) elects a single leader worker so the scheduler runs once per deployment. Per run:

  • Master switch — skill_evolution.enabled (default false) gates the whole feature; curator.enabled (default false) gates the curator specifically. Both must be true for the curator to act. should_run_now() and _curator_thread_target() both enforce this — so manual POST /api/curator/run also respects the master switch. Config is config.yaml.skill_evolution deep-merged with per-user users/{user_id}/skill_evolution.json.
  • Per-user run lock — .curator_state carries running / started_at; _try_acquire_run_lock() prevents the same user's curator from running concurrently (scheduler vs. manual trigger, or double manual clicks). A lock whose started_at is older than _CURATOR_STALE_LOCK_HOURS (6h) is treated as process-crash residue and can be taken over.
  • Concurrency cap — maybe_run_curator() collects all due users and hands them to _dispatch_curator_runs(), which runs them through a ThreadPoolExecutor capped at _CURATOR_MAX_CONCURRENCY (5) — avoids a thundering herd of LLM calls when many users come due at once (e.g. first deploy).
  • Failure back-off — _release_run_lock() always stamps last_attempt_at. When a first run keeps crashing (last_run_at never set), should_run_now() retries on _CURATOR_RETRY_HOURS (6h) instead of every hourly tick.
  • should_run_now() is the real gate: the hourly tick only checks "is the per-user interval_hours (default 168h) elapsed". Timestamp parsing tolerates naive (tz-less) strings — a corrupt last_run_at triggers a re-run rather than a permanent deadlock.
  • Published-skill filter — SkillStore.list_published_names() is a batch query; the curator fetches the published-name set once per run instead of one DB round-trip per candidate skill.
  • LLM review timeout — _run_llm_review() runs agent.invoke() in a worker thread bounded by curator.review_timeout_seconds (default 600s). The model's per-request request_timeout can't bound a multi-turn React agent loop, so on timeout the run is abandoned (a warning is written to curator_log, visible in the Web UI) and the curator finishes cleanly + releases the run lock. The worker context is copied via contextvars.copy_context() so tool calls still resolve the correct user_id.
  • Review model — _run_llm_review() uses curator.model_name (a config.yaml models[] entry name) if set, else the primary model; it is editable from the Web UI evolution panel. The skill security scanner (scan_skill_content in security_scanner.py) reuses the same skill_evolution.curator.model_name setting — review and moderation share one model. There is no separate moderation-model config field.

Model Factory (packages/harness/deerflow/models/factory.py)

  • create_chat_model(name, thinking_enabled) instantiates LLM from config via reflection
  • Supports thinking_enabled flag with per-model when_thinking_enabled overrides
  • Supports vLLM-style thinking toggles via when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking for Qwen reasoning models, while normalizing legacy thinking configs for backward compatibility
  • Supports supports_vision flag for image understanding models
  • Config values starting with $ resolved as environment variables
  • Missing provider modules surface actionable install hints from reflection resolvers (for example uv add langchain-google-genai)

vLLM Provider (packages/harness/deerflow/models/vllm_provider.py)

  • VllmChatModel subclasses langchain_openai:ChatOpenAI for vLLM 0.19.0 OpenAI-compatible endpoints
  • Preserves vLLM's non-standard assistant reasoning field on full responses, streaming deltas, and follow-up tool-call turns
  • Designed for configs that enable thinking through extra_body.chat_template_kwargs.enable_thinking on vLLM 0.19.0 Qwen reasoning models, while accepting the older thinking alias

IM Channels System (app/channels/)

Bridges external messaging platforms (Feishu, Slack, Telegram, DingTalk) to the DeerFlow agent via the LangGraph Server.

Architecture: Channels communicate with Gateway through the langgraph-sdk HTTP client (same as the frontend), ensuring threads are created and managed server-side. The internal SDK client injects process-local internal auth plus a matching CSRF cookie/header pair so Gateway accepts state-changing thread/run requests from channel workers without relying on browser session cookies.

Components:

  • message_bus.py - Async pub/sub hub (InboundMessage → queue → dispatcher; OutboundMessage → callbacks → channels)
  • store.py - JSON-file persistence mapping channel_name:chat_id[:topic_id] → thread_id (keys are channel:chat for root conversations and channel:chat:topic for threaded conversations)
  • manager.py - Core dispatcher: creates threads via client.threads.create(), routes commands, keeps Slack/Telegram on client.runs.wait(), and uses client.runs.stream(["messages-tuple", "values"]) for Feishu incremental outbound updates
  • base.py - Abstract Channel base class (start/stop/send lifecycle)
  • service.py - Manages lifecycle of all configured channels from config.yaml
  • slack.py / feishu.py / telegram.py / dingtalk.py - Platform-specific implementations (feishu.py tracks the running card message_id in memory and patches the same card in place; dingtalk.py optionally uses AI Card streaming for in-place updates when card_template_id is configured)

Message Flow:

  1. External platform -> Channel impl -> MessageBus.publish_inbound()
  2. ChannelManager._dispatch_loop() consumes from queue
  3. For chat: look up/create thread through Gateway's LangGraph-compatible API
  4. Feishu chat: runs.stream() → accumulate AI text → publish multiple outbound updates (is_final=False) → publish final outbound (is_final=True)
  5. Slack/Telegram chat: runs.wait() → extract final response → publish outbound
  6. Feishu channel sends one running reply card up front, then patches the same card for each outbound update (card JSON sets config.update_multi=true for Feishu's patch API requirement)
  7. DingTalk AI Card mode (when card_template_id configured): runs.stream() → create card with initial text → stream updates via PUT /v1.0/card/streaming → finalize on is_final=True. Falls back to sampleMarkdown if card creation or streaming fails
  8. For commands (/new, /status, /models, /memory, /help): handle locally or query Gateway API
  9. Outbound → channel callbacks → platform reply

Configuration (config.yaml -> channels):

  • langgraph_url - LangGraph-compatible Gateway API base URL (default: http://localhost:8001/api)
  • gateway_url - Gateway API URL for auxiliary commands (default: http://localhost:8001)
  • In Docker Compose, IM channels run inside the gateway container, so localhost points back to that container. Use http://gateway:8001/api for langgraph_url and http://gateway:8001 for gateway_url, or set DEER_FLOW_CHANNELS_LANGGRAPH_URL / DEER_FLOW_CHANNELS_GATEWAY_URL.
  • Per-channel configs: feishu (app_id, app_secret), slack (bot_token, app_token), telegram (bot_token), dingtalk (client_id, client_secret, optional card_template_id for AI Card streaming)

Memory System — Memory V2 (packages/harness/deerflow/agents/memory/)

Pluggable memory architecture (Hermes-style). See docs/MEMORY_V2_DESIGN_ZH.md and docs/HINDSIGHT_SETUP_ZH.md for the full design.

Components:

  • provider.py - MemoryProvider abstract base (12 lifecycle/hook methods)
  • manager.py - MemoryManager orchestrates builtin + ≤1 external provider; build_memory_manager() is the construction entry point
  • providers/builtin.py - BuiltinFileProvider: local USER.md / MEMORY.md, § delimiter, char quotas, file locking, atomic writes, frozen snapshot
  • providers/hindsight.py - HindsightProvider: HTTP client to a self-hosted Hindsight Docker instance, dual-bank routing
  • security.py - injection/exfiltration scan + invisible-unicode block for memory content
  • message_processing.py - filter_messages_for_memory (last-turn extraction for Hindsight retain)

Two stores (binary classification):

  • USER.md at {base_dir}/users/{user_id}/USER.md — user profile, shared across agents
  • MEMORY.md at {base_dir}/users/{user_id}/agents/{agent_id}/MEMORY.md — agent-private working notes
  • user_id resolved via get_effective_user_id(); "default" in no-auth mode

Write paths:

  • LLM actively calls the memory tool (tools/builtins/memory_tool.py) → writes USER.md/MEMORY.md, mirrors to Hindsight
  • LLM calls hindsight_recall/reflect/retain tools (tools/builtins/hindsight_tools.py) — only in tools/hybrid memory_mode
  • Hindsight auto_retain — every turn auto-stored to the work bank (via MemoryMiddleware.after_agent)

Injection:

  • builtin frozen snapshot → injected into the system prompt via lead_agent/prompt.py _get_memory_context
  • Hindsight dynamic recall → injected per model call via MemoryMiddleware.wrap_model_call (transient — not persisted to state, so it never leaks to the frontend)

Hindsight dual-bank isolation (B-scheme):

  • global user bank deerflow-user-{user_id} — cross-agent
  • agent work bank deerflow-work-{user_id}-{agent_id} — per-agent
  • hindsight-client is an optional dependency; when missing, HindsightProvider.is_available() returns False and the system falls back to builtin-only

Migration: PYTHONPATH=. python scripts/migrate_memory_to_v2.py [--all|--user-id X] [--dry-run] converts legacy memory.json into USER.md/MEMORY.md.

Configuration (config.yaml → memory, nested): enabled, provider (builtin/hindsight), builtin.{memory_char_limit,user_char_limit,...}, hindsight.{mode,api_url,api_key,bank_id_template,work_bank_id_template,memory_mode,...}, injection.{enabled,max_tokens,include_builtin,include_hindsight,context_tag}, security.{scan_content,block_invisible_unicode}. Per-user runtime overrides (whitelisted fields) live at {base_dir}/users/{user_id}/memory_config.json — see get_effective_memory_config().

Gateway API — see the Memory router below (/api/memory/v2*).

Reflection System (packages/harness/deerflow/reflection/)

  • resolve_variable(path) - Import module and return variable (e.g., module.path:variable_name)
  • resolve_class(path, base_class) - Import and validate class against base class

Config Schema

config.yaml key sections:

  • models[] - LLM configs with use class path, supports_thinking, supports_vision, provider-specific fields
  • vLLM reasoning models should use deerflow.models.vllm_provider:VllmChatModel; for Qwen-style parsers prefer when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking, and DeerFlow will also normalize the older thinking alias
  • tools[] - Tool configs with use variable path and group
  • tool_groups[] - Logical groupings for tools
  • sandbox.use - Sandbox provider class path
  • skills.path / skills.container_path - Host and container paths to skills directory
  • title - Auto-title generation (enabled, max_words, max_chars, prompt_template)
  • summarization - Context summarization (enabled, trigger conditions, keep policy)
  • subagents.enabled - Master switch for subagent delegation
  • memory - Memory system (enabled, storage_path, debounce_seconds, model_name, max_facts, fact_confidence_threshold, injection_enabled, max_injection_tokens)
  • auth_login - Custom login modes layered on the base auth (deerflow/config/login_config.py):
    • username_login.{require_password, password} - Password gate for POST /api/v1/auth/login/username. When require_password=true, the (otherwise passwordless) username login additionally requires ?password=<shared-secret>; empty password falls back to 123ewq. Gate only — it does not change which account logs in.
    • token_login.{enabled, token_info_url, service_authorization, timeout_seconds, verify_ssl, ca_cert_path} - Token-exchange login (POST /api/v1/auth/login/token). The gateway POSTs to token_info_url with the caller-supplied token placed directly in the Authorization: Bearer <token> header (no cookie; the static service_authorization field is deprecated/unused, kept only for backward compat), reads data.username from a {"state":"200",...} body, then signs the matching local user in (auto-registering if absent). token_info_url is HTTPS — verify_ssl: false skips cert verification for self-signed/internal UAT hosts, or set ca_cert_path to a PEM bundle (takes precedence, keeps verification on). See docs/AUTH_LOGIN.md for the full request/response contract.

Both endpoints are whitelisted in auth_middleware._PUBLIC_EXACT_PATHS. The shared _get_or_create_user_by_username() helper (in routers/auth.py) backs both /login/username and /login/token.

extensions_config.json:

  • mcpServers - Map of server name → config (enabled, type, command, args, env, url, headers, oauth, description)
  • skills - Map of skill name → state (enabled)

Both can be modified at runtime via Gateway API endpoints or DeerFlowClient methods.

Embedded Client (packages/harness/deerflow/client.py)

DeerFlowClient provides direct in-process access to all DeerFlow capabilities without HTTP services. All return types align with the Gateway API response schemas, so consumer code works identically in HTTP and embedded modes.

Architecture: Imports the same deerflow modules that Gateway API uses. Shares the same config files and data directories. No FastAPI dependency.

Agent Conversation:

  • chat(message, thread_id) — synchronous, accumulates streaming deltas per message-id and returns the final AI text
  • stream(message, thread_id) — subscribes to LangGraph stream_mode=["values", "messages", "custom"] and yields StreamEvent:
    • "values" — full state snapshot (title, messages, artifacts); AI text already delivered via messages mode is not re-synthesized here to avoid duplicate deliveries
    • "messages-tuple" — per-chunk update: for AI text this is a delta (concat per id to rebuild the full message); tool calls and tool results are emitted once each
    • "custom" — forwarded from StreamWriter
    • "end" — stream finished (carries cumulative usage counted once per message id)
  • Agent created lazily via create_agent() + _build_middlewares(), same as make_lead_agent
  • Supports checkpointer parameter for state persistence across turns
  • reset_agent() forces agent recreation (e.g. after memory or skill changes)
  • See docs/STREAMING.md for the full design: why Gateway and DeerFlowClient are parallel paths, LangGraph's stream_mode semantics, the per-id dedup invariants, and regression testing strategy

Gateway Equivalent Methods (replaces Gateway API):

Category Methods Return format
Models list_models(), get_model(name) {"models": [...]}, {name, display_name, ...}
MCP get_mcp_config(), update_mcp_config(servers) {"mcp_servers": {...}}
Skills list_skills(), get_skill(name), update_skill(name, enabled), install_skill(path) {"skills": [...]}
Memory get_memory(), reload_memory(), get_memory_config(), get_memory_status() dict
Uploads upload_files(thread_id, files), list_uploads(thread_id), delete_upload(thread_id, filename) {"success": true, "files": [...]}, {"files": [...], "count": N}
Artifacts get_artifact(thread_id, path) → (bytes, mime_type) tuple

Key difference from Gateway: Upload accepts local Path objects instead of HTTP UploadFile, rejects directory paths before copying, and reuses a single worker when document conversion must run inside an active event loop. Artifact returns (bytes, mime_type) instead of HTTP Response. The new Gateway-only thread cleanup route deletes .deer-flow/threads/{thread_id} after LangGraph thread deletion; there is no matching DeerFlowClient method yet. update_mcp_config() and update_skill() automatically invalidate the cached agent.

Tests: tests/test_client.py (77 unit tests including TestGatewayConformance), tests/test_client_live.py (live integration tests, requires config.yaml)

Gateway Conformance Tests (TestGatewayConformance): Validate that every dict-returning client method conforms to the corresponding Gateway Pydantic response model. Each test parses the client output through the Gateway model — if Gateway adds a required field that the client doesn't provide, Pydantic raises ValidationError and CI catches the drift. Covers: ModelsListResponse, ModelResponse, SkillsListResponse, SkillResponse, SkillInstallResponse, McpConfigResponse, UploadResponse, MemoryConfigResponse, MemoryStatusResponse.

Development Workflow

Test-Driven Development (TDD) — MANDATORY

Every new feature or bug fix MUST be accompanied by unit tests. No exceptions.

  • Write tests in backend/tests/ following the existing naming convention test_<feature>.py
  • Run the full suite before and after your change: make test
  • Tests must pass before a feature is considered complete
  • For lightweight config/utility modules, prefer pure unit tests with no external dependencies
  • If a module causes circular import issues in tests, add a sys.modules mock in tests/conftest.py (see existing example for deerflow.subagents.executor)
# Run all tests
make test

# Run a specific test file
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v

Running the Full Application

From the project root directory:

make dev

This starts all services and makes the application available at http://localhost:2026.

All startup modes:

Local Foreground Local Daemon Docker Dev Docker Prod
Dev ./scripts/serve.sh --dev
make dev
./scripts/serve.sh --dev --daemon
make dev-daemon
./scripts/docker.sh start
make docker-start
—
Prod ./scripts/serve.sh --prod
make start
./scripts/serve.sh --prod --daemon
make start-daemon
— ./scripts/deploy.sh
make up
Action Local Docker Dev Docker Prod
Stop ./scripts/serve.sh --stop
make stop
./scripts/docker.sh stop
make docker-stop
./scripts/deploy.sh down
make down
Restart ./scripts/serve.sh --restart [flags] ./scripts/docker.sh restart —

Nginx routing:

  • /api/langgraph/* → Gateway embedded runtime (8001), rewritten to /api/*
  • /api/* (other) → Gateway API (8001)
  • / (non-API) → Frontend (3000)

Running Backend Services Separately

From the backend directory:

# Gateway API
make gateway

Direct access (without nginx):

  • Gateway: http://localhost:8001

Frontend Configuration

The frontend uses environment variables to connect to backend services:

  • NEXT_PUBLIC_LANGGRAPH_BASE_URL - Defaults to /api/langgraph (through nginx)
  • NEXT_PUBLIC_BACKEND_BASE_URL - Defaults to empty string (through nginx)

When using make dev from root, the frontend automatically connects through nginx.

Key Features

File Upload

Multi-file upload with automatic document conversion:

  • Endpoint: POST /api/threads/{thread_id}/uploads
  • Supports: PDF, PPT, Excel, Word documents (converted via markitdown)
  • Rejects directory inputs before copying so uploads stay all-or-nothing
  • Reuses one conversion worker per request when called from an active event loop
  • Files stored in thread-isolated directories
  • Agent receives uploaded file list via UploadsMiddleware

See docs/FILE_UPLOAD.md for details.

Plan Mode

TodoList middleware for complex multi-step tasks:

  • Controlled via runtime config: config.configurable.is_plan_mode = True
  • Provides write_todos tool for task tracking
  • One task in_progress at a time, real-time updates

See docs/plan_mode_usage.md for details.

Context Summarization

Automatic conversation summarization when approaching token limits:

  • Configured in config.yaml under summarization key
  • Trigger types: tokens, messages, or fraction of max input
  • Keeps recent messages while summarizing older ones

See docs/summarization.md for details.

Vision Support / Image Recognition (识图)

Two paths, selected by config.yaml → vision:

Dedicated vision model (delegated recognition) — set vision.model_name to a model from models[]. The view_image tool sends the image to that model and returns its text description as the tool result, so a text-only main chat model can still "see" images. view_image accepts an optional question argument for targeted queries; the default prompt is vision.prompt (overridable), and vision.max_tokens caps the recognition output. Resolution/gating lives in deerflow/models/vision.py (resolve_vision_model_name, vision_enabled, is_delegated_vision, recognize_image).

Native vision (fallback) — when vision.model_name is unset, behavior falls back to the main chat model, which only works if it has supports_vision: true:

  • ViewImageMiddleware injects base64 image data into the conversation before the LLM call
  • view_image stores the image in state.viewed_images for that middleware

Both the view_image tool and ViewImageMiddleware are registered whenever vision_enabled(model_name) is true (dedicated model configured or main model supports vision). Config schema: deerflow/config/vision_config.py (VisionConfig); tests: tests/test_vision.py.

deerflow/uploads/manager.py:open_upload_file_no_symlink writes uploads without following a pre-existing destination symlink. O_NOFOLLOW is applied where available (POSIX) but is optional — Windows lacks os.O_NOFOLLOW/O_NONBLOCK, so requiring it previously made every Windows upload fail with UnsafeUploadPathError. Pre-existing symlinks are still rejected via lstat, and the opened fd is re-checked with fstat. O_BINARY is added on Windows. Regression: tests/test_uploads_windows_safe.py.

Code Style

  • Uses ruff for linting and formatting
  • Line length: 240 characters
  • Python 3.12+ with type hints
  • Double quotes, space indentation

Documentation

See docs/ directory for detailed documentation: