Handoff goes back to initial agent

Hi,

I’m trying to make the simplest possible example of a multiagent system with a handoff tool (as a surrogate of a more complex system).

Agent A: addition and substraction

Agent M: multiplication and division

After handoff to agent M, even if I delete all the conversation history, the graph returns immediately to Agent A.

Why? What is the correct way of implementing this?



# agents/base_react_agent.py
from typing import Annotated, Literal
from langgraph.types import Command
from typing_extensions import TypedDict

# LangGraph
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage, AIMessage,SystemMessage
from langgraph.types import Overwrite
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langchain.messages import RemoveMessage  


from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call

# Memory
from langgraph.checkpoint.memory import InMemorySaver
llm = init_chat_model("openai:gpt-4o-mini")

class State(TypedDict):
    messages: Annotated[list, add_messages]

# Subgraph 1: Sum and subtract two numbers
@tool
def sum_two_numbers(arg1: float, arg2: float) -> float:
    """Sum two numbers"""
    return arg1 + arg2

@tool
def subtract_two_numbers(arg1: float, arg2: float) -> float:
    """Subtract two numbers"""
    return arg1 - arg2

# Subgraph 2: Multiply and divide two numbers
@tool
def multiply_two_numbers(arg1: float, arg2: float) -> float:
    """Multiply two numbers"""
    return arg1 * arg2

@tool
def divide_two_numbers(arg1: float, arg2: float) -> float:
    """Divide two numbers"""
    return arg1 / arg2

@tool
def handoff_tool(agent: str) -> Literal["subgraph_1", "subgraph_2"]:
    """Handoff to another agent based on it's name"""
    print("Handoff to:", agent)
    return Command(
            goto=agent,
            graph=Command.PARENT,
            update = {
                # "messages": [ToolMessage(content=f"Handing off to {agent}", tool_call_id=uuid.uuid4(), tool_name="handoff_tool")]
                # "messages": [AIMessage(content=f"Handing off to {agent}")]
                "messages": [RemoveMessage( id = REMOVE_ALL_MESSAGES)]
                # "messages": Overwrite(value = "You are a recently spawned agent")
            }
        )

@wrap_tool_call
def tool_wrap(request, handler):
    print(f"I'm a tool call middleware")
    """Handle tool execution errors with custom messages."""
    try:
        return handler(request)
    except Exception as e:
        # Return a custom error message to the model
        return ToolMessage(
            content=f"Tool error: Please check your input and try again. ({str(e)})",
            tool_call_id=request.tool_call["id"]
        )

SYSTEM_PROMPT_1 = """ You are an assistant that can sum and subtract two numbers.
    Your name is Andres you perform additions and substractions, You can handoff to subgraph_2, an agent that can perform divisions and multiplications using the handoff tool"""
SYSTEM_PROMPT_2 = """ You are an assistant that can multiply and divide two numbers.
    Your name is Maria you perform multiplications and divisions, You can handoff to subgraph_1, an agent that can perform additions and substractions using the handoff tool"""

agent_1 = create_agent( 
    model="openai:gpt-4o-mini",
    system_prompt=SYSTEM_PROMPT_1,
    tools=[sum_two_numbers, subtract_two_numbers, handoff_tool],
    middleware=[tool_wrap])

agent_2 = create_agent(
    model="openai:gpt-4o-mini",
    tools=[multiply_two_numbers, divide_two_numbers, handoff_tool],
    middleware=[tool_wrap],
    system_prompt=SYSTEM_PROMPT_2)


main_graph_builder = StateGraph(State)
main_graph_builder.add_node("subgraph_1", agent_1)
main_graph_builder.add_node("subgraph_2", agent_2)
main_graph_builder.set_entry_point("subgraph_1")
main_graph = main_graph_builder.compile()

checkpointer = InMemorySaver()

main_graph = main_graph_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}



