Agent Autonomy — Field Guide

🧭 9 chapters · the autonomy spectrum · agent loops · human-in-the-loop · durable execution · guardrails · go deeper with the LlamaIndex primer →

📦 A framework-agnostic field guide in the How It Works family — the concepts here are general, not a description of this site. For how this site itself works, read the written guide →

✅ Implemented in this app — Autonomy Ladder liveAn L0-L3 policy registry gates how much budget, retry depth, and human-in-the-loop oversight each served endpoint gets — widening a level requires a recorded eval, not just a flag flip.Try it: rag_service.autonomy

01. The Autonomy Spectrum

There are two main ways to build a system. Workflows have predetermined code paths. They are designed to operate in a certain order. Agents are different. They are dynamic. Agents define their own processes and tool usage. Workflows give you a fixed sequence that runs step by step. Agents can adapt and choose their own path as they go. Many systems actually combine both ideas. You get the predictability of a fixed plan with the flexibility to change based on new information. That blend is common in real applications. For example, an agent might use a workflow for routine checks. That keeps things reliable. Then it switches to dynamic decision making for open ended problems. This way you do not lose control. But you also get the ability to handle unexpected tasks. So think of it as a spectrum. On one end you have strict workflows. On the other end you have free agents. Most real systems sit somewhere in between. They mix the two approaches to get the best of both worlds.

An agent-style loop that iteratively improves a joke based on evaluation feedback.

python
@task
def llm_call_evaluator(joke: str):
    """LLM evaluates the joke"""
    feedback = evaluator.invoke(f"Grade the joke {joke}")
    return feedback

@entrypoint()
def optimizer_workflow(topic: str):
    feedback = None
    while True:
        joke = llm_call_generator(topic, feedback).result()
        feedback = llm_call_evaluator(joke).result()
        if feedback.grade == "funny":
            break
    return joke
In plain words

Imagine a restaurant where a head chef decides whether to cook everything solo or call in a specialist pastry chef for desserts. This chapter is about building computer systems that coordinate multiple specialists—called agents—to handle complex jobs, but you get to choose how much control the head chef keeps. The system can follow a fixed recipe step by step (a workflow), or let each specialist decide their own cooking steps (an agent), and many real kitchens mix both.

The source lays out exact patterns for this. In Subagents, the head chef always assigns a task to a line cook, waits for the result, then passes it along—each cook starts fresh every order. In Handoffs, a cook can hand a ticket directly to another station, and that station takes over completely, staying active for repeat orders. Skills let the head chef pull up a recipe card when needed without handing off. Router uses a host who decides which chef gets the order, then the chosen chef works alone. These patterns differ in how many communication calls they need and whether they keep memory (state) between turns. For example, on a repeated request like "buy coffee again," Handoffs and Skills save 40–50% of calls because state persists, while Subagents repeat the full four-call cycle each time.

The non‑obvious edge is statefulness: Handoffs and Skills reuse loaded context, making them cheaper for repeated tasks, but Subagents intentionally wipe state each time to isolate each agent’s knowledge. Without these patterns, you’d either cram every tool into one agent—causing poor tool choices—or build a rigid one‑recipe system that cannot adapt to new orders, exactly the failure a beginner would feel when their system stops working for an unexpected request.

System design

In the Subagents pattern, the mechanism proceeds in a fixed order: the main agent, created via create_agent with a model and tools, receives the user’s request and decides whether to invoke a subagent as a tool. Each subagent runs in its own isolated context, executes its own tool calls, and returns results to the main agent. The main agent then integrates those results into its next reasoning step. If a subagent invocation fails (e.g., the subagent tool returns an error), the main agent receives that error as a tool output and can either retry, escalate, or continue with a fallback. This ordered chain ensures that all routing and final responses pass through the main agent.

The invariant preserved by this design is that the main agent is the single point of coordination and context maintenance. Subagents are explicitly stateless by design; each invocation follows the same flow, and the main agent holds all conversation history across turns. This guarantees that no subagent can alter the conversation state or bypass the main agent’s oversight. The source names this as the central property: “the main agent maintains conversation context”.

The key trade‑off is an extra LLM call per subagent invocation compared to alternatives like Handoffs or Skills (4 calls versus 3), as seen in the repeat‑request table. The obvious alternative—allowing subagents to respond directly to the user—was rejected because it would decentralize control and risk fragmenting context. The cost this rejection avoids is the complexity of synchronizing state across multiple agents and losing the ability to enforce sequential constraints or centralized error handling. As the source states, “this overhead provides centralized control.”

A concrete failure mode is a subagent tool call that times out or returns nonsensical output. The operator would observe, via LangSmith traces, that the subagent invocation produced an error or unexpected response, while the main agent’s subsequent reasoning may be corrupted. The trace would show the subagent’s raw output and the main agent’s decision to proceed or fail—providing a clear signal of where the breakdown occurred.

Interview Q&A

Q (warm-up): In the autonomy spectrum, what distinguishes a fixed workflow from an autonomous agent according to the source materials?
A The fixed workflow is exemplified by the router_workflow function, which uses llm_call_router and a set of if-elif branches to pre‑determine the next step (story, joke, or poem). In contrast, an autonomous agent pattern like Handoffs allows agents to transfer control to each other via tool calls, adapting their path dynamically. The key difference is that workflows have predetermined code paths, while agents define their own processes and tool usage as they execute.
Follow-up Why would you still choose a fixed workflow over an autonomous agent for a simple task?
A Fixed workflows provide predictable, stateless execution with no risk of context drift, and they incur the same number of calls as autonomous patterns for single tasks (e.g., 3 calls for both Router and Handoffs in the single-task comparison).
Weak answer misses The concrete identifier router_workflow and the use of llm_call_router for branching; a shallow answer might just say “workflows are predictable” without naming the exact function.

Q (medium): In the Subagents pattern, why is there an extra LLM call compared to the Handoffs pattern for a single task?
A The source explicitly states: “Subagents adds one extra call because results flow back through the main agent—this overhead provides centralized control.” The Subagents pattern uses a main agent that coordinates subagents as tools, so each subagent’s output must first return to the main agent before a final response is generated, requiring 4 calls versus 3 for Handoffs.
Follow-up What specific benefit does that extra call bring?
A It provides strong context isolation: subagents are “stateless by design” and start fresh each time, ensuring no cross‑contamination between subagent contexts.
Weak answer misses The exact phrase “results flow back through the main agent” and the call counts (4 vs. 3) are the real details that a vague answer would omit.

Q (hard — design question): For multi‑domain tasks where each request is independent and one‑off, why use a stateless Router pattern instead of a stateful Handoffs pattern?
A The Router pattern is stateless—each request requires an LLM routing call from scratch, as described: “Routers are stateless—each request requires an LLM routing call.” For one‑off tasks, the statefulness of Handoffs provides no advantage, and the Router’s stateless design gives strong context isolation with no risk of leftover state affecting the next unrelated domain. Additionally, the source shows that for a single task, both patterns use exactly 3 LLM calls, so there is no performance penalty.
Follow-up Under what condition would the Handoffs pattern become significantly more efficient than Router?
A On repeat requests (e.g., “Buy coffee” twice), Handoffs saves 40–50% of calls because the coffee agent remains active, avoiding a handoff; Router statelessly repeats the full 3‑call flow each time.
Weak answer misses The specific cost comparison for repeat requests (2 calls vs. 3) and the characterization of Routers as stateless—not just “simpler.”

Q (hard): The orchestrator‑worker pattern is described as providing flexibility when subtasks cannot be predefined. How does this pattern illustrate the blend of fixed workflow and autonomous agent behavior?
A The orchestrator has a fixed workflow: it breaks down tasks into subtasks, delegates to workers, and synthesizes outputs—explicitly listed in the source as “Breaks down tasks into subtasks / Delegates subtasks to workers / Synthesizes worker outputs into a final result.” However, the workers themselves are autonomous (they generate content via LLM calls), and the orchestrator is used “when subtasks cannot be predefined the way they can with parallelization.” This blend gives the predictability of a predetermined orchestration plan with the flexibility of dynamic worker execution.
Follow-up What concrete data structure in the source formalizes the orchestration plan?
A The Sections model (with a list of Section objects containing name and description) is used to structure the subtasks before delegating to workers.
Weak answer misses The explicit three‑step description of the orchestrator workflow and the Sections/Section models—answers that only mention “break down tasks” without citing the schema miss the grounded mechanism.

Failure modes

