From 33af651939236999497cbf73fe51b210eb99e899 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 15 Dec 2025 16:49:10 -0500 Subject: [PATCH 1/4] x --- src/oss/langchain/multi-agent.mdx | 386 +++++++++++++++++++++++++----- 1 file changed, 326 insertions(+), 60 deletions(-) diff --git a/src/oss/langchain/multi-agent.mdx b/src/oss/langchain/multi-agent.mdx index 1de077ad5e..93e7d3ae47 100644 --- a/src/oss/langchain/multi-agent.mdx +++ b/src/oss/langchain/multi-agent.mdx @@ -27,7 +27,7 @@ Here are the main patterns for building multi-agent systems, each suited to diff | Pattern | How it works | |--------------|--------------| | [**Supervisor**](#supervisor) | A supervisor agent coordinates sub-agents and background jobs as tools. Centralized control — all routing passes through the supervisor. Multiple coordination approaches available. | -| [**State machine**](#state-machine) | Single agent whose configuration (prompt, tools, capabilities) changes as it moves through different states. Efficient for sequential workflows — one tool call both performs the action and transitions to the next state. | +| [**State machine (handoffs)**](#state-machine-handoffs) | Behavior changes dynamically based on state. Tool calls update a state variable, and the system adjusts behavior accordingly. Supports both handoffs between distinct agents and dynamic configuration changes. | | [**Skills**](#skills) | Specialized prompts loaded on-demand. The main agent stays in control and gains additional context as needed. | | [**Router**](#router) | A routing step classifies input and directs it to one or more specialized agents. Results are collected and returned to the user. | | [**Custom workflow**](#custom-workflow) | Build bespoke logic with LangGraph, mixing deterministic and agentic steps. Reuse or customize agents as needed. | @@ -36,11 +36,11 @@ Here are the main patterns for building multi-agent systems, each suited to diff You can mix patterns! For example, a **supervisor** can manage **workflow** sub-graphs or use the **router** pattern as a tool (querying multiple knowledge bases in parallel, then synthesizing results). A **state machine** can invoke **skills** at specific stages (loading specialized context only when reaching certain steps). The **[one tool for all agents](#one-tool-for-all-agents-spawning)** approach can work within a **custom workflow** to parallelize independent tasks while maintaining deterministic overall structure. -**Common mechanisms:** These patterns share common mechanisms for coordinating multi-agent behavior. Most rely on **tool calling** as the primary coordination mechanism—tools can invoke sub-agents ([supervisor](#supervisor)), update state to trigger routing or behavior changes ([state machine](#state-machine)), load context on-demand ([skills](#skills)), or even invoke entire multi-agent systems (like wrapping a [router](#router) as a tool). +**Common mechanisms:** These patterns share common mechanisms for coordinating multi-agent behavior. Most rely on **tool calling** as the primary coordination mechanism—tools can invoke sub-agents ([supervisor](#supervisor)), update state to trigger routing or behavior changes ([state machine](#state-machine-handoffs)), load context on-demand ([skills](#skills)), or even invoke entire multi-agent systems (like wrapping a [router](#router) as a tool). Tools drive dynamic behavior: updating state to route between agents, loading context, changing configuration, or spawning sub-agents. Patterns can also use middleware to modify system prompts or implement deterministic routing ([custom workflows](#custom-workflow)). -**[Progressive disclosure](/oss/langchain/progressive-disclosure-skills)** loads information on-demand, valuable for managing large amounts of context. It combines retrieval with dynamic state updates (registering tools, discovering sub-agents). However, it adds latency through extra tool calls—simpler patterns like [state machine](#state-machine) suffice when context is manageable or sequential constraints are needed. +**[Progressive disclosure](/oss/langchain/progressive-disclosure-skills)** loads information on-demand, valuable for managing large amounts of context. It combines retrieval with dynamic state updates (registering tools, discovering sub-agents). However, it adds latency through extra tool calls—simpler patterns like [state machine](#state-machine-handoffs) suffice when context is manageable or sequential constraints are needed. ## Supervisor @@ -475,9 +475,13 @@ sequenceDiagram **Handling job completion:** When a job finishes, your application needs to notify the user. One approach: surface a notification that, when clicked, sends a `HumanMessage` like "Check job_123 and summarize the results." This prompts the supervisor to retrieve and present the findings. -## State machine +## State machine (handoffs) -In the **state machine** architecture, you have a single agent whose behavior (prompt, tools, and capabilities) changes dynamically based on state. The state machine is implemented via [middleware](/oss/langchain/middleware/overview) that performs transitions through tool calls. +In the **state machine** architecture, behavior changes dynamically based on state. The core mechanism is simple: tool calls update a state variable (e.g., `current_step` or `active_agent`), and the system adjusts behavior based on that state. This pattern supports both handoffs between distinct agents and dynamic configuration changes within a single agent. + + +**Handoffs** is a pattern for transitioning between agents or states using tool calls, popularized by [OpenAI's agent handoffs](https://openai.github.io/openai-agents-python/handoffs/). Handoffs appear as tools to the LLM (e.g., `transfer_to_sales_agent`) and are useful for coordinating specialized agents across different domains or for enforcing sequential constraints that unlock capabilities step by step. + ```mermaid stateDiagram-v2 @@ -504,14 +508,11 @@ stateDiagram-v2 **Key characteristics:** -* State-driven configuration: A single agent whose prompts and tools change dynamically based on a state variable (e.g., `current_step`) -* Direct user interaction: Each step's configuration handles user messages directly +* State-driven behavior: Behavior changes based on a state variable (e.g., `current_step` or `active_agent`) +* Tool-based transitions: Tools update the state variable to move between states +* Direct user interaction: Each state's configuration handles user messages directly * Persistent state: State survives across conversation turns - -**Alternative to handoffs**: This pattern can be more efficient than handoff-based approaches like [OpenAI's agent handoffs](https://openai.github.io/openai-agents-python/handoffs/) or [LangGraph swarm](https://github.com/langchain-ai/langgraph-swarm-py). Depending on implementation, handoffs may require two tool calls (transfer control, then act) while state machines can combine both. Because it's a single agent switching behaviors rather than distinct agents, it's simpler to streamline the tool calls and manage the conversation context correctly. - - Use the state machine pattern when you need to enforce sequential constraints (unlock capabilities only after preconditions are met), the agent needs to converse directly with the user across different states, or you're building multi-stage conversational flows. This pattern is particularly valuable for customer support scenarios where you need to collect information in a specific sequence — for example, collecting a warranty ID before processing a refund. -At the core, the state machine pattern relies on [persistent state](/oss/langchain/short-term-memory) that survives across conversation turns: - -1. **State variable**: A field in your state schema (e.g., `current_step: str`) tracks which step is currently active. -2. **State update tool**: The agent uses a tool to change the value of `current_step` when transitioning to the next step. -3. **Dynamic configuration**: On each turn, middleware reads `current_step` from the persisted state and dynamically configures the appropriate system prompt, tools, and behavior. +The state machine pattern relies on [persistent state](/oss/langchain/short-term-memory) that survives across conversation turns: -This pattern creates a state machine where each step represents a distinct state with its own configuration and capabilities. +1. **State variable**: A field in your state schema (e.g., `current_step` or `active_agent`) tracks the current state. +2. **State update tool**: Tools change the state variable value when transitioning between states. +3. **Dynamic behavior**: On each turn, the system reads the state variable and adjusts behavior—either by applying different configuration (system prompt, tools) or routing to a different agent. - + :::python ```python @@ -680,6 +679,315 @@ const agent = createAgent({ +### Creating handoff tools + +A common pattern is wrapping handoffs in tool calls. The tool returns a `Command` object that updates state to trigger transitions. + +**Single agent with middleware:** + +With a single agent, tools update the state variable (e.g., `current_step`) and middleware applies the appropriate configuration: + +:::python +```python +from langchain_core.tools import tool +from langchain.tools import ToolRuntime +from langchain.messages import ToolMessage # [!code highlight] +from langgraph.types import Command + +@tool +def record_warranty_status( + status: str, + runtime: ToolRuntime[None, SupportState] +) -> Command: + """Record warranty status and transition to next step.""" + return Command( + update={ + "messages": [ # [!code highlight] + ToolMessage( # [!code highlight] + content=f"Warranty status recorded: {status}", # [!code highlight] + tool_call_id=runtime.tool_call_id # [!code highlight] + ) # [!code highlight] + ], # [!code highlight] + "warranty_status": status, + "current_step": "specialist" # Update state to trigger transition + } + ) +``` +::: +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { Command } from "@langchain/langgraph"; +import { ToolMessage } from "@langchain/core/messages"; // [!code highlight] +import { z } from "zod"; + +const recordWarrantyStatus = tool( + async ({ status }, config) => { + return new Command({ + update: { + messages: [ // [!code highlight] + new ToolMessage({ // [!code highlight] + content: `Warranty status recorded: ${status}`, // [!code highlight] + tool_call_id: config.toolCall?.id! // [!code highlight] + }) // [!code highlight] + ], // [!code highlight] + warrantyStatus: status, + currentStep: "specialist" // Update state to trigger transition + } + }); + }, + { + name: "record_warranty_status", + description: "Record warranty status and transition to next step.", + schema: z.object({ + status: z.string() + }) + } +); +``` +::: + +The middleware then reads `current_step` from state and applies the appropriate agent configuration. + +**Multiple agent subgraphs:** + +With multiple distinct agents as subgraph nodes, handoff tools need to navigate to the parent graph using `goto` and `graph=Command.PARENT`: + +:::python +```python +@tool +def transfer_to_sales(): + """Transfer to the sales agent.""" + return Command( + goto="sales_agent", # Navigate to the sales agent node + update={"active_agent": "sales_agent"}, + graph=Command.PARENT # Navigate in parent graph + ) +``` +::: +:::js +```typescript +const transferToSales = tool( + async () => { + return new Command({ + goto: "sales_agent", // Navigate to the sales agent node + update: { activeAgent: "sales_agent" }, + graph: Command.PARENT // Navigate in parent graph + }); + }, + { + name: "transfer_to_sales", + description: "Transfer to the sales agent.", + schema: z.object({}) + } +); +``` +::: + + + +This example shows how to build a multi-agent system where each agent is a separate node and handoff tools coordinate between them. + +:::python +```python +from typing import Literal +from langchain.agents import AgentState, create_agent +from langgraph.graph import StateGraph, START +from langgraph.types import Command +from langchain_core.tools import tool + +# 1. Define state with active_agent tracker +class MultiAgentState(AgentState): + active_agent: str = "sales_agent" # Track which agent is active + +# 2. Create handoff tools +@tool +def transfer_to_sales(): + """Transfer to the sales agent.""" + return Command( + goto="sales_agent", + update={"active_agent": "sales_agent"}, + graph=Command.PARENT + ) + +@tool +def transfer_to_support(): + """Transfer to the support agent.""" + return Command( + goto="support_agent", + update={"active_agent": "support_agent"}, + graph=Command.PARENT + ) + +# 3. Create agents with handoff tools +sales_agent = create_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[transfer_to_support], + prompt="You are a sales agent. Help with sales inquiries." +) + +support_agent = create_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[transfer_to_sales], + prompt="You are a support agent. Help with technical issues." +) + +# 4. Create agent nodes that invoke the agents +def call_sales_agent(state: MultiAgentState): + """Node that calls the sales agent.""" + response = sales_agent.invoke(state) + return response + +def call_support_agent(state: MultiAgentState): + """Node that calls the support agent.""" + response = support_agent.invoke(state) + return response + +# 5. Create router node +def route_to_agent(state: MultiAgentState) -> Literal["sales_agent", "support_agent"]: + """Route to the active agent based on state.""" + return state["active_agent"] + +# 6. Build the graph +builder = StateGraph(MultiAgentState) +builder.add_node("sales_agent", call_sales_agent) +builder.add_node("support_agent", call_support_agent) + +# Start with conditional routing based on initial active_agent +builder.add_conditional_edges( + START, + route_to_agent, + ["sales_agent", "support_agent"] +) + +# After each agent, route to the active agent (enables handoffs) +builder.add_conditional_edges( + "sales_agent", + route_to_agent, + ["sales_agent", "support_agent"] +) +builder.add_conditional_edges( + "support_agent", + route_to_agent, + ["sales_agent", "support_agent"] +) + +graph = builder.compile() +``` +::: + +:::js +```typescript +import { StateGraph, START, MessagesAnnotation, Annotation } from "@langchain/langgraph"; +import { createAgent } from "langchain"; +import { tool } from "@langchain/core/tools"; +import { Command } from "@langchain/langgraph"; +import { z } from "zod"; + +// 1. Define state with active_agent tracker +const MultiAgentState = Annotation.Root({ + ...MessagesAnnotation.spec, + activeAgent: Annotation({ + reducer: (x, y) => y ?? x, + default: () => "sales_agent" + }) +}); + +// 2. Create handoff tools +const transferToSales = tool( + async () => { + return new Command({ + goto: "sales_agent", + update: { activeAgent: "sales_agent" }, + graph: Command.PARENT + }); + }, + { + name: "transfer_to_sales", + description: "Transfer to the sales agent.", + schema: z.object({}) + } +); + +const transferToSupport = tool( + async () => { + return new Command({ + goto: "support_agent", + update: { activeAgent: "support_agent" }, + graph: Command.PARENT + }); + }, + { + name: "transfer_to_support", + description: "Transfer to the support agent.", + schema: z.object({}) + } +); + +// 3. Create agents with handoff tools +const salesAgent = createAgent({ + llm: model, + tools: [transferToSupport], + prompt: "You are a sales agent. Help with sales inquiries." +}); + +const supportAgent = createAgent({ + llm: model, + tools: [transferToSales], + prompt: "You are a support agent. Help with technical issues." +}); + +// 4. Create agent nodes that invoke the agents +const callSalesAgent = async (state: typeof MultiAgentState.State) => { + const response = await salesAgent.invoke(state); + return response; +}; + +const callSupportAgent = async (state: typeof MultiAgentState.State) => { + const response = await supportAgent.invoke(state); + return response; +}; + +// 5. Create router function +const routeToAgent = (state: typeof MultiAgentState.State): string => { + return state.activeAgent; +}; + +// 6. Build the graph +const builder = new StateGraph(MultiAgentState) + .addNode("sales_agent", callSalesAgent) + .addNode("support_agent", callSupportAgent); + +// Start with conditional routing based on initial activeAgent +builder.addConditionalEdges( + START, + routeToAgent, + ["sales_agent", "support_agent"] +); + +// After each agent, route to the active agent (enables handoffs) +builder.addConditionalEdges( + "sales_agent", + routeToAgent, + ["sales_agent", "support_agent"] +); +builder.addConditionalEdges( + "support_agent", + routeToAgent, + ["sales_agent", "support_agent"] +); + +const graph = builder.compile(); +``` +::: + + + +**Implementation considerations:** + +* **Conversation history management**: When transitioning between agents, decide what conversation history each agent receives. You can pass the full history, filter it to relevant portions, or summarize prior conversations to reduce context size. +* **Tool semantics**: Clarify whether handoff tools only update routing state or also perform actions. For example, should `transfer_to_sales()` just change the active agent, or should it also create a sales ticket? Design tool descriptions and prompts to avoid ambiguity. + ## Skills @@ -1097,48 +1405,6 @@ const conversationalAgent = createAgent({ -### Stateful (handoffs) - -In the stateful approach, agents transfer control to one another using a handoff tool that updates the `active_agent` state variable. On the next user message, the conversation is directed to the currently active agent rather than re-routing from scratch. - -```mermaid -stateDiagram-v2 - [*] --> Triage Agent - Triage Agent --> Triage Agent: User message - Triage Agent --> Sales Agent: transfer_to_sales() - Sales Agent --> Sales Agent: User message - Sales Agent --> Support Agent: transfer_to_support() - Support Agent --> Support Agent: User message - Support Agent --> Triage Agent: transfer_to_triage() - - note right of Triage Agent - active_agent: "triage" - Tools: [transfer_to_sales, transfer_to_support] - end note - - note right of Sales Agent - active_agent: "sales" - Tools: [transfer_to_support, transfer_to_triage] - end note - - note right of Support Agent - active_agent: "support" - Tools: [transfer_to_triage, transfer_to_sales] - end note -``` - -**Key characteristics:** - -* State-driven routing: The `active_agent` state variable determines which agent handles the next user message -* Tool-based transfers: Agents use handoff tools (e.g., `transfer_to_sales()`) to update the active agent -* Persistent context: The conversation continues with the new agent maintaining state -* Distinct agents: Unlike [state machine](#state-machine), this pattern supports completely different agents with their own prompts, tools, and capabilities - -This is how handoffs via tools work in multi-agent systems. It's similar to the [state machine pattern](#state-machine) in that both rely on a state variable to change behavior. However, the state machine pattern uses a single agent whose configuration changes, while stateful routing supports distinct agents with separate implementations. - -**Ambiguity in tool semantics:** One challenge with this pattern is potential ambiguity in whether a handoff tool call implements both the action and the transfer of control. For example, if a triage agent calls `transfer_to_sales()`, does it just update the routing state, or should it also perform some action (like creating a sales ticket) before transferring? This requires careful design of tool descriptions and agent prompts to clarify the intended semantics. - - ## Custom workflow In the **custom workflow** architecture, you build bespoke workflows tailored to your specific use case using LangGraph. The graph structure is defined at construction time and can include conditional branches, parallel execution, and agentic routing based on LLM outputs. From e65130572bf7952c84f6470f3b07ee5eeede3c07 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 15 Dec 2025 22:21:13 -0500 Subject: [PATCH 2/4] x --- src/oss/langchain/multi-agent.mdx | 160 +++++++++++++++--------------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/src/oss/langchain/multi-agent.mdx b/src/oss/langchain/multi-agent.mdx index eb128e1a3f..d2a6370bf5 100644 --- a/src/oss/langchain/multi-agent.mdx +++ b/src/oss/langchain/multi-agent.mdx @@ -524,13 +524,81 @@ Use the state machine pattern when you need to enforce sequential constraints (u Learn how to build a customer support agent using the state machine pattern, where a single agent transitions between different configurations. +### Core mechanism + The state machine pattern relies on [persistent state](/oss/langchain/short-term-memory) that survives across conversation turns: 1. **State variable**: A field in your state schema (e.g., `current_step` or `active_agent`) tracks the current state. 2. **State update tool**: Tools change the state variable value when transitioning between states. 3. **Dynamic behavior**: On each turn, the system reads the state variable and adjusts behavior—either by applying different configuration (system prompt, tools) or routing to a different agent. - +There are two ways to implement state machines: **[single agent with middleware](#single-agent-with-middleware)** (one agent with dynamic configuration) or **[multiple agent subgraphs](#multiple-agent-subgraphs)** (distinct agents as graph nodes). + +### Single agent with middleware + +A single agent changes its behavior based on state. Middleware intercepts each model call and dynamically adjusts the system prompt and available tools. Tools update the state variable to trigger transitions: + +:::python +```python +from langchain_core.tools import tool +from langchain.tools import ToolRuntime +from langchain.messages import ToolMessage +from langgraph.types import Command + +@tool +def record_warranty_status( + status: str, + runtime: ToolRuntime[None, SupportState] +) -> Command: + """Record warranty status and transition to next step.""" + return Command( + update={ + "messages": [ + ToolMessage( + content=f"Warranty status recorded: {status}", + tool_call_id=runtime.tool_call_id + ) + ], + "warranty_status": status, + "current_step": "specialist" # Update state to trigger transition + } + ) +``` +::: +:::js +```typescript +import { tool } from "@langchain/core/tools"; +import { Command } from "@langchain/langgraph"; +import { ToolMessage } from "@langchain/core/messages"; +import { z } from "zod"; + +const recordWarrantyStatus = tool( + async ({ status }, config) => { + return new Command({ + update: { + messages: [ + new ToolMessage({ + content: `Warranty status recorded: ${status}`, + tool_call_id: config.toolCall?.id! + }) + ], + warrantyStatus: status, + currentStep: "specialist" // Update state to trigger transition + } + }); + }, + { + name: "record_warranty_status", + description: "Record warranty status and transition to next step.", + schema: z.object({ + status: z.string() + }) + } +); +``` +::: + + :::python ```python @@ -679,79 +747,9 @@ const agent = createAgent({ -### Creating handoff tools - -A common pattern is wrapping handoffs in tool calls. The tool returns a `Command` object that updates state to trigger transitions. - -**Single agent with middleware:** - -With a single agent, tools update the state variable (e.g., `current_step`) and middleware applies the appropriate configuration: - -:::python -```python -from langchain_core.tools import tool -from langchain.tools import ToolRuntime -from langchain.messages import ToolMessage # [!code highlight] -from langgraph.types import Command - -@tool -def record_warranty_status( - status: str, - runtime: ToolRuntime[None, SupportState] -) -> Command: - """Record warranty status and transition to next step.""" - return Command( - update={ - "messages": [ # [!code highlight] - ToolMessage( # [!code highlight] - content=f"Warranty status recorded: {status}", # [!code highlight] - tool_call_id=runtime.tool_call_id # [!code highlight] - ) # [!code highlight] - ], # [!code highlight] - "warranty_status": status, - "current_step": "specialist" # Update state to trigger transition - } - ) -``` -::: -:::js -```typescript -import { tool } from "@langchain/core/tools"; -import { Command } from "@langchain/langgraph"; -import { ToolMessage } from "@langchain/core/messages"; // [!code highlight] -import { z } from "zod"; - -const recordWarrantyStatus = tool( - async ({ status }, config) => { - return new Command({ - update: { - messages: [ // [!code highlight] - new ToolMessage({ // [!code highlight] - content: `Warranty status recorded: ${status}`, // [!code highlight] - tool_call_id: config.toolCall?.id! // [!code highlight] - }) // [!code highlight] - ], // [!code highlight] - warrantyStatus: status, - currentStep: "specialist" // Update state to trigger transition - } - }); - }, - { - name: "record_warranty_status", - description: "Record warranty status and transition to next step.", - schema: z.object({ - status: z.string() - }) - } -); -``` -::: - -The middleware then reads `current_step` from state and applies the appropriate agent configuration. - -**Multiple agent subgraphs:** +### Multiple agent subgraphs -With multiple distinct agents as subgraph nodes, handoff tools need to navigate to the parent graph using `goto` and `graph=Command.PARENT`: +Multiple distinct agents exist as separate nodes in a graph. Handoff tools navigate between agent nodes using `Command.PARENT` to specify which node to execute next: :::python ```python @@ -784,9 +782,9 @@ const transferToSales = tool( ``` ::: - + -This example shows how to build a multi-agent system where each agent is a separate node and handoff tools coordinate between them. +This example shows a multi-agent system with separate sales and support agents. Each agent is a separate graph node, and handoff tools allow agents to transfer conversations to each other. :::python ```python @@ -983,10 +981,14 @@ const graph = builder.compile(); + +Use **single agent with middleware** for most state machine use cases—it's simpler. Only use **multiple agent subgraphs** when you need bespoke agent implementations (e.g., a node that's itself a complex graph with reflection or retrieval steps). + + **Implementation considerations:** -* **Conversation history management**: When transitioning between agents, decide what conversation history each agent receives. You can pass the full history, filter it to relevant portions, or summarize prior conversations to reduce context size. -* **Tool semantics**: Clarify whether handoff tools only update routing state or also perform actions. For example, should `transfer_to_sales()` just change the active agent, or should it also create a sales ticket? Design tool descriptions and prompts to avoid ambiguity. +* **Conversation history**: Decide what conversation history each agent/state receives—full history, filtered portions, or summaries. +* **Tool semantics**: Clarify whether handoff tools only update routing state or also perform actions (e.g., should `transfer_to_sales()` also create a ticket?). ## Skills @@ -1122,7 +1124,7 @@ graph LR Key characteristics: * Router decomposes the query -* One or more specialized agents are invoked in parallel +* Zero or more specialized agents are invoked in parallel * Results are synthesized into a coherent response Two approaches: @@ -1438,7 +1440,7 @@ The conversational agent handles memory and context; the router stays stateless. If you need the router itself to maintain state, use [persistence](/oss/langchain/short-term-memory) to store message history. When routing to an agent, fetch previous messages from state and selectively include them in the agent's context—this is a lever for [context engineering](/oss/langchain/context-engineering). -**This is challenging.** Maintaining coherent history across multiple agents is difficult, especially when agents run in parallel. Agents can become confused about context or produce inconsistent responses. Synthesizing results also becomes harder—you need to track which agents have responded and how to combine partial results across turns. Consider the [state machine pattern](#state-machine) or [subagents pattern](#subagents) instead—both are simpler to reason about. +**Stateful routers require custom history management.** If the router switches between agents across turns, conversations may not feel fluid to end users when agents have different tones or prompts. With parallel invocation, you'll need to maintain history at the router level (inputs and synthesized outputs) and leverage this history in routing logic. Consider the [state machine pattern](#state-machine) or [subagents pattern](#subagents) instead—both provide clearer semantics for multi-turn conversations. ## Custom workflow From 6879c00c1d467ca4a6081a96d5df028f1b14c79a Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 15 Dec 2025 22:42:00 -0500 Subject: [PATCH 3/4] x --- .../customer-support-state-machine.mdx | 4 -- src/oss/langchain/multi-agent.mdx | 56 ++++++++----------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/oss/langchain/customer-support-state-machine.mdx b/src/oss/langchain/customer-support-state-machine.mdx index 3d8f10fd41..0291523114 100644 --- a/src/oss/langchain/customer-support-state-machine.mdx +++ b/src/oss/langchain/customer-support-state-machine.mdx @@ -17,10 +17,6 @@ In this tutorial, you'll build a customer support agent that does the following: Unlike the [supervisor pattern](/oss/langchain/supervisor) 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. - -This workflow can also be implemented using handoffs between distinct agents. However, that approach depending on implementation may introduce unnecessary overhead: one tool call to transfer control, then another to act on the user's request. The state machine pattern is efficient—a single tool call both performs the action (like recording warranty status) and transitions to the next step. - - Here's the workflow we'll build: ```mermaid diff --git a/src/oss/langchain/multi-agent.mdx b/src/oss/langchain/multi-agent.mdx index d2a6370bf5..f5af78b195 100644 --- a/src/oss/langchain/multi-agent.mdx +++ b/src/oss/langchain/multi-agent.mdx @@ -44,7 +44,7 @@ Tools drive dynamic behavior: updating state to route between agents, loading co ## Subagents -In the **subagents** architecture, a central main agent coordinates subagents by calling them as tools. The main agent decides which subagent to invoke, what input to provide, and how to combine results. Subagents are stateless—they don't remember past interactions, with all conversation memory maintained by the main agent. This provides context isolation: each subagent invocation works in a clean context window, preventing context bloat in the main conversation. +In the **subagents** architecture, a central main agent (often referred to as a **supervisor**) coordinates subagents by calling them as tools. The main agent decides which subagent to invoke, what input to provide, and how to combine results. Subagents are stateless—they don't remember past interactions, with all conversation memory maintained by the main agent. This provides context isolation: each subagent invocation works in a clean context window, preventing context bloat in the main conversation. **Key characteristics:** @@ -61,7 +61,7 @@ Use the subagents pattern when you have multiple distinct domains (e.g., calenda href="/oss/langchain/supervisor" arrow cta="Learn more" > - Learn how to build a personal assistant using the subagents pattern, where a central main agent coordinates specialized worker agents. + Learn how to build a personal assistant using the subagents pattern, where a central main agent (supervisor) coordinates specialized worker agents. Main approaches to implementing subagent coordination: @@ -477,33 +477,33 @@ sequenceDiagram ## State machine (handoffs) -In the **state machine** architecture, behavior changes dynamically based on state. The core mechanism is simple: tool calls update a state variable (e.g., `current_step` or `active_agent`), and the system adjusts behavior based on that state. This pattern supports both handoffs between distinct agents and dynamic configuration changes within a single agent. +In the **state machine** architecture, behavior changes dynamically based on state. The core mechanism: tools update a state variable (e.g., `current_step` or `active_agent`) that persists across turns, and the system reads this variable to adjust behavior—either applying different configuration (system prompt, tools) or routing to a different agent. This pattern supports both handoffs between distinct agents and dynamic configuration changes within a single agent. -**Handoffs** is a pattern for transitioning between agents or states using tool calls, popularized by [OpenAI's agent handoffs](https://openai.github.io/openai-agents-python/handoffs/). Handoffs appear as tools to the LLM (e.g., `transfer_to_sales_agent`) and are useful for coordinating specialized agents across different domains or for enforcing sequential constraints that unlock capabilities step by step. +The term **handoffs** was coined by [OpenAI](https://openai.github.io/openai-agents-python/handoffs/) for using tool calls (e.g., `transfer_to_sales_agent`) to transfer control between agents or states. ```mermaid -stateDiagram-v2 - [*] --> Triage - Triage --> Specialist: record_warranty_status() - Specialist --> Resolution: record_issue_type() - Resolution --> [*] - - note right of Triage - Step 1: Collect information - Tools: [record_warranty_status] - end note - - note right of Specialist - Step 2: Classify the issue - Tools: [record_issue_type] - end note - - note right of Resolution - Step 3: Provide solution - Tools: [provide_solution, escalate_to_human] - end note +sequenceDiagram + participant User + participant Agent + participant Workflow State + + User->>Agent: "My device is broken" + Note over Agent,Workflow State: warranty_collector
Available: record_warranty_status + Agent->>Workflow State: record_warranty_status("in_warranty") + Note over Workflow State: → issue_classifier + Agent-->>User: "Can you describe the issue?" + + User->>Agent: "The screen is cracked" + Note over Agent,Workflow State: issue_classifier
Available: record_issue_type + Agent->>Workflow State: record_issue_type("hardware") + Note over Workflow State: → resolution_specialist + Agent-->>User: "I can help with that" + + User->>Agent: "What should I do?" + Note over Agent,Workflow State: resolution_specialist
Available: provide_solution, escalate_to_human + Agent-->>User: "Here's the warranty repair process..." ``` **Key characteristics:** @@ -524,14 +524,6 @@ Use the state machine pattern when you need to enforce sequential constraints (u Learn how to build a customer support agent using the state machine pattern, where a single agent transitions between different configurations. -### Core mechanism - -The state machine pattern relies on [persistent state](/oss/langchain/short-term-memory) that survives across conversation turns: - -1. **State variable**: A field in your state schema (e.g., `current_step` or `active_agent`) tracks the current state. -2. **State update tool**: Tools change the state variable value when transitioning between states. -3. **Dynamic behavior**: On each turn, the system reads the state variable and adjusts behavior—either by applying different configuration (system prompt, tools) or routing to a different agent. - There are two ways to implement state machines: **[single agent with middleware](#single-agent-with-middleware)** (one agent with dynamic configuration) or **[multiple agent subgraphs](#multiple-agent-subgraphs)** (distinct agents as graph nodes). ### Single agent with middleware From 9f807db0eff492bde6e6351c01cf05ba35bd52e0 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 15 Dec 2025 22:47:10 -0500 Subject: [PATCH 4/4] x --- src/oss/langchain/multi-agent.mdx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/oss/langchain/multi-agent.mdx b/src/oss/langchain/multi-agent.mdx index f5af78b195..682d2e645e 100644 --- a/src/oss/langchain/multi-agent.mdx +++ b/src/oss/langchain/multi-agent.mdx @@ -489,20 +489,18 @@ sequenceDiagram participant Agent participant Workflow State - User->>Agent: "My device is broken" - Note over Agent,Workflow State: warranty_collector
Available: record_warranty_status + User->>Agent: "My phone is broken" + Note over Agent,Workflow State: Step: Get warranty status
Tools: record_warranty_status + Agent-->>User: "Is your device under warranty?" + + User->>Agent: "Yes, it's still under warranty" Agent->>Workflow State: record_warranty_status("in_warranty") - Note over Workflow State: → issue_classifier + Note over Agent,Workflow State: Step: Classify issue
Tools: record_issue_type Agent-->>User: "Can you describe the issue?" User->>Agent: "The screen is cracked" - Note over Agent,Workflow State: issue_classifier
Available: record_issue_type Agent->>Workflow State: record_issue_type("hardware") - Note over Workflow State: → resolution_specialist - Agent-->>User: "I can help with that" - - User->>Agent: "What should I do?" - Note over Agent,Workflow State: resolution_specialist
Available: provide_solution, escalate_to_human + Note over Agent,Workflow State: Step: Provide resolution
Tools: provide_solution, escalate_to_human Agent-->>User: "Here's the warranty repair process..." ```