[PR #6232] feat(langgraph): supports calling (multiple) handoffs and non-handoff tools at the same time #4919

Closed
opened 2026-02-20 17:50:57 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/langchain-ai/langgraph/pull/6232

State: closed
Merged: No


Description:

This PR extends ToolNode to support executing non-handoff tools in parallel while treating handoff tool calls (handoff_prefix, default "transfer_to") with special handling. Specifically:

Non-handoff tool calls are executed concurrently (threadpool or asyncio).

Multiple handoff calls are detected: only the first one is processed; subsequent ones are ignored with an error message.

Handoff calls can incorporate previous tool outputs into their state.messages.

Added error handling via a custom tool_error_handler, with explicit handling of AtaConnectionKickedOffError.

In chat_agent_executor, introduced a v3 mode to forward all tool_calls together, distinguishing handoffs more cleanly.

This improves agent flexibility in scenarios where multiple tool invocations and handoff operations need to be coordinated in the same step.

Issue:

N/A (new feature; not tied to a specific issue, but can help agent developers who need multi-tool and handoff orchestration).

Dependencies:

No new external dependencies introduced.

Reuses existing LangGraph execution utilities (get_executor_for_config, _run_one, _arun_one).

Tests & Docs:

from dotenv import load_dotenv
load_dotenv()

import asyncio
from typing import Annotated
from langchain.tools import tool
from langchain_openai import AzureChatOpenAI
from langgraph.prebuilt import create_react_agent, ToolNode
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langgraph_swarm import create_handoff_tool, create_swarm   # test with langgraph_swarm==0.0.14 to avoid implementing swarm again

model = AzureChatOpenAI(
    deployment_name="gpt-4o",
)


@tool
def get_weather(city: Annotated[str, "The city to get the weather for"]) -> dict:
    """Get the current weather information for a specified city."""
    print("[debug] get_weather called")
    return {
        "city": city,
        "temperature_range": "14-20C",
        "conditions": "Sunny with wind.",
    }


@tool
def search_web(query: Annotated[str, "The content you awant to search"]) -> dict:
    """Search the web for information retrieval."""
    print("[debug] search_web called")
    return {"content": "The nvidia stock price is $185"}


@tool
def query_device(device_id: Annotated[str, "The Device id"]) -> dict:
    """Get the current status of a device."""
    print("[debug] query_device! called")
    return {"online": True, "device_name": "Happy Device", "password": "a123456789"}


# Configure the agent with Azure OpenAI
agent = create_react_agent(
    model,
    ToolNode(
        [
            query_device,
            create_handoff_tool(agent_name="weather_agent"),
            create_handoff_tool(agent_name="web_agent"),
        ]
    ),
    prompt="You are a helpful assistant",
    name="agent",
)

weather_agent = create_react_agent(
    model,
    ToolNode(
        [
            get_weather,
            create_handoff_tool(agent_name="agent"),
            create_handoff_tool(agent_name="web_agent"),
        ]
    ),
    prompt="You are a helpful assistant that can search the weather",
    name="weather_agent",
)

web_agent = create_react_agent(
    model,
    ToolNode(
        [
            search_web,
            create_handoff_tool(agent_name="agent"),
            create_handoff_tool(agent_name="weather_agent"),
        ]
    ),
    prompt="You are a helpful assistant that can search the web",
    name="web_agent",
)

workflow = create_swarm([agent, weather_agent, web_agent], default_active_agent="agent")


# Compile with checkpointer/store
checkpointer = InMemorySaver()
store = InMemoryStore()

app = workflow.compile(checkpointer=checkpointer, store=store)


async def main():
    res = await app.ainvoke(
        {
            "messages": [
                (
                    "user",
                    "What is the stock price of nvidia; What is the status of device '123'",
                )
            ],
        },
        config={"configurable": {"thread_id": "123"}},
    )

    for m in res["messages"]:
        m.pretty_print()


if __name__ == "__main__":
    asyncio.run(main())

**Original Pull Request:** https://github.com/langchain-ai/langgraph/pull/6232 **State:** closed **Merged:** No --- ### Description: This PR extends ToolNode to support executing non-handoff tools in parallel while treating handoff tool calls (handoff_prefix, default "transfer_to") with special handling. Specifically: Non-handoff tool calls are executed concurrently (threadpool or asyncio). Multiple handoff calls are detected: only the first one is processed; subsequent ones are ignored with an error message. Handoff calls can incorporate previous tool outputs into their state.messages. Added error handling via a custom tool_error_handler, with explicit handling of AtaConnectionKickedOffError. In chat_agent_executor, introduced a v3 mode to forward all tool_calls together, distinguishing handoffs more cleanly. This improves agent flexibility in scenarios where multiple tool invocations and handoff operations need to be coordinated in the same step. ### Issue: N/A (new feature; not tied to a specific issue, but can help agent developers who need multi-tool and handoff orchestration). ### Dependencies: No new external dependencies introduced. Reuses existing LangGraph execution utilities (get_executor_for_config, _run_one, _arun_one). ### Tests & Docs: ``` python from dotenv import load_dotenv load_dotenv() import asyncio from typing import Annotated from langchain.tools import tool from langchain_openai import AzureChatOpenAI from langgraph.prebuilt import create_react_agent, ToolNode from langgraph.checkpoint.memory import InMemorySaver from langgraph.store.memory import InMemoryStore from langgraph_swarm import create_handoff_tool, create_swarm # test with langgraph_swarm==0.0.14 to avoid implementing swarm again model = AzureChatOpenAI( deployment_name="gpt-4o", ) @tool def get_weather(city: Annotated[str, "The city to get the weather for"]) -> dict: """Get the current weather information for a specified city.""" print("[debug] get_weather called") return { "city": city, "temperature_range": "14-20C", "conditions": "Sunny with wind.", } @tool def search_web(query: Annotated[str, "The content you awant to search"]) -> dict: """Search the web for information retrieval.""" print("[debug] search_web called") return {"content": "The nvidia stock price is $185"} @tool def query_device(device_id: Annotated[str, "The Device id"]) -> dict: """Get the current status of a device.""" print("[debug] query_device! called") return {"online": True, "device_name": "Happy Device", "password": "a123456789"} # Configure the agent with Azure OpenAI agent = create_react_agent( model, ToolNode( [ query_device, create_handoff_tool(agent_name="weather_agent"), create_handoff_tool(agent_name="web_agent"), ] ), prompt="You are a helpful assistant", name="agent", ) weather_agent = create_react_agent( model, ToolNode( [ get_weather, create_handoff_tool(agent_name="agent"), create_handoff_tool(agent_name="web_agent"), ] ), prompt="You are a helpful assistant that can search the weather", name="weather_agent", ) web_agent = create_react_agent( model, ToolNode( [ search_web, create_handoff_tool(agent_name="agent"), create_handoff_tool(agent_name="weather_agent"), ] ), prompt="You are a helpful assistant that can search the web", name="web_agent", ) workflow = create_swarm([agent, weather_agent, web_agent], default_active_agent="agent") # Compile with checkpointer/store checkpointer = InMemorySaver() store = InMemoryStore() app = workflow.compile(checkpointer=checkpointer, store=store) async def main(): res = await app.ainvoke( { "messages": [ ( "user", "What is the stock price of nvidia; What is the status of device '123'", ) ], }, config={"configurable": {"thread_id": "123"}}, ) for m in res["messages"]: m.pretty_print() if __name__ == "__main__": asyncio.run(main()) ```
yindo added the pull-request label 2026-02-20 17:50:57 -05:00
yindo closed this issue 2026-02-20 17:50:57 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#4919