Failure: Checkpoint Storage Growth

  • Trigger – A long-running thread (e.g., multi-turn conversation) accumulates large state over many super-steps while using the default checkpointing that writes the full value of every state channel at each step.
  • GuardDeltaChannel (referenced in the source as [DeltaChannel](https://reference.langchain.com/python/langgraph/channels/delta/DeltaChannel) and described as storing “only incremental deltas instead of the full accumulated value”). It is the recommended mitigation, though it is noted as beta and requires langgraph>=1.2.
  • Posture – fail‑soft. The system continues to run, but storage growth degrades performance until limits are hit. The source states this “can produce significant storage growth over time” but does not abort execution.
  • Operator signal – Increasing storage usage metrics on the checkpoint store; potential slowdown in graph execution due to larger writes.
  • Recovery – Manual intervention: reconfigure the graph to use DeltaChannel for append‑heavy channels. No automatic retry or fallback is present in the source.

Failure: LLM Call Routing Failure

  • Trigger – The llm_call_router function (from the router_workflow example in langgraph-workflows-agents.md) returns a string that does not match any of the expected branch conditions ("story", "joke", "poem"). The if-elif-else block lacks a default else clause, so the variable llm_call is never assigned.
  • Guard – None. The source shows no exception handler, validation, or fallback around the llm_call_router call or the subsequent if block.
  • Posture – fail‑hard. The Python runtime raises an UnboundLocalError when the code later tries to call llm_call(input_).result().
  • Operator signal – A Python traceback error ending with UnboundLocalError: local variable 'llm_call' referenced before assignment.
  • Recovery – Manual code change to add a default branch (e.g., else: llm_call = llm_call_1) or to validate the router output. No automatic retry or fallback value is provided.

Failure: Process Crash During Async Durability

  • Trigger – Graph execution with durability="async" (from langgraph-durable-execution.md) and the process crashes while the asynchronous checkpoint write is in flight, before the next step begins.
  • Guard – None. The source explicitly acknowledges the risk: “there's a small risk that LangGraph does not write checkpoints if the process crashes during execution.” The durability="async" parameter is a persistence mode, not a guard that catches or handles the crash.
  • Posture – fail‑hard. The intermediate state is lost, and the graph cannot resume from that point. The source states “you cannot recover from system failures … that occur mid‑execution” for the exit mode; for async it similarly implies no recovery from a crash.
  • Operator signal – Silent absence of the expected checkpoint; when restarting the graph after the crash, the state reverts to the last fully written checkpoint (if any), or the thread is missing.
  • Recovery – Manual restart from the last durable checkpoint. No automatic retry or backoff exists in the source.

Failure: DeltaChannel API Breaking Change

  • Trigger – Using DeltaChannel in langgraph>=1.2 under the beta designation, then upgrading to a future release where the API (method signatures, constructor arguments, or behavior) changes incompatibly.
  • Guard – None. The source only provides a warning: “DeltaChannel … is currently in beta. The API may change in future releases.” No validation, deprecation path, or fallback is shown.
  • Posture – fail‑hard. Running the same code after the upgrade results in runtime errors (e.g., TypeError or AttributeError) because the old API is no longer supported.
  • Operator signal – Python error traceback referencing DeltaChannel (e.g., TypeError: DeltaChannel.__init__() got an unexpected keyword argument).
  • Recovery – Manual code revision to match the new API. The source does not describe any automatic migration.

Failure: Handoff Failure in Multi‑Agent Pattern

  • Trigger – In the handoffs pattern (langchain-multi-agent.md), an agent attempts to transfer control to another agent via a tool call (e.g., calling the buy_coffee tool on behalf of the coffee agent). The target agent’s state is unavailable or crashes during the handoff.
  • Guard – None. The source describes the handoffs pattern as “Agents transfer control to each other via tool calls” but does not include any exception handler, retry logic, or fallback for a failed handoff.
  • Posture – fail‑hard. The tool call fails or the handoff never completes, leaving the user without a response.
  • Operator signal – An error from the tool call (e.g., ToolExecutionError or a silent timeout) or a missing subsequent response in the trace.
  • Recovery – Manual inspection and restart of the thread or agent. No automatic retry or fallback value is provided in the source.

02. Levels Of Autonomy

A single call to a large language model can write a poem or a joke. That is the simplest level. Next, you might chain several calls in a fixed order. For example, an agent can write a story, then a joke, then a poem. But each step is predetermined. To add flexibility, you can use a router. The router decides which task to run based on the user’s request. That gives the model more control. The next level gives the model access to tools. An agent can call a tool to write a file or run a query. But you may want to pause for approval when the action is risky. That is where conditional interrupts come in. Finally, the agent can run in a loop. It remembers past interactions using short-term memory. It chooses its own next action and can decide when the task is done. Only climb to a higher level when the problem demands that extra freedom. For simple tasks, a single call or fixed chain is enough. More complex tasks need the ability to use tools or make decisions on the fly. That trade-off keeps the system efficient. Each jump adds power but also complexity. So you only take that step when the problem truly needs it.

A router uses structured output to decide between poem, story, or joke based on user input.

python
class Route(BaseModel):
    step: Literal["poem", "story", "joke"] = Field(
        None, description="The next step in the routing process"
    )

router = llm.with_structured_output(Route)

def llm_call_router(state: State):
    decision = router.invoke([
        SystemMessage(content="Route the input to story, joke, or poem based on the user's request."),
        HumanMessage(content=state["input"]),
    ])
    return {"decision": decision.step}

def route_decision(state: State):
    if state["decision"] == "story":
        return "llm_call_1"
    elif state["decision"] == "joke":
        return "llm_call_2"
    elif state["decision"] == "poem":
        return "llm_call_3"
In plain words

Imagine you're teaching a new cook how to prepare meals. The simplest task is asking them to recite a recipe—just one instruction, like a single call to an AI that writes a poem or joke. That's the most basic level of autonomy. This subsystem gives the AI more control step by step, from fixed sequences to choosing tasks, using tools, and pausing for approval on risky moves.

Now go deeper. The next level is a fixed chain: the cook follows a set order, like writing a story, then a joke, then a poem—each step predetermined. To add flexibility, you use a router: the cook decides which recipe fits the request, selecting the right task from a list. Then you give the cook tools—like an oven or knife—so they can write a file or run a query. But when a tool is dangerous, like slicing with a sharp blade, you can pause for approval. That's a conditional interrupt—a checkpoint where you must say yes before the action proceeds.

The trickiest part is the interrupt: without it, the cook might perform an irreversible step you didn't authorize. The source calls this “human-in-the-loop”—a guard that stops execution until you confirm. If this subsystem were missing, the AI would blast through risky actions without ever checking with you. You'd have no chance to veto a file overwrite or a costly query. The result is a runaway agent that acts on its own, leaving you to clean up messes you never wanted.

System design

The subsystem described in “Levels of Autonomy” builds on the HumanInTheLoopMiddleware and its interrupt_on configuration. The ordered mechanism begins when the agent invokes a tool such as write_file or execute_sql. For each tool call, the middleware evaluates the when predicate—for example, writes_outside_workspace or is_write_query. If the predicate returns True (or is omitted), the call is paused and added to an interrupt batch; the agent waits for a human decision (approve, edit, or reject). If the predicate returns False, the call runs without interruption. On failure—for instance, if the predicate itself raises an exception—the middleware’s behavior is unspecified in the source, but the system would likely escalate to an interrupt or log an error; the agent would stop at that point because the interrupt mechanism cannot determine approval.

The invariant this design preserves is conditional interrupt gating: only tool calls whose arguments match a domain‑specific risk criterion are paused for human review; all others are auto‑approved. This is explicitly named in the when predicate mechanism, which receives a ToolCallRequest object and returns a boolean. The guarantee is that every call to a tool listed in interrupt_on is either approved by the predicate or halted for explicit human consent, preventing unintended writes to paths outside /workspace/ or non‑SELECT SQL queries without an operator seeing them.

The key trade‑off is between automation speed and selective safety. The obvious alternative is to interrupt on every call to a risky tool (the default behavior when when is omitted). That approach would force a human to approve even benign reads, adding latency and operator fatigue. The design rejects that alternative explicitly: it uses predicates like writes_outside_workspace and is_write_query to evaluate only the arguments. The cost avoided is the overhead of unnecessary manual approvals—every safe write_file inside /workspace/ or read‑only SELECT passes through without a pause, keeping the agent fast for routine operations while still catching genuinely dangerous actions.

A concrete failure mode: the is_write_query predicate might incorrectly return False for a DROP TABLE statement because the developer’s regex only checks for SELECT at the start, but DROP is not a SELECT. In that case the execute_sql call runs without interruption, and the operator sees no interrupt raised—the agent completes normally. The signal the operator would actually see is a later LangSmith trace showing the unexpected DROP TABLE execution; the trace would record the tool call with its arguments and the is_write_query predicate’s outcome, making the false‑negative obvious if someone reviews the trace. Without that trace, the only symptom might be a corrupted database, but the source’s recommendation to pair with LangSmith observability provides the necessary visibility.

Interview Q&A

Q – What’s the simplest level of LLM autonomy you can implement in LangGraph, and how would you structure it to produce a joke?
A – The simplest level is a single LLM call, represented as a node in a graph. In the workflow example, the llm_call_generator task takes a topic and invokes llm.invoke(f"Write a joke about {topic}"), returning the joke string. No routing or tools are needed.
Follow-up – How would you parse the LLM’s output into a structured form without extra calls?
A – Use with_structured_output(Feedback) on the LLM, as shown by the evaluator variable, which returns a Feedback schema directly from the model.
Weak answer misses – That the evaluator uses with_structured_output to get structured feedback, not just raw text.

Q – How do you chain multiple LLM calls in a fixed order, and what mechanism controls whether the chain continues or branches?
A – You chain tasks in a directed graph. The workflow defines check_punchline as a gate function that returns "Pass" or "Fail" based on simple punctuation checks. If it returns "Pass", the graph proceeds to improve_joke and then polish_joke; otherwise it could follow a different edge. This gives a deterministic, multi-step pipeline.
Follow-up – Why use a simple Python gate instead of another LLM call for the check?
A – The context shows check_punchline uses a cheap string check ("?" in joke or "!" in joke), avoiding an unnecessary model call while still enabling conditional routing.
Weak answer misses – The gate function’s exact criteria ("?" or "!") and that it returns string literals to drive graph edges.

Q – How does the Router pattern increase flexibility over a fixed chain, and what is its main cost?
A – The Router pattern uses an LLM call to decide which agent or skill to invoke based on the user’s request, rather than a hard-coded sequence. However, routers are stateless—each turn requires a fresh LLM routing call, leading to a constant cost per request (e.g., 3 calls per task). The context notes that “Routers are stateless—each request requires an LLM routing call.”
Follow-up – Can routers be optimized to reduce that overhead?
A – Yes, by wrapping the router as a tool inside a stateful agent (e.g., Handoffs or Skills), which saves 40‑50% of calls on repeat requests because state persists.
Weak answer misses – The exact cost comparison: Router uses 3 calls per task, while stateful patterns use 2 calls on repeat requests.

Q – How do you pause an agent’s execution to get human approval before a risky tool call, and what data structure must you provide to resume?
A – Use the interrupt mechanism with a resume payload that contains decisions matching the order of actions. The example shows resume={"decisions": [{"type": "respond", "message": "Blue."}]}, where the message becomes a successful ToolMessage. The entire resume must specify version="v2" to work with streaming.
Follow-up – How do you handle multiple pending actions that each need separate human review?
A – Provide an array of decisions in the same order as the interrupt, e.g., [{"type": "approve"}, {"type": "edit", "edited_action": {...}}, {"type": "reject", "message": "..."}].
Weak answer misses – That the respond type returns the message as a successful tool result, and that the order of decisions must match the interrupt exactly.

Q – Why would you design a system using the Subagents pattern (where results flow back through a main agent) instead of the Handoffs pattern (where agents transfer control directly)?
A – Subagents provide centralized control and strong context isolation because each subagent starts fresh. The trade-off is one extra call per request (e.g., 4 calls vs. 2 for Handoffs on repeat tasks). The context states, “Subagents add one extra call because results flow back through the main agent—this overhead provides centralized control.” Handoffs, by contrast, are stateful and save calls but share state across agents.
Follow-up – In what scenario would the extra call of Subagents be unacceptable?
A – When latency or cost is critical on every request, because Subagents maintain constant cost per call (stateless), whereas Handoffs reuse persisted state and reduce calls by 40‑50% on repeat requests.
Weak answer misses – That Subagents are “stateless by design” (each invocation fresh) while Handoffs maintain agent state across turns, as shown in the repeat‑request comparison table.

Failure modes

Tool server_info access without None check

  • Trigger — A tool implementation accesses runtime.server_info.assistant_id (or any attribute) without first checking whether runtime.server_info is None. This occurs when the tool is executed locally or in a test environment rather than on LangGraph Server.
  • Guard — The conditional if server is not None: shown in the example in langchain-tools.md. That same example guards the attribute access with this exact check before using server.assistant_id or server.user.identity.
  • Posturefail-hard: Accessing .assistant_id on a None object raises an AttributeError and aborts the tool execution (and likely the entire agent run).
  • Operator signalAttributeError: 'NoneType' object has no attribute 'assistant_id' (or a similar TypeError if using is None incorrectly). No graceful degradation; the error propagates up.
  • Recovery — The developer must add the if server is not None: guard before accessing server attributes, or run the tool on a LangGraph Server deployment. No automatic retry or fallback is provided.

Missing checkpoint saver for Human‑in‑the‑Loop middleware

  • Trigger — An agent is created with HumanInTheLoopMiddleware but no checkpoint saver (such as InMemorySaver) is passed to the agent’s configuration. The source explicitly states: “Human-in-the-loop requires checkpointing.”
  • Guard — No exception handler or validation is shown in the provided context. The langchain-human-in-the-loop.md file only notes the requirement; the middleware’s internal validation is not exposed.
  • Posturefail-hard: The absence of a checkpoint saver likely causes a ValueError or TypeError during agent creation or at the first interrupt, aborting the run.
  • Operator signal — An error message such as ValueError: Human-in-the-loop requires a checkpoint saver (exact wording not guaranteed from source) or a missing attribute error when the middleware tries to use the saver.
  • Recovery — The developer must add a checkpoint saver, e.g., InMemorySaver from langgraph.checkpoint.memory.InMemorySaver, to the agent’s checkpointer parameter. No automatic recovery; manual fix and restart.

Decision order mismatch in HITL resume

  • Trigger — When resuming from an interrupt that paused multiple tool calls, the human provides a resume dictionary with decisions in a different order than the actions appeared in the interrupt request.
  • Guard — No guard is shown in the source. The documentation (langchain-human-in-the-loop.md) gives a strict requirement: “Decisions must be provided in the same order as the actions appear in the interrupt request.” No code-level validation is described.
  • Posturefail-hard: The middleware likely raises an error (e.g., ValueError: decision order mismatch) or misassigns decisions (e.g., approving the wrong tool), causing unpredictable behavior that aborts or corrupts the run.
  • Operator signal — If an error is raised, the operator sees an exception with a message about order mismatch. If misassignment occurs, the agent may execute a tool with unintended arguments or reject the wrong action.
  • Recovery — The operator must reinvoke with the correct decision order. No automatic retry; the run is stuck until the Command is re‑issued correctly.

Use of runtime.stream_writer outside LangGraph execution context

  • Trigger — A tool implementation calls runtime.stream_writer (for streaming intermediary output) when the tool is not running inside a LangGraph execution environment (e.g., during local testing or in a non‑LangGraph agent).
  • Guard — The context only provides a note: “If you use runtime.stream_writer inside your tool, the tool must be invoked within a LangGraph execution context.” No exception handler, fallback, or explicit guard is shown.
  • Posturefail-hard: Attempting to use stream_writer outside the proper context raises a RuntimeError (implied by the “must be invoked” warning), which aborts the tool.
  • Operator signalRuntimeError: stream_writer is only available within a LangGraph execution context (exact wording not in source, but similar).
  • Recovery — The developer must either remove the stream_writer usage or ensure the tool runs inside a LangGraph graph. No automatic fallback; the error stops execution.

Significant edit of tool arguments in HITL causing unintended re‑execution

  • Trigger — During human‑in‑the‑loop approval, the human edits the original tool arguments significantly (e.g., changing a SQL query from SELECT to DELETE) instead of making conservative changes.
  • Guard — No code guard exists. The source (langchain-human-in-the-loop.md) includes a tip: “When editing tool arguments, make changes conservatively. Significant modifications to the original arguments may cause the model to re-evaluate its approach and potentially execute the tool multiple times or take unexpected actions.” This is advisory only.
  • Posturefail-soft: The tool is still executed with the edited arguments, but the model may re‑evaluate and issue additional tool calls, leading to multiple executions or side effects. The run does not abort, but behavior becomes unpredictable.
  • Operator signal — The operator observes in LangSmith traces that the same tool was called multiple times consecutively, or that unexpected tool calls appear after the resumed step. No error is raised.
  • Recovery — No automated recovery. The operator must manually inspect traces and, if undesired, rollback to a prior checkpoint and re‑approve with conservative edits.

03. The Agent Loop

An agent works in a loop. It looks at the current state of the conversation and decides what to do next. The runtime then performs that action and adds the result back to the state. This cycle repeats until the agent chooses to stop. Each step uses the large language model's context window. That window can only hold so many messages. Too many steps would push older messages out. That is why a limit on the number of steps is important. It prevents the loop from running without end. It also controls how much the process costs. The agent's memory is saved with a checkpointer. This lets the thread be picked up again later. The model makes each decision by routing the input to one of several possible tasks. Those tasks might be writing a story or a joke. After each task, the result feeds back into the state. The loop continues until the model signals it is finished. A bound on steps keeps everything practical and efficient.

prints are omitted but that's fine. The code shows the loop: each invoke processes the message and updates state. The second invoke uses memory from first. This matches the agent loop concept. variable assignment is fine.

Thus final answer.Agent invocation with checkpointer enables short-term memory across conversation steps.

python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

def get_user_info():
    return "No user profile on file."

agent = create_agent(
    model="google_genai:gemini-3.5-flash",
    tools=[get_user_info],
    checkpointer=InMemorySaver(),
)

thread_config = {"configurable": {"thread_id": "1"}}

response = agent.invoke(
    {"messages": [{"role": "user", "content": "Hi! My name is Bob."}]},
    thread_config,
)["messages"][-1].content

response = agent.invoke(
    {"messages": [{"role": "user", "content": "What's my name?"}]},
    thread_config,
)["messages"][-1].content
In plain words

Imagine an agent as a chef with a tiny notepad who must cook a multi‑course meal. Each time the chef decides what to do next—chop vegetables, check the oven—he writes the step on his notepad, then does it, and adds the result. The chef can only fit so many notes on that one page. This loop helps him break down a big order into manageable steps and keep track of progress.

Actually, the agent works the same way: it looks at the current state of the conversation (the notepad), decides the next action, and the runtime performs that action, adding the result back to the state. This cycle repeats until the agent chooses to stop. But each step takes up space in the large language model’s context window—the chef’s notepad. Too many steps would push older notes right off the page. That’s why the loop uses middleware like SummarizationMiddleware to compress the old notes before they overflow, and MemoryMiddleware to save persistent instructions (like a recipe card) so the chef doesn’t forget them between sessions. These keep the notepad manageable and reliable.

The trickiest point is the MemoryMiddleware: even though the chef’s notepad fills and gets summarized, the memory middleware loads static instructions (e.g., from ./AGENTS.md) at startup, so key rules survive across multiple loops. Without this subsystem, the chef would lose earlier steps as new ones pile on, forget the core recipe, and end up serving a burnt dish—or loop forever because he can’t remember what he already did.

System design

The agent loop operates as an ordered cycle driven by a checkpointer. First, MemoryMiddleware loads persistent instructions from ./AGENTS.md and SkillsMiddleware loads domain knowledge from ./skills/, initializing the agent’s state. On each iteration, the LLM receives the current state (including conversation history managed by SummarizationMiddleware, which compresses history before the context window overflows). The agent decides an action—calling a tool like search, or delegating via SubAgentMiddleware—which is executed and its result added back to state. The checkpointer persists the state after each step, using a unique thread_id to isolate conversations. If a step fails mid-execution, pending writes from other nodes that completed successfully are stored; when the thread is resumed, the graph restarts from the last successful super‑step without re‑running those nodes. This mechanism ensures the loop can make progress even after transient failures and that the agent’s memory is always recoverable.

The design preserves the invariant of fault‑tolerance and error recovery (as explicitly named in langraph-durable-execution.md): the checkpointer guarantees that the thread’s accumulated state can be restarted from the last successful step. Combined with SummarizationMiddleware’s proactive compression, the system ensures the context window never exceeds its capacity, preventing the “context loss or errors” that occur when a full history overflows. The checkpointer also enforces a write boundary—only completed nodes contribute to the persisted snapshot, with pending writes held apart until the entire super‑step finishes. This gives operators precise control over thread continuity, as the LangSmith API exposes both current and historical thread state.

The key trade‑off is between unbounded context growth and the overhead of summarization. The obvious alternative—letting the loop run indefinitely with the LLM’s full context window—is rejected because “most LLMs still perform poorly over long contexts” (from langchain-short-term-memory.md), leading to distraction, slower responses, and higher costs. Instead, the system imposes a step limit and uses SummarizationMiddleware to compress history on‑the‑fly, trading the ability to see every past detail for reliability, predictable cost, and consistent performance. The cost avoided is the degradation of LLM quality and exponential cost growth that would come from naively expanding the context window. This mirrors the broader pattern of context engineering: deciding what information each agent sees at each step, rather than dumping everything upfront.

A concrete failure mode is when a tool call, e.g., search, times out during execution. The operator would see a failed run in the LangSmith logs, with the thread’s state showing a pending write for that super‑step in the checkpointer’s history. The agent’s state would not advance, and the next invocation with the same thread_id would resume from the last successful checkpoint, re‑attempting only the failed node. The signal is a stalled thread and an error message in the run trace; the operator can inspect the checkpoint list to identify the exact super‑step where the failure occurred, and resume or fork the thread as needed.

Interview Q&A

Q — The agent runs a loop, but why does it need a fixed step limit? Why not let it run until done naturally?
A — The step limit protects against infinite loops and controls cost, because the LLM’s context window can only hold so many messages; without a limit, older messages get pushed out and the agent may repeat actions indefinitely.
Follow-up — If the step limit is too low, might good agents be cut short? How would you tune it?
Yes; you balance it against the max tokens the context window can hold and the typical depth of your task, so the agent can finish before the limit.
Weak answer misses – The fact that the step limit also controls cost by bounding LLM calls is often omitted; a shallow answer only cites infinite-loop prevention.


Q — The agent loop accumulates history and tool results. How does the system avoid overflowing the context window on long conversations?
A — It uses summarization middleware, specifically SummarizationMiddleware, which compresses history before the window overflows, and MemoryMiddleware which loads persistent instructions at startup to carry knowledge across sessions.
Follow-up — Wouldn’t summarization lose important detail? How does the agent know when to skip summarization?
The middleware typically triggers only when token counts approach the model’s context limit, preserving recent detail while compacting older turns.
Weak answer misses – The distinction between SummarizationMiddleware (compression) and MemoryMiddleware (persistent knowledge) is overlooked; a shallow answer says “it summarizes everything”.


Q — The context describes patterns like Subagents and Handoffs. Why would a designer choose Handoffs over Subagents in the loop?
A — Handoffs save 40–50% of LLM calls on repeat requests because the handing-off agent stays active and state persists, whereas Subagents are stateless by design and start fresh every time, requiring a full flow for each repeat.
Follow-up — When would the extra call overhead of Subagents actually be desirable?
When strong context isolation is needed, so one subagent’s mistakes don’t pollute another’s state; Subagents provide that isolation at the cost of more calls.
Weak answer misses – The specific call-count comparison (Subagents: 4 calls per turn vs Handoffs: 2 calls) is the concrete evidence; a shallow answer says “Handoffs are faster” without quantifying.


Q — The agent loop uses a checkpointer to save memory. Why is that needed when the loop already has a context window and middleware?
A — The checkpointer lets the thread persist state across pauses or restarts, so the agent can resume exactly where it left off, while the context window and middleware handle in‑turn compression and knowledge loading.
Follow-up — Could the checkpointer be replaced by writing all state to a file and reloading?
Not efficiently; the checkpointer (used with StateBackend and FilesystemMiddleware) preserves the exact agent loop state including intermediate tool results and conversation order.
Weak answer misses – The role of StateBackend and FilesystemMiddleware in persisting loop state is the crucial detail; a shallow answer says “it’s just a save/load”.


Q — The loop decides what to do next by examining the conversation state. In the Skills pattern, how does the agent switch contexts without leaving the loop?
A — Skills are loaded on‑demand via SkillsMiddleware, which surfaces specialized prompts and knowledge without requiring a handoff or subagent—the agent stays in control and the loop remains unbroken.
Follow-up — What happens if two skills have overlapping instructions? How does the loop resolve conflicts?
The middleware merges skill contexts in the order specified (the sources list), and the agent’s own instructions take precedence; there is no automatic conflict resolution other than prompt ordering.
Weak answer misses – The exact mechanism SkillsMiddleware with sources=["./skills/"] is the grounding; a shallow answer says “the agent loads skill prompts” without naming the middleware.

Failure modes

Context window overflow — The agent accumulates too many messages, tool results, and intermediate steps, pushing older context out of the LLM’s context window.

  • Trigger — The conversation or tool‑call history exceeds the model’s token limit; no explicit threshold is set in the source, but every step appends to state.
  • GuardSummarizationMiddleware (cited in langchain-agents.md as “compresses history before overflow hits”).
  • Posturefail‑soft: the middleware summarizes the history, degrading by discarding detail while allowing the loop to continue.
  • Operator signal — The source does not specify a log line; the operator would observe a silent drop in context fidelity (e.g., earlier messages become summaries, not verbatim).
  • Recovery — Automatic: SummarizationMiddleware runs on each step before the window overflows; no manual intervention required.

Infinite loop (agent never stops) — The agent repeatedly decides to call tools or produce intermediate outputs instead of emitting a final answer, consuming unbounded steps and cost.

  • Trigger — The LLM’s decision‑making fails to reach a stopping condition (e.g., it keeps calling tools, or the tool results keep leading to more actions).
  • Guard — No explicit step‑limit guard is identified in the source. The chapter notes “a limit on the number of steps is important” but does not name a variable, function, or middleware that enforces it.
  • Posturefail‑hard: the loop runs indefinitely; the run does not terminate, and no checkpoint is written mid‑execution unless a durability mode is configured—but the loop itself is never aborted by the system.
  • Operator signal — Absence of a final response; the LangSmith trace shows infinite super‑steps; rising cost metrics.
  • Recovery — Manual kill of the run (e.g., cancel the thread in LangSmith or the deployment platform); no automatic retry.

Tool execution failure — A node (tool call) throws an exception mid‑execution while other nodes in the same super‑step may have succeeded.

  • Trigger — The underlying tool raises an error (e.g., network timeout, invalid input, resource exhaustion) during the graph’s execution step.
  • Guard — The Pending writes mechanism (documented in langgraph-durable-execution.md under “Pending writes”): “LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that super-step. When you resume graph execution from that super-step you don't re-run the successful nodes.”
  • Posturefail‑soft: the current run stops, but the successful nodes’ writes are saved; the operator can resume from the last successful super‑step without re‑running successful work.
  • Operator signal — A node‑level error is logged (exact format not given); the run enters a paused state.
  • Recovery — Manual resume (e.g., call graph.stream again with the same thread ID) or automatic resume via LangGraph’s fault‑tolerance; the failed node is retried on resumption.

Checkpoint write failure (synchronous mode) — The checkpointer cannot persist the full state before the next step starts, e.g., due to disk full or network failure in the store.

  • Trigger — The durability="sync" mode is active and the write to the checkpointer (e.g., InMemorySaver or a database) fails.
  • Guard — The source does not show an explicit exception handler or fallback for a failed synchronous write. The BaseCheckpointSaver interface is referenced but no catch clause is provided.
  • Posturefail‑hard: the execution likely aborts because the synchronous write is required before the next step can proceed; the run halts with an error.
  • Operator signal — The checkpointer throws an exception (e.g., OSError, DatabaseError); the LangSmith trace would show a failed super‑step with a write error.
  • Recovery — Manual intervention: inspect the checkpointer’s storage (disk, DB), resolve the issue, and restart the graph from the last successfully written checkpoint.

Human interrupt resolution with mismatched decisions — The human provides decisions in the wrong order, omits a decision, or uses an incorrect type (e.g., reject where respond is expected).

  • Trigger — Multiple tool calls are paused simultaneously by HumanInTheLoopMiddleware, and the human’s resume dictionary does not match the required order or structure (documented in langchain-human-in-the-loop.md: “Decisions must be provided in the same order as the actions appear”).
  • Guard — No explicit validation guard is shown in the source; the middleware expects correct format and would likely throw a validation error.
  • Posturefail‑closed: the agent refuses to accept the malformed resume; the interrupt remains unresolved, and execution does not continue.
  • Operator signal — The agent.invoke call raises an exception (e.g., ValueError: Decisions must match the order of actions – exact text not given, but implied by the documentation rule).
  • Recovery — Manual step: re‑invoke the agent with the correctly ordered and typed decisions list; no automatic retry.

04. Tools Turn Text Into Action

Giving a model tools lets it turn words into real actions. The model decides which tool to call and what arguments to use. A clear description and a typed signature help the model make the right choice. The runtime then runs the tool and returns the output as an observation. That observation enters the model's context and influences the next decision. This cycle repeats. The model picks another tool or generates a final response based on the new information.

Why does a clear description matter? The model needs to understand what each tool provides. A good explanation guides it to pick the right one. A typed signature tells the model what kind of input the tool expects. For example, a tool for weather queries might require a city name as a string. Without that, the model could pass the wrong kind of data. That leads to errors.

When the tool runs, it produces a result. That result re-enters the model's memory. The model sees it as a new piece of information. It can then use that result to decide what to do next. Maybe it calls another tool. Or it gives a final answer. This back and forth creates a powerful loop. The model learns from each step.

In practice, the main agent keeps track of the conversation. It holds the context. Observations from tools become part of that context. The model can refer to them later. That makes the interaction feel natural and responsive. It is like having a conversation where each answer builds on the last one.

So giving a model tools with clear names, descriptions, and input types turns it from a simple text generator into an active problem solver. It can take real actions and react to what happens.

A tool defined with @tool, typed signature, and description lets the model choose the right action.

python
from langchain.tools import tool

@tool
def search_database(query: str, limit: int = 10) -> str:
    """Search the customer database for records matching the query.

    Args:
        query: Search terms to look for
        limit: Maximum number of results to return
    """
    return f"Found {limit} results for '{query}'"
In plain words

Imagine a chef who can only talk about cooking but never actually touch the stove. Tools are like giving that chef real kitchen appliances—they let the chef turn words like “chop the onions” into actual chopped onions. This subsystem lets an AI model go beyond just talking and actually carry out real-world actions like searching databases or doing math.

The model doesn’t automatically know how to use each appliance; the developer writes a clear label (the tool’s description) and marks each button with its required inputs (type hints). When the conversation calls for it—say, “what’s 5 + 3?”—the model picks the calculator tool, passes the expression, and the runtime executes it and feeds the result back as an observation. That observation becomes part of the model’s context, exactly like a chef seeing the blended smoothie and deciding whether to add more fruit. The cycle repeats: the model either picks another tool or generates a final answer based on the new information.

The trickiest part is that not every tool runs automatically. Some tools are “headless”—they only provide a schema of what the tool expects, not the actual logic. When the model calls such a tool, the runtime interrupts instead of executing locally, pausing to let a human or an external service complete the action (like clicking a button in a browser). This guard prevents the AI from blindly performing unsafe or browser-only actions it cannot control. Without this subsystem, the AI would be stuck talking—it could generate a recipe for the weather but never fetch a real forecast, leaving a user with useless words instead of action.

System design

The mechanism begins when the chat model receives a conversation context containing both user input and any prior tool outputs. The model evaluates whether to invoke a tool based on the context; if it decides to act, it selects the appropriate tool and generates the required input arguments according to the tool’s typed schema. The runtime then executes the tool function—for instance, a @tool‑decorated function like search_database—and returns the string result back into the model’s context as a new observation. That observation influences the model’s next decision, forming a cycle: the model may call another tool or produce a final response. On failure (e.g., an invalid argument), the tool’s execution returns an error string, which becomes part of the observation and is fed back, allowing the model to retry or adjust.

The design preserves what the source calls a well‑defined input‑output schema invariant: every tool must have required type hints that define its input schema, and its docstring must be informative enough for the model to understand when to use it. This invariant guarantees that the model can reliably generate correct argument structures, because the runtime can validate the call against the schema. Without this guarantee—if type hints were optional or descriptions vague—the model might produce syntactically invalid tool calls, breaking the cycle and forcing the application to handle unpredictable errors. The source explicitly requires type hints and an informative docstring to maintain this contract.

The key trade‑off is description detail versus token efficiency. By requiring a concise but informative docstring, the system rejects the alternative of providing no tool description or an overly verbose one. A missing description forces the model to guess the tool’s purpose, leading to frequent mis‑invocations and wasted LLM turns. An overly long description consumes context budget and can distract the model. The chosen approach balances these extremes: a focused description guides correct selection without bloating the prompt, avoiding the cost of many erroneous tool calls and the latency of repeated recoveries. This trade‑off is why the @tool decorator’s docstring is not optional—it is the primary signal the model uses to route its decisions.

A concrete failure mode occurs when a tool’s description ambiguously describes its parameters. For example, suppose search_database has a docstring that omits the fact that limit must be a positive integer. The model might invoke it as search_database(query="recent orders", limit=-5). The runtime returns an error string like "limit must be >= 0". The operator monitoring the system would see this error signal in a LangSmith trace, specifically under the tool call for search_database, with an entry showing the input arguments and a failed execution status. The LangSmith Engine may flag the trace as anomalous and propose a fix—such as updating the docstring to specify the valid range. This signal directly points to the need for a clearer description to restore the invariant.

Interview Q&A

Q — How does a model know which tool to call and what arguments to provide?

A — The model reads the tool’s description and input schema, which are defined by the docstring and type hints of the @tool decorated function. For example, @tool def search_database(query: str, limit: int = 10) -> str: gives a description ("Search the customer database...") and typed parameters. The model uses that information to decide when to invoke the tool and which arguments to supply.

Follow-up — What happens if the docstring is missing or the type hints are incomplete?

Weak answer misses — The model may misselect tools or pass incorrect arguments; the context explicitly states that type hints are required and the docstring “should be informative and concise” to help the model understand the tool’s purpose.


Q — After the model decides to call a tool, which component actually executes it and how does the result flow back to the model?

A — A ToolNode node in a LangGraph graph executes the tool. For instance, you add ToolNode([search, calculator]) as a node named "tools". When the model produces a tool call, the graph routes to this node, which runs the tool and returns the output as a ToolMessage that becomes part of the state, allowing the model to continue the cycle.

Follow-up — What if the tool is a headless tool with no client-side implementation?

Weak answer misses — The run interrupts instead of executing locally; a HeadlessTool has no .implement() API on the Python side, so execution must be done externally and the result submitted via a resume command.


Q — Why does the runtime execute the tool rather than letting the model call it directly? (Design question)

A — Executing the tool in the runtime enables it to run in its proper environment (browser, external service, human review) without giving the model direct system access. The headless tool pattern illustrates this: a tool created with only name, description, and args_schema causes the run to interrupt, allowing your app to inspect the payload, perform the action in the right environment, and then resume the graph. This separation provides security and flexibility.

Follow-up — How does the JS SDK automate the resume flow for headless tools?

Weak answer misses — The supported JS SDK hooks detect headless-tool interrupts, run the matching client implementation, and submit the resume command automatically, as stated in the context. Without these hooks, you would have to manually handle the resume.


Q — In a multi‑step scenario where a tool call requires human approval, how does the resume mechanism ensure the model’s next decision uses the human’s input correctly?

A — When a tool call is interrupted for human review, the app sends a resume command with a decisions list. For example, resume={"decisions": [{"type": "respond", "message": "Blue."}]}. The context specifies that the message is returned to the agent as a successful ToolMessage, so the model sees the human reply as the tool result and can proceed with the next decision based on that observation.

Follow-up — What must you do when multiple actions are under review in the same interrupt?

Weak answer misses — You must provide a decision for each action in the same order they appear in the interrupt, using types like approve, edit, or reject, with an optional message. The context shows the exact schema for multiple decisions.

Failure modes

System Crash During Tool Execution

  • Trigger: A process crash (e.g., OOM, hardware failure) occurs while the tool is running or while the runtime is preparing the observation, and the graph is using durability mode "exit".
  • Guard: None. The "exit" mode only persists checkpoints when graph execution exits successfully, with an error, or on a human‑interrupt. No retry, fallback, or validation covers mid‑execution crashes.
  • Posture: fail-hard. The graph run aborts completely; the intermediate state is lost and cannot be resumed.
  • Operator signal: No checkpoint is written for the interrupted step. The thread stored in the checkpoint saver (e.g., InMemorySaver) remains at the last safe checkpoint, and an attempted resume fails with a missing‑state error or returns stale data.
  • Recovery: Manual re‑run from the last preserved checkpoint. The operator must inspect the crash origin, fix the cause, and re‑execute the graph from the beginning of the interrupted super‑step.

Tool Invoked Outside LangGraph Execution Context

  • Trigger: A tool that uses runtime.stream_writer is called directly (e.g., unit test, standalone script) without being inside a LangGraph graph execution.
  • Guard: None shown in the source. The documentation states “the tool must be invoked within a LangGraph execution context” but provides no exception handler or fallback for the missing context.
  • Posture: fail-hard. The stream_writer attempt raises an error at runtime, aborting the tool and the calling process.
  • Operator signal: An exception such as RuntimeError: stream_writer not available outside LangGraph execution (implied by the requirement) appears in logs.
  • Recovery: The developer must refactor the call to happen inside a valid LangGraph graph run. No retry or backoff is available.

server_info Returns None During Local Development

  • Trigger: A tool reads runtime.server_info while the agent is running locally (e.g., during testing or offline prototyping) rather than on LangGraph Server.
  • Guard: The explicit check if server is not None: in the tool’s body (see get_assistant_scoped_data). When None, the tool skips the server‑specific logic and returns "done".
  • Posture: fail-soft. The tool degrades gracefully by omitting the server‑scoped data but still completes and returns an observation.
  • Operator signal: The absence of the print lines "Assistant: ..." and "User: ..." in the tool’s output. No error is raised; the behavior is silent.
  • Recovery: No retry needed. The tool continues normally. The operator can optionally deploy to LangGraph Server to obtain server info.

Human‑in‑the‑Loop Interrupt Decisions Supplied in Wrong Order

  • Trigger: The developer resumes a paused agent with a resume={"decisions": [...]} list where the order does not match the order of actions in the interrupt request, or a decision type is missing.
  • Guard: None shown in the source. The documentation warns “Decisions must be provided in the same order as the actions appear in the interrupt request” but no validation or error handling is demonstrated.
  • Posture: fail-hard. The HumanInTheLoopMiddleware likely raises an exception (e.g., ValueError or IndexError) because it cannot match decisions to the interrupted tool calls, aborting the resume.
  • Operator signal: An exception trace (likely with a message like “decision mismatch”) appears in the logs, and the agent remains in the interrupted state.
  • Recovery: Manual correction. The operator must re‑inspect the interrupt request’s action list, order the decisions correctly, and re‑invoke the agent with the corrected resume. No automatic retry.

DeltaChannel Beta Storage Corruption

  • Trigger: The graph uses DeltaChannel to incrementally store state deltas, and either langgraph<1.2 (unsupported) or a beta bug causes incorrect delta accumulation, resulting in wrong checkpoint values.
  • Guard: None enforced at runtime. The documentation provides a Warning that “DeltaChannel requires langgraph>=1.2 and is currently in beta. The API may change.” No exception handler or fallback to full state writing is shown.
  • Posture: fail-soft (but silently corrupt). The graph continues to execute and write checkpoints, but the stored state may be inconsistent, leading to incorrect observations in subsequent steps.
  • Operator signal: No immediate alert. Over time, troubleshooting reveals that state snapshots differ from expected values (e.g., a multi‑turn conversation accumulates unexpected data). Storage growth appears abnormal or output logic diverges from intended behavior.
  • Recovery: Manual rollback to full‑state checkpointing by removing DeltaChannel usage. The operator must replay affected threads from a known‑good checkpoint using full state channels. No automatic retry.

05. Humans In The Loop

The human in the loop pattern pauses an agent run for a person’s review. The pause happens when a tool call matches an interrupt condition. You can set allowed decisions like approve, edit, or reject. A predicate checks the tool’s arguments. If the check returns true, the run stops and waits. If false, the call runs without a pause. The run waits until you respond. That wait can last as long as needed. The agent’s state is saved by a checkpointer. So you can resume the run later from exactly where it paused. Your decision is threaded back into the agent. Then the agent continues from that point. The point of the pause is to catch actions that might be risky or hard to undo. For example, writing a file outside the workspace or changing a database. Those actions pause unless you approve them. That gives you control over important steps. The agent does not burn resources while waiting. It simply stops and holds its place. When you come back, you pick up right there. That makes the pattern practical for long running tasks.

A human‑in‑the‑loop pause is triggered by an interrupt condition; the agent’s state is saved with a thread_id and later resumed.

python
from langgraph.types import Command


# You must provide a thread ID to associate the execution with a conversation thread.
config = {"configurable": {"thread_id": "some_id"}}
# Run the graph until the interrupt is hit.
result = agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "Delete old records from the database"}
        ]
    },
    config=config,
    version="v2",
)

