jongkwan.dev
Development · Essay №053

Agent Orchestration with LangGraph

Multi-agent orchestration and durable execution design built on the LangGraph StateGraph

Jongkwan Lee2026년 2월 23일16 min read
Contents

Turn the execution flow, state, and failure recovery of a multi-agent system into a LangGraph StateGraph.

Why orchestration is needed

In a multi-agent system, communication between agents, state management, and control of the execution flow matter as much as the reasoning ability of any individual agent. Wiring nine agents into a plain sequential for-loop hits a set of hard limits.

LimitDescriptionConsequence
No failure recoveryAn error at step 7 forces a rerun from the beginningWasted API calls and cost
No parallel executionIndependent analysis steps still wait in lineHigher total latency
No conditional branchingThe whole pipeline runs even when data quality is badDecisions made on corrupted data
No state trackingNothing records how far the run gotDebugging and auditing are impossible
No human interventionHigh-risk decisions have no approval stepAutomation risk is left exposed

LangGraph addresses all of these with a graph-based state machine (StateGraph).


Core LangGraph concepts

StateGraph: a graph-based workflow

LangGraph models an agent workflow as a stateful, controlled execution graph. A node is one agent's unit of execution, and an edge defines a state transition. The graph is laid out as a directed acyclic graph (DAG).

python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
 
class PipelineState(TypedDict):
    project_id: str
    market: str
    config: dict
    scan_results: list[ScanResult]
    news_items: list[NewsItem]
    position_alerts: list[PositionAlert]
    signals: list[Signal]
    debate_result: dict | None
    final_decision: dict | None
    errors: list[str]
 
graph = StateGraph(PipelineState)

PipelineState is the shared state for the whole workflow. Each node reads and updates it, and the checkpointer saves a snapshot after every step.

Core building blocks

Building blockRole
NodeAn agent's unit of execution (a function)
EdgeA state transition between nodes
Conditional EdgeA branch chosen by a condition
StateShared state (a TypedDict)
CheckpointerSaves state when each node completes (like a save point in a game)
ThreadIdentifier for one execution instance

Designing the StateGraph DAG: a trading pipeline

Mapping onto the three Sense-Think-Act layers

The nine agents split across the three Sense-Think-Act layers, and the LangGraph StateGraph wires them into a DAG.

Graph definition

python
graph = StateGraph(PipelineState)
 
# --- Sense Layer ---
graph.add_node("data_quality_gate", data_quality_gate)
graph.add_node("volume_scanner", volume_scanner)
graph.add_node("news_collector", news_collector)
graph.add_node("position_sensor", position_sensor)
 
# --- Think Layer (fan-out / fan-in) ---
graph.add_node("quant_analyst", quant_analyst)
graph.add_node("llm_analyst", llm_analyst)
graph.add_node("sector_rotation", sector_rotation)
graph.add_node("analysis_merge", merge_analysis_results)
graph.add_node("bull_bear_debate", bull_bear_debate)
graph.add_node("ensemble_voter", ensemble_voter)
 
# --- Act Layer ---
graph.add_node("risk_sentinel", risk_sentinel)
graph.add_node("trade_executor", trade_executor)
 
# Edge definitions
graph.set_entry_point("data_quality_gate")
 
# Data Quality Gate: conditional branch
graph.add_conditional_edges(
    "data_quality_gate",
    lambda s: "continue" if not s["errors"] else "end",
    {"continue": "volume_scanner", "end": END},
)
 
# Sense Layer runs sequentially
graph.add_edge("volume_scanner", "news_collector")
graph.add_edge("news_collector", "position_sensor")
 
# Fan-out: parallel analysis
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
 
# Fan-in: results converge
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")
 
# Second half of the Think Layer
graph.add_edge("analysis_merge", "bull_bear_debate")
graph.add_edge("bull_bear_debate", "ensemble_voter")
graph.add_edge("ensemble_voter", "risk_sentinel")
 
# Risk Sentinel: conditional branch
graph.add_conditional_edges(
    "risk_sentinel",
    lambda s: "execute" if s["final_decision"].get("approved") else "end",
    {"execute": "trade_executor", "end": END},
)
graph.add_edge("trade_executor", END)

