Aari 5eb59cb130
fix(sandbox): stop multi-worker orphan reconcile from killing peer sandboxes (#4221)
* fix(sandbox): stop multi-worker orphan reconcile from killing peer sandboxes

Docker sandboxes are shared across gateway workers, but each worker kept its
own in-memory warm pool. Startup reconciliation adopted every running
container, so a peer idle reaper could destroy sandboxes another worker still
owned and tool calls hit 502 / Connection refused.

Add file-based ownership leases under sandbox-leases/, only adopt true
orphans, refuse idle/replica/shutdown destroy while a foreign lease is live,
and renew the lease on create/get/release/reclaim.

Fixes #4206

* fix(sandbox): close lease fail-open, hot-path IO, and check→destroy race

Address review of the multi-worker orphan lease (#4206):

- read_lease returns None only for a genuinely-absent lease and raises
  (CorruptLeaseError/OSError) when a lease is unreadable or corrupt, so the
  ownership check fails closed instead of mistaking an unprovable peer lease
  for a free container. clear_lease still removes a stuck/corrupt file.
- get() no longer renews the lease (blocking mkdir/fsync/os.replace on the
  event loop path used by ensure_sandbox_initialized_async); active leases are
  renewed off the event loop from the idle checker (_renew_active_leases).
- The ownership check and container stop run under a per-sandbox flock guard
  (lease_ownership_guard); every lease write takes the same guard so a peer's
  touch cannot interleave with a destroy. Same-host multi-worker scope, not a
  multi-pod distributed lock.

Also fixes the ruff format lint on the branch. Adds regression tests: corrupt
and unreadable lease fail closed, a tests/blocking_io anchor keeping get()
non-blocking on the event loop, and a peer-touch/destroy interleave test.

* fix(sandbox): share container ownership across gateway instances

Rework of the #4206 fix per review: ownership state is shared through a
third-party service instead of being maintained per gateway instance,
following the stream_bridge precedent (sandbox.ownership.type:
memory | redis). The file lease and its same-host flock guard are
deleted, not ported — they only covered workers on one host, while the
deployment that hits #4206 is a load-balanced multi-instance gateway.

A lease answers "who reaps this container", not "who may use it".
Containers are deterministic per (user, thread), so consecutive turns
legitimately land on different instances: take() transfers ownership on
acquire, while claim() gates every adopt/reap path.

Leases carry a state — own: or del: — so a takeover is refused against a
teardown in progress. Without it an unconditional take() would overwrite
a destroyer's claim and the peer's container stop would land on a
sandbox the new owner had already handed to an agent.

renew() distinguishes a lapsed lease from one a peer took; only the
latter drops the sandbox. Collapsing them meant a Redis restart evicted
every in-flight sandbox on every instance at once.

Renewal runs on its own thread with a TTL derived from its interval,
never from idle_timeout: renewal used to ride the idle checker, which
does not start at idle_timeout: 0, so leases silently lapsed on a
supported config.

Ownership establishment is fail-closed: a sandbox whose ownership cannot
be published is never handed out, and a just-created container is
destroyed rather than leaked as an adoptable orphan. Every destroy path
claims before untracking.

The memory store is single-instance only and says so; the resolver reads
app_config.stream_bridge and the env var in the bridge's own order, so
deployments already using Redis get a redis ownership store without
extra config.

* fix(sandbox): wait out a recovery grace before adopting a keyless container

An absent ownership lease meant two opposite things on two paths. Renewal
reads it as LAPSED and re-establishes it: nobody took the lease, so the
container is still ours. Reconciliation read the same absent key as "orphan"
and adopted on sight.

After the store loses its keys (a Redis restart without persistence, or
eviction under maxmemory) every owner is alive and merely pre-renewal-tick.
Whichever instance reconciled first therefore adopted every live container;
each real owner's next renewal reported LOST and dropped a sandbox it was
serving mid-turn, leaving it for the adopter to idle-destroy — #4206 through
the back door, in the very case the LAPSED handling was added to make safe.
Not limited to startup: an already-running instance hits the same window from
the idle checker's periodic reconcile.

_adoptable_after_grace requires an untracked container to be seen unowned
across a full lease TTL before it can be adopted. That rebuilds the delay the
state loss erased: a live owner republishes within one renewal interval,
shorter than the TTL by construction, while a crashed owner never does, so its
containers are still adopted one grace later rather than leaking. A republished
lease resets the grace; a pausing-only timer would still expire over a live
owner's lease. The peek is read-only — the atomic claim still gates adoption.

The grace is skipped when the store cannot coordinate across processes: no peer
can hold a lease such a store would show us, so single-instance deployments
keep instant orphan cleanup, and a grace could not help a multi-worker gateway
on memory anyway.

* fix(sandbox): hold the teardown lease for as long as the container stop runs

claim(..., for_destroy=True) wrote the del: marker with the ordinary lease TTL
and nothing refreshed it. renew() extends only own: and deliberately reports a
teardown as LOST, and the destroy paths drop the sandbox from the maps the
renewal loop iterates — so a container stop that outlived the TTL let the marker
lapse, a peer's take() succeeded against the still-running container, and the
stop then landed on the turn that had just been handed it. That is the exact
window the del: state exists to close, reopened by its own expiry.

The two lease states alone never made the per-sandbox flock redundant, as I
claimed when deleting it: a held lock cannot expire, a lease can. The exclusion
has to be held deliberately rather than assumed to outlast the work it guards.

_held_teardown_lease wraps both _backend.destroy() call sites and re-claims the
marker every renewal_interval_seconds until the stop returns. No store change is
needed: claim(for_destroy=True) already refreshes an existing del: marker on
both backends.

Reachable without an abnormal backend. The schema bounds only
renewal_interval_seconds (> 0) and ttl_multiplier (>= 2), so a legal config puts
the TTL below a normal container stop; and LocalContainerBackend._stop_container
passes no timeout to subprocess.run, so a wedged daemon blocks unbounded even at
the default 120s TTL.

The TTL stays finite on purpose: the heartbeat dies with the process, so a
destroyer that crashes mid-stop still releases the container one TTL later
instead of marking it undestroyable forever.

* fix(sandbox): hold the teardown lease on every del: stop, and pin the claims that had no test

90936b49 said `_held_teardown_lease` wrapped "both" `_backend.destroy()` call
sites. There are three. `_drop_unhealthy_sandbox` marks `del:` and then blocks on
the same unbounded stop, and it untracks *before* claiming, so `_renew_owned_leases`
cannot see the id either — nothing refreshed the marker. Reproduced against a real
redis: the peer's `take()` succeeds 1.0s into a 2.5s stop. Same window, third path.

That miss came from the habit the rest of this commit addresses: a property
asserted in prose, with no test that could falsify it. Auditing every load-bearing
claim in this feature — AGENTS.md, the store docstrings, the provider's design
comments — against the test that would go red turned up several more, each
verified by mutating the code and watching the suite stay green.

Tests that could not fail:

- `test_reconcile_fails_closed_when_ownership_unknown` reached the grace gate, not
  the claim. A bare MagicMock answers `owner()` with a truthy mock, so the
  container read as peer-owned and deferred; `claim()` was never called. It stayed
  green with `_claim_ownership` failing open. Adding the grace ahead of the claim
  is what hollowed it out — inserting a gate can silently disarm the tests for
  the gate behind it.
- `test_adoption_grace_restarts_when_a_live_owner_republishes` never distinguished
  reset from pause. Those diverge only on a *second* lapse, which it never drove,
  so it passed with the reset deleted.

Claims with no test at all, each now pinned (mutation → red, per test):

- `destroy()`, `_evict_oldest_warm`, `_reclaim_warm_pool_sandbox`,
  `_register_created_sandbox` and `shutdown()`'s warm loop were each the one
  untested sibling of an "every path does X" enumeration. `shutdown()` was never
  driven with a non-empty warm pool, so a loop bypassing the ownership claim —
  stopping a live peer's container on our exit — went unnoticed.
- Renewal's unknown-is-not-lost rule, the single deliberate exception to
  fail-closed. Inverting it drops every active and warm sandbox on every instance
  the moment the store blinks.
- Both hops of the stream-bridge redis inference. Deleting either left the suite
  green while every config.yaml-native multi-instance deployment silently fell
  back to memory — #4206 reopened on exactly the deployments the inference exists
  for.

Claims narrowed instead, because they promised more than the code delivers:

- "run against both backends ... cannot drift" — CI provisions no redis, so the
  merge gate runs the memory tier only and the Lua never executes there.
- "Every destroy path claims before untracking" — `_drop_unhealthy_sandbox`
  untracks first, deliberately, under its `expected_info` TOCTOU guard.
- "Atomic: concurrent claims from different instances cannot both succeed" — true
  via Lua on redis, vacuous on the single-instance memory store, and pinned by
  neither, since the contract suite drives sequential calls. A concurrency test
  against the memory store would make the claim look covered while the mechanism
  that carries it still never runs in CI.

* fix(sandbox): release the teardown marker when a destroy() stop fails

The three `del:`-marked stop paths disagreed on failure. `_destroy_warm_entry`
releases on both outcomes and says why: the stop failed, so the container is
probably still up, and a marker left behind refuses its own thread's `take()`
until the TTL lapses. `_drop_unhealthy_sandbox` does the same. `destroy()` had no
such guard — a raising backend propagated straight past `_release_ownership`, and
the thread could not re-acquire for a full TTL.

Fails safe rather than fatal: a stuck marker stops peers from touching the
container, it is not the cross-instance kill. But the paths must agree, and this
one is the odd one out.

Release, then re-raise. Swallowing would be the easier symmetry with
`_destroy_warm_entry`'s `return False`, but `destroy()` has no failure return and
`shutdown()` logs per sandbox off the exception, so swallowing would silently
narrow what callers can see.

Found by comparing the three paths after @fancyboi999 asked for release to be
handled "consistently with the other destroy paths" on the unhealthy path — which
0d2377b2 already does. This is the sibling that wasn't.

* fix(deploy): bump chart config_version to 27 for sandbox.ownership

config.example.yaml went to 27 with the new sandbox.ownership section, but
the chart embeds its own copy and stayed at 26, so validate-chart failed.

A bare bump: the chart already sets stream_bridge.type=redis, which is what
resolve_ownership_config infers a redis ownership store from, so no field
change is needed.

* fix(sandbox): release the teardown lease from its heartbeat, not the caller

`_held_teardown_lease` joined its heartbeat only briefly and the caller
cleared the `del:` marker right after the stop. A refresh `claim` still in
flight (`RedisOwnershipStore` had no socket timeout, so a round trip could
block) could land *after* that release and rewrite `del:` on a container
whose stop had already completed — refusing a fresh `take()` (or rolling
back a fresh create) until the TTL.

Move the release into the heartbeat's own `finally`, after its loop stops,
so no refresh can run after it. The three destroy paths no longer release
after the `with` (`destroy()`'s no-container branch still does, since no
lease was held there). Bound every store round trip with a socket timeout
so the in-flight refresh — and thus the deferred release — stays finite,
and broaden the heartbeat's `except` so an unexpected error cannot strand
the marker during a long stop.

Also fold in the review follow-ups: stop re-resolving an already-resolved
ownership config in the factory, document the Redis-outage-vs-TTL boundary
in config.example.yaml, and add a tests/blocking_io anchor pinning that
`release()`'s store round trip stays off the event loop.

* fix(sandbox): refuse a non-destroy claim that would unwind our own teardown

`claim(for_destroy=False)` against our own `del:` lease fell through and
overwrote it with `own:`, cancelling a teardown that was already in flight.
The container stop cannot be recalled, so downgrading the marker would let a
`take()` hand out a container that is about to die -- #4206, self-inflicted.

No caller does this today: the two non-destroy callers run against an absent
key (the LAPSED re-claim) or an unowned one (post-grace reconcile). The
contract has to forbid it rather than rely on that staying true.

Fixed in both backends. The redis rule lives in Lua and the memory rule in
Python, so fixing one only would let them drift silently -- and the shared
contract suite is what is supposed to catch that drift, so it now covers this.

Also adds a contention test for `claim`. The suite drove sequential calls
only, so it pinned the exclusion predicate but not the atomicity that
predicate depends on; eight instances now race for one container and exactly
one must win.

* fix(sandbox): bound the container stop so it cannot outlive its teardown lease

`_stop_container` passed no `timeout` to `subprocess.run`, so a wedged
container runtime blocks it forever. The `del:` marker is what keeps a peer
from re-acquiring the container while the stop runs, but a marker is a lease
and a lease can lapse: a store outage longer than the TTL frees it, a peer's
`take()` succeeds against the still-running container, and the stop then
lands on the turn that was just handed it -- the exact #4206 failure.

The teardown heartbeat already covers the case where the store stays
reachable. This bounds the worst case independently of the ownership layer,
which is the point: it holds even when the ownership layer is the thing that
failed.

A timeout is not swallowed like a `CalledProcessError`. That error means the
runtime answered "I could not stop it"; a timeout means we do not know, and
the container is probably still running -- returning normally would let
`_destroy_warm_entry` report a clean stop and drop the warm entry, leaking a
running container nothing tracks.

* fix(sandbox): exclude this instance's own reapers from its acquire path

An ownership lease excludes peers and nothing else. `claim()` and `take()`
both succeed against our own `own:` lease by design -- that is what lets a
destroy path claim what it already owns -- so `del:` says nothing to this
process's other threads. Meanwhile every reaper decides outside `_lock`,
because a store round trip must not be held under the lock that guards every
acquire. So each reaper acts on a decision its own acquire path may already
have invalidated, and the store cannot see the difference.

Six paths end in an irreversible act (a container stop, or closing a
host-side client) on a decision made outside the lock. All six reproduce:

  _evict_oldest_warm      re-checks warm membership, then releases the lock
  _reap_expired_warm      no re-check at all
  _cleanup_idle_sandboxes re-verifies idle, then releases the lock
  _renew_owned_leases     acts on a stale renew() -> LOST
  release()               same staleness on its own refresh
  _drop_unhealthy_sandbox untracks before claiming, opening discovery

Both warm reapers are a regression from the deferred pop this branch
introduced: `WarmPoolLifecycleMixin` popped under the lock, so a reclaim's
membership check failed and the race could not occur. Deferring the pop is
still right (popping first loses the container on a refused claim), so the
exclusion has to be made explicit instead. The idle path is pre-existing in
shape, but this branch widened it from a few instructions to a network round
trip by claiming ownership before untracking.

Two guards, because the two directions want opposite answers:

Reaping -- nothing may promote it. The reaper reserves the id, and every
promote path refuses a reserved id exactly as it refuses a peer's `del:`
(drop and cold-start). The "is this still reapable?" test travels with the
reservation as a predicate and runs in the same critical section, because
checking first and reserving second is the window, not a narrower version of
it.

Forgetting -- the peer legitimately wins, so the promote is what to detect.
`_publish_ownership` bumps a per-id acquire epoch; the callers that decide
from a store round trip snapshot it first, and the pop is skipped if it
moved. Object identity cannot substitute: the reuse path re-publishes
ownership while handing out the same tracked `AioSandbox`, so an identity
check sees nothing and the pop closes a client mid-turn.

`still_reapable` is required rather than defaulting to unconditional -- the
safe default is the one that makes a new call site think about it. That
diverges from the mixin hook, which is safe because this provider overrides
both mixin callers, and loud rather than silent if those are ever dropped.

Also closes a client leak on the discover path: "nothing to roll back" was
true of the container but not of the HTTP client constructed before the
publish, which the sibling create path already closes.

The shared-store test view rebound `owner_id` outside the store's lock, so a
concurrent claim could execute under the wrong id and read its own lease as a
peer's. Serialized, so the heartbeat-hold tests stop flaking.

* fix(sandbox): mark acquire intent before the ownership round trip

A guard must become visible no later than the transition it guards. The
acquire epoch cannot manage that for `take()`: the takeover is durable before
`take()` returns -- redis has committed the SET while the reply is still in
flight -- and the epoch can only be written afterwards. In that interval the
store already says the container is ours while the epoch still reads as it
did when a renewal decided `LOST`, so the stale forget walks through, drops
the maps and closes the client the acquire is about to hand back. Acquire
then returns an id the provider no longer tracks and `get()` answers `None`
for the rest of the turn.

`_publish_ownership` now publishes an intent mark under `_lock` before the
round trip; the epoch keeps covering the other half, "an acquire completed
since you decided". `_forget_lost_sandbox` honours the intent mark
unconditionally rather than only when an epoch is supplied -- today's
epoch-less callers cannot reach the window, but "no epoch" reading as "no
guard" is how the next caller of a dangerous primitive gets written.

The same invariant had four more instances, all reproduced:

  reuse returns a decision the forget already invalidated -- before the mark
    is set a `LOST` is both current and correct, so the forget legitimately
    runs and the entry reuse decided to hand out is gone. Re-check after
    publishing and fall through to discovery instead.
  reclaim installs an entry a reaper reserved after its check -- the warm
    entry is still visible during the stop, and the reaper's claim succeeds
    because reclaim's own take() just made the lease ours. Re-check likewise.
  the reservation was released before the entry was removed -- the pop
    belonged to the caller, leaving a gap where the container is stopped, the
    entry is still in `_warm_pool`, and nothing marks it.
    `_destroy_warm_entry` removes it itself, inside the reservation; the pop
    stays deferred relative to the stop, just not to the reservation.
  reconcile adopts a container this instance is tearing down -- adoption is a
    promote and needs the same reservation check as the others. Neither
    existing guard excludes it: the claim succeeds because the lease is ours,
    and on `memory` the recovery grace is skipped outright.

The pre-round-trip checks in reuse and reclaim are kept as early-outs, since
they skip a health check and a store round trip on a doomed entry, and are
pinned to that job rather than to a correctness role they no longer hold.

The teardown reservation predicate runs under `_lock`, so it must not touch
the lock. Documented rather than engineered around: making the lock reentrant
to tolerate it would trade a loud hang for a quiet class of re-entrancy bugs
across the rest of the provider.

* fix(sandbox): honor local teardown after ownership publish

* fix(sandbox): clear a stale warm entry when an id becomes active

Active and warm are exclusive states, and the two register paths were the
only place that could hold both: they inserted into `_sandboxes` without
popping `_warm_pool`, so one container ended up with two reapers.
`_reap_expired_warm` judges an entry by its warm timestamp and never
consults `_last_activity`, so it stops a container an agent is actively
using while `_sandboxes` still hands out its client.

Reachable because `_reconcile_orphans` adopts an untracked-but-running
container into the warm pool inside the register's publish -> track
window, and on the `memory` store it adopts on sight:
`_adoptable_after_grace` short-circuits when `supports_cross_process` is
False, so an id carrying this process's own lease reads as adoptable.
That window is new to this branch -- on main the track was a single
locked insert with nothing before it.

Both register paths now pop the warm entry inside the same locked
section that installs the active one.

* fix(sandbox): harden ownership renewal teardown

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-21 09:09:40 +08:00
2026-06-19 22:48:30 +08:00
2026-02-13 11:49:51 +08:00
2026-07-20 23:56:37 +08:00

🦌 DeerFlow - 2.0

English | 中文 | 日本語 | Français | Русский

Python Node.js License: MIT

bytedance%2Fdeer-flow | Trendshift

On February 28th, 2026, DeerFlow claimed the 🏆 #1 spot on GitHub Trending following the launch of version 2. Thanks a million to our incredible community — you made this happen! 💪🔥

DeerFlow (Deep Exploration and Efficient Research Flow) is an open-source super agent harness that orchestrates sub-agents, memory, and sandboxes to do almost anything — powered by extensible skills.

https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18

Note

DeerFlow 2.0 is a ground-up rewrite. It shares no code with v1. If you're looking for the original Deep Research framework, it's maintained on the 1.x branch — contributions there are still welcome. Active development has moved to 2.0.

Official Website

Learn more and see real demos on our official website.

Sister Projects

image
  • LLM Space - Meet our secret weapon behind DeerFlow — one desktop tool to prototype agent ideas, inspect each harness step, replay failures, and benchmark performance.

Coding Plan from ByteDance Volcengine

InfoQuest

DeerFlow has newly integrated the intelligent search and crawling toolset independently developed by BytePlus--InfoQuest (supports free online experience)

InfoQuest_banner

Table of Contents

One-Line Agent Setup

If you use Claude Code, Codex, Cursor, Windsurf, or another coding agent, you can hand it the setup instructions in one sentence:

Help me clone DeerFlow if needed, then bootstrap it for local development by following https://raw.githubusercontent.com/bytedance/deer-flow/main/Install.md

That prompt is intended for coding agents. It tells the agent to clone the repo if needed, choose Docker when available, and stop with the exact next command plus any missing config the user still needs to provide.

Quick Start

Configuration

  1. Clone the DeerFlow repository

    git clone https://github.com/bytedance/deer-flow.git
    cd deer-flow
    
  2. Run the setup wizard

    From the project root directory (deer-flow/), run:

    make setup
    

    This launches an interactive wizard that guides you through choosing an LLM provider, optional web search, and execution/safety preferences such as sandbox mode, bash access, and file-write tools. It generates a minimal config.yaml and writes your keys to .env. Takes about 2 minutes.

    The wizard also lets you configure an optional web search provider, or skip it for now.

    Run make doctor at any time to verify your setup and get actionable fix hints. If you are opening a GitHub issue about a local setup or runtime problem, run make support-bundle. The command prints reporter next steps, writes a *-issue-summary.md file to paste into the issue, a *-issue-draft.md file for AI-assisted issue filing, and an optional evidence zip under .deer-flow/support-bundles/. If an AI assistant files the issue, start from the draft and replace every REQUIRED placeholder instead of inventing missing facts. Attach the zip only if a maintainer asks for it, or if the summary alone is not enough. Maintainers and AI triage tools can start with triage.json; the bundle includes redacted diagnostics and file manifests only, and does not include .env, raw conversation messages, or user file contents.

    Advanced / manual configuration: If you prefer to edit config.yaml directly, run make config instead to copy the full template. See config.example.yaml for the complete reference including CLI-backed providers (Codex CLI, Claude Code OAuth), OpenRouter, Responses API, subagent runtime caps such as subagents.max_total_per_run, and more.

    Manual model configuration examples
    models:
      - name: gpt-4o
        display_name: GPT-4o
        use: langchain_openai:ChatOpenAI
        model: gpt-4o
        api_key: $OPENAI_API_KEY
    
      - name: openrouter-gemini-2.5-flash
        display_name: Gemini 2.5 Flash (OpenRouter)
        use: langchain_openai:ChatOpenAI
        model: google/gemini-2.5-flash-preview
        api_key: $OPENROUTER_API_KEY
        base_url: https://openrouter.ai/api/v1
    
      - name: gpt-5-responses
        display_name: GPT-5 (Responses API)
        use: langchain_openai:ChatOpenAI
        model: gpt-5
        api_key: $OPENAI_API_KEY
        use_responses_api: true
        output_version: responses/v1
    
      - name: qwen3-32b-vllm
        display_name: Qwen3 32B (vLLM)
        use: deerflow.models.vllm_provider:VllmChatModel
        model: Qwen/Qwen3-32B
        api_key: $VLLM_API_KEY
        base_url: http://localhost:8000/v1
        supports_thinking: true
        when_thinking_enabled:
          extra_body:
            chat_template_kwargs:
              enable_thinking: true
    

    OpenRouter and similar OpenAI-compatible gateways should be configured with langchain_openai:ChatOpenAI plus base_url. If you prefer a provider-specific environment variable name, point api_key at that variable explicitly (for example api_key: $OPENROUTER_API_KEY).

    To route OpenAI models through /v1/responses, keep using langchain_openai:ChatOpenAI and set use_responses_api: true with output_version: responses/v1.

    For vLLM 0.19.0, use deerflow.models.vllm_provider:VllmChatModel. For Qwen-style reasoning models, DeerFlow toggles reasoning with extra_body.chat_template_kwargs.enable_thinking and preserves vLLM's non-standard reasoning field across multi-turn tool-call conversations. Legacy thinking configs are normalized automatically for backward compatibility. Reasoning models may also require the server to be started with --reasoning-parser .... If your local vLLM deployment accepts any non-empty API key, you can still set VLLM_API_KEY to a placeholder value.

    CLI-backed provider examples:

    models:
      - name: gpt-5.4
        display_name: GPT-5.4 (Codex CLI)
        use: deerflow.models.openai_codex_provider:CodexChatModel
        model: gpt-5.4
        supports_thinking: true
        supports_reasoning_effort: true
    
      - name: claude-sonnet-4.6
        display_name: Claude Sonnet 4.6 (Claude Code OAuth)
        use: deerflow.models.claude_provider:ClaudeChatModel
        model: claude-sonnet-4-6
        max_tokens: 4096
        supports_thinking: true
    
    • Codex CLI reads ~/.codex/auth.json
    • Claude Code accepts CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_CREDENTIALS_PATH, or ~/.claude/.credentials.json
    • ACP agent entries are separate from model providers — if you configure acp_agents.codex, point it at a Codex ACP adapter such as npx -y @zed-industries/codex-acp
    • On macOS, export Claude Code auth explicitly if needed:
    eval "$(python3 scripts/export_claude_code_oauth.py --print-export)"
    

    API keys can also be set manually in .env (recommended) or exported in your shell:

    OPENAI_API_KEY=your-openai-api-key
    TAVILY_API_KEY=your-tavily-api-key
    

Running the Application

Deployment Sizing

Use the table below as a practical starting point when choosing how to run DeerFlow:

Deployment target Starting point Recommended Notes
Local evaluation / make dev 4 vCPU, 8 GB RAM, 20 GB free SSD 8 vCPU, 16 GB RAM Good for one developer or one light session with hosted model APIs. 2 vCPU / 4 GB is usually not enough.
Docker development / make docker-start 4 vCPU, 8 GB RAM, 25 GB free SSD 8 vCPU, 16 GB RAM Image builds, bind mounts, and sandbox containers need more headroom than pure local dev.
Long-running server / make up 8 vCPU, 16 GB RAM, 40 GB free SSD 16 vCPU, 32 GB RAM Preferred for shared use, multi-agent runs, report generation, or heavier sandbox workloads.
  • These numbers cover DeerFlow itself. If you also host a local LLM, size that service separately.
  • Linux plus Docker is the recommended deployment target for a persistent server. macOS and Windows are best treated as development or evaluation environments.
  • If CPU or memory usage stays pinned, reduce concurrent runs first, then move to the next sizing tier.

Development (hot-reload, source mounts):

make docker-init    # Pull sandbox image (only once or when image updates)
make docker-start   # Start services (auto-detects sandbox mode from config.yaml)

make docker-start starts provisioner only when config.yaml uses provisioner mode (sandbox.use: deerflow.community.aio_sandbox:AioSandboxProvider with provisioner_url).

Docker builds use the upstream uv registry by default. If you need faster mirrors in restricted networks, export UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple and NPM_REGISTRY=https://registry.npmmirror.com before running make docker-init or make docker-start.

Backend processes automatically pick up config.yaml changes on the next config access, so model metadata updates do not require a manual restart during development.

Tip

On Linux, if Docker-based commands fail with permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, add your user to the docker group and re-login before retrying. See CONTRIBUTING.md for the full fix.

Production (builds images locally, mounts runtime config and data):

make up     # Build images and start all production services
make down   # Stop and remove containers

Access: http://localhost:2026

For persistent deployments, configure database.backend as sqlite or postgres. The selected backend is shared by the LangGraph checkpointer, LangGraph Store, and DeerFlow application data. The deprecated checkpointer section, when present, overrides the first two for backward compatibility.

The unified nginx endpoint is same-origin by default and does not emit browser CORS headers. If you run a split-origin or port-forwarded browser client, set GATEWAY_CORS_ORIGINS to comma-separated exact origins such as http://localhost:3000; the Gateway then applies the CORS allowlist and matching CSRF origin checks.

Browser login uses HttpOnly session cookies. The login page offers a "keep me signed in" option that extends the browser session when the request is HTTPS (including trusted X-Forwarded-Proto: https) or localhost HTTP. The localhost exception uses the direct request Host and ignores forwarded host headers. Public HTTP deployments, including many temporary sandbox URLs, fall back to session cookies by default. DeerFlow never stores the password in browser storage; the UI may remember only the email address.

DeerFlow still uses Forwarded / X-Forwarded-* headers to recover the browser-facing scheme and origin behind a proxy. The bundled nginx sets X-Forwarded-Proto, but preserves an upstream HTTPS value and does not overwrite every forwarded header. Configure the outer trusted proxy to replace or strip client-supplied forwarding headers before traffic reaches DeerFlow.

Important

The Gateway still owns active run tasks in process, so production defaults to a single Gateway worker (GATEWAY_WORKERS=1). The Redis stream bridge (stream_bridge.type: redis) shares SSE delivery and Last-Event-ID replay across workers, with a rolling retained-buffer TTL (stream_ttl_seconds) as a cleanup safety net. Malformed reconnect IDs live-tail new events instead of replaying the retained buffer. It does not make run cancellation, request de-duplication, or IM channel state fully cross-worker by itself; use single-worker Gateway or explicit sticky routing/ownership before raising GATEWAY_WORKERS.

See CONTRIBUTING.md for detailed Docker development guide.

Option 2: Local Development

If you prefer running services locally:

Prerequisite: complete the "Configuration" steps above first (make setup). make dev requires a valid config.yaml in the project root. Set DEER_FLOW_PROJECT_ROOT to define that root explicitly, or DEER_FLOW_CONFIG_PATH to point at a specific config file. Runtime state defaults to .deer-flow under the project root and can be moved with DEER_FLOW_HOME; skills default to skills/ under the project root and can be moved with DEER_FLOW_SKILLS_PATH. Run make doctor to verify your setup before starting. On Windows, run the local development flow from Git Bash. Native cmd.exe and PowerShell shells are not supported for the bash-based service scripts, and WSL is not guaranteed because some scripts rely on Git for Windows utilities such as cygpath.

  1. Check prerequisites:

    make check  # Verifies Node.js 22+, pnpm, uv, nginx
    
  2. Install dependencies:

    make install  # Install backend + frontend dependencies + pre-commit hooks
    
  3. (Optional) Pre-pull sandbox image:

    # Recommended if using Docker/Container-based sandbox
    make setup-sandbox
    
  4. (Optional) Load sample memory data for local review:

    python scripts/load_memory_sample.py
    

    This copies the sample fixture into the default local runtime memory file so reviewers can immediately test Settings > Memory. See backend/docs/MEMORY_SETTINGS_REVIEW.md for the shortest review flow.

  5. Start services:

    make dev
    
  6. Access: http://localhost:2026

Startup Modes

DeerFlow runs the agent runtime inside the Gateway API. Development mode enables hot-reload; production mode uses a pre-built frontend.

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

Gateway owns /api/langgraph/* and translates those public LangGraph-compatible paths to its native /api/* routers behind nginx.

Docker Production Deployment

deploy.sh supports building and starting separately:

# One-step (build + start)
deploy.sh

# Two-step (build once, start later)
deploy.sh build              # build all images
deploy.sh start              # start pre-built images

# Stop
deploy.sh down

Advanced

Sandbox Mode

DeerFlow supports multiple sandbox execution modes:

  • Local Execution (runs sandbox code directly on the host machine)
  • Docker Execution (runs sandbox code in isolated Docker containers)
  • Docker Execution with Kubernetes (runs sandbox code in Kubernetes pods via provisioner service)

For Docker development, service startup follows config.yaml sandbox mode. In Local/Docker modes, provisioner is not started.

See the Sandbox Configuration Guide to configure your preferred mode.

MCP Server

DeerFlow supports configurable MCP servers and skills to extend its capabilities. For HTTP/SSE MCP servers, OAuth token flows are supported (client_credentials, refresh_token). For stdio MCP servers, per-tool call timeouts can be configured with tool_call_timeout. MCP routing hints can also prefer a specific MCP tool for matching requests without forbidding other tools. When tool_search defers MCP schemas, matching routing metadata can auto-promote up to tool_search.auto_promote_top_k deferred schemas before the model call. See the MCP Server Guide for detailed instructions.

IM Channels

DeerFlow supports receiving tasks from messaging apps. Channels auto-start when configured — no public IP required for any of them.

DeerFlow can also expose user-owned IM channel connections in the workspace UI. When channel_connections is enabled, logged-in users can bind Telegram, Slack, Discord, Feishu/Lark, DingTalk, WeChat, or WeCom from the sidebar / Settings > Channels. It reuses the existing outbound channels.* transports, so no public IP or provider callback URL is required. Incoming IM messages then run under the connected DeerFlow user account. See IM Channel Connections for setup and security notes.

Channel Transport Difficulty
Telegram Bot API (long-polling) Easy
Slack Socket Mode Moderate
Feishu / Lark WebSocket Moderate
WeChat Tencent iLink (long-polling) Moderate
WeCom WebSocket Moderate
DingTalk Stream Push (WebSocket) Moderate

Configuration in config.yaml:

channels:
  # LangGraph-compatible Gateway API base URL (default: http://localhost:8001/api)
  langgraph_url: http://localhost:8001/api
  # Gateway API URL (default: http://localhost:8001)
  gateway_url: http://localhost:8001

  # Optional: global session defaults for all mobile channels
  session:
    assistant_id: lead_agent  # or a custom agent name; custom agents are routed via lead_agent + agent_name
    config:
      recursion_limit: 100
    context:
      thinking_enabled: true
      is_plan_mode: false
      subagent_enabled: false

  feishu:
    enabled: true
    app_id: $FEISHU_APP_ID
    app_secret: $FEISHU_APP_SECRET
    # domain: https://open.feishu.cn       # China (default)
    # domain: https://open.larksuite.com   # International

  wecom:
    enabled: true
    bot_id: $WECOM_BOT_ID
    bot_secret: $WECOM_BOT_SECRET

  slack:
    enabled: true
    bot_token: $SLACK_BOT_TOKEN     # xoxb-...
    app_token: $SLACK_APP_TOKEN     # xapp-... (Socket Mode)
    allowed_users: []               # empty = allow all

  telegram:
    enabled: true
    bot_token: $TELEGRAM_BOT_TOKEN
    allowed_users: []               # empty = allow all

  wechat:
    enabled: false
    bot_token: $WECHAT_BOT_TOKEN
    ilink_bot_id: $WECHAT_ILINK_BOT_ID
    qrcode_login_enabled: true      # optional: allow first-time QR bootstrap when bot_token is absent
    allowed_users: []               # empty = allow all
    polling_timeout: 35             # timing values must be positive finite seconds
    polling_retry_delay: 5
    qrcode_poll_interval: 2
    qrcode_poll_timeout: 180
    state_dir: ./.deer-flow/wechat/state
    max_inbound_image_bytes: 20971520
    max_outbound_image_bytes: 20971520
    max_inbound_file_bytes: 52428800
    max_outbound_file_bytes: 52428800

    # Optional: per-channel / per-user session settings
    session:
      assistant_id: mobile-agent  # custom agent names are also supported here
      context:
        thinking_enabled: false
      users:
        "123456789":
          assistant_id: vip-agent
          config:
            recursion_limit: 150
          context:
            thinking_enabled: true
            subagent_enabled: true

  dingtalk:
    enabled: true
    client_id: $DINGTALK_CLIENT_ID             # Client ID of your DingTalk application
    client_secret: $DINGTALK_CLIENT_SECRET     # Client Secret of your DingTalk application
    allowed_users: []                          # empty = allow all
    card_template_id: ""                       # Optional: AI Card template ID for streaming typewriter effect

Notes:

  • assistant_id: lead_agent calls the default LangGraph assistant directly.
  • If assistant_id is set to a custom agent name, DeerFlow still routes through lead_agent and injects that value as agent_name, so the custom agent's SOUL/config takes effect for IM channels.
  • IM channel workers call Gateway's LangGraph-compatible API internally and automatically attach process-local internal auth plus the CSRF cookie/header pair required for thread and run creation.
  • Feishu/Lark now queues rapid follow-up messages per mapped DeerFlow thread_id instead of immediately surfacing the generic busy reply, and topic replies keep a per-message card with a compact source-message preview across queued/running/final patches.

Set the corresponding API keys in your .env file:

# Telegram
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxYZ

# Slack
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...

# Feishu / Lark
FEISHU_APP_ID=cli_xxxx
FEISHU_APP_SECRET=your_app_secret

# WeChat iLink
WECHAT_BOT_TOKEN=your_ilink_bot_token
WECHAT_ILINK_BOT_ID=your_ilink_bot_id

# WeCom
WECOM_BOT_ID=your_bot_id
WECOM_BOT_SECRET=your_bot_secret

# DingTalk
DINGTALK_CLIENT_ID=your_client_id
DINGTALK_CLIENT_SECRET=your_client_secret

Telegram Setup

  1. Chat with @BotFather, send /newbot, and copy the HTTP API token.
  2. Set TELEGRAM_BOT_TOKEN in .env and enable the channel in config.yaml.

Slack Setup

  1. Create a Slack App at api.slack.com/apps → Create New App → From scratch.
  2. Under OAuth & Permissions, add Bot Token Scopes: app_mentions:read, chat:write, im:history, im:read, im:write, files:write.
  3. Enable Socket Mode → generate an App-Level Token (xapp-…) with connections:write scope.
  4. Under Event Subscriptions, subscribe to bot events: app_mention, message.im.
  5. Set SLACK_BOT_TOKEN and SLACK_APP_TOKEN in .env and enable the channel in config.yaml.

Feishu / Lark Setup

  1. Create an app on Feishu Open Platform → enable Bot capability.
  2. Add permissions: im:message, im:message.p2p_msg:readonly, im:resource.
  3. Under Events, subscribe to im.message.receive_v1 and select Long Connection mode.
  4. Copy the App ID and App Secret. Set FEISHU_APP_ID and FEISHU_APP_SECRET in .env and enable the channel in config.yaml.

WeChat Setup

  1. Enable the wechat channel in config.yaml.
  2. Either set WECHAT_BOT_TOKEN in .env, or set qrcode_login_enabled: true for first-time QR bootstrap.
  3. When bot_token is absent and QR bootstrap is enabled, watch backend logs for the QR content returned by iLink and complete the binding flow.
  4. After the QR flow succeeds, DeerFlow persists the acquired token under state_dir for later restarts.
  5. For Docker Compose deployments, keep state_dir on a persistent volume so the get_updates_buf cursor and saved auth state survive restarts.

WeCom Setup

  1. Create a bot on the WeCom AI Bot platform and obtain the bot_id and bot_secret.
  2. Enable channels.wecom in config.yaml and fill in bot_id / bot_secret.
  3. Set WECOM_BOT_ID and WECOM_BOT_SECRET in .env.
  4. Make sure backend dependencies include wecom-aibot-python-sdk. The channel uses a WebSocket long connection and does not require a public callback URL.
  5. The current integration supports inbound text, image, and file messages. Final images/files generated by the agent are also sent back to the WeCom conversation.

DingTalk Setup

  1. Create a DingTalk application in the DingTalk Developer Console and enable Robot capability.
  2. Set the message receiving mode to Stream Mode in the robot configuration page.
  3. Copy the Client ID and Client Secret, set DINGTALK_CLIENT_ID and DINGTALK_CLIENT_SECRET in .env, and enable the channel in config.yaml.
  4. (Optional) To enable streaming AI Card replies (typewriter effect), create an AI Card template on the DingTalk Card Platform, then set card_template_id in config.yaml to the template ID. You also need to apply for the Card.Streaming.Write and Card.Instance.Write permissions.

When DeerFlow runs in Docker Compose, IM channels execute inside the gateway container. In that case, do not point channels.langgraph_url or channels.gateway_url at localhost; use container service names such as http://gateway:8001/api and http://gateway:8001, or set DEER_FLOW_CHANNELS_LANGGRAPH_URL and DEER_FLOW_CHANNELS_GATEWAY_URL.

Commands

Once a channel is connected, you can interact with DeerFlow directly from the chat:

Command Description
/new Start a new conversation
/status Show current thread info
/models List available models
/memory View memory
/help Show help

Messages without a command prefix are treated as regular chat — DeerFlow creates a thread and responds conversationally.

Request Trace Correlation

Gateway request trace correlation is disabled by default so existing HTTP responses and log formats stay unchanged. To enable it, set:

logging:
  enhance:
    enabled: true
    format: text

When enabled, every Gateway HTTP response includes X-Trace-Id, logs include trace_id, and Langfuse traces created by that request include metadata.deerflow_trace_id with the same value.

LangSmith Tracing

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

Add the following to your .env file:

LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=lsv2_pt_xxxxxxxxxxxxxxxx
LANGSMITH_PROJECT=xxx

Langfuse Tracing

DeerFlow also supports Langfuse observability for LangChain-compatible runs.

Add the following to your .env file:

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

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

Trace correlation fields. Every agent run is annotated with Langfuse's reserved trace attributes so the Sessions and Users pages light up automatically:

  • session_id = LangGraph thread_id — groups every trace of the same conversation
  • user_id = effective user from get_effective_user_id() (falls back to default in no-auth mode)
  • trace_name = assistant id (defaults to lead-agent)
  • tags = [env:<DEER_FLOW_ENV>, model:<model_name>] (omitted when not set)
  • metadata.deerflow_trace_id = DeerFlow request correlation id, matching X-Trace-Id when request trace correlation is enabled

These are injected into RunnableConfig.metadata at the graph invocation root for both the gateway path (runtime/runs/worker.py::run_agent) and the embedded path (client.py::DeerFlowClient.stream), so any LangChain-compatible callback can read them. Set DEER_FLOW_ENV (or ENVIRONMENT) to tag traces by deployment environment.

Monocle Tracing

DeerFlow also supports Monocle, an OpenTelemetry-based tracer for agentic applications. It records each run end-to-end: LLM calls, agent steps, and tool and MCP invocations, with their inputs, outputs, timings, and token counts.

Add the following to your .env file:

MONOCLE_TRACING=true
MONOCLE_EXPORTERS=file          # file, console, okahu, s3, blob, gcs (default: file)
OKAHU_API_KEY=okh_xxxxxxxx      # required only for the `okahu` exporter

Each run writes one trace file to .monocle/; open it in the Monocle VS Code extension to inspect the span timeline and token counts. Connect to Okahu, an agent-observability platform, to analyze traces across runs and run trace-based and agentic evaluations (via the okahu exporter).

Traces capture span inputs and outputs verbatim — prompts, tool arguments, and model responses — plus token usage and timings. The file exporter keeps them on local disk and never rotates or cleans them up, so prune .monocle/ periodically; the remote exporters (okahu, s3, blob, gcs) send that same data off-box, so enable only destinations you trust. Monocle is initialized once at Gateway startup: a configuration error (unknown exporter, missing OKAHU_API_KEY) is logged there and tracing stays off until the Gateway restarts.

Using Multiple Providers

LangSmith and Langfuse attach as LangChain callbacks, so you can enable both and DeerFlow reports each run to both. If an enabled provider is missing required credentials or fails to initialize, DeerFlow fails fast and names it. Monocle uses a global OpenTelemetry provider rather than a callback; Langfuse shares that provider, so all three can run together. Because both span processors sit on the same shared provider, Monocle's exporters also see Langfuse's spans when both are enabled.

For Docker deployments, tracing is disabled by default. Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in your .env to enable it.

From Deep Research to Super Agent Harness

DeerFlow started as a Deep Research framework — and the community ran with it. Since launch, developers have pushed it far beyond research: building data pipelines, generating slide decks, spinning up dashboards, automating content workflows. Things we never anticipated.

That told us something important: DeerFlow wasn't just a research tool. It was a harness — a runtime that gives agents the infrastructure to actually get work done.

So we rebuilt it from scratch.

DeerFlow 2.0 is no longer a framework you wire together. It's a super agent harness — batteries included, fully extensible. Built on LangGraph and LangChain, it ships with everything an agent needs out of the box: a filesystem, memory, skills, sandbox-aware execution, and the ability to plan and spawn sub-agents for complex, multi-step tasks.

Use it as-is. Or tear it apart and make it yours.

Core Features

Skills & Tools

Skills are what make DeerFlow do almost anything.

A standard Agent Skill is a structured capability module — a Markdown file that defines a workflow, best practices, and references to supporting resources. DeerFlow ships with built-in skills for research, report generation, slide creation, web pages, image and video generation, and more. But the real power is extensibility: add your own skills, replace the built-in ones, or combine them into compound workflows.

Skills are loaded progressively — only when the task needs them, not all at once. This keeps the context window lean and makes DeerFlow work well even with token-sensitive models.

A skill directory is a package boundary: once DeerFlow finds its SKILL.md, nested SKILL.md files under that package (for example evaluation fixtures) remain supporting data and are not registered as runtime skills. Namespace directories without their own SKILL.md can still group nested skills.

Users can explicitly activate an enabled skill for a single turn by starting the request with /skill-name, for example /data-analysis analyze uploads/foo.csv. DeerFlow loads that skill's SKILL.md as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as /new and /help.

An enabled skill's allowed-tools policy applies only after that skill is explicitly slash-activated or captured in the thread's active skill context after a read_file load. Merely enabling, advertising, or listing a skill in a custom agent's skills allowlist does not reduce the lead agent's normal toolset. During a slash-activated run, that explicit skill's policy is authoritative: reading another SKILL.md may provide instructions but cannot widen the slash skill's tools. Without slash activation, policies from skills actually loaded into active context retain their union semantics. Once active, the policy filters both model-visible tool schemas and tool execution. Framework discovery tools (tool_search and describe_skill) remain available so an allowed deferred tool or installed skill can still be discovered, but discovery and promotion never grant permission to execute a business tool omitted from allowed-tools. task is not framework-exempt; a restrictive skill must list it explicitly to delegate to a subagent. Per-step policy decisions are internal runtime context and are removed from observable or persisted context copies. Registry failures and an active set with no remaining valid skill fail closed to framework-safe tools; individual stale paths are ignored only when another valid active skill remains. This is best-effort behavioral scoping, not a hard security boundary: loading skill instructions through another tool is not captured, and active-skill entries can be evicted from bounded context.

When you install .skill archives through the Gateway, DeerFlow accepts standard optional frontmatter metadata such as version, author, and compatibility instead of rejecting otherwise valid external skills.

If a trusted operator manages the configured skills directory through an external mount such as MinIO, NFS, or CSI, an administrator can call POST /api/skills/reload after changing files. This invalidates skill prompt caches for the current Gateway process and waits up to the bounded refresh timeout so subsequent runs rescan the latest files; running tasks are unchanged. A loader-level filesystem failure returns a generic server error and preserves the last successfully loaded process cache rather than publishing an empty catalog. Uvicorn workers and Kubernetes Pods must each be targeted separately. Direct mount writes bypass the validation, SkillScan, and history applied by DeerFlow's install/edit APIs, so only operator-controlled systems should have write access.

Skill installs and agent-managed skill edits run through SkillScan, a native deterministic safety scanner before the LLM-based skill scanner. Phase 1 runs offline with no Semgrep/OpenGrep dependency, blocks high-confidence CRITICAL findings such as private keys or shell execution, and passes warning findings to the LLM scanner for contextual review. Python instance-client exfiltration checks follow a minimal same-scope evidence chain: a simple name bound to a known client constructor, optional name-to-name aliases, and an actual outbound method or context-manager use supported by that constructor. Constructor roots must be proven imports; bare canonical-looking names are not inferred as modules. Nested scopes do not inherit client handles and inherit only constructor import aliases that are never rebound in the enclosing scope. Comprehensions, walrus-bearing statements, annotations, complex binding targets, unsupported operations, and ambiguous branch flows produce no finding from this signal; skipped constructs conservatively invalidate every name they may bind so stale client state cannot create a finding. A deterministic work budget or recursion limit reached by this best-effort analysis does not discard findings already collected for the file. Set skill_scan.enabled: false in config.yaml to disable only the deterministic analyzers; safe archive extraction and the LLM scanner still run.

DeerFlow also ships with skill-reviewer, a public skill for read-only skill quality review. It uses the built-in review_skill_package tool to inspect installed skills, local packages, archives, or pasted SKILL.md content without activating the target skill, binding its secrets, executing its scripts, or installing it. The tool returns a compact, tag-neutralized JSON payload to the model context and keeps the full raw review payload in the tool artifact for programmatic consumers. The deterministic review core reuses DeerFlow parsing and SkillScan facts, emits versioned JSON contracts under contracts/skill_review/, and can be run from the backend CLI:

cd backend
uv run python -m deerflow.skills.review.cli ../skills/public/data-analysis --format text --fail-on error --fail-on-incomplete

Tools follow the same philosophy. DeerFlow comes with a core toolset — web search, web fetch, rendered web capture, file operations, bash execution — and supports custom tools via MCP servers and Python functions. Swap anything. Add anything.

Gateway-generated follow-up suggestions now normalize both plain-string model output and block/list-style rich content before parsing the JSON array response, so provider-specific content wrappers do not silently drop suggestions.

The Web UI composer can polish draft input before sending. The rewrite runs as a short Gateway LLM request using the input_polish model configuration, keeps slash skill prefixes such as /data-analysis, and only replaces the local draft after the user clicks the polish button; it does not create a thread run or persist a message.

Unsent Web UI composer drafts survive page reloads and switching between conversations within the same browser tab. Drafts are isolated by user, agent, and conversation, include a selected slash skill when present, and are cleared once a send is accepted. Attachments and quoted conversation context are intentionally not persisted.

The Web UI composer also supports browser-based voice dictation when the browser exposes the Web Speech API. The microphone button transcribes speech into the local draft only; DeerFlow receives only the transcribed text, while audio handling is delegated to the browser or operating system speech-recognition service according to that environment's policy. Users can review or edit the text before sending.

Interrupted first-turn runs still persist a fallback conversation title, so stopping a streaming response does not leave the thread as "Untitled" after refresh.

In the Web UI, completed assistant turns can be branched into a new main conversation. The new thread starts from that turn's checkpoint. Because workspace files are not checkpointed, the branch only receives a best-effort copy of the current workspace when you branch from the latest turn; branching from an older turn keeps just the restored message history so the branch never inherits files that were created in a later part of the conversation.

Web UI chat links percent-encode custom thread identifiers before placing them in route segments, so reserved URL characters such as # and ? do not change which conversation is opened.

# Paths inside the sandbox container
/mnt/skills/public
├── research/SKILL.md
├── report-generation/SKILL.md
├── slide-creation/SKILL.md
├── web-page/SKILL.md
└── image-generation/SKILL.md

/mnt/skills/custom
└── your-custom-skill/SKILL.md      ← yours

Claude Code Integration

The claude-to-deerflow skill lets you interact with a running DeerFlow instance directly from Claude Code. Send research tasks, check status, manage threads — all without leaving the terminal.

Install the skill:

npx skills add https://github.com/bytedance/deer-flow --skill claude-to-deerflow

Then make sure DeerFlow is running (default at http://localhost:2026) and use the /claude-to-deerflow command in Claude Code.

What you can do:

  • Send messages to DeerFlow and get streaming responses
  • Choose execution modes: flash (fast), standard, pro (planning), ultra (sub-agents)
  • Check DeerFlow health, list models/skills/agents
  • Manage threads and conversation history
  • Upload files for analysis

Environment variables (optional, for custom endpoints):

DEERFLOW_URL=http://localhost:2026            # Unified proxy base URL
DEERFLOW_GATEWAY_URL=http://localhost:2026    # Gateway API
DEERFLOW_LANGGRAPH_URL=http://localhost:2026/api/langgraph  # LangGraph API

See skills/public/claude-to-deerflow/SKILL.md for the full API reference.

Session Goals

Use /goal <completion condition> to attach one active completion condition to the current thread. The goal is thread-scoped state, not a skill activation, so it stays active across turns until DeerFlow determines it has been satisfied or you clear it.

Supported commands:

/goal finish the implementation and make all tests pass
/goal              # show the active goal
/goal clear        # clear it

After each Gateway-backed run, DeerFlow evaluates the visible conversation against the active goal with a non-thinking evaluator model. The evaluator must return a typed blocker (missing_evidence, needs_user_input, run_failed, external_wait, or goal_not_met_yet) plus visible evidence. DeerFlow only injects a hidden continuation when the latest assistant turn is durably checkpointed, the blocker is goal_not_met_yet, the thread did not change during evaluation, and the no-progress breaker has not fired. The safety cap defaults to 8 hidden continuations, and repeated identical non-progress evaluations stop after 2 attempts. /goal clear and any user-authored new input win over queued continuations. When the goal is satisfied, DeerFlow clears it automatically and publishes the updated thread state.

The Web UI shows the active goal above the composer. The same command is available from the TUI and supported IM channels. In the Web UI and supported IM channels, setting /goal <completion condition> also starts a run with the condition as the task; status and clear commands only manage goal state.

Manual Context Compaction

Use /compact in the Web UI composer to summarize older context for the current thread. DeerFlow keeps the full chat visible, but future model calls use the compacted summary plus recent messages. The command is ignored when there is not enough history to compact, and it is blocked while the thread has a run in flight.

Sub-Agents

Complex tasks rarely fit in a single pass. DeerFlow decomposes them.

The lead agent can spawn sub-agents on the fly — each with its own scoped context, tools, and termination conditions. Sub-agents run in parallel when possible, report back structured results, and the lead agent synthesizes everything into a coherent output. Their internal AI and tool messages stay scoped to the delegated graph instead of entering the parent chat stream. Long-running sub-agents compact older history when summarization is enabled and re-inject the summary as guarded, hidden durable context before continuing, so recent assistant/tool activity remains grounded in the task. Provider/model request failures are reported as failed sub-agent tasks rather than successful results, so the lead agent and Web UI can react to them correctly. Collapsed sub-agent cards show the effective model and, when the provider returns usage metadata, a cumulative token total that updates after each completed sub-agent LLM call and persists after a reload. When token usage tracking is enabled, completed sub-agent usage is also attributed back to the dispatching step.

This is how DeerFlow handles tasks that take minutes to hours: a research task might fan out into a dozen sub-agents, each exploring a different angle, then converge into a single report — or a website — or a slide deck with generated visuals. One harness, many hands.

Sandbox & File System

DeerFlow doesn't just talk about doing things. It has its own computer.

Each task gets its own execution environment with a full filesystem view — skills, workspace, uploads, outputs. The agent reads, writes, and edits files. It can view images and, when configured safely, execute shell commands.

After each run, DeerFlow records a workspace change summary for the run-owned workspace and outputs directories. The Web UI shows a compact "files changed" badge on the assistant turn; opening it reveals created, modified, and deleted files with text diffs when safe to display. Uploads are excluded because they are user inputs, not agent-generated changes. Large, binary, or sensitive-looking files are shown as metadata only.

With AioSandboxProvider, shell execution runs inside isolated containers. With LocalSandboxProvider, file tools still map to per-thread directories on the host, but host bash is disabled by default because it is not a secure isolation boundary. Re-enable host bash only for fully trusted local workflows. Host bash commands have a wall-clock timeout, and long-lived processes should be started in the background with output redirected to a workspace log.

This is the difference between a chatbot with tool access and an agent with an actual execution environment.

# Paths inside the sandbox container
/mnt/user-data/
├── uploads/          ← your files
├── workspace/        ← agents' working directory
└── outputs/          ← final deliverables

Context Engineering

Isolated Sub-Agent Context: Each sub-agent runs in its own isolated context. This means that the sub-agent will not be able to see the context of the main agent or other sub-agents. This is important to ensure that the sub-agent is able to focus on the task at hand and not be distracted by the context of the main agent or other sub-agents.

Summarization: Within a session, DeerFlow manages context aggressively — summarizing completed sub-tasks, offloading intermediate results to the filesystem, compressing what's no longer immediately relevant. This lets it stay sharp across long, multi-step tasks without blowing the context window.

Strict Tool-Call Recovery: When a provider or middleware interrupts a tool-call loop, DeerFlow now strips provider-level raw tool-call metadata on forced-stop assistant messages and injects placeholder tool results for dangling calls before the next model invocation. This keeps OpenAI-compatible reasoning models that strictly validate tool_call_id sequences from failing with malformed history errors.

Visible Tool-Run Completion: For interactive turns, DeerFlow retries an empty post-tool final response once, then surfaces a visible error instead of reporting a silent successful run.

Long-Term Memory

Most agents forget everything the moment a conversation ends. DeerFlow remembers.

Across sessions, DeerFlow builds a persistent memory of your profile, preferences, and accumulated knowledge. The more you use it, the better it knows you — your writing style, your technical stack, your recurring workflows. Memory is stored locally and stays under your control.

Memory updates now skip duplicate fact entries at apply time, so repeated preferences and context do not accumulate endlessly across sessions.

DeerFlow is model-agnostic — it works with any LLM that implements the OpenAI-compatible API. That said, it performs best with models that support:

  • Long context windows (100k+ tokens) for deep research and multi-step tasks
  • Reasoning capabilities for adaptive planning and complex decomposition
  • Multimodal inputs for image understanding and video comprehension
  • Strong tool-use for reliable function calling and structured outputs

Embedded Python Client

DeerFlow can be used as an embedded Python library without running the full HTTP services. The DeerFlowClient provides direct in-process access to all agent and Gateway capabilities, returning the same response schemas as the HTTP Gateway API. The HTTP Gateway also exposes DELETE /api/threads/{thread_id} to remove DeerFlow-managed local thread data after the LangGraph thread itself has been deleted:

from deerflow.client import DeerFlowClient

client = DeerFlowClient()

# Chat
response = client.chat("Analyze this paper for me", thread_id="my-thread")

# Streaming (LangGraph SSE protocol: values, messages-tuple, end)
for event in client.stream("hello"):
    if event.type == "messages-tuple" and event.data.get("type") == "ai":
        print(event.data["content"])

# Configuration & management — returns Gateway-aligned dicts
models = client.list_models()        # {"models": [...]}
skills = client.list_skills()        # {"skills": [...]}
client.update_skill("web-search", enabled=True)
client.upload_files("thread-1", ["./report.pdf"])  # {"success": True, "files": [...]}
client.set_goal("thread-1", "finish the implementation and make all tests pass")
client.get_goal("thread-1")       # {"goal": {...}} or {"goal": None}
client.clear_goal("thread-1")

All dict-returning methods are validated against Gateway Pydantic response models in CI (TestGatewayConformance), ensuring the embedded client stays in sync with the HTTP API schemas. See backend/packages/harness/deerflow/client.py for full API documentation.

Scheduled Tasks

DeerFlow now includes a first-class scheduled-task MVP in the workspace.

Current MVP capabilities:

  • Manage tasks at /workspace/scheduled-tasks
  • Choose whether each scheduled task reuses a thread or creates a fresh thread per run
  • Support once and cron schedules
  • Run background scheduled executions as non-interactive DeerFlow runs (ask_clarification is not exposed there)
  • Use skip overlap behavior for due cron executions that collide with an active run on the same reused thread
  • Pause, resume, trigger, inspect history, and delete tasks
  • Execute scheduled work through the normal DeerFlow run lifecycle

Current MVP limits:

  • No conversation-created schedule_task tool yet
  • No text-only notification jobs
  • No channel or GitHub dispatch targets
  • No interval schedule type in this first cut

Enable background polling with config.yaml -> scheduler.enabled. Manual trigger uses the same scheduled-task resource and execution path.

Terminal Workbench (TUI)

deerflow is a terminal-native workbench for people who live in the shell. It runs embedded over DeerFlowClient — no Gateway, frontend, nginx, or Docker required — while honoring the same config.yaml, checkpointer, skills, memory, MCP, and sandbox settings as the rest of DeerFlow.

DeerFlow TUI

uv pip install 'deerflow-harness[tui]'        # optional 'textual' dependency

deerflow                                      # launch the terminal UI (TTY required)
deerflow --continue                           # resume the most recent thread
deerflow --resume THREAD                      # resume a thread by id
deerflow --print "summarize this repo"        # headless one-shot answer to stdout
deerflow --json  "hello"                       # headless newline-delimited StreamEvents

A keyboard-driven chat surface with a streaming transcript (Markdown-rendered answers), compact tool-activity cards, a / slash-command palette, display-only /clear, /goal goal management, /model and /threads pickers, input history, and Esc / Ctrl+C interrupt. /clear removes rows from the current terminal display without deleting the thread or its persisted conversation; /new and /clear ask you to wait during an active run instead of resetting in-flight display state. Sessions opened in the TUI also appear in the Web UI sidebar — it writes the shared thread store under the local default user, so terminal and web stay in sync without running the Gateway.

See backend/docs/TUI.md for the full guide.

Documentation

⚠️ Security Notice

Improper Deployment May Introduce Security Risks

DeerFlow has key high-privilege capabilities including system command execution, resource operations, and business logic invocation, and is designed by default to be deployed in a local trusted environment (accessible only via the 127.0.0.1 loopback interface). If you deploy the agent in untrusted environments — such as LAN networks, public cloud servers, or other multi-endpoint accessible environments — without strict security measures, it may introduce security risks, including:

  • Unauthorized illegal invocation: Agent functionality could be discovered by unauthorized third parties or malicious internet scanners, triggering bulk unauthorized requests that execute high-risk operations such as system commands and file read/write, potentially causing serious security consequences.
  • Compliance and legal risks: If the agent is illegally invoked to conduct cyberattacks, data theft, or other illegal activities, it may result in legal liability and compliance risks.

Security Recommendations

Note: We strongly recommend deploying DeerFlow in a local trusted network environment. If you need cross-device or cross-network deployment, you must implement strict security measures, such as:

  • IP allowlist: Use iptables, or deploy hardware firewalls / switches with Access Control Lists (ACL), to configure IP allowlist rules and deny access from all other IP addresses.
  • Authentication gateway: Configure a reverse proxy (e.g., nginx) and enable strong pre-authentication, blocking any unauthenticated access.
  • Network isolation: Where possible, place the agent and trusted devices in the same dedicated VLAN, isolated from other network devices.
  • Stay updated: Continue to follow DeerFlow's security feature updates.

Contributing

We welcome contributions! Please see CONTRIBUTING.md for development setup, workflow, and guidelines.

Regression coverage includes Docker sandbox mode detection and provisioner kubeconfig-path handling tests in backend/tests/. Backend blocking-IO diagnostics are available from the repository root with make detect-blocking-io: it statically scans backend business code for blocking IO that may run on the backend event loop, prints a concise summary, and writes complete JSON findings to .deer-flow/blocking-io-findings.json. The JSON includes compact review records with priority, location, blocking_call, event_loop_exposure, reason, and code. Gateway artifact serving now forces active web content types (text/html, application/xhtml+xml, image/svg+xml) to download as attachments instead of inline rendering, reducing XSS risk for generated artifacts.

License

This project is open source and available under the MIT License.

Acknowledgments

DeerFlow is built upon the incredible work of the open-source community. We are deeply grateful to all the projects and contributors whose efforts have made DeerFlow possible. Truly, we stand on the shoulders of giants.

We would like to extend our sincere appreciation to the following projects for their invaluable contributions:

  • LangChain: Their exceptional framework powers our LLM interactions and chains, enabling seamless integration and functionality.
  • LangGraph: Their innovative approach to multi-agent orchestration has been instrumental in enabling DeerFlow's sophisticated workflows.

These projects exemplify the transformative power of open-source collaboration, and we are proud to build upon their foundations.

Key Contributors

A heartfelt thank you goes out to the core authors of DeerFlow, whose vision, passion, and dedication have brought this project to life:

Your unwavering commitment and expertise have been the driving force behind DeerFlow's success. We are honored to have you at the helm of this journey.

Star History

Star History Chart

Description
An open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.
Readme MIT 368 MiB
Languages
Python 82%
TypeScript 14.8%
MDX 1.3%
HTML 0.7%
Shell 0.5%
Other 0.6%