print(result.interrupts)
In plain words

It’s like a chef who starts cooking a dish but must stop and ask the manager to approve a special ingredient before adding it. This pattern is for letting a person step into an automated process—a “human in the loop”—so they can review, approve, or change what the machine is about to do before it happens.

The machine runs an agent, and when that agent needs to use a tool (like “buy_coffee” or “send_email”), it can pause instead of acting automatically. A built-in interrupt condition decides when to stop: a predicate checks the tool’s arguments (for example, if the amount is over $100). If the check returns true, the run halts and waits. The agent’s current state is saved by a checkpointer, so even after hours or days the run can be resumed exactly where it paused. Once the person responds with an approval, edit, or rejection, that decision is fed back, and the agent continues from that point.

The most important subtlety is that the pause can last indefinitely—the agent does not time out or fail. Without this pattern, an agent might get stuck waiting for a human who never replies, or worse, it could execute a dangerous action immediately (like deleting a critical file) because no guard stopped it. A beginner would feel that pain as an irreversible mistake the machine made without asking permission.

System design

The Human-in-the-Loop subsystem is orchestrated by the HumanInTheLoopMiddleware, which is added to an agent’s middleware list at creation time via create_agent. The ordered mechanism begins when a tool call is made. The middleware checks the tool’s name against the interrupt_on mapping — a dictionary where keys are tool identifiers (e.g., "write_file", "execute_sql") and values are either True (allowing all decision types) or a dictionary specifying allowed_decisions. If a match is found and the value is not False, the run is paused before the tool executes. The agent’s state is persisted by a checkpointer — in the example, InMemorySaver — ensuring the thread can be resumed at any later time. The paused run waits indefinitely for a human decision delivered via a Command with a resume payload. That payload contains a decisions list, one entry per paused action in the same order as the interrupt request. Each decision has a type field ("approve", "edit", "reject", or "respond") and an optional message. After the decision is injected, the agent continues from the exact point of interruption.

