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>
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 viaRunManager+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.mdfor user-facing changes (features, setup, usage instructions) - Update
CLAUDE.mdfor 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 fromconfig.yaml)tests/test_provisioner_kubeconfig.py(kubeconfig file/directory handling)
Boundary check (harness → app import firewall):
tests/test_harness_boundary.py— ensurespackages/harness/deerflow/never imports fromapp.*
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 inlanggraph.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
AgentStatewith: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 thinkingmodel_name- Select specific LLM modelis_plan_mode- Enable TodoList middlewaresubagent_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):
- 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}); resolvesuser_idviaget_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 - UploadsMiddleware - Tracks and injects newly uploaded files into conversation
- SandboxMiddleware - Acquires sandbox, stores
sandbox_idin state - 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"] - LLMErrorHandlingMiddleware - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run
- GuardrailMiddleware - Pre-tool-call authorization via pluggable
GuardrailProviderprotocol (optional, ifguardrails.enabledin config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-inAllowlistProvider(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. - SandboxAuditMiddleware - Audits sandboxed shell/file operations for security logging before tool execution continues
- ToolErrorHandlingMiddleware - Converts tool exceptions into error
ToolMessages so the run can continue instead of aborting - SummarizationMiddleware - Context reduction when approaching token limits (optional, if enabled)
- TodoListMiddleware - Task tracking with
write_todostool (optional, if plan_mode) - TokenUsageMiddleware - Records token usage metrics when token tracking is enabled (optional)
- TitleMiddleware - Auto-generates thread title after first complete exchange and normalizes structured message content before prompting the title model
- MemoryMiddleware - Memory V2:
wrap_model_callinjects Hindsight recall transiently;after_agentauto-retains the turn to Hindsight - ViewImageMiddleware - Injects base64 image data before LLM call (conditional on vision support)
- DeferredToolFilterMiddleware - Hides deferred tool schemas from the bound model until tool search is enabled (optional)
- SubagentLimitMiddleware - Truncates excess
tasktool calls from model response to enforceMAX_CONCURRENT_SUBAGENTSlimit (optional, ifsubagent_enabled) - LoopDetectionMiddleware - Detects repeated tool-call loops; hard-stop responses clear both structured
tool_callsand raw provider tool-call metadata before forcing a final text answer - ClarificationMiddleware - Intercepts
ask_clarificationtool calls, interrupts viaCommand(goto=END)(must be last) 8a. ToolMetricsMiddleware - Appended immediately afterToolErrorHandlingMiddlewarein_build_runtime_middlewares, so it sits deepest in thewrap_tool_callchain. Records onetool_call_metricsrow per tool/skill/MCP invocation — timing duration and bucketing failures into a coarseerror_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 errorToolMessages (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill intotool_call_metrics.skill_name: the lead agent invokes a skill byread_file-ing its/mnt/skills/<category>/<name>/SKILL.mdpath (per the<skill_system>prompt block — there is no dedicated "use skill" tool), soskill_nameis recovered by scanning every tool call's string arguments for theskills/{public,custom}/{name}/path shape (_SKILL_PATH_RE), plus the explicitnameargument ofskill_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.recordupserts 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 onesuccess); a skill that only ever failed yields oneerrorrow whoseerror_messageaccumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with norun_id, keep one row each. The read→merge→write is guarded by a per-event-loopasyncio.Lock. Surfaced by the adminGET /api/admin/tool-metricsrouter (which has akind=skillfilter and a per-skill/summarybreakdown). - 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 - SummarizationMiddleware - Context reduction when approaching token limits (optional, if enabled)
- TodoListMiddleware - Task tracking with
write_todostool (optional, if plan_mode) - TokenUsageMiddleware - Records token usage metrics when token tracking is enabled (optional)
- TitleMiddleware - Auto-generates thread title after first complete exchange and normalizes structured message content before prompting the title model
- MemoryMiddleware - Queues conversations for async memory update (filters to user + final AI responses)
- ViewImageMiddleware - Injects base64 image data before LLM call (conditional on vision support)
- DeferredToolFilterMiddleware - Hides deferred tool schemas from the bound model until tool search is enabled (optional)
- SubagentLimitMiddleware - Truncates excess
tasktool calls from model response to enforceMAX_CONCURRENT_SUBAGENTSlimit (optional, ifsubagent_enabled) - LoopDetectionMiddleware - Detects repeated tool-call loops; hard-stop responses clear both structured
tool_callsand raw provider tool-call metadata before forcing a final text answer - ClarificationMiddleware - Intercepts
ask_clarificationtool calls, interrupts viaCommand(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:
- Explicit
config_pathargument DEER_FLOW_CONFIG_PATHenvironment variableconfig.yamlin current directory (backend/)config.yamlin 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:
- Explicit
config_pathargument DEER_FLOW_EXTENSIONS_CONFIG_PATHenvironment variableextensions_config.jsonin current directory (backend/)extensions_config.jsonin 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— nativeJSONon SQLite/Postgres;TEXT(with manualjson.dumps/loads) on MySQL, where older servers lack JSON support.PortableLongText— a large text column: maps to MySQLLONGTEXT(4 GB) and plainTEXTon SQLite/Postgres (already unbounded). Use for big JSON/markdown blobs that would overflow MySQL's 64 KBTEXTcap — e.g.ai_writing_sessions.transcript.BeijingDateTime— stores timestamps as Beijing (UTC+8) wall-clock time. The app records time withdatetime.now(UTC), but MySQLDATETIME/ SQLite have no timezone and persist whatever wall clock the driver serializes — so a rawDateTimecolumn would store UTC and read 8 hours behind Beijing.BeijingDateTimeconverts UTC→Beijing on bind and re-attaches+08:00on result, so the database holds Beijing time while Python still gets timezone-aware datetimes (instant comparisons stay correct, including in WHERE clauses). PostgreSQLtimestamptzstores the instant unambiguously, so values pass through unchanged there. Uses a fixed UTC+8 offset (China has no DST), so notzdatadependency.
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 mappingsAioSandboxProvider(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()checkssandbox_id == "local"
Sandbox Tools (in packages/harness/deerflow/sandbox/tools.py):
bash- Execute commands with path translation and error handlingls- Directory listing (tree format, max 2 levels)read_file- Read file contents with optional line rangewrite_file- Write/append to files, creates directoriesstr_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:
- Config-defined tools - Resolved from
config.yamlviaresolve_variable() - MCP tools - From enabled MCP servers (lazy initialized, cached with mtime invalidation)
- 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)
- 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 extractionfirecrawl/- Web scraping via Firecrawl API
ACP agent tools:
invoke_acp_agent- Invokes external ACP-compatible agents fromconfig.yaml- ACP launchers must be real ACP adapters. The standard
codexCLI is not ACP-compatible by itself; configure a wrapper such asnpx -y @zed-industries/codex-acpor an installedcodex-acpbinary - 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 bytools.py image_search/- Image search via DuckDuckGo
MCP System (packages/harness/deerflow/mcp/)
- Uses
langchain-mcp-adaptersMultiServerMCPClientfor 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 scansskills/{public,custom}forSKILL.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/installextracts .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(defaultfalse) gates the whole feature;curator.enabled(defaultfalse) gates the curator specifically. Both must betruefor the curator to act.should_run_now()and_curator_thread_target()both enforce this — so manualPOST /api/curator/runalso respects the master switch. Config isconfig.yaml.skill_evolutiondeep-merged with per-userusers/{user_id}/skill_evolution.json. - Per-user run lock —
.curator_statecarriesrunning/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 whosestarted_atis 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 aThreadPoolExecutorcapped 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 stampslast_attempt_at. When a first run keeps crashing (last_run_atnever 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-userinterval_hours(default 168h) elapsed". Timestamp parsing tolerates naive (tz-less) strings — a corruptlast_run_attriggers 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()runsagent.invoke()in a worker thread bounded bycurator.review_timeout_seconds(default 600s). The model's per-requestrequest_timeoutcan't bound a multi-turn React agent loop, so on timeout the run is abandoned (a warning is written tocurator_log, visible in the Web UI) and the curator finishes cleanly + releases the run lock. The worker context is copied viacontextvars.copy_context()so tool calls still resolve the correctuser_id. - Review model —
_run_llm_review()usescurator.model_name(aconfig.yamlmodels[]entry name) if set, else the primary model; it is editable from the Web UI evolution panel. The skill security scanner (scan_skill_contentinsecurity_scanner.py) reuses the sameskill_evolution.curator.model_namesetting — 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_enabledflag with per-modelwhen_thinking_enabledoverrides - Supports vLLM-style thinking toggles via
when_thinking_enabled.extra_body.chat_template_kwargs.enable_thinkingfor Qwen reasoning models, while normalizing legacythinkingconfigs for backward compatibility - Supports
supports_visionflag 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)
VllmChatModelsubclasseslangchain_openai:ChatOpenAIfor vLLM 0.19.0 OpenAI-compatible endpoints- Preserves vLLM's non-standard assistant
reasoningfield on full responses, streaming deltas, and follow-up tool-call turns - Designed for configs that enable thinking through
extra_body.chat_template_kwargs.enable_thinkingon vLLM 0.19.0 Qwen reasoning models, while accepting the olderthinkingalias
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 mappingchannel_name:chat_id[:topic_id]→thread_id(keys arechannel:chatfor root conversations andchannel:chat:topicfor threaded conversations)manager.py- Core dispatcher: creates threads viaclient.threads.create(), routes commands, keeps Slack/Telegram onclient.runs.wait(), and usesclient.runs.stream(["messages-tuple", "values"])for Feishu incremental outbound updatesbase.py- AbstractChannelbase class (start/stop/send lifecycle)service.py- Manages lifecycle of all configured channels fromconfig.yamlslack.py/feishu.py/telegram.py/dingtalk.py- Platform-specific implementations (feishu.pytracks the running cardmessage_idin memory and patches the same card in place;dingtalk.pyoptionally uses AI Card streaming for in-place updates whencard_template_idis configured)
Message Flow:
- External platform -> Channel impl ->
MessageBus.publish_inbound() ChannelManager._dispatch_loop()consumes from queue- For chat: look up/create thread through Gateway's LangGraph-compatible API
- Feishu chat:
runs.stream()→ accumulate AI text → publish multiple outbound updates (is_final=False) → publish final outbound (is_final=True) - Slack/Telegram chat:
runs.wait()→ extract final response → publish outbound - Feishu channel sends one running reply card up front, then patches the same card for each outbound update (card JSON sets
config.update_multi=truefor Feishu's patch API requirement) - DingTalk AI Card mode (when
card_template_idconfigured):runs.stream()→ create card with initial text → stream updates viaPUT /v1.0/card/streaming→ finalize onis_final=True. Falls back tosampleMarkdownif card creation or streaming fails - For commands (
/new,/status,/models,/memory,/help): handle locally or query Gateway API - 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
gatewaycontainer, solocalhostpoints back to that container. Usehttp://gateway:8001/apiforlanggraph_urlandhttp://gateway:8001forgateway_url, or setDEER_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, optionalcard_template_idfor 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-MemoryProviderabstract base (12 lifecycle/hook methods)manager.py-MemoryManagerorchestrates builtin + ≤1 external provider;build_memory_manager()is the construction entry pointproviders/builtin.py-BuiltinFileProvider: local USER.md / MEMORY.md, § delimiter, char quotas, file locking, atomic writes, frozen snapshotproviders/hindsight.py-HindsightProvider: HTTP client to a self-hosted Hindsight Docker instance, dual-bank routingsecurity.py- injection/exfiltration scan + invisible-unicode block for memory contentmessage_processing.py-filter_messages_for_memory(last-turn extraction for Hindsight retain)
Two stores (binary classification):
USER.mdat{base_dir}/users/{user_id}/USER.md— user profile, shared across agentsMEMORY.mdat{base_dir}/users/{user_id}/agents/{agent_id}/MEMORY.md— agent-private working notesuser_idresolved viaget_effective_user_id();"default"in no-auth mode
Write paths:
- LLM actively calls the
memorytool (tools/builtins/memory_tool.py) → writes USER.md/MEMORY.md, mirrors to Hindsight - LLM calls
hindsight_recall/reflect/retaintools (tools/builtins/hindsight_tools.py) — only intools/hybridmemory_mode - Hindsight
auto_retain— every turn auto-stored to the work bank (viaMemoryMiddleware.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-clientis 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 withuseclass path,supports_thinking,supports_vision, provider-specific fields- vLLM reasoning models should use
deerflow.models.vllm_provider:VllmChatModel; for Qwen-style parsers preferwhen_thinking_enabled.extra_body.chat_template_kwargs.enable_thinking, and DeerFlow will also normalize the olderthinkingalias tools[]- Tool configs withusevariable path andgrouptool_groups[]- Logical groupings for toolssandbox.use- Sandbox provider class pathskills.path/skills.container_path- Host and container paths to skills directorytitle- Auto-title generation (enabled, max_words, max_chars, prompt_template)summarization- Context summarization (enabled, trigger conditions, keep policy)subagents.enabled- Master switch for subagent delegationmemory- 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 forPOST /api/v1/auth/login/username. Whenrequire_password=true, the (otherwise passwordless) username login additionally requires?password=<shared-secret>; emptypasswordfalls back to123ewq. 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 totoken_info_urlwith the caller-supplied token placed directly in theAuthorization: Bearer <token>header (no cookie; the staticservice_authorizationfield is deprecated/unused, kept only for backward compat), readsdata.usernamefrom a{"state":"200",...}body, then signs the matching local user in (auto-registering if absent).token_info_urlis HTTPS —verify_ssl: falseskips cert verification for self-signed/internal UAT hosts, or setca_cert_pathto 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 textstream(message, thread_id)— subscribes to LangGraphstream_mode=["values", "messages", "custom"]and yieldsStreamEvent:"values"— full state snapshot (title, messages, artifacts); AI text already delivered viamessagesmode is not re-synthesized here to avoid duplicate deliveries"messages-tuple"— per-chunk update: for AI text this is a delta (concat peridto rebuild the full message); tool calls and tool results are emitted once each"custom"— forwarded fromStreamWriter"end"— stream finished (carries cumulativeusagecounted once per message id)
- Agent created lazily via
create_agent()+_build_middlewares(), same asmake_lead_agent - Supports
checkpointerparameter 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_modesemantics, 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 conventiontest_<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.modulesmock intests/conftest.py(see existing example fordeerflow.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 --devmake dev |
./scripts/serve.sh --dev --daemonmake dev-daemon |
./scripts/docker.sh startmake docker-start |
— |
| Prod | ./scripts/serve.sh --prodmake start |
./scripts/serve.sh --prod --daemonmake start-daemon |
— | ./scripts/deploy.shmake up |
| Action | Local | Docker Dev | Docker Prod |
|---|---|---|---|
| Stop | ./scripts/serve.sh --stopmake stop |
./scripts/docker.sh stopmake docker-stop |
./scripts/deploy.sh downmake 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_todostool 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.yamlundersummarizationkey - 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:
ViewImageMiddlewareinjects base64 image data into the conversation before the LLM callview_imagestores the image instate.viewed_imagesfor 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.
Upload symlink-safety (cross-platform)
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
rufffor linting and formatting - Line length: 240 characters
- Python 3.12+ with type hints
- Double quotes, space indentation
Documentation
See docs/ directory for detailed documentation:
- CONFIGURATION.md - Configuration options
- ARCHITECTURE.md - Architecture details
- API.md - API reference
- SETUP.md - Setup guide
- FILE_UPLOAD.md - File upload feature
- PATH_EXAMPLES.md - Path types and usage
- summarization.md - Context summarization
- plan_mode_usage.md - Plan mode with TodoList
- BACKEND_RUN_STOP_ZH.md - Windows-first backend start/stop and troubleshooting guide