Fan-out / fan-in parallelism

What parallelism buys

Inside the decision loop, Quant Analyst (p95 < 300ms), Sector Rotation (p95 < 300ms), and LLM Analyst (p95 < 8s) are independent of one another. Here p95 means the 95th percentile latency: 95% of requests finish within that time. Run sequentially they take at least 8.6 seconds; run in parallel the total converges on the slowest one, roughly 8 seconds.

How fan-out works in LangGraph

When one node has edges to several nodes, LangGraph fans out automatically and runs them in parallel. Fan-in happens when several nodes point at the same next node; that node waits until every predecessor has finished.

python
# position_sensor -> [quant, llm, sector] : fan-out
graph.add_edge("position_sensor", "quant_analyst")
graph.add_edge("position_sensor", "llm_analyst")
graph.add_edge("position_sensor", "sector_rotation")
 
# [quant, llm, sector] -> analysis_merge : fan-in
graph.add_edge("quant_analyst", "analysis_merge")
graph.add_edge("llm_analyst", "analysis_merge")
graph.add_edge("sector_rotation", "analysis_merge")

The analysis_merge node combines the three agents' outputs into one report. Because each agent's result accumulates in the shared state, the join node can read all of them.


Checkpointing and durable execution

How the checkpointer behaves

The LangGraph checkpointer saves a snapshot of the entire state after every super-step, meaning after each node completes. That enables the following.

CapabilityDescription
Fault toleranceResume from the last checkpoint after a failure
Human-in-the-loopPause at a given step and wait for human approval
Time travelRoll back to an earlier state and simulate a different decision
Audit trailPer-step state is recorded, so decisions can be traced

A PostgreSQL-backed checkpointer

python
from langgraph.checkpoint.postgres import PostgresSaver
 
checkpointer = PostgresSaver.from_conn_string(DATABASE_URL)
app = graph.compile(checkpointer=checkpointer)
 
# Run
result = await app.ainvoke(
    initial_state,
    config={"configurable": {"thread_id": "pipeline-run-uuid"}}
)

thread_id is the primary key for checkpoint lookups. Rerunning with the same thread_id resumes automatically from the step after the last checkpoint.

Restart sequence

Durable execution principles

Durable execution is a core LangGraph pattern, and it follows these principles.

PrincipleTrading application
DeterministicUp to Risk Sentinel, the same input yields the same output
IdempotentOrder submission is deduplicated by idempotency_key
Side effects isolatedOrder submission, an external API call, is wrapped as a task

In trading, exactly-once execution is hard to guarantee perfectly because of the broker and exchange boundary. So each order carries an idempotency_key, and on restart an idempotency check verifies whether an order with that key was already submitted.


Human-in-the-loop

When a human is needed

In automated trading, some situations call for human confirmation rather than a purely mechanical judgment.

  • Additional trades when the daily loss limit is close
  • Sudden regime shifts such as a VIX spike or a black swan event
  • Large buys made while news reliability is very low
  • The first live trade right after a new strategy or prompt change

Implementing human-in-the-loop in LangGraph

The Risk Sentinel output carries a requires_human_approval flag, and the LangGraph interrupt() function pauses execution.

python
from langgraph.types import interrupt
 
def risk_sentinel(state: PipelineState) -> PipelineState:
    intent = state["final_decision"]
    portfolio = get_portfolio_snapshot(state["project_id"])
 
    decision = evaluate_risk(intent, portfolio)
 
    if decision.requires_human_approval:
        # interrupt(): pause and wait for human approval. The return value is what the human sends on resume.
        approved = interrupt(
            f"High-risk trade detected: {decision.reasons}"
        )
        state["final_decision"]["approved"] = approved
        return state
 
    state["final_decision"]["approved"] = decision.approved
    return state

A node that calls interrupt() stops at that point, and when the user resumes with Command(resume=value) the node runs again from its start. The older approach of raising NodeInterrupt was replaced by the interrupt() function in LangGraph v0.4. Thanks to checkpoints, none of the steps before the pause have to run again.