The invariant the design preserves is thread-level checkpoint persistence: the agent’s entire state is written to the checkpointer when an interrupt occurs, so that the run can be resumed later with full context. This guarantees that no progress is lost and that execution resumes exactly where it paused — no partial tool effects, no state corruption. The source explicitly states that state is “persisted to a database (or memory) using a checkpointer so the thread can be resumed at any time.” This makes the pause-resume cycle idempotent with respect to the agent’s internal graph: the same interrupt point can be revisited safely, and each decision consumes exactly one paused step.

The design’s key trade-off is safety against automatic execution versus human latency. The obvious alternative is to let side-effecting tools run without any pause, which would be faster and simpler. The HITL middleware rejects that by forcing a human review for every tool configured in interrupt_on — for example, "execute_sql" with allowed_decisions restricted to ["approve", "reject"] (no editing) to prevent arbitrary SQL execution. The cost this rejection avoids is the risk of irreversible damage (e.g., DELETE FROM queries) or unexpected model re-evaluations from aggressive argument edits. As the source warns, “Significant modifications to the original arguments may cause the model to re-evaluate its approach and potentially execute the tool multiple times or take unexpected actions.” Thus, the trade-off accepts human delay in exchange for controlled, auditable tool invocation.

A concrete failure mode occurs when an operator uses the wrong decision type for a side-effecting tool. For instance, they submit {"type": "respond", "message": "Rejected"} for execute_sql instead of {"type": "reject"}. Because "respond" returns the message as a successful tool result without executing the tool, the agent treats the SQL call as completed — it proceeds as if the query ran successfully, but no database change occurred. The signal an operator would see is a normal agent continuation with no error, yet downstream results will be inconsistent (e.g., a later read shows no deletion when the agent believes it happened). The source explicitly cautions: “Do not use respond to deny side-effecting tools, because its message is treated as a successful tool result.” A routine audit of tool execution logs or state inspection via the checkpointer would reveal the mismatch.

Interview Q&A

Q – What triggers an agent to pause and wait for human review in the human-in-the-loop pattern?

A – The agent pauses when a tool call matches the condition defined in interrupt_on. This is a policy that compares the tool’s arguments; if the predicate returns True, the run is interrupted, and a GraphOutput is returned with an interrupts attribute containing the actions that need a decision.

Follow-up – How does the agent avoid pausing for actions that do not require review?

Weak answer misses – The predicate check enables selective interruption: calls that evaluate to False are never added to the interrupt batch, so only actions needing a decision are presented to the reviewer.

Weak answer misses – The exact mechanism is the predicate filtering before batching, as described: “Calls that evaluate to False are never added to the interrupt batch.”


Q – When a human rejects a tool call, what feedback does the agent receive, and how is that feedback structured?

A – A rejection is sent via Command with a resume dictionary containing a decisions list. One decision entry has "type": "reject" and an optional "message" that explains why the action was rejected. The message is added to the conversation as feedback, helping the agent understand why the action was denied and what it should do instead.

Follow-up – What happens if you omit the message in a rejection decision?

Weak answer misses – When omitted, the middleware uses a default rejection message that tells the model the tool was not executed and not to retry the same tool call unless the user asks.

Weak answer misses – The source explicitly states: “When you omit message, the middleware uses a default rejection message that tells the model the tool was not executed and not to retry the same tool call unless the user asks.”


Q – How does the system persist the agent’s state so that a paused run can be resumed much later, and what exactly is required to resume?

A – The human-in-the-loop pattern leverages LangGraph’s persistence layer via a checkpointer. The run is associated with a conversation thread through a thread_id in the config. To resume, you invoke the agent again with the same thread_id and pass a Command with the resume payload. The checkpointer saves the state at the point of interrupt, allowing resumption from exactly where it paused, even after an indefinite wait.

Follow-up – Why must you provide a thread ID when invoking the agent with human-in-the-loop?