def stream_graph_updates(user_input: str):
    for event in main_graph.stream({"messages": [{"role": "user", "content": user_input}]}, config=config):
    #    pprint.pprint(event)
       for node_name, value in event.items():

            # Skip empty returns
            if value is None:
                continue

            # ---- PRINT THE AGENT NAME ----
            print(f"\n### Agent speaking: {node_name}\n")

       for key, value in event.items():
            if value is None:
                continue  # skip empty returns (like Command handoffs)
            if "messages" in value and value["messages"]:
                if "content" in value["messages"][-1]:
                    print("Assistant:", value["messages"][-1].content)
                else:
                    print("Assistant:", str(value["messages"][-1]))

                # print("------------META-------------")
                # print("Message length:", len(value["messages"]))
                # print(50*"-")
                # for message in value["messages"]:
                #         pprint.pprint(message)
                # print(50*"-")
                # print("------------META-------------")

while True:

    user_input = input("User: ")
    if user_input.lower() in ["quit", "exit", "q"]:
        print("Goodbye!")
    stream_graph_updates(user_input)
    



hi @reconlabs-sergio

try this (it’s suboptimal for math operation though):slight_smile:

import os
from typing import Annotated, Literal, Optional
from time import perf_counter

from dotenv import load_dotenv
from typing_extensions import TypedDict

# LangGraph / LangChain imports
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import START, END, StateGraph
from langgraph.graph.message import add_messages


# ------------------------------------------------------------
# Environment setup
# ------------------------------------------------------------
load_dotenv()

# Model can be overridden with OPENAI_MODEL; default to GPT-5 as requested
DEFAULT_MODEL = "openai:gpt-5"
MODEL_ID = os.getenv("OPENAI_MODEL", DEFAULT_MODEL)

DEBUG = os.getenv("DEBUG_HANDOFF", "1").lower() in ("1", "true", "yes", "y")

def dprint(*args):
    if DEBUG:
        print("[DEBUG]", *args)


def debug_pretty_print_messages(messages, header: Optional[str] = None):
    """
    Pretty-print a list of messages when DEBUG is enabled, with start/end delimiters.
    """
    if not DEBUG:
        return
    label = header or "messages"
    dprint(f"----- {label} -----")
    for message in messages or []:
        try:
            message.pretty_print()
        except Exception:
            # Fallback in case a message lacks pretty_print
            print(message)
    dprint(f"----- {label} -----")


# ------------------------------------------------------------
# State definition
# ------------------------------------------------------------
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    current_agent: Optional[Literal["subgraph_1", "subgraph_2"]]


# ------------------------------------------------------------
# Tools
# ------------------------------------------------------------
@tool
def sum_two_numbers(arg1: float, arg2: float) -> float:
    """Sum two numbers"""
    return arg1 + arg2


@tool
def subtract_two_numbers(arg1: float, arg2: float) -> float:
    """Subtract two numbers"""
    return arg1 - arg2


@tool
def multiply_two_numbers(arg1: float, arg2: float) -> float:
    """Multiply two numbers"""
    return arg1 * arg2


@tool
def divide_two_numbers(arg1: float, arg2: float) -> float:
    """Divide two numbers"""
    return arg1 / arg2


@tool
def handoff_tool(agent: Literal["subgraph_1", "subgraph_2"]) -> str:
    """Request a handoff to a named agent (returns the node name)."""
    return agent


# ------------------------------------------------------------
# Agents (prebuilt)
# ------------------------------------------------------------
SYSTEM_PROMPT_1 = (
    "You are an assistant that can sum and subtract two numbers. "
    "Your name is Andres. If the user requests multiplication or division, "
    "call the `handoff_tool` with 'subgraph_2'."
)

SYSTEM_PROMPT_2 = (
    "You are an assistant that can multiply and divide two numbers. "
    "Your name is Maria. If the user requests addition or subtraction, "
    "call the `handoff_tool` with 'subgraph_1'."
)

agent_1 = create_agent(
    model=MODEL_ID,
    system_prompt=SYSTEM_PROMPT_1,
    tools=[sum_two_numbers, subtract_two_numbers, handoff_tool],
)