Branching with conditional edges

Data quality gate

The pipeline entry point validates data quality and aborts the whole run when the data falls below the bar.

python
graph.add_conditional_edges(
    "data_quality_gate",
    lambda s: "continue" if not s["errors"] else "end",
    {"continue": "volume_scanner", "end": END},
)

The data quality gate checklist:

CheckThresholdOn failure
API response timep95 < 3sAbort after 3 retries
Quote freshnessnow - data timestamp < 5 minWarn, abort past 10 min
Price outliersWithin 30% of the previous closeCheck for splits and rights offerings
Missing required symbolsAt least 90% of the universe fetchedAbort below the threshold
News API availabilityAt least one source respondsProceed without news if all fail

Risk Sentinel branch

The risk verdict routes the run to execute or to end.

python
graph.add_conditional_edges(
    "risk_sentinel",
    lambda s: "execute" if s["final_decision"].get("approved") else "end",
    {"execute": "trade_executor", "end": END},
)

The Risk Sentinel verdict has five possible values.

VerdictMeaningFollow-up
APPROVEApproved as proposedPassed to Trade Executor
RESIZEApproved after size adjustmentExecuted with the adjusted intent
HOLDConditionally deferredRe-evaluated once the condition holds
REJECTRejectedLogged, then the run ends
KILLEmergency stopCancel every open order

Event-sourced state persistence

A three-tier storage layout

State persistence in the trading system uses three tiers.

StoreNatureRole
KafkaImmutable, append-onlyRecords every event, supports replay and audit
RedisMutable, TTL-basedReal-time cache, pub/sub, rate limiting, agent weights
PostgreSQLDurableLangGraph checkpoints, trade history, reflection memos

Event envelope schema

Every message between agents uses a common envelope.

json
{
  "event_id": "uuid-v7",
  "ts": "2026-02-21T09:15:00.123+09:00",
  "event_type": "QUANT_DONE",
  "correlation_id": "pipeline-run-uuid",
  "project_id": "project-uuid",
  "agent": "quant_analyst",
  "payload": {
    "symbol": "005930",
    "signal": 0.72,
    "confidence": 0.68,
    "feature_vector": { "rsi_14": 32.5, "macd_signal": 0.003 }
  },
  "schema_version": "v2",
  "idempotency_key": "uuid-for-dedup"
}

idempotency_key and correlation_id carry the weight here. The first prevents duplicate processing; the second ties together every event belonging to a single pipeline run.


LangGraph compared with AutoGen

Framework characteristics

DimensionLangGraphAutoGen
Modeling approachGraph / state machineEvent-driven conversation
Execution flowDeclared explicitly as a DAGMessaging between agents
State managementShared TypedDict stateConversation history
CheckpointingBuilt in (PostgreSQL, SQLite)Must be implemented externally
Durable executionNative supportNot supported
Human-in-the-loopBuilt-in interruptUserProxyAgent
Parallel executionBuilt-in fan-out / fan-inTeams / GroupChat
DebuggingTime travel (state rewind)Conversation logs

The TradingAgents framework

TradingAgents proposes a multi-agent structure modeled on a real trading organization. Seven roles (fundamental analyst, sentiment analyst, news analyst, technical analyst, bull/bear researcher, trader, risk manager) collaborate across a five-stage pipeline.

text
Analyst Team  ->  Researcher Team  ->  Trader  ->  Risk Team  ->  Fund Manager
(4 in parallel)   (bull/bear debate)   (synthesis) (3-view debate) (final approval)

In a Q1 2024 backtest it reported cumulative returns of 26.62% on AAPL (Sharpe 8.21), 24.36% on GOOGL (Sharpe 6.39), and 23.21% on AMZN (Sharpe 5.60). These are the numbers reported in the paper (arxiv:2412.20138), drawn from a narrow window of three symbols over three months. A Sharpe near 8 is hard to reproduce in live trading, so treat live reproduction as an open question.

Choosing a framework