Weak answer misses – The thread_id is required to associate the execution with a conversation thread so the conversation can be paused and resumed, as stated: “You must provide a thread ID to associate the execution with a conversation thread, so the conversation can be paused and resumed (as is needed for human review).”

Weak answer misses – The source explicitly requires a thread ID for LangGraph’s persistence.


Q – Why does the subsystem provide separate reject and respond decision types instead of simply using reject for all human feedback that denies execution?

Areject is meant to deny a proposed action and optionally give feedback to the agent, telling it not to retry the same call. respond is designed for “ask user” style tools where the tool’s real implementation is the human’s reply — the message content is returned directly as a ToolMessage, indicating successful execution, not denial. Using respond to deny would mislead the model into thinking the tool completed successfully, so the two types are kept distinct for semantic correctness.

Follow-up – Can you use respond to provide a rejection message if the tool is not a placeholder for human input?

Weak answer misses – No — the source warns: “Do not use respond to deny a proposed action, because it tells the model that the tool completed successfully.”

Weak answer misses – The exact statement is: “Do not use respond to deny a proposed action, because it tells the model that the tool completed successfully.”

Failure modes

Missing checkpointer

  • Trigger — The agent is configured with HumanInTheLoopMiddleware and interrupt conditions, but no checkpointer (e.g., InMemorySaver) is provided in the agent’s creation or invocation config.
  • Guard — The source does not show an explicit exception handler; the middleware’s requirement is stated only as a prose note: “Human-in-the-loop requires checkpointing to handle interrupts.” In practice, the framework may raise a hard error (e.g., ValueError) at runtime, but no named guard is present in the snippets.
  • Posture — fail-hard: the run aborts at the first interrupt attempt because no state can be saved.
  • Operator signal — A ValueError or RuntimeError with a message such as “Checkpointer is required for interrupt handling” or an empty StateSnapshot with no parent_config when trying to resume.
  • Recovery — Manual step: add a checkpointer (e.g., InMemorySaver() or a persistent saver) to the agent’s creation or to the configurable of the invocation config.

Missing thread ID in invocation config

  • Trigger — The agent is invoked with a checkpointer, but the config dictionary lacks the "thread_id" key inside "configurable". The source explicitly states “you must specify a thread_id”.
  • Guard — No named guard in the snippets; likely the checkpointer’s put or get method raises an exception (e.g., KeyError or ValueError) when the key is missing.
  • Posture — fail-hard: the run cannot start or fails immediately.
  • Operator signal — An exception like KeyError: 'thread_id' or a message “configurable must include thread_id” from the checkpointer or the graph executor.
  • Recovery — Manual step: add "configurable": {"thread_id": "<unique-id>"} to the invocation config.

Decision order mismatch for simultaneous interrupts

  • Trigger — Two or more tool calls are paused at the same super-step. The operator provides a resume dict with "decisions" list whose order does not match the order of actions as they appear in the interrupt request’s tasks (visible via StateSnapshot.tasks).
  • Guard — The source does not show any validation for decision order. The only documentation is a warning: “Decisions must be provided in the same order as the actions appear in the interrupt request.” No code-level guard exists in the provided context.
  • Posture — fail-soft (incorrect behavior): the agent processes decisions mismatched to tools, potentially rejecting a tool that should have been approved, or passing a wrong edit. The run does not abort but produces unintended side effects.
  • Operator signal — No error is raised. The operator sees unexpected tool results (e.g., a rejected tool was actually executed, or the agent continues with an edited argument that does not match the intended tool).
  • Recovery — Manual step: inspect the StateSnapshot.tasks list (ordered by the framework) to determine the correct sequence, then re‑invoke with a corrected resume decisions list.

Invalid decision type or malformed resume payload

  • Trigger — The resume dict contains a decision with an unrecognised type (e.g., "type": "cancel") or missing required fields (e.g., no "decisions" list, or a decision object without "type"). This can happen if the human‑in‑the‑loop frontend sends malformed data.
  • Guard — No explicit guard in the source. The Command object may accept any dict, and the middleware likely expects a specific schema. No except clause is shown.
  • Posture — fail-hard: the agent invocation raises an error when parsing the resume payload (e.g., KeyError or ValidationError), aborting the run.
  • Operator signal — An exception stack trace indicating a key lookup failure (e.g., KeyError: 'type') or a schema validation error from the middleware.
  • Recovery — Manual step: correct the payload to match the expected format: {"decisions": [{"type": "approve"|"edit"|"reject"|"respond", ...}]}.

In-memory checkpointer state lost after process restart

  • Trigger — The agent uses InMemorySaver (or any non‑persistent checkpointer) as its only checkpointer. A human‑in‑the‑loop interrupt occurs, the run pauses, and the hosting process is restarted before the operator resumes.
  • Guard — No guard; the source presents InMemorySaver as a valid choice without warning about durability, though it is inherently ephemeral. The documentation for checkpointers describes them as storing “memory” but does not guarantee persistence across process restarts for InMemorySaver.
  • Posture — fail-hard: the interrupted run is permanently lost. No state can be resumed; the thread ID becomes orphaned.
  • Operator signal — When trying to resume, get_state returns an empty or missing StateSnapshot (e.g., None or a snapshot with no parent_config and no tasks), or the checkpointer raises an error that the thread ID does not exist.
  • Recovery — Manual step: replay the entire run from scratch or switch to a durable checkpointer (e.g., SqliteSaver or a cloud‑backed saver) and restart the process to retain state.

Interrupt predicate incorrectly returns false

  • Trigger — The interrupt_on mapping uses a predicate (e.g., a callable or a simple True/False) that evaluates a tool’s arguments and returns False when it should return True. For example, a predicate meant to pause on write_file with a path containing “/etc/” fails to detect the path due to a regex error.
  • Guard — The predicate itself is the only guard, but its correctness is not enforced by any framework code shown in the context. The source provides no validation or testing of the predicate logic.
  • Posture — fail-soft (silent bypass): the tool executes without human review, contrary to the intended policy. The run continues normally, unaware of the missed interrupt.
  • Operator signal — No alert is raised. The operator may only notice later (e.g., an unintended file was written) by auditing logs or tool call results. There is no interrupt‑related log or metric generated.
  • Recovery — Manual step: review and fix the predicate logic, then if needed, replay the affected run using “time travel” (get_state to a prior checkpoint) and fork the state to apply the correct interrupt.

06. Durable Execution

An agent uses a checkpointer to save its state. After each step, the runtime records the state. It stores that state in a database under a thread identifier. A thread groups all interactions in a single conversation. So if the run stops for any reason, the agent can resume exactly where it left off. It does not need to start over from scratch. The checkpoint holds the full message history. That includes human inputs and model responses. The agent can access the whole context for that thread. Short term memory is kept at thread level persistence. This keeps different conversations separate. The checkpointer stores state so the thread can be resumed at any time. That is the key benefit. The trade off is that each step must be safe to replay, but that is a detail for developers. The point is reliability. The agent can survive long waits. Because the state is saved, it never loses its place. Every interaction is recorded permanently. Even long conversations are preserved. They do not get lost. The agent can pick up later and continue smoothly. The system works across many turns in a thread. It makes long running agents practical.

Retrieving the latest state snapshot by thread identifier.

python

config = {"configurable": {"thread_id": "1"}}
graph.get_state(config)

# get a state snapshot for a specific checkpoint_id
config = {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}}
graph.get_state(config)
In plain words

Imagine reading a long, choose-your-own-adventure book, and every time you finish a page, you place a bookmark that records not just the page number but also the exact paragraph, the choices you’ve made, and which turns are still ahead. That bookmark is the checkpointer: it saves the agent’s entire state after every step so that if the power goes out, the book drops on the floor, or your phone dies, you can open to the exact spot and keep reading as if nothing happened. This subsystem exists to guarantee that no work is ever lost and the agent never needs to start a conversation from scratch.

After each action, the runtime writes a checkpoint into a database, tying it to a thread identifier—like a book’s chapter number that groups every exchange in one conversation. Each checkpoint stores the full message history (human inputs and model responses) and includes fields such as next (which nodes remain to run), tasks (the precise jobs waiting to execute), and config (the thread’s ID plus a unique checkpoint ID). You can retrieve the entire timeline for that thread by calling get_state_history, which returns snapshots ordered from newest to oldest, so you can rewind to any earlier point.

The non-obvious rule is that each checkpoint also keeps a parent_config pointing to its predecessor—except for the very first one, which is None. This creates a linked chain of saved states, meaning the system can resume not just from the last checkpoint but from any intermediate step in the thread’s history. If a subgraph is running, the tasks field holds a subgraph snapshot (state), letting the agent resume even inside a delegated sub-task’s own context. Without this, an interruption would flush all memory: the agent would forget the user’s earlier questions and responses, forcing the user to repeat everything—the very failure this bookmark prevents.

System design

In the durable execution subsystem described, the ordered mechanism proceeds as follows: an agent's graph is invoked with a checkpointer and a config containing a thread_id. At each super‑step, after nodes complete their computation, the runtime writes a checkpoint—a snapshot of the full state, including all message history across human inputs and model responses. The BaseCheckpointSaver interface persists that state to a database keyed by the thread identifier. The checkpoint is recorded either synchronously (before the next super‑step begins) if durability="sync" is chosen, or asynchronously via durability="async". Should a node fail during a super‑step, the system stores “pending writes” from any other nodes that succeeded, so those writes need not be recomputed on recovery. On a crash, the graph can be resumed from the last fully persisted checkpoint, restoring the thread’s accumulated context exactly as it was.

The invariant the design preserves is checkpoint‑based fault tolerance and recoverability. The source states that checkpoints are persisted and “can be used to restore the state of a thread at a later time.” While not exactly‑once execution—nodes may re‑run after a failure—the guarantee is that the state is restored to a consistent snapshot at a super‑step boundary. Combined with pending writes, the system ensures that no successful node’s output is lost even if another node in the same super‑step fails. This design also enables “time travel” (replaying or forking from any checkpoint) and keeps short‑term memory confined to a single thread, keeping different conversations isolated.

The key trade‑off is between performance and durability, explicitly embodied in the three durability modes: "exit", "async", and "sync". "exit" persists only on successful exit or interrupt, giving the best performance for long‑running graphs but rejecting any recovery from mid‑execution crashes. The obvious alternative—persisting after every step—introduces overhead that the design accepts via "sync" mode, which “ensures that LangGraph writes every checkpoint before continuing execution, providing high durability at the cost of some performance overhead.” The cost avoided by this trade‑off is the loss of intermediate state: without synchronous writes, a process crash could leave the thread without the latest checkpoint, forcing a full restart from the last saved boundary. The DeltaChannel mechanism further optimizes storage by storing only incremental deltas for append‑heavy channels, trading some latency for reduced checkpoint size.

A concrete failure mode occurs when using durability="async": if the process crashes during execution after the step runs but before the asynchronous checkpoint write completes, the checkpoint is never written. The operator would see that the graph’s run stops without a final checkpoint. When they attempt to resume the thread by passing the same thread_id, the system loads the previous checkpoint, revealing that the last step’s output is missing. The signal is a state snapshot that does not include the latest changes, and the operator can verify this by inspecting the thread’s history via StateSnapshot. The source notes that even in "async" mode “there’s a small risk that LangGraph does not write checkpoints if the process crashes during execution.” Using a persistent checkpointer like AsyncPostgresSaver rather than InMemorySaver is recommended in production to survive such crashes, though the risk of missing an asynchronous write remains.

Interview Q&A

Q — How does the agent ensure it can resume exactly where it left off if the run stops mid-execution?
A — The runtime uses a checkpointer to save a StateSnapshot after each super‑step. The snapshot is stored under a thread_id that groups all interactions in a single conversation. When the run resumes, the system loads the latest checkpoint for that thread and replays any pending writes from the last super‑step, so no work from successfully completed nodes is lost.
Follow-up — What mechanism prevents re‑execution of nodes that already wrote their output?
A — Pending writes are stored as tasks: if a node fails mid‑super‑step, the successful nodes’ writes are kept and do not need to be recomputed when the graph resumes.
Weak answer misses — The key detail of pending writes and the fact that they are not full StateSnapshot checkpoints but task-level storage.


Q — Why does the checkpointer require a thread_id rather than saving state in a single global store?
A — Threads provide isolation between different conversations; each thread contains the accumulated state of one sequence of runs. The system enforces this by mandating a thread_id in the config ({"configurable": {"thread_id": "1"}}), and short‑term memory lives at the thread level so that separate conversations remain separate.
Follow-up — How do you retrieve the full history of a thread’s state?
A — Call graph.get_state_history(config), which returns a list of StateSnapshot objects in reverse chronological order (most recent first).
Weak answer misses — That the chronological ordering has the most recent checkpoint first, and that StateSnapshot includes fields like parent_config, metadata, and tasks.


Q — How does the system handle node failures without forcing a full restart from the beginning?
A — Checkpointing provides fault‑tolerance: if a node fails at a given super‑step, the graph can restart from the last successful checkpoint. Additionally, pending writes from any other nodes that succeeded in that super‑step are stored as tasks, so those nodes do not need to be re‑run.
Follow-up — What exactly is saved for those successful nodes if the checkpoint only stores super‑step boundaries?
A — The pending writes are saved separately from full StateSnapshot checkpoints; they are task‑level writes used for fault tolerance and are not full snapshots.
Weak answer misses — The distinction between full checkpoints at super‑step boundaries and pending task writes for intra‑step recovery.


Q — Why is the state saved at super‑step boundaries instead of after every individual node?
A — Full StateSnapshot checkpoints are written after each super‑step to keep the snapshot overhead manageable. Pending writes record node outputs within a super‑step, so if a later node fails, only the failed node and its successors need to be recomputed, not the whole super‑step. This design balances storage cost with recovery granularity.
Follow-up — Can you replay execution from an earlier checkpoint to explore alternative paths?
A — Yes, checkpointers support time travel: you can fork the graph state at any past checkpoint (identified by its checkpoint_id) to try different trajectories.
Weak answer misses — The explicit mechanism of checkpoint_id and parent_config in StateSnapshot.


