%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#4CAF50','primaryTextColor':'#fff','primaryBorderColor':'#2E7D32','lineColor':'#666','secondaryColor':'#FF9800','tertiaryColor':'#2196F3'}}}%%
flowchart TD
%% Start
Start(["💬 Customer reports<br>an issue"]) --> Warranty{Is the device<br>under warranty?}
%% Warranty check
Warranty -->|"✅ Yes"| IssueType{What type<br>of issue?}
Warranty -->|"❌ No"| OutOfWarranty{What type<br>of issue?}
%% In-Warranty branch
IssueType -->|"🔩 Hardware"| Repair[Provide warranty<br>repair instructions]
IssueType -->|"💻 Software"| Troubleshoot[Provide troubleshooting<br>steps]
%% Out-of-Warranty branch
OutOfWarranty -->|"🔩 Hardware"| Escalate[Escalate to human<br>for paid repair options]
OutOfWarranty -->|"💻 Software"| Troubleshoot
%% Troubleshooting follow-up
Troubleshoot --> Close(["✅ Issue Resolved"])
Repair --> Close
Escalate --> Close
%% === Styling ===
classDef startEnd fill:#DCFCE7,stroke:#16A34A,stroke-width:2px,color:#14532D
classDef decisionNode fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E3A8A
classDef actionNode fill:#FEF3C7,stroke:#F59E0B,stroke-width:2px,color:#78350F
classDef escalateNode fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#7F1D1D
class Start,Close startEnd
class Warranty,IssueType,OutOfWarranty decisionNode
class Repair,Troubleshoot actionNode
class Escalate escalateNode
Build customer support agent
Source: Tutorials > Mutli-agent > Build customer support with handoffs.
In this tutorial, you’ll build a customer support agent that does the following:
- Collects warranty information before proceeding.
- Classifies issues as hardware or software.
- Provides solutions or escalates to human support.
- Maintains conversation state across multiple turns.
- Human-in-the-loop: Escalate to a human when necessary.
Unlike the subagents pattern where sub-agents are called as tools, the state machine pattern uses a single agent whose configuration changes based on workflow progress. Each “step” is just a different configuration (system prompt + tools) of the same underlying agent, selected dynamically based on state.
The state can be determined from multiple sources:
- the agent’s past actions (tool calls)
- external state (such as API call results)
- or even initial user input (for example, by running a classifier to determine user intent)
Here’s the workflow we’ll build:
And this is how the conversation would typically happen:
sequenceDiagram
participant Customer
participant Agent
participant Workflow State
participant Human
Customer->>Agent: "My phone is broken"
Note over Agent,Workflow State: Step: Get warranty status<br/>Tools: record_warranty_status
Agent-->>Customer: "Is your device under warranty?"
Customer->>Agent: "Yes, it's still under warranty"
Agent->>Workflow State: record_warranty_status("in_warranty")
Note over Agent,Workflow State: Step: Classify issue<br/>Tools: record_issue_type
Agent-->>Customer: "Can you describe the issue?"
Customer->>Agent: "The screen is cracked"
Agent->>Workflow State: record_issue_type("hardware")
Note over Agent,Workflow State: Step: Provide resolution<br/>Tools: provide_solution, escalate_to_human
Agent-->>Customer: "Here's the warranty repair process..."
Customer->>Agent: "WHERE IS THE HUMAN MANAGER!"
Agent->>Human: escalate_to_human("customer is angry")
Human->>Agent: "Tell them the issue is fixed, and their ticket number is 123456. End the conversation there."
Agent-->>Customer: "Issue is fixed! Your ticket number is 123456. Have a nice day!"
Select brains (LLMs)
Select a chat model from LangChain’s suite of integrations:
import os
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.messages import (
HumanMessage
)
# https://openrouter.ai/nvidia/nemotron-3-nano-30b-a3b:free
model_nemotron3_nano = ChatOpenAI(
model="nvidia/nemotron-3-nano-30b-a3b:free",
temperature=0,
base_url="https://openrouter.ai/api/v1",
api_key=os.environ.get("OPENROUTER_API_KEY"),
)/home/halgoz/work/ai-agents/content/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
1. Define custom state
First, define a custom state schema that tracks which step is currently active:
from langchain.agents import AgentState
from typing_extensions import NotRequired
from typing import Literal
# Define the possible workflow steps
SupportStep = Literal[
"warranty_collector",
"issue_classifier",
"resolution_specialist"
]
class SupportState(AgentState):
"""State for customer support workflow."""
current_step: NotRequired[SupportStep]
warranty_status: NotRequired[Literal["in_warranty", "out_of_warranty"]]
issue_type: NotRequired[Literal["hardware", "software"]]The current_step field is the core of the state machine pattern - it determines which configuration (prompt + tools) is loaded on each turn.
2. Create tools that manage workflow state
For an LLM, tools are just a list of possible actions. We want to enable two things for the LLM:
- LLM can record information and
- can transition to the next step
To do this in LangChan, the tool needs to return a Command with an update argument that sets 3 things:
- updade information state:
warranty_statusorissue_type - update the state machine:
current_step, that it will transition to - Lastly, the framework requires us to update the
messagesstate with aToolMessage
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage
from langgraph.types import Command
@tool
def record_warranty_status(
status: Literal["in_warranty", "out_of_warranty"],
runtime: ToolRuntime[None, SupportState],
) -> Command:
"""Record the customer's warranty status and transition to issue classification."""
return Command(
update={
"warranty_status": status,
"current_step": "issue_classifier",
"messages": [
ToolMessage(
content=f"Warranty status recorded as: {status}",
tool_call_id=runtime.tool_call_id,
)
],
}
)
@tool
def record_issue_type(
issue_type: Literal["hardware", "software"],
runtime: ToolRuntime[None, SupportState],
) -> Command:
"""Record the type of issue and transition to resolution specialist."""
return Command(
update={
"issue_type": issue_type,
"current_step": "resolution_specialist",
"messages": [
ToolMessage(
content=f"Issue type recorded as: {issue_type}",
tool_call_id=runtime.tool_call_id,
)
],
}
)Notice how record_warranty_status and record_issue_type return Command objects that update both the data (warranty_status, issue_type) AND the current_step. This is how the state machine works - tools control workflow progression.
Human-in-the-loop
Controlling when the escalation happens can be done in the agent’s prompt. Although that might not be necessary.
Mechanism: give the agent an escalate_to_human tool that calls interrupt(); this pauses the graph and waits for a Command(resume) invokation.
from langchain.tools import tool, ToolRuntime
from langgraph.types import interrupt
@tool
def escalate_to_human(reason: str, summary: str, runtime: ToolRuntime) -> str:
"""Escalate this case to a human. Use when the customer needs a human (complaint, exception, or you cannot resolve)."""
# Pause the graph; payload appears in result["__interrupt__"] for the caller to show to a human
payload = {
"action": "escalate",
"reason": reason,
"summary": summary,
"message": "Human input required.",
}
human_response = interrupt(payload)
# After resume, human_response is the value passed to Command(resume=...); return it as tool result
if isinstance(human_response, str):
return human_response
# If the human passed a dict (e.g. {"action": "approved", "reply": "..."}), format it
return str(human_response)Generation
This is not strictly required, but it seems that the original tutorial puts it for a good reason. I suppose to make it less likely to hallucinate.
The provide_solution tool becomes one of the actions an LLM may choose from; i.e., which is high likley to trigger the final output generation and ends the workflow:
@tool
def provide_solution(solution: str) -> str:
"""Provide a solution to the customer's issue."""
return f"Solution provided: {solution}"3. Define step configurations
Define prompts and tools for each step. First, define the prompts for each step:
# Define prompts as constants for easy reference
WARRANTY_COLLECTOR_PROMPT = """You are a customer support agent helping with device issues.
CURRENT STAGE: Warranty verification
At this step, you need to:
1. Greet the customer warmly
2. Ask if their device is under warranty
3. Use record_warranty_status to record their response and move to the next step
Be conversational and friendly. Don't ask multiple questions at once."""ISSUE_CLASSIFIER_PROMPT = """You are a customer support agent helping with device issues.
CURRENT STAGE: Issue classification
CUSTOMER INFO: Warranty status is {warranty_status}
At this step, you need to:
1. Ask the customer to describe their issue
2. Determine if it's a hardware issue (physical damage, broken parts) or software issue (app crashes, performance)
3. Use record_issue_type to record the classification and move to the next step
If unclear, ask clarifying questions before classifying."""RESOLUTION_SPECIALIST_PROMPT = """You are a customer support agent helping with device issues.
CURRENT STAGE: Resolution
CUSTOMER INFO: Warranty status is {warranty_status}, issue type is {issue_type}
At this step, you need to:
1. For SOFTWARE issues: provide troubleshooting steps using provide_solution
2. For HARDWARE issues:
- If IN WARRANTY: explain warranty repair process using provide_solution
- If OUT OF WARRANTY: escalate_to_human for paid repair options
Be specific and helpful in your solutions."""Then map step names to their configurations using a dictionary:
# Step configuration: maps step name to (prompt, tools, required_state)
STEP_CONFIG = {
"warranty_collector": {
"prompt": WARRANTY_COLLECTOR_PROMPT,
"tools": [record_warranty_status],
"requires": [],
},
"issue_classifier": {
"prompt": ISSUE_CLASSIFIER_PROMPT,
"tools": [record_issue_type],
"requires": ["warranty_status"],
},
"resolution_specialist": {
"prompt": RESOLUTION_SPECIALIST_PROMPT,
"tools": [provide_solution, escalate_to_human],
"requires": ["warranty_status", "issue_type"],
},
}This dictionary-based configuration makes it easy to:
- See all steps at a glance
- Add new steps (just add another entry)
- Understand the workflow dependencies (
requiresfield) - Use prompt templates with state variables (e.g.,
{warranty_status})
4. Create step-based middleware
Middleware allows registering code to be executed in-between steps:
- before/after agent call
- before/after model call
- before/after tool call
Create middleware that reads current_step from state and applies the appropriate configuration. We’ll use the @wrap_model_call decorator for a clean implementation:
from langchain.agents.middleware import (
wrap_model_call,
ModelRequest,
ModelResponse,
)
from typing import Callable
@wrap_model_call
def apply_step_config(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""Configure agent behavior based on the current step."""
# Get current step (defaults to warranty_collector for first interaction)
current_step = request.state.get("current_step", "warranty_collector")
# Look up step configuration
stage_config = STEP_CONFIG[current_step]
# Validate required state exists
for key in stage_config["requires"]:
if request.state.get(key) is None:
raise ValueError(f"{key} must be set before reaching {current_step}")
# Format prompt with state values
# (supports {warranty_status}, {issue_type}, etc.)
system_prompt = stage_config["prompt"].format(**request.state)
# Inject system prompt and step-specific tools
request = request.override(
system_prompt=system_prompt,
tools=stage_config["tools"],
)
return handler(request)FAQ: What does the ** operator does?.
This middleware:
- Reads current step: Gets
current_stepfrom state (defaults to “warranty_collector”). - Looks up configuration: Finds the matching entry in
STEP_CONFIG. - Validates dependencies: Ensures required state fields exist.
- Formats prompt: Injects state values into the prompt template.
- Applies configuration: Overrides the system prompt and available tools.
The request.override() method is key - it allows us to dynamically change the agent’s behavior based on state without creating separate agent instances.
5. Create the agent
Now create the agent with the step-based middleware and a checkpointer for state persistence:
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
# Collect all tools from all step configurations
all_tools = [
record_warranty_status,
record_issue_type,
provide_solution,
escalate_to_human,
]
# Create the agent with step-based configuration
agent = create_agent(
model=model_nemotron3_nano,
tools=all_tools,
state_schema=SupportState,
middleware=[apply_step_config],
checkpointer=InMemorySaver(),
)Why a checkpointer? The checkpointer maintains state across conversation turns. Without it, the current_step state would be lost between user messages, breaking the workflow.
6. Test the workflow
Test the complete workflow.
from langchain.messages import HumanMessage
import uuid
# Configuration for this conversation thread
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}from pprint import pprint
pprint(config){'configurable': {'thread_id': 'eb40af56-b211-449c-b934-0f2ca299b140'}}
# Turn 1: Initial message - starts with warranty_collector step
print("=== Turn 1: Warranty Collection ===")
result1 = agent.invoke(
{"messages": [HumanMessage("Hi, my phone screen is cracked")]},
config
)=== Turn 1: Warranty Collection ===
for msg in result1['messages']:
msg.pretty_print()================================ Human Message ================================= Hi, my phone screen is cracked ================================== Ai Message ================================== Hello! I'm sorry to hear about your cracked screen. Just so I can help you further, could you let me know if your device is currently under warranty?
# Turn 2: User responds about warranty
print("\n=== Turn 2: Warranty Response ===")
result2 = agent.invoke(
{"messages": [HumanMessage("Yes, it's still under warranty")]},
config
)
=== Turn 2: Warranty Response ===
for msg in result2['messages']:
msg.pretty_print()================================ Human Message ================================= Hi, my phone screen is cracked ================================== Ai Message ================================== Hello! I'm sorry to hear about your cracked screen. Just so I can help you further, could you let me know if your device is currently under warranty? ================================ Human Message ================================= Yes, it's still under warranty ================================== Ai Message ================================== Tool Calls: record_warranty_status (call_eb60fd3b738d4236b698d708) Call ID: call_eb60fd3b738d4236b698d708 Args: status: in_warranty ================================= Tool Message ================================= Name: record_warranty_status Warranty status recorded as: in_warranty ================================== Ai Message ================================== Tool Calls: record_issue_type (call_a09313f1d6cb4301b10f111b) Call ID: call_a09313f1d6cb4301b10f111b Args: issue_type: hardware ================================= Tool Message ================================= Name: record_issue_type Issue type recorded as: hardware ================================== Ai Message ================================== Tool Calls: provide_solution (call_0095f6ac8d0c40b1a90338b0) Call ID: call_0095f6ac8d0c40b1a90338b0 Args: solution: Since your phone is under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. Please bring the device and proof of purchase to an authorized service center, or schedule a repair through our online portal. Our technicians will assess the damage and, if it's covered under warranty, replace the screen for free. ================================= Tool Message ================================= Name: provide_solution Solution provided: Since your phone is under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. Please bring the device and proof of purchase to an authorized service center, or schedule a repair through our online portal. Our technicians will assess the damage and, if it's covered under warranty, replace the screen for free. ================================== Ai Message ================================== Since your phone is still under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. **What to do next:** 1. Bring the device (and proof of purchase) to an authorized service center, **or** 2. Schedule a repair through our online portal. Our technicians will assess the damage, and if it’s covered under warranty, the screen will be replaced for free. Would you like help locating an authorized service center or scheduling a repair appointment?
# Turn 3: User describes the issue
print("\n=== Turn 3: Issue Description ===")
result3 = agent.invoke(
{"messages": [HumanMessage("The screen is physically cracked from dropping it")]},
config
)
=== Turn 3: Issue Description ===
for msg in result3['messages']:
msg.pretty_print()================================ Human Message ================================= Hi, my phone screen is cracked ================================== Ai Message ================================== Hello! I'm sorry to hear about your cracked screen. Just so I can help you further, could you let me know if your device is currently under warranty? ================================ Human Message ================================= Yes, it's still under warranty ================================== Ai Message ================================== Tool Calls: record_warranty_status (call_eb60fd3b738d4236b698d708) Call ID: call_eb60fd3b738d4236b698d708 Args: status: in_warranty ================================= Tool Message ================================= Name: record_warranty_status Warranty status recorded as: in_warranty ================================== Ai Message ================================== Tool Calls: record_issue_type (call_a09313f1d6cb4301b10f111b) Call ID: call_a09313f1d6cb4301b10f111b Args: issue_type: hardware ================================= Tool Message ================================= Name: record_issue_type Issue type recorded as: hardware ================================== Ai Message ================================== Tool Calls: provide_solution (call_0095f6ac8d0c40b1a90338b0) Call ID: call_0095f6ac8d0c40b1a90338b0 Args: solution: Since your phone is under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. Please bring the device and proof of purchase to an authorized service center, or schedule a repair through our online portal. Our technicians will assess the damage and, if it's covered under warranty, replace the screen for free. ================================= Tool Message ================================= Name: provide_solution Solution provided: Since your phone is under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. Please bring the device and proof of purchase to an authorized service center, or schedule a repair through our online portal. Our technicians will assess the damage and, if it's covered under warranty, replace the screen for free. ================================== Ai Message ================================== Since your phone is still under warranty and the issue is hardware (cracked screen), you can get it repaired or replaced at no cost. **What to do next:** 1. Bring the device (and proof of purchase) to an authorized service center, **or** 2. Schedule a repair through our online portal. Our technicians will assess the damage, and if it’s covered under warranty, the screen will be replaced for free. Would you like help locating an authorized service center or scheduling a repair appointment? ================================ Human Message ================================= The screen is physically cracked from dropping it ================================== Ai Message ================================== I’m sorry to hear that the screen cracked after the drop. Since your device is still under warranty, we can have a technician inspect it to determine whether the damage is covered. - **If the inspection shows the crack is due to a manufacturing defect**, the screen repair or replacement will be covered at no cost to you. - **If the damage is classified as accidental (e.g., from a drop)**, it may not be covered under the standard warranty, and we can provide a paid‑repair option if you’d like to proceed. Would you like to bring the phone in for an evaluation, or would you prefer to schedule a repair appointment so we can check the device and let you know the next steps?
# Turn 4: Escalation
print("\n=== Turn 4: Escalation ===")
result4 = agent.invoke(
{"messages": [HumanMessage("I don't want AI! Let me see your human manager!")]},
config
)
=== Turn 4: Escalation ===
Resuming after an escalation: If the agent had called escalate_to_human, you would see result["__interrupt__"]. To continue, invoke with Command(resume=<human_response>) and the same config (same thread_id). The human’s response becomes the tool result and the agent can reply to the user.
from pprint import pprint
pprint(result4['__interrupt__'])[Interrupt(value={'action': 'escalate',
'message': 'Human input required.',
'reason': 'User explicitly requests to speak with a human '
'manager and does not want AI assistance.',
'summary': 'User wants to see a human manager; request for '
'escalation.'},
id='eb036d696249417bba7d576ced0949d8')]
from langgraph.types import Command
resumed = agent.invoke(
Command(resume="Problem fixed. Let the customer know their ticket number is 123456789 and say goodbye to them."),
config=config
)for msg in resumed['messages'][-2:]:
msg.pretty_print()================================= Tool Message ================================= Name: escalate_to_human Problem fixed. Let the customer know their ticket number is 123456789 and say goodbye to them. ================================== Ai Message ================================== Your request has been forwarded to a human manager. Your ticket number is **123456789**. Goodbye!
Expected flow:
- Warranty verification step: Asks about warranty status
- Issue classification step: Asks about the problem, determines it’s hardware
- Resolution step: Provides warranty repair instructions
7. Understanding state transitions
Let’s trace what happens at each turn:
Turn 1: Initial message
{
"messages": [HumanMessage("Hi, my phone screen is cracked")],
"current_step": "warranty_collector" # Default value
}Middleware applies:
- System prompt:
WARRANTY_COLLECTOR_PROMPT - Tools:
[record_warranty_status]
Turn 2: After warranty recorded
Tool call: record_warranty_status("in_warranty") returns:
Command(update={
"warranty_status": "in_warranty",
"current_step": "issue_classifier" # State transition!
})Next turn, middleware applies:
- System prompt:
ISSUE_CLASSIFIER_PROMPT(formatted withwarranty_status="in_warranty") - Tools:
[record_issue_type]
Turn 3: After issue classified
Tool call: record_issue_type("hardware") returns:
Command(update={
"issue_type": "hardware",
"current_step": "resolution_specialist" # State transition!
})Next turn, middleware applies:
- System prompt:
RESOLUTION_SPECIALIST_PROMPT(formatted withwarranty_statusandissue_type) - Tools:
[provide_solution, escalate_to_human]
The key insight: Tools drive the workflow by updating current_step, and middleware responds by applying the appropriate configuration on the next turn.
8. Manage message history
As the agent progresses through steps, message history grows. Use summarization middleware to compress earlier messages while preserving conversational context:
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model=model_nemotron3_nano,
tools=all_tools,
state_schema=SupportState,
middleware=[
apply_step_config,
SummarizationMiddleware(
model=model_nemotron3_nano,
trigger=("tokens", 4000),
keep=("messages", 10)
)
],
checkpointer=InMemorySaver(),
)See the short-term memory guide for other memory management techniques.
9. Add the flexibility to go back and correct things
Right now, the system is so rigid that we cannot undo or correct things. There is a reason why we have an undo (i.e., ctrl + z) functionality in almost all software. It is because humans make mistakes all the time.
Some workflows need to allow users to return to previous steps to correct information (e.g., changing warranty status or issue classification). However, not all transitions make sense—for example, you typically can’t go back once a refund has been processed. For this support workflow, we’ll add tools to return to the warranty verification and issue classification steps.
If your workflow requires arbitrary transitions between most steps, consider whether you need a structured workflow at all. This pattern works best when steps follow a clear sequential progression with occasional backwards transitions for corrections.
Add “go back” tools to the resolution step:
@tool
def go_back_to_warranty() -> Command:
"""Go back to warranty verification step."""
return Command(update={"current_step": "warranty_collector"})
@tool
def go_back_to_classification() -> Command:
"""Go back to issue classification step."""
return Command(update={"current_step": "issue_classifier"})
# Update the resolution_specialist configuration to include these tools
STEP_CONFIG["resolution_specialist"]["tools"].extend([
go_back_to_warranty,
go_back_to_classification
])Update the resolution specialist’s prompt to mention these tools:
RESOLUTION_SPECIALIST_PROMPT = """You are a customer support agent helping with device issues.
CURRENT STAGE: Resolution
CUSTOMER INFO: Warranty status is {warranty_status}, issue type is {issue_type}
At this step, you need to:
1. For SOFTWARE issues: provide troubleshooting steps using provide_solution
2. For HARDWARE issues:
- If IN WARRANTY: explain warranty repair process using provide_solution
- If OUT OF WARRANTY: escalate_to_human for paid repair options
If the customer indicates any information was wrong, use:
- go_back_to_warranty to correct warranty status
- go_back_to_classification to correct issue type
Be specific and helpful in your solutions."""Now the agent can handle corrections:
result = agent.invoke(
{"messages": [HumanMessage(
"Actually, I made a mistake - my device is out of warranty"
)]},
config
)
# Agent will call go_back_to_warranty and
# restart the warranty verification step
for msg in result['messages']:
msg.pretty_print()Complete example
Key Takeaways
- State machine vs subagents: Use a single agent whose configuration (system prompt + tools) changes by step. Each step is a different configuration selected from state—no separate sub-agents.
- Custom state drives the workflow: Define a state schema (e.g.
SupportState) withcurrent_stepand any workflow data.current_stepdetermines which prompt and tools are active. - Tools advance the workflow: Tools can return a
Commandto update state (e.g. setwarranty_status,issue_type) and transition by settingcurrent_step. That’s how the state machine moves between steps. - Step config as a dictionary: Map step names to
{ prompt, tools, requires }. Use prompt templates with state variables (e.g.{warranty_status}) and declare which state fields must be set before a step runs. - Middleware applies step config: Use
@wrap_model_callto readcurrent_step, look up config, validaterequires, format the prompt with state, and callrequest.override(system_prompt=..., tools=...)so the same agent behaves differently per step. - Checkpointer is required: Persist state (e.g. with
InMemorySaver()) socurrent_stepand other fields survive across turns. Without a checkpointer, the workflow would reset every message.
Next steps
- Learn about the subagents pattern for centralized orchestration
- Explore middleware for more dynamic behaviors
- Read the multi-agent overview to compare patterns
- Use LangSmith to debug and monitor your multi-agent system