← the journey
02

stop 02 · published

Lead-agent factory

source anchors · @7e7f041

Your prompt has crossed the frontend, passed through the Gateway, and become a run. Now it reaches make_lead_agent(). This stop is about how runtime options plus application config become a runnable LangGraph agent graph.

Hold onto one idea: this function assembles runtime options and application config into an agent graph. Reasoning happens later, inside the model-tool-middleware loop. This function only bolts the parts together and hands off the compiled graph. It does not even run the graph itself.

flowchart TD
FE["Frontend / API"] --> GW["DeerFlow Gateway · run_agent()"]
GW --> MK["make_lead_agent(config)"]
MK -->|"compat: resolve AppConfig"| RC["_get_runtime_config(config)"]
RC --> MLA["_make_lead_agent(config, app_config=...)"]
MLA --> CA["create_agent(model, tools, middleware, prompt, ThreadState)"]
CA --> G["compiled LangGraph agent graph"]
How a run reaches a compiled agent graph

One factory, two entry points

Open agent.py and you’ll find a pair of near-twins: make_lead_agent() and _make_lead_agent(). The underscore is not cosmetic: it marks the most important boundary in the file.

make_lead_agent(config) takes a single argument because LangGraph Server requires graph factories to be callable from a config-only entry (see langgraph.json ). Its job is light: resolve an AppConfig (prefer the runtime-injected one, fall back to get_app_config()), then hand off to the underscore version. It is a compatibility adapter.

_make_lead_agent(config, *, app_config) is the actual assembly function. That * makes app_config keyword-only — because both arguments are “config-like,” and positional calls are too easy to confuse. Its work is deterministic: build the graph, step by step, from runtime options plus application config.

How the runtime options get assembled

_get_runtime_config() merges config["configurable"] and config["context"]. Note this is not a mathematical union — it’s ordered: lay down configurable first, then let context overwrite same-named keys.

The fields it reads are per-run options, not global application settings:

model_name / model
thinking_enabled
reasoning_effort
is_plan_mode
subagent_enabled
max_concurrent_subagents
is_bootstrap
agent_name

How the model is chosen

Model resolution has a clear priority:

request model_name
  > custom agent config model
  > AppConfig.models[0]

If thinking_enabled=True but the selected model does not support thinking, the factory logs a warning and turns thinking_enabled back off. You can ask for thinking; the model still has to support it.

The final formula

Once every part is in place, the line emits this:

create_agent(
    model=create_chat_model(...),
    tools=filtered_tools,
    middleware=build_middlewares(...),
    system_prompt=apply_prompt_template(...),
    state_schema=ThreadState,
)

The mental model, in one line:

Lead Agent graph = model + tools + middleware + system_prompt + ThreadState

create_agent() provides the standard model-and-tool loop. DeerFlow supplies the materials (model, tools) and the policies (middleware, prompt, state schema).

Clean assembly · _make_lead_agent

  • receives an explicit AppConfig
  • assembles deterministically from runtime options
  • easy to test, easy to reason about

Compat cost · make_lead_agent

  • single-arg ABI (LangGraph Server requires it)
  • app_config via runtime config + global fallback
  • cfg is overloaded; config is mutated in place

Where it will trip you up


That is the assembly line. Its value is determinism: it assembles the graph from configuration, without reasoning here or changing policy on the fly. The next stop looks at tool assembly, because what the model can call decides what this graph can do.