Q — How does the system maintain conversation memory across multiple human interactions in the same thread?
A — The checkpointer persists the thread’s state between invocations. When a follow‑up message is sent with the same thread_id, the graph loads the last checkpoint, which contains the full message history (human inputs and model responses), and continues execution from that state.
Follow-up — What field in the checkpoint indicates whether a new run is a continuation or a fresh start?
A — The metadata.source field, which can be "input" (new invocation), "loop", or "update", distinguishes the origin of the execution.
Weak answer misses — The metadata dictionary inside the StateSnapshot, and specifically the source key.

Failure modes

Failure 1: Async durability crash before checkpoint write

  • Trigger – A process crash occurs during the "async" durability mode while the checkpointer has not yet finished writing the current checkpoint.
  • Guard – None. The source explicitly states that in async mode “there’s a small risk that LangGraph does not write checkpoints if the process crashes during execution” and identifies no retry, catch block, or fallback for this race.
  • Posture – Fail-soft (the run is lost; the graph has no record of the incomplete step, but the system continues accepting new runs).
  • Operator signal – No checkpoint is created for the interrupted super‑step; get_state_history will show the thread state only up to the last fully persisted checkpoint, and the operator sees a missing checkpoint_id for that step.
  • Recovery – Manual: the operator must detect the gap (e.g., by observing that source: 'loop' checkpoints are fewer than expected) and re‑submit the input that triggered the failed step. No automatic retry.

Failure 2: Exit durability crash before final checkpoint

  • Trigger – The graph is configured with durability="exit", and the system crashes before the run exits (successfully, with an error, or with a human interrupt).
  • Guard – None. The source states that in "exit" mode “intermediate state is not saved, so you cannot recover from system failures … that occur mid‑execution”. No error handler or retry mechanism is provided.
  • Posture – Fail-soft (the run is lost; no state is written for that thread).
  • Operator signal – The thread’s StateSnapshot has no checkpoint after the initial input; get_state_history returns only the source: 'input' checkpoint.
  • Recovery – Manual: the operator must restart the graph with the original input and a new thread_id (or overwrite the existing thread).

Failure 3: Checkpoint database write failure (disk full or connection loss)

  • Trigger – The BaseCheckpointSaver implementation (e.g., InMemorySaver if configured to a filesystem backend) throws an exception when attempting to persist the checkpoint, for example OSError or a database connection timeout.
  • Guard – None. The source provides no except clause, retry loop, or fallback around the checkpoint write call; the mode ("sync", "async", "exit") dictates when the write happens, not what happens if it fails.
  • Posture – Fail-hard: the exception propagates and the run aborts (the graph does not continue to the next step).
  • Operator signal – An unhandled exception traceback containing the error from the checkpointer backend (e.g., sqlite3.OperationalError or InMemorySaver raising a write error).
  • Recovery – Manual: the operator must fix the storage issue (e.g., free disk space, restart database), then replay the graph from the last successful checkpoint using get_state_history to find the parent_config.

Failure 4: DeltaChannel append‑state corruption due to beta bug

  • Trigger – The graph uses DeltaChannel (requires langgraph>=1.2, beta) with append‑heavy state; an undiscovered API bug writes an incremental delta that cannot be correctly applied when recovering state from a checkpoint.
  • Guard – None. The source includes a warning (“currently in beta. The API may change”) but no runtime guard, validation, or fallback (e.g., no version check or consistency hash).
  • Posture – Fail-soft (the checkpoint is stored, but get_state_history returns inconsistent values; the next step may read corrupted state or raise an internal error when resolving deltas).
  • Operator signal – A ValueError or IndexError during state resolution, or the StateSnapshot.values field contains unexpected data (e.g., duplicated entries) for channels using DeltaChannel.
  • Recovery – Manual: downgrade to full‑value checkpoints by removing DeltaChannel usage, or wait for a patched release; replay from an earlier parent checkpoint that does not use the beta feature.

Failure 5: Thread ID collision overwrites conversation history

  • Trigger – Two independent runs are invoked with the same thread_id (e.g., {"configurable": {"thread_id": "1"}}) without explicitly creating a new thread; the second run writes its initial checkpoint, which becomes the new head of that thread and overwrites the previous conversation’s state.
  • Guard – None. The source does not provide a uniqueness check or automated thread‑ID generation; it only states that “you must specify a thread_id”.
  • Posture – Fail-closed (the old conversation is lost; the thread now holds only the new run’s state).
  • Operator signalget_state_history for thread_id "1" no longer contains checkpoints from the earlier conversation; the first checkpoint has source: 'input' with the new input, and parent_config is None.
  • Recovery – Manual: the operator must use a different thread_id for each new conversation, or implement an external thread‑ID registry. No automatic recovery; the overwritten state is unrecoverable.

Failure 6: Pending writes cause partial state on resume after a node failure

  • Trigger – A node fails mid‑execution at a super‑step; the runtime stores pending writes from nodes that completed successfully. When the graph resumes from that super‑step, the successful nodes are not re‑run, but if the pending writes were incomplete (e.g., a crash occurred while writing them), the resumed state may be missing some updates.
  • Guard – The pending writes mechanism itself (described in the “pending writes” section) is the intended guard, but there is no additional validation that pending writes were fully persisted before the resume.
  • Posture – Fail-soft (the graph runs, but the resumed state may be missing writes from the previously successful nodes, leading to incorrect downstream results).
  • Operator signal – The StateSnapshot for the resumed super‑step shows values that are inconsistent with the expected output of the successful nodes; the operator may notice a gap when comparing with get_state_history from a healthy run.
  • Recovery – Manual: the operator must restart the entire thread from an earlier checkpoint (found via get_state_history) and re‑run the failed run, possibly with the failing node’s logic fixed.

07. Short And Long Memory

Agents use two kinds of memory. The first is short-term memory. It keeps track of the current chat. It holds the list of messages that go back and forth between the human and the model. This list grows as you talk. But models can only hold so much text at once. When the list gets too long, the system must drop or shrink older messages to make room. So short-term memory sticks to one thread, one session. It helps the model follow that single chat.

Long-term memory works in a new way. It saves what the agent learns across many chats. It does not store raw messages. Instead, it stores key facts. A fact might be a user's taste, or something that happened before. The agent writes these facts to its own store. There are a few kinds. Some hold plain facts. Some hold past events. Some hold rules to follow. The model pulls these facts back later to make its replies fit you.

So where do the two kinds live? Short-term memory lives inside the thread. A checkpointer saves it, so the thread can pick up again. Long-term memory lives outside any one thread. The agent can call it up in a brand-new chat. That means it can recall something from yesterday, even when today's chat starts fresh. Short-term memory handles the flow of the moment. Long-term memory holds knowledge that lasts. Each does a different job. So they are kept apart.

Short-term memory lives in the state; long-term memory is stored and retrieved via a store.

python
def call_model(state: State, store: BaseStore):
    namespace = ("agent_instructions", )
    instructions = store.get(namespace, key="agent_a")[0]
    prompt = prompt_template.format(instructions=instructions.value["instructions"])
    # ...

def update_instructions(state: State, store: BaseStore):
    namespace = ("instructions",)
    instructions = store.search(namespace)[0]
    prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
    output = llm.invoke(prompt)
    new_instructions = output['new_instructions']
    store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
    # ...
In plain words

Imagine you’re a busy assistant with two tools: a sticky note for the current conversation and a filing cabinet for facts you learn across all your chats. The sticky note holds the recent back-and-forth messages so you can follow the thread, but it’s small—when too many messages pile up, you have to toss the oldest ones to keep from overflowing. The filing cabinet, by contrast, stores important nuggets like “the boss prefers bullet points” and keeps them handy for every future chat. That’s the core idea: short-term memory tracks the live conversation in a single session, while long-term memory remembers key details across sessions.

In everyday terms, short-term memory works by maintaining a list of every message between you and the human. As you talk, the list grows. The system uses a checkpointer to save that list to a database, so if you’re interrupted, you can pick up right where you left off. But because the model can only process so much text at once (its context window), older messages must be dropped or condensed when the list gets too long. Long-term memory skips raw messages and instead stores distilled facts—like user preferences or learned rules—in a store that can be pulled up in any thread, at any time.

The non-obvious rule is that the context window isn’t infinite, so short-term memory deliberately discards old messages. This isn’t a bug: it’s a hard limit the model itself imposes. Without the checkpointer saving the state, you’d lose the entire conversation the moment you hit that limit. And without long-term memory’s store, the agent would forget your name, your favorite format, or key lessons from yesterday’s chat, starting from scratch each time. That failure feels like a friend who can’t remember anything you told them five minutes ago—frustrating and useless for any real relationship.

System design

The subsystem begins with a checkpointer that persists the agent’s state per thread. On each invocation, the state is read at the start of every step, capturing the full message history (human and model messages). As the conversation unfolds, the agent updates the state; when a step completes or the graph is invoked again, the checkpointer writes the new state back, enabling resumption at any point. If a node fails mid‑execution at a given super-step, LangGraph records pending writes from any other nodes that completed successfully at that super‑step, so that on resume the successful nodes are not re‑run—only the failed node is retried. For long conversations, the system may apply implicit “techniques to remove or ‘forget’ stale information” (as noted in the short‑term memory documentation), though the exact mechanism is not specified; the core flow remains: read state → execute step → write checkpoint.

The design preserves the invariant of fault‑tolerance and error recovery via checkpointing. Specifically, checkpoints provide “durable execution”: the thread’s accumulated state is durably stored after each step, and if the graph crashes, it can be restarted from the last successful step. This guarantees that no committed work is lost within a super‑step, because pending writes from successful nodes are stored even when a peer node fails. The invariant is not exactly‑once execution of nodes (since a failed node will be retried), but rather that the state of the thread is always consistent and resumable.

The key trade‑off rejects the alternative of keeping the entire conversation history in the LLM’s context window. That naive approach would cause the model to get “distracted” by stale content, suffer slower response times, and incur higher costs as the context grows. Instead, the system externalizes memory to a checkpointer and optionally applies forgetting or summarization strategies, accepting the overhead of an external storage and retrieval step. The cost avoided is the quadratic degradation of LLM performance on long contexts: the model no longer has to process an ever‑growing list of raw messages, which would both degrade quality and increase latency.

A concrete failure mode occurs when a graph node fails mid‑execution at a super‑step. The operator would observe a graph execution error in the logs, such as an exception thrown by a node function. Simultaneously, the system writes pending checkpoints from any peer nodes that completed successfully. The operator would then see a subsequent resume event: the graph restarts from the failed super‑step, skipping the successful nodes and re‑executing only the failed one. The signal is a log entry containing the thread_id and the step number of the failed super‑step, along with a record of pending writes that were stored. No manual intervention is required; the checkpointer automatically recovers state.

Interview Q&A

Q — How does short‑term memory work in a LangGraph agent, and what mechanism keeps the conversation across turns?
A — Short‑term memory is implemented via the graph’s state, which is persisted by a checkpointer. The code shows InMemorySaver() being passed as the checkpointer, and each invocation uses a thread_config with a thread_id (e.g., "1"). When the agent is called again with the same thread_id, the checkpointer restores the prior state so the agent can refer to messages from earlier turns.
Follow-up — What happens if the message list grows too large for the model’s context window?
Weak answer misses — The follow‑up answer must mention the trim_messages middleware (or equivalent built‑in mechanism) that is applied to the agent, as shown in the OpenAI example: middleware=[trim_messages].

Q — Why does LangGraph use an explicit checkpointer (InMemorySaver) for short‑term memory instead of just storing the state in‑memory on the agent object?
A — A checkpointer decouples state persistence from the agent’s lifecycle, enabling the agent to be resumed after any interruption and supporting multiple concurrent threads. The source shows thread_config is passed per invocation, and the checkpointer stores state keyed by thread_id. This design also allows swapping to a persistent database without changing agent logic.
Follow-up — Could you achieve the same effect by manually passing a messages list each turn?
Weak answer misses — The answer must cite that the checkpointer automatically handles state updates at each step (‘‘State is persisted … so the thread can be resumed at any time’’) and that manual passing would lose the ability to resume interrupted steps.

Q — How does LangGraph’s Store implement long‑term memory, and what operations does it expose for retrieval?
A — The Store supports both semantic search via SearchOp.query and filtering by content via SearchOp.filter. These operations let the agent retrieve relevant memories from a collection across sessions. The source also warns that using a collection of memories can make it challenging to provide comprehensive context because individual memories may not capture full relationships between them.
Follow-up — Why would a unified profile approach be preferred over a collection of semantic memories?
Weak answer misses — The weak answer overlooks the warning that a collection of memories may lack contextual relationships, as stated: “this structure might not capture the full context or relationships between memories.”

Q — What is episodic memory in the context of AI agents, and how is it typically implemented?
A — Episodic memory involves recalling past events or actions. The source references the CoALA paper, which distinguishes semantic memory (facts) from episodic memory (experiences). In practice, episodic memories are often implemented through few‑shot example prompting, where the agent learns from past sequences to perform tasks correctly by updating the prompt with input‑output examples.
Follow-up — Why would you use few‑shot examples instead of storing the entire past episode in the Store?
Weak answer misses — A shallow answer fails to mention that few‑shot learning “lets you ‘program’ your LLM by updating the prompt” and that it’s often easier to “show” than “tell,” as the source quotes Karpathy.

Q — You mentioned that short‑term memory uses trim_messages to keep the state within the model’s context window. How does the system decide which messages to drop, and what trade‑off does that introduce?
A — The trim_messages middleware (shown in the OpenAI example) is applied to the agent to automatically shrink the message list. The source does not detail the exact trimming strategy, but it implies that older messages are removed to make room. The trade‑off is that the agent may lose context from earlier turns, which can affect its ability to answer questions that depend on long‑forgotten parts of the conversation.
Follow-up — Could you use a more sophisticated strategy like summarization instead of dropping messages?
Weak answer misses — The key missing detail is that the source only shows the trim_messages middleware and does not mention any built‑in summarization strategy; the answer must stay grounded in the provided code snippets.

Failure modes