LangGraph fits a trading system better. The reasons are that (1) checkpointing makes failure recovery possible, (2) conditional edges model risk gates naturally, (3) human-in-the-loop is built in, and (4) time travel makes it possible to simulate what a different decision would have produced.

AutoGen is strong at free-form conversation between agents and dynamic team composition, so its GroupChat pattern can suit a debate structure such as bull/bear. Using both frameworks together is also viable.


Per-agent SLOs and failure modes

Each agent gets a service level objective (SLO) expressed as a latency target. In the table, p99 means the 99th percentile latency: 99% of requests finish within that time.

Latency and throughput targets

AgentSuggested latency SLOConcurrencyMain failure modesMitigation
Volume Scannerp99 < 200msHundreds of symbols per runDelayed or missing dataWatermarks, cooldown
News Collectorp95 < 3sTens of articles per runDuplicates, rumorsDedup, source reliability scores
Position Sensorp99 < 200msAll symbols per projectStale quotesRedis cache, DB reconciliation
Quant Analystp95 < 300msOnce per ticketFeature errorsPinned feature versions
LLM Analystp95 < 8s10 concurrentHallucination, overconfidenceCited evidence, critic loop
Sector Rotationp95 < 300msPeriodicRegime misreadsHysteresis
Bull/Bear Debatep95 < 10sA few concurrentFailure to convergeRound cap of 2
Ensemble Voterp95 < 200msOnce per ticketOverfittingCalibration
Risk Sentinelp99 < 50msRequired before every orderLimits not appliedHard rules take precedence
Trade Executorp99 < 100msOrders per secondRejected or unfilled ordersRequote, timeout

Per-agent I/O flow

IS in the Trade Executor output is implementation shortfall, the gap between the target price and the actual fill price, expressed in basis points (1bps = 0.01%).


Operations: testing, canary rollout, kill switch

A three-stage test regime

StageDescriptionRegulatory basis
Regression testsVerify existing behavior on every code changeMiFID II RTS 6: conformance testing
SimulationBacktesting plus paper tradingSEC Rule 15c3-5
Canary rolloutStart at 1-5% of capital, then expandGoogle SRE canary releases

Canary rollout principles

In trading the canary unit is capital and exposure rather than traffic.

  • New strategy or model change: start at 1-5% of total capital, then monitor performance, risk, and slippage
  • LLM prompt or debate protocol change: apply first to a limited sector or a handful of symbols, then check for drift
  • Keep pinned versions and event-log-based replay ready so any problem can be rolled back to the previous version immediately

Kill switch

The kill switch is an emergency control that cancels every open order and halts the pipeline.

Every LangGraph node checks the Redis kill switch flag before it runs. If the flag is set, the node exits immediately.


Infrastructure stack summary

TechnologyRolePrimary use
LangGraphOrchestrationStateGraph DAG, checkpoints, conditional branching, human-in-the-loop
KafkaEvent streamPipeline, trade, and position event topics; audit log
RedisReal-time cacheQuote cache, pub/sub, rate limiter, kill switch, agent weights
CeleryScheduling and async workBeat (periodic triggers), position sensor polling, reflection, reports
PostgreSQLDurable storageCheckpoints, trade history, reflection memos, weight snapshots

LangGraph answers the question of how agents should be wired together and executed. It bundles a graph-based DAG, checkpointing, conditional branching, parallel execution, and human-in-the-loop into one framework, so a production-grade multi-agent system can be built without hand-extending a sequential pipeline.


Summary

A sequential for-loop leaves failure recovery, parallel execution, conditional branching, state tracking, and human intervention all as things you must build yourself. LangGraph folds them into a single StateGraph: nodes and edges declare the execution flow, and a TypedDict holds the shared state. The checkpointer saves state after every node, so a failed run resumes from its last point, and interrupt() collects human approval before a high-risk decision. Conditional edges model the data quality gate and the risk gate naturally.

LangGraph fits domains such as trading, where checkpointing, conditional branching, and human-in-the-loop are all required. AutoGen is worth pairing in when free-form conversational debate is the central need.


References