agent_2 = create_agent(
    model=MODEL_ID,
    system_prompt=SYSTEM_PROMPT_2,
    tools=[multiply_two_numbers, divide_two_numbers, handoff_tool],
)


# ------------------------------------------------------------
# Routing helpers
# ------------------------------------------------------------
def route_by_handoff(state: AgentState):
    """
    Inspect the latest ToolMessage from `handoff_tool` and route accordingly.
    If no handoff is requested, end the turn.
    """
    if DEBUG:
        dprint("----- START: route_by_handoff -----")
        _t0_route_by_handoff = perf_counter()
    dprint("route_by_handoff: evaluating handoff")
    current = state.get("current_agent")
    dprint("route_by_handoff: current_agent =", current)
    for message in reversed(state["messages"]):
        if isinstance(message, ToolMessage) and getattr(message, "name", None) == "handoff_tool":
            destination = str(message.content).strip()
            dprint(
                "route_by_handoff: found ToolMessage",
                "name =", getattr(message, "name", None),
                "content =", repr(message.content),
                "-> dest =", destination,
            )
            if destination in ("subgraph_1", "subgraph_2"):
                # Avoid self-loop; if the agent asks to hand off to itself, end this turn
                if destination == current:
                    dprint("route_by_handoff: destination equals current; returning END")
                    if DEBUG:
                        dprint(f"----- END: route_by_handoff ({(perf_counter() - _t0_route_by_handoff) * 1000.0:.1f} ms) -----")
                    return END
                dprint("route_by_handoff: routing to", destination)
                if DEBUG:
                    dprint(f"----- END: route_by_handoff ({(perf_counter() - _t0_route_by_handoff) * 1000.0:.1f} ms) -----")
                return destination
            dprint("route_by_handoff: invalid destination; returning END")
            if DEBUG:
                dprint(f"----- END: route_by_handoff ({(perf_counter() - _t0_route_by_handoff) * 1000.0:.1f} ms) -----")
            return END
    dprint("route_by_handoff: no handoff; returning END")
    if DEBUG:
        dprint(f"----- END: route_by_handoff ({(perf_counter() - _t0_route_by_handoff) * 1000.0:.1f} ms) -----")
    return END


def start_router(state: AgentState):
    """
    On new user turns, start where we left off if available; otherwise subgraph_1.
    """
    if DEBUG:
        dprint("----- START: start_router -----")
        _t0_start_router = perf_counter()
    choice = state.get("current_agent") or "subgraph_1"
    dprint("start_router:", "current_agent =", state.get("current_agent"), "->", choice)
    if DEBUG:
        dprint(f"----- END: start_router ({(perf_counter() - _t0_start_router) * 1000.0:.1f} ms) -----")
    return choice


# ------------------------------------------------------------
# Node wrappers to track last active agent
# ------------------------------------------------------------
def agent_1_node(state: AgentState):
    if DEBUG:
        dprint("----- START: agent_1_node -----")
        _t0_agent_1 = perf_counter()
    debug_pretty_print_messages(
        state.get("messages", []),
        "agent_1_node: invoking agent_1; incoming messages =",
    )
    out = agent_1.invoke(state)
    # Ensure we remember last active agent for next user turn
    debug_pretty_print_messages(
        out.get("messages", []),
        "agent_1_node: produced output; messages =",
    )
    out["current_agent"] = "subgraph_1"
    if DEBUG:
        dprint(f"----- END: agent_1_node ({(perf_counter() - _t0_agent_1) * 1000.0:.1f} ms) -----")
    return out


def agent_2_node(state: AgentState):
    if DEBUG:
        dprint("----- START: agent_2_node -----")
        _t0_agent_2 = perf_counter()
    debug_pretty_print_messages(
        state.get("messages", []),
        "agent_2_node: invoking agent_2; incoming messages =",
    )
    out = agent_2.invoke(state)
    debug_pretty_print_messages(
        out.get("messages", []),
        "agent_2_node: produced output; messages =",
    )
    out["current_agent"] = "subgraph_2"
    if DEBUG:
        dprint(f"----- END: agent_2_node ({(perf_counter() - _t0_agent_2) * 1000.0:.1f} ms) -----")
    return out