Missing Thread ID

  • Trigger — The agent is invoked without a thread_id in the configurable portion of the config. The source states: “you must specify a thread_id as part of the configurable portion of the config”.
  • Guard — No explicit exception handler is shown in the source. The checkpointer (e.g., InMemorySaver) likely raises a runtime error when thread_id is absent.
  • Posture — Fail-hard: the run aborts immediately because the checkpointer cannot associate state with a thread.
  • Operator signal — An error message indicating that thread_id is required, likely a KeyError or a similar validation error.
  • Recovery — Add a valid thread_id to the config ({"configurable": {"thread_id": "1"}}) and re-invoke the agent.

Context Window Overflow Without Trigger

  • Trigger — The message list in short-term memory exceeds the model’s token limit, yet the trigger condition on SummarizationMiddleware (e.g., ("tokens", 4000)) never fires—perhaps because token counting is inaccurate or the limit is set too high.
  • Guard — No guard is present. The middleware relies solely on the trigger parameter; if it does not activate, no summarization or trimming occurs.
  • Posture — Fail-hard: the LLM call fails with a context‑length error, aborting the run.
  • Operator signal — The model returns a “maximum context length exceeded” error, visible in logs or trace output.
  • Recovery — Manually trim the message list, adjust the trigger threshold, or implement additional input validation outside the middleware.

Summarization Model Failure

  • Trigger — The model assigned to SummarizationMiddleware (e.g., "gpt-5.4-mini") is unavailable, returns an error, or produces a malformed summary that cannot be parsed.
  • Guard — No exception handler or fallback is shown in the source. The middleware directly calls the summarization model without a retry or try‑except block.
  • Posture — Fail-hard: the summarization step fails, leaving the graph in an undefined state and preventing execution from continuing.
  • Operator signal — An error from the model call (e.g., HTTP 500, timeout, or invalid response) recorded in the trace.
  • Recovery — Restart the run after the model issue is resolved, or replace the summarization model with a more reliable one.

Checkpoint Write Failure in Sync Durability

  • Trigger — The graph is executed with durability="sync", and the checkpointer (e.g., InMemorySaver or AsyncPostgresSaver) fails to write the checkpoint—for instance, because the storage backend is unreachable or out of space.
  • Guard — No guard is shown. The sync persistence mode writes “synchronously before the next step starts” without a documented retry or fallback in the source.
  • Posture — Fail-hard: execution halts because the next step depends on the successful checkpoint.
  • Operator signal — An exception from the checkpointer’s write operation, visible in logs as a database or I/O error.
  • Recovery — Resolve the storage issue (e.g., reconnect database, free space) and re‑run from the last successful checkpoint.

Pending Writes Loss on Process Crash

  • Trigger — A node fails mid‑execution at a super‑step; successful nodes have their outputs stored as pending writes. The process crashes before these pending writes are committed to the checkpointer.
  • Guard — The source describes the pending‑writes mechanism but does not show a guard for the crash window: “there's a small risk that LangGraph does not write checkpoints if the process crashes during execution” (in the "async" durability mode; similar risk exists before pending writes are stored).
  • Posture — Fail-soft: execution can be resumed from the last fully committed checkpoint, but the outputs of the successful nodes in the failing super‑step are lost.
  • Operator signal — No immediate error; the operator notices missing state or unexpected results when the graph resumes from an earlier step.
  • Recovery — Manually re‑run the failed super‑step by resuming execution from the last checkpoint, acknowledging that some side effects may be re‑executed.

08. Many Agents One System

When one agent tries to handle too many tasks, its memory gets cluttered. The model struggles to keep track of everything. It starts making worse decisions because the context window fills up with stale information. That is the core problem. A single broad agent becomes inefficient.

A better approach is to split the work among several focused agents. Each one owns a smaller domain. In one common design, a central coordinator looks at each task and routes it to the agent best suited to handle it. But that coordinator holds no memory between requests. Every new task needs a fresh routing decision from the model. That costs extra time and money.

Another design uses direct handoffs between peers. One agent stays active and passes control straight to another. The shared state carries over, so the second agent does not start from nothing. On repeat requests, that saves a round of routing. The needed context is already in the conversation, so the agent can act without loading everything again.

You can also give each agent its own isolated context. That keeps their reasoning clean and separate. But the isolation has a price. Each fresh start repeats work and adds more model calls.

The trade-off is clear. Stateful patterns keep costs down on repeat work, but they need more moving parts to coordinate. Isolation keeps things clean, but it costs more calls. So the real choice is between saving calls and keeping strong boundaries between agents.

An agent uses a checkpointer to persist short-term memory across interactions within the same thread.

python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="google_genai:gemini-3.5-flash",
    tools=[get_user_info],
    checkpointer=InMemorySaver(),
)

thread_config = {"configurable": {"thread_id": "1"}}
response = agent.invoke(
    {"messages": [{"role": "user", "content": "Hi! My name is Bob."}]},
    thread_config,
)["messages"][-1].content

response = agent.invoke(
    {"messages": [{"role": "user", "content": "What's my name?"}]},
    thread_config,
)["messages"][-1].content
In plain words

Imagine a small restaurant run by a single chef who tries to cook every dish, take every order, and remember every modification from the morning rush. By dinner, the chef’s notepad is a mess of crossed-out orders, and the cook mixes up ingredients—making poor decisions because the overwhelming number of tasks has filled their mental workspace with stale information. That’s the core problem this subsystem solves: when one agent tries to handle too many tasks, its memory gets cluttered and performance drops. The solution is to split the work among several focused agents, each owning a smaller domain, so no single brain gets overloaded.

In one common design, a central coordinator acts like an expeditor at the restaurant’s pass: it looks at each incoming task and routes it to the best-suited line cook (the specialized agent). But here’s the catch—the expeditor holds no memory between requests. Every new order triggers a fresh routing decision, even if it’s identical to one it just handled. The source calls this the Router pattern: each request requires an initial LLM routing call (one model call to decide who goes next) before the chosen agent executes its tool. For a one-shot order, that’s three model calls total. But if a customer repeats the same order, the Router still spends three calls again—it’s stateless and cannot reuse past assignments.

The non-obvious detail is why statelessness both helps and hurts. By forgetting everything between requests, the Router never accumulates stale information—its context window stays clean. However, that same forgetting forces it to pay the routing cost on every single turn, even for identical repeats. In contrast, stateful patterns like Handoffs or Skills let the active agent stay in place, saving one call on repeat orders by skipping the handoff (the expeditor doesn’t need to redirect). Without this subsystem’s specialized routing, a single broad agent would trudge forward with an ever-growing memory of irrelevant tasks, making worse decisions as its context window fills—the digital equivalent of a chef who refuses to hire line cooks and eventually burns every dish.

System design

The Router pattern addresses the core problem of a single, broadly-scoped agent whose context window becomes cluttered with stale information, degrading decision quality. The ordered mechanism begins with a routing step that classifies each incoming task based solely on its current input, without any memory of prior requests. This step directs the task to one or more specialized agents, each owning a narrow domain. Those agents execute in isolation, then the results are synthesized into a combined response. On failure—for instance, if the classification step produces no confident match—the router may either default to a generic agent or return an error; because the coordinator holds no conversation state, no retry logic is built in, and the task must be re‑routed from scratch by the caller. The entire flow is stateless and deterministic per invocation.

The invariant the design preserves is statelessness of the routing step—each request undergoes an independent classification with no dependency on prior turns. This guarantees that the coordinator never accumulates stale context and that specialized agents remain focused on their narrow domain. The source explicitly notes that in the Router pattern, “a routing step classifies input and directs it to one or more specialized agents,” and that key insight for single tasks is that the pattern requires exactly three calls (classification, then specialized agent execution, then synthesis). This statelessness ensures that no memory overhead or context-distraction leaks across requests, preserving the clarity of each agent’s operational boundary.

The key trade‑off is efficiency gains from stateless routing versus loss of cross‑turn awareness. The obvious alternative rejected is the Subagents pattern, where a main agent holds conversation context and invokes stateless sub‑agents as tools. In Subagents, the main agent’s memory allows it to reuse context from earlier turns, but at a cost of four calls per task (one extra for coordination), and for repeat requests the count doubles to eight total calls because sub-agents are “stateless by design—each invocation follows the same flow.” The Router pattern avoids this overhead by eliminating the main agent’s memory entirely, requiring only three calls per task and six for a repeat request. By rejecting memory, the design sidesteps the cost of “getting ‘distracted’ by stale or off‑topic content” that plagues context‑first approaches, instead trading away the ability to recall previous routing decisions.

A concrete failure mode occurs when the routing step misclassifies an input due to ambiguous phrasing and lack of conversation history. The operator would see that the synthesized response is factually incorrect or irrelevant—for example, a task about “buy coffee” might be routed to a search‑query agent instead of a purchasing agent because the classifier has no record that the user previously bought coffee. The signal is an inconsistent or wrong output, which could be logged as a classification‑confidence alert or an error in the final synthesis. Without memory, the system cannot correct this course on its own; the operator must intervene by manually re‑routing or adjusting the classifier’s prompt in a separate operational cycle.

Interview Q&A

Q — What is the core design of the Router pattern, and how does it prevent a single agent’s context from becoming cluttered?
A — The Router pattern uses a central coordinator that, on each request, makes an LLM routing call to decide which specialized agent should handle the task. That coordinator is stateless — it holds no memory between requests — so every new task gets a fresh routing decision. This prevents the context window from filling with stale information from prior tasks, avoiding the degradation you see when a single broad agent accumulates too many tools and decisions.
Follow-up — Doesn’t a stateless router waste calls on repeated identical requests?
A — Yes, each repeated request requires its own LLM routing call, so the Router costs 3 calls per request (3 → 6 for two turns), whereas stateful patterns like Handoffs or Skills reuse context and drop to 2 calls on the repeat turn.
Weak answer misses — Fails to name that the Router’s cost per request is exactly 3 calls in the one-shot case, and that Handoffs and Skills save 40–50% of calls on repeat requests by retaining state.


Q — Why would you design the coordinator as stateless (Router) rather than keeping a persistent agent that remembers past interactions (like Handoffs or Skills)?
A — The Router pattern’s statelessness provides strong context isolation — no stale or irrelevant history leaks between requests, which is critical when tasks come from unrelated domains or users. By contrast, Handoffs and Skills keep the agent still active and reuse loaded context, which saves calls but risks context pollution if the system must handle one-off requests with very different scopes.
Follow-up — Doesn’t that isolation come at the cost of parallel tool calling across domains?
A — No, the Router actually supports parallel execution (checked in the “Optimize for” table) because after the routing call, multiple specialized agents can run independently, unlike Handoffs which must execute sequentially.
Weak answer misses — Ignores that the Router is explicitly listed as good for large-context domains and parallel execution, two scenarios where stateless routing shines.


Q — How does the Router pattern behave under a repeat request scenario, and what concrete call-count evidence supports its performance difference?
A — For a repeat request (e.g., “Buy coffee” twice), the Router uses 3 calls per request (6 total) because each request forces a fresh LLM routing call — the router is stateless. In contrast, Handoffs and Skills reuse the loaded agent state and skip the routing call on the second turn, needing only 2 calls (5 total). The tables show exactly: Router = 3 → 6, Handoffs/Skills = 3 → 5.
Follow-up — Could you make the Router stateful by wrapping it as a tool inside a stateful agent?
A — Yes, the context explicitly notes “Can be optimized by wrapping as a tool in a stateful agent,” which would convert it to a Handoffs-like pattern, saving calls on repeats.
Weak answer misses — Omits the concrete numbers: 3 calls → 6 total for Router vs. 2 calls → 5 total for Handoffs/Skills, and the fact that the Router is best for single requests but not for repeat requests.


Q — In a multi-domain scenario (e.g., comparing Python, JavaScript, Rust), how does the Router pattern compare to Skills in terms of calls and token usage, and why?
A — For multi-domain, Router uses 5 calls and 9K tokens while Skills uses 3 calls but 15K tokens. Skills accumulates context (all language documentation) into a single agent, leading to high token usage. Router, being stateless, routes each domain to a separate agent, keeping individual contexts small — its parallel execution lets it complete with fewer tokens despite more calls.
Follow-up — Skills has fewer calls — why would anyone choose Router over it for such a task?
A — Router’s 9K tokens vs. Skills’ 15K tokens means Router avoids context oversaturation, making it better for large-context domains where a single agent would exceed the window.
Weak answer misses — Fails to cite the exact token numbers (9K vs. 15K) and the fact that Skills’ high token usage comes from context accumulation while Router leverages parallel execution from its stateless coordinator.

Failure modes

Failure 1: Redundant Routing Due to Stateless Coordinator

Trigger: A user repeats the same request in the same conversation (e.g., “Buy coffee” followed by “Buy coffee again”). The coordinator, having no memory between requests, treats the second task as entirely new and executes a fresh routing decision.

Guard: No guard exists. The coordinator’s design explicitly “holds no memory between requests” (source statement from the Router pattern description). Neither a retry, fallback, nor validation is defined for this situation.

Posture: Fail‑soft — the system continues to operate, but each repeated request requires a full routing cycle, wasting calls and increasing latency. The coordinator does not abort or refuse the repetition.

Operator signal: The call‑count metric from the summary table shows that for a repeat request, the Router pattern costs 6 total calls (3 initial + 3 repeat), compared to only 5 calls for the Handoffs or Skills patterns. An operator monitoring call volume would see this excess.

Recovery: No automatic recovery is offered. The operator must manually redesign the system by switching to a pattern that retains conversation context (e.g., Handoffs or Skills), or by adding a memory layer such as MemoryMiddleware (though that middleware belongs to a different agent framework and is not part of the current subsystem). Without intervention, every repeated request incurs the same overhead.


Failure 2: Misrouting Due to Ambiguous Task Without History

Trigger: A user submits a task that is ambiguous without preceding context (e.g., “What about the other one?” when only the current request exists). Because the coordinator has no memory of prior interactions, it cannot resolve the ambiguity and may route the task to an inappropriate subagent.

Guard: No guard is present. The source describes no validation, fallback, or rerouting mechanism for ambiguous inputs. The Router pattern relies solely on the task content as presented.

