← the journey
02

stop 02 · published

Lead-agent factory

architecture risk ●●●○○
source anchors · @0fb18e3

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 is an assembly line, not the brain. 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 real 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 best quality is being boring: deterministic, predictable, and free of hidden surprises. Before we take apart middleware, the next stop looks at tool assembly, because what the model can call decides what this graph can actually do.