# ------------------------------------------------------------
# Graph assembly
# ------------------------------------------------------------
graph_builder = StateGraph(AgentState)
graph_builder.add_node("subgraph_1", agent_1_node)
graph_builder.add_node("subgraph_2", agent_2_node)

# Where to start on a brand-new thread or when state lacks a current agent
graph_builder.add_conditional_edges(
    START,
    start_router,
    {"subgraph_1": "subgraph_1", "subgraph_2": "subgraph_2"},
)

# Conditional routing after each node based on `handoff_tool`
graph_builder.add_conditional_edges(
    "subgraph_1",
    route_by_handoff,
    {"subgraph_2": "subgraph_2", END: END},
)
graph_builder.add_conditional_edges(
    "subgraph_2",
    route_by_handoff,
    {"subgraph_1": "subgraph_1", END: END},
)

# Checkpointing
checkpointer = InMemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)

# A default config with a stable thread ID for interactive runs
CONFIG = {"configurable": {"thread_id": "handoff-demo"}}


# ------------------------------------------------------------
# Interactive runner
# ------------------------------------------------------------
def run_interactive():
    print("Handoff demo. Try prompts like: 'multiply 6 by 7' or 'add 5 and 9'.")
    print("Type 'quit' to exit.\n")
    while True:
        try:
            user_input = input("User: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nGoodbye!")
            break
        if user_input.lower() in {"q", "quit", "exit"}:
            print("Goodbye!")
            break
        events = graph.stream({"messages": [{"role": "user", "content": user_input}]}, config=CONFIG)
        for event in events:
            dprint("event keys:", list(event.keys()))
            for node_name, value in event.items():
                if not value:
                    continue
                print(f"\n### Agent speaking: {node_name}\n")
                messages = value.get("messages", [])
                if messages:
                    msg = messages[-1]
                    content = getattr(msg, "content", None)
                    dprint("last_message_type:", type(msg).__name__)
                    if isinstance(msg, ToolMessage):
                        dprint("last_tool_message:", "name =", getattr(msg, "name", None), "content =", repr(msg.content))
                    if content:
                        print(f"Assistant: {content}")
        print()


if __name__ == "__main__":
    # Validate that an API key exists; dotenv should have loaded it if present
    if not os.getenv("OPENAI_API_KEY"):
        print(
            "Warning: OPENAI_API_KEY is not set. Set it in your environment or a .env file."
        )
    if DEBUG:
        print("[DEBUG] DEBUG_HANDOFF enabled")
    run_interactive()



Hi @pawel-twardziak !

Thank you. Your approach works. Is this the “official” way of doing handoffs?

  • While the docs state that there are different ways of using handoffs, it seems that for this case the Command function is the suggested approach.
  • I’m getting a significant latency increase with your code (3s vs. 0.7s), do you know why?

Best,

Sergio

Hi @reconlabs-sergio

I am not sure we can say this is the official one, I just build it on top of your code to make it work.

Yeah, as I mentioned this is suboptimal, I dunno why actually. It seems like the reasoning process takes time.

I added debugging and here is an example output:

[DEBUG] DEBUG_HANDOFF enabled
Handoff demo. Try prompts like: 'multiply 6 by 7' or 'add 5 and 9'.
Type 'quit' to exit.

User: multiply 6 by 7
[DEBUG] ----- START: start_router -----
[DEBUG] start_router: current_agent = None -> subgraph_1
[DEBUG] ----- END: start_router (0.0 ms) -----
[DEBUG] ----- START: agent_1_node -----
[DEBUG] ----- agent_1_node: invoking agent_1; incoming messages = -----
================================ Human Message =================================