Posture: Fail‑soft — the system does not crash, but the misrouted subagent returns incorrect or irrelevant output, which is then passed to the user. The error propagates silently.

Operator signal: The operator would observe a lack of user satisfaction (e.g., negative feedback, repeated similar queries) or a spike in low‑quality outputs. No explicit error log is generated because the system does not detect misrouting.

Recovery: No automated recovery exists. The operator must either implement a context‑retaining pattern (e.g., Handoffs) or add a disambiguation step (e.g., a clarification tool) in the coordinator’s prompt. Without manual changes, every ambiguous request will be misrouted.


Failure 3: Unhandled Subagent Failure in a Stateless Flow

Trigger: A subagent invoked by the coordinator throws an exception or times out (e.g., a tool search fails, or the subagent’s LLM returns an error). Because the coordinator holds no memory of previous routing decisions, it cannot distinguish a transient failure from a permanent one and has no retry logic.

Guard: No guard is defined for this failure. The source does not list any except clause, retry wrapper, or fallback function attached to the Router pattern. The SubAgentMiddleware (shown in the Subagents pattern code) is not used here; the Router pattern is separate.

Posture: Fail‑hard — the failure propagates to the user without recovery. The coordinator likely returns the error message directly, aborting the task.

Operator signal: The operator sees an error trace or an unhandled exception in the logs, originating from the subagent. No retry count or backoff is logged because none is implemented.

Recovery: Manual intervention is required. The operator must restart the task, possibly after fixing the subagent or adding retry logic. Without a guard, every subagent failure is fatal for that request.


Failure 4: Coordinator Bottleneck Under High Concurrency

Trigger: Many tasks arrive simultaneously (e.g., during peak usage). The coordinator, as a single point of routing, must process each sequentially (or with limited parallelism). The stateless design prevents it from batching or deduplicating repeated requests, so it repeatedly re‑evaluates the same routing for identical incoming tasks.

Guard: No guard exists for concurrency. The source describes no load‑balancing, queue, or bandwidth limitation on the Router pattern. The coordinator is a single actor with no built‑in concurrency control.

Posture: Fail‑soft — the system does not crash, but latency increases significantly. Tasks may queue up and eventually time out. The coordinator continues accepting requests even under overload.

Operator signal: The operator would observe elevated latency metrics, increasing task failure rates due to timeouts, and a growing backlog. No specific log line for “overload” is generated by the pattern.

Recovery: No automated recovery is provided. The operator must scale horizontally (e.g., deploy multiple instances of the coordinator) or switch to a pattern that allows parallel execution (e.g., Subagents or Router with parallel subagent invocation). Without changes, the bottleneck will recur under load.

09. Guardrails For Autonomy

To put safe autonomy into practice, you gate every action that could cause harm. For example, the system can pause a tool call and ask a human to approve it before it runs. You can set conditions that only interrupt when the tool is about to do something risky. A file write that tries to go outside the workspace directory triggers a review. A database query that tries to change data, rather than simply read it, also gets paused. Calls that pass the safety check run without any interruption. That way a reviewer only sees the actions that truly need a decision. The trade off is speed. If you pause every call, the agent becomes slow. By making the guardrails conditional you let safe actions flow automatically while still locking down dangerous ones. Autonomy is earned step by step as you add those checks.

Conditional interrupts pause only risky tool calls, allowing safe ones to pass automatically.

python
from langchain.agents import create_agent, HumanInTheLoopMiddleware, ToolCallRequest

def writes_outside_workspace(request: ToolCallRequest) -> bool:
    path = request.tool_call["args"].get("path", "")
    return not path.startswith("/workspace/")

def is_write_query(request: ToolCallRequest) -> bool:
    query = request.tool_call["args"].get("query", "")
    return not query.lstrip().upper().startswith("SELECT")

agent = create_agent(
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "write_file": {"allowed_decisions": ["approve", "edit", "reject"], "when": writes_outside_workspace},
                "execute_sql": {"allowed_decisions": ["approve", "reject"], "when": is_write_query},
            },
        ),
    ],
)
In plain words

Think of it like a security guard at a building entrance who only stops people carrying suspicious packages, but lets everyone else walk straight in. This subsystem is for making sure an automated system—say, a digital assistant that can write files or query databases—doesn’t accidentally do something harmful before a human checks it.

The system works by watching every action the assistant tries to take. Before a dangerous tool actually runs, the assistant pauses and asks a person to approve it. For example, if the assistant tries to write a file outside its allowed folder, the system calls a headless tool—a tool that doesn't run itself but instead sends a signal to the outside. The system interrupts the run and waits. A human sees the request, decides yes or no, and then resumes the graph, letting the tool complete or not. Safe actions, like reading a database, never pause. Only risky ones trigger the guard.

The tricky part is deciding what counts as risky. The system uses a set of conditions—rules like "only interrupt if the tool tries to change data, not just read it." These conditions are checked automatically before any tool runs. Without this safety net, the assistant could delete a whole database or overwrite critical files without anyone knowing until it's too late. A beginner would feel that failure as a sudden, irreversible mistake—like a prank that erased homework they hadn't saved.

System design

The Guardrails for Autonomy subsystem enforces a human-in-the-loop gate on every tool invocation that could harm the system, using the HumanInTheLoopMiddleware configured via the interrupt_on parameter. The ordered mechanism starts when an agent, such as one built with create_agent, decides to call a tool like write_file or execute_sql. Before the tool runs, the middleware checks the tool name against the interrupt_on mapping. If the mapping allows an interrupt—for example, "write_file": True or "execute_sql": {"allowed_decisions": ["approve", "reject"]}—the agent pauses and surfaces a message constructed from the description_prefix (e.g., "Tool execution pending approval") plus the tool name and arguments. The human reviewer then responds with one of the allowed decisions: approve, reject, edit, or respond. If the tool is mapped to False (like read_data), the call proceeds without interruption. On a failure—such as a connectivity issue with the checkpointing layer (InMemorySaver is shown locally, but production would use a persistent store)—the interrupt request may be lost, but the design preserves a strong invariant: every side-effecting action occurs only after explicit human confirmation when the tool is classified as risky. Safe calls pass through without delay, so the invariant isolates unsafe operations without blocking benign ones.

The core trade-off is speed versus safety. The design accepts that each pause adds latency for risky actions, which is necessary because an untrained model might arbitrarily execute destructive queries or overwrite files outside the workspace. The obvious alternative it rejects is pausing every tool call unconditionally—a pattern that would cripple responsiveness for safe operations like read_data. Instead, the system uses a declarative interrupt_on mapping that explicitly lists which tools require oversight and what decision types are permitted. This rejection avoids the cost of unnecessary human waiting time on every turn, while still catching harmful actions like a write_file that tries to escape allowed directories or an execute_sql that attempts to mutate data. The cost of the alternative would be a system too slow to be usable for autonomous workflows.

One concrete failure mode occurs when the model invokes a tool that is mapped to False but actually performs a dangerous side effect. For example, if a poorly written read_data tool were implemented with write permissions or the developer forgot to add it to interrupt_on, an operator would see no interrupt at all—the action would execute silently. The signal an operator would actually see is the absence of any HumanInTheLoopMiddleware pause in the LangSmith trace, combined with an unexpected state change in the system (e.g., a deleted row or an unauthorized file write). The operator can then inspect the trace output, notice that read_data ran without an interrupt, and identify that the tool was misconfigured or that its classification in interrupt_on was incorrect. This highlights that the guardrail's effectiveness depends entirely on the accuracy of the interrupt_on mapping and the discipline of assigning each tool an appropriate risk level.

Interview Q&A

Q – How does the subsystem decide when to pause a tool call for human review instead of letting it run automatically?

A – The subsystem uses an interrupt_on policy that evaluates each tool call; calls that match the policy trigger an interrupt. The policy can check conditions like file‑write destination or database‑write intent. Calls that evaluate to False are never added to the interrupt batch, so only risky actions are paused.

Follow-up – What data structure holds the pending actions and supported decisions?
A – The interrupt produces a GraphOutput with an interrupts attribute containing action_requests and review_configs that list the allowed decision types.

Weak answer misses – The fact that review_configs is paired with action_requests to define per‑action decisions like approve, reject, or edit.


Q – The design chooses to pause only risky tool calls rather than requiring human approval for every call. Why this way instead of a blanket approval gate?

A – A blanket gate would slow the system unnecessarily, as stated: “Calls that evaluate to False are never added to the interrupt batch.” The interrupt_on policy allows fine‑grained conditions (e.g., file write outside workspace, database mutation) so that a reviewer “only sees the actions that truly need a decision,” trading speed for safety only when necessary.

Follow-up – How does the system resume an interrupted execution after the reviewer provides a decision?
A – It uses Command with a resume dictionary containing a decisions list, passed to agent.invoke with the same thread config to continue the paused conversation.

Weak answer misses – The resume payload must include the decisions list in the same order as the actions in the interrupt, and each decision can be approve, reject, edit, or respond.


Q – Explain the difference between the reject and respond decision types. When would you use respond?

Areject denies the tool call without executing it and optionally provides a message telling the agent not to retry. respond is used for “ask user” style tools where the tool’s real implementation is the human’s reply; the message is returned directly as a successful ToolMessage without running the tool. Use respond only when the tool is a placeholder for human input, never to deny an action.

Follow-up – What happens if you mistakenly use respond to deny a proposed action?
A – It tells the model that the tool completed successfully, which can lead the agent to believe the action was performed, violating the safety guarantee.

Weak answer misses – The message field in reject is “added to the conversation as feedback,” whereas in respond it is returned as the tool result, and “do not use respond to deny a proposed action.”


Q – How does the subsystem support streaming when human‑in‑the‑loop is active? What must the developer configure to see real‑time updates as the agent pauses and resumes?

A – The developer uses stream() instead of invoke() with stream_mode=['updates', 'messages'] and version="v2". This streams both agent progress and LLM tokens in the unified v2 format, providing real‑time updates when interrupts are hit and when the agent resumes.

Follow-up – What happens to the interrupt result in a streaming context?
A – The GraphOutput with interrupts is still produced but is delivered as part of the stream updates rather than as a single return value.

Weak answer misses – The requirement to use version="v2" explicitly; without it, the streaming mode may not return interrupts in the expected format.

Failure modes

1. Misconfigured interrupt_on mapping

  • Trigger — The developer omits a risky tool from the interrupt_on dictionary (e.g., sets "write_file": False or leaves it out entirely) so the tool proceeds without ever pausing for human review.
  • Guard — No guard exists. The HumanInTheLoopMiddleware only applies interrupts for tools present in interrupt_on; absent or False entries are silently skipped.
  • Posture — Fail‑soft. The system continues execution without the safety gate, degrading to an unsafe but functional run.
  • Operator signal — The operator observes a tool call completing without any interrupt message, and later sees an unapproved side effect in the system state.
  • Recovery — Manual step: the developer must correct the interrupt_on configuration and re-run the agent; no automatic retry occurs.

2. Missing checkpoint saver for human‑in‑the‑loop

  • Trigger — The developer creates an agent with HumanInTheLoopMiddleware but omits a checkpoint saver such as InMemorySaver.
  • Guard — The source states: “Human‑in‑the‑loop requires checkpointing to handle interrupts.” There is no fallback; the middleware will likely raise a runtime error when the first interrupt is attempted.
  • Posture — Fail‑hard. The run aborts at the point of interruption because the graph cannot persist the paused state.
  • Operator signal — An exception trace containing a message about missing checkpointing, or a KeyError when trying to save state. The graph will not resume.
  • Recovery — Manual step: add a checkpoint saver (e.g., InMemorySaver) to the agent’s configuration and restart the run from the beginning.

3. Reject decision without a domain‑specific message for a side‑effecting tool

  • Trigger — A human reviewer uses type: "reject" with no message field (or leaves it empty) for a tool like execute_sql that has side effects.
  • Guard — The middleware supplies a default rejection message that tells the model not to retry “unless the user asks.” The source warns that for side‑effecting tools the default may be insufficient.
  • Posture — Fail‑soft. The tool is not executed, but the default message may cause the agent to incorrectly infer it can retry or ask a follow‑up question, leading to extra cycles.
  • Operator signal — The agent unexpectedly issues another tool call or asks for clarification, increasing latency and potentially causing a different unwanted action.
  • Recovery — Manual step: the reviewer must resubmit a new reject decision with an explicit message (e.g., “This action is unsafe. Do not retry.”). No automatic backoff is defined.

4. Respond decision used in place of reject for a side‑effecting tool

  • Trigger — The human reviewer selects type: "respond" (instead of "reject") for a dangerous tool call, providing a free‑text reply.
  • Guard — The source explicitly says: “Do not use respond to deny side‑effecting tools, because its message is treated as a successful tool result.” There is no runtime enforcement; the middleware returns the message as if the tool had finished successfully.
  • Posture — Fail‑soft. The tool is not actually executed, but the agent receives a “success” result and may proceed as if the side effect occurred, leading to a false sense of safety.
  • Operator signal — The operator sees the graph continue as though the tool completed, but the expected side effect is missing. No error is raised.
  • Recovery — Manual step: the operator must inspect the trace and manually roll back any downstream decisions, then resubmit with a reject decision. No automatic recovery.

5. Decision order mismatch when resuming multiple pauses

  • Trigger — The agent pauses multiple tool calls in one interrupt (e.g., write_file and execute_sql), and the human provides decisions in a different order than the interrupt request.
  • Guard — The source states: “Decisions must be provided in the same order as the actions appear in the interrupt request.” There is no validation or re‑ordering guard; the middleware processes the list as‑is.
  • Posture — Fail‑hard (likely). The mismatched order applies the wrong decision to each tool, potentially approving a dangerous call or rejecting a safe one. The graph may then raise an exception from inconsistent state.
  • Operator signal — Unexpected tool failures or a StateError when the middleware tries to assign a decision to a mismatched tool call. Alternatively, the graph might continue silently with incorrect results.
  • Recovery — Manual step: the operator must use get_state() to inspect the paused tasks, resubmit a correctly ordered resume dictionary, and potentially undo any unintended changes. No automatic retry.