multiply 6 by 7
[DEBUG] ----- agent_1_node: invoking agent_1; incoming messages = -----
[DEBUG] ----- agent_1_node: produced output; messages = -----
================================ Human Message =================================

multiply 6 by 7
================================== Ai Message ==================================
Tool Calls:
  handoff_tool (call_hjzBGdxu36OkrtsRyYf6aGHy)
 Call ID: call_hjzBGdxu36OkrtsRyYf6aGHy
  Args:
    agent: subgraph_2
================================= Tool Message =================================
Name: handoff_tool

subgraph_2
================================== Ai Message ==================================

I’ve handed this off to the appropriate agent (subgraph_2) to handle multiplication.
[DEBUG] ----- agent_1_node: produced output; messages = -----
[DEBUG] ----- END: agent_1_node (11084.5 ms) -----
[DEBUG] ----- START: route_by_handoff -----
[DEBUG] route_by_handoff: evaluating handoff
[DEBUG] route_by_handoff: current_agent = subgraph_1
[DEBUG] route_by_handoff: found ToolMessage name = handoff_tool content = 'subgraph_2' -> dest = subgraph_2
[DEBUG] route_by_handoff: routing to subgraph_2
[DEBUG] ----- END: route_by_handoff (0.0 ms) -----
[DEBUG] event keys: ['subgraph_1']

### Agent speaking: subgraph_1

[DEBUG] last_message_type: AIMessage
Assistant: I’ve handed this off to the appropriate agent (subgraph_2) to handle multiplication.
[DEBUG] ----- START: agent_2_node -----
[DEBUG] ----- agent_2_node: invoking agent_2; incoming messages = -----
================================ Human Message =================================

multiply 6 by 7
================================== Ai Message ==================================
Tool Calls:
  handoff_tool (call_hjzBGdxu36OkrtsRyYf6aGHy)
 Call ID: call_hjzBGdxu36OkrtsRyYf6aGHy
  Args:
    agent: subgraph_2
================================= Tool Message =================================
Name: handoff_tool

subgraph_2
================================== Ai Message ==================================

I’ve handed this off to the appropriate agent (subgraph_2) to handle multiplication.
[DEBUG] ----- agent_2_node: invoking agent_2; incoming messages = -----
[DEBUG] ----- agent_2_node: produced output; messages = -----
================================ Human Message =================================

multiply 6 by 7
================================== Ai Message ==================================
Tool Calls:
  handoff_tool (call_hjzBGdxu36OkrtsRyYf6aGHy)
 Call ID: call_hjzBGdxu36OkrtsRyYf6aGHy
  Args:
    agent: subgraph_2
================================= Tool Message =================================
Name: handoff_tool

subgraph_2
================================== Ai Message ==================================

I’ve handed this off to the appropriate agent (subgraph_2) to handle multiplication.
================================== Ai Message ==================================
Tool Calls:
  multiply_two_numbers (call_tmIMJEP35OeZWsI5bjwsWaUa)
 Call ID: call_tmIMJEP35OeZWsI5bjwsWaUa
  Args:
    arg1: 6
    arg2: 7
================================= Tool Message =================================
Name: multiply_two_numbers

42.0
================================== Ai Message ==================================

42
[DEBUG] ----- agent_2_node: produced output; messages = -----
[DEBUG] ----- END: agent_2_node (11039.5 ms) -----
[DEBUG] ----- START: route_by_handoff -----
[DEBUG] route_by_handoff: evaluating handoff
[DEBUG] route_by_handoff: current_agent = subgraph_2
[DEBUG] route_by_handoff: found ToolMessage name = handoff_tool content = 'subgraph_2' -> dest = subgraph_2
[DEBUG] route_by_handoff: destination equals current; returning END
[DEBUG] ----- END: route_by_handoff (0.0 ms) -----
[DEBUG] event keys: ['subgraph_2']

### Agent speaking: subgraph_2

[DEBUG] last_message_type: AIMessage
Assistant: 42