feat: dynamic tool registration via middleware (#34842)

dependent upon https://github.com/langchain-ai/langgraph/pull/6711

1. relax constraint in `factory.py` to allow for tools not
pre-registered in the `ModelRequest.tools` list
2. always add tool node if `wrap_tool_call` or `awrap_tool_call` is
implemented
3. add tests confirming you can register new tools at runtime in
`wrap_model_call` and execute them via `wrap_tool_call`

allows for the following pattern

```py
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool

from libs.langchain_v1.langchain.agents.factory import create_agent
from libs.langchain_v1.langchain.agents.middleware.types import (
    AgentMiddleware,
    ModelRequest,
    ToolCallRequest,
)


@tool
def get_weather(location: str) -> str:
    """Get the current weather for a location."""
    return f"The weather in {location} is sunny and 72°F."


@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
    """Calculate the tip amount for a bill."""
    tip = bill_amount * (tip_percentage / 100)
    return f"Tip: ${tip:.2f}, Total: ${bill_amount + tip:.2f}"

class DynamicToolMiddleware(AgentMiddleware):
    """Middleware that adds and handles a dynamic tool."""

    def wrap_model_call(self, request: ModelRequest, handler):
        updated = request.override(tools=[*request.tools, calculate_tip])
        return handler(updated)

    def wrap_tool_call(self, request: ToolCallRequest, handler):
        if request.tool_call["name"] == "calculate_tip":
            return handler(request.override(tool=calculate_tip))
        return handler(request)


agent = create_agent(model="openai:gpt-4o-mini", tools=[get_weather], middleware=[DynamicToolMiddleware()])
result = agent.invoke({
    "messages": [HumanMessage("What's the weather in NYC? Also calculate a 20% tip on a $85 bill")]
})
for msg in result["messages"]:
    msg.pretty_print()
```
This commit is contained in:
Sydney Runkle
2026-01-23 10:12:48 -05:00
committed by GitHub
parent 5a956b745f
commit bc8620189c
5 changed files with 474 additions and 39 deletions

View File

@@ -63,6 +63,32 @@ if TYPE_CHECKING:
STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes." STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
DYNAMIC_TOOL_ERROR_TEMPLATE = """
Middleware added tools that the agent doesn't know how to execute.
Unknown tools: {unknown_tool_names}
Registered tools: {available_tool_names}
This happens when middleware modifies `request.tools` in `wrap_model_call` to include
tools that weren't passed to `create_agent()`.
How to fix this:
Option 1: Register tools at agent creation (recommended for most cases)
Pass the tools to `create_agent(tools=[...])` or set them on `middleware.tools`.
This makes tools available for every agent invocation.
Option 2: Handle dynamic tools in middleware (for tools created at runtime)
Implement `wrap_tool_call` to execute tools that are added dynamically:
class MyMiddleware(AgentMiddleware):
def wrap_tool_call(self, request, handler):
if request.tool_call["name"] == "dynamic_tool":
# Execute the dynamic tool yourself or override with tool instance
return handler(request.override(tool=my_dynamic_tool))
return handler(request)
""".strip()
FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [ FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [
# if model profile data are not available, these models are assumed to support # if model profile data are not available, these models are assumed to support
# structured output # structured output
@@ -775,14 +801,15 @@ def create_agent(
# Tools that require client-side execution (must be in ToolNode) # Tools that require client-side execution (must be in ToolNode)
available_tools = middleware_tools + regular_tools available_tools = middleware_tools + regular_tools
# Only create ToolNode if we have client-side tools # Create ToolNode if we have client-side tools OR if middleware defines wrap_tool_call
# (which may handle dynamically registered tools)
tool_node = ( tool_node = (
ToolNode( ToolNode(
tools=available_tools, tools=available_tools,
wrap_tool_call=wrap_tool_call_wrapper, wrap_tool_call=wrap_tool_call_wrapper,
awrap_tool_call=awrap_tool_call_wrapper, awrap_tool_call=awrap_tool_call_wrapper,
) )
if available_tools if available_tools or wrap_tool_call_wrapper or awrap_tool_call_wrapper
else None else None
) )
@@ -997,6 +1024,10 @@ def create_agent(
ValueError: If `ToolStrategy` specifies tools not declared upfront. ValueError: If `ToolStrategy` specifies tools not declared upfront.
""" """
# Validate ONLY client-side tools that need to exist in tool_node # Validate ONLY client-side tools that need to exist in tool_node
# Skip validation when wrap_tool_call is defined, as middleware may handle
# dynamic tools that are added at runtime via wrap_model_call
has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper
# Build map of available client-side tools from the ToolNode # Build map of available client-side tools from the ToolNode
# (which has already converted callables) # (which has already converted callables)
available_tools_by_name = {} available_tools_by_name = {}
@@ -1004,29 +1035,23 @@ def create_agent(
available_tools_by_name = tool_node.tools_by_name.copy() available_tools_by_name = tool_node.tools_by_name.copy()
# Check if any requested tools are unknown CLIENT-SIDE tools # Check if any requested tools are unknown CLIENT-SIDE tools
unknown_tool_names = [] # Only validate if wrap_tool_call is NOT defined (no dynamic tool handling)
for t in request.tools: if not has_wrap_tool_call:
# Only validate BaseTool instances (skip built-in dict tools) unknown_tool_names = []
if isinstance(t, dict): for t in request.tools:
continue # Only validate BaseTool instances (skip built-in dict tools)
if isinstance(t, BaseTool) and t.name not in available_tools_by_name: if isinstance(t, dict):
unknown_tool_names.append(t.name) continue
if isinstance(t, BaseTool) and t.name not in available_tools_by_name:
unknown_tool_names.append(t.name)
if unknown_tool_names: if unknown_tool_names:
available_tool_names = sorted(available_tools_by_name.keys()) available_tool_names = sorted(available_tools_by_name.keys())
msg = ( msg = DYNAMIC_TOOL_ERROR_TEMPLATE.format(
f"Middleware returned unknown tool names: {unknown_tool_names}\n\n" unknown_tool_names=unknown_tool_names,
f"Available client-side tools: {available_tool_names}\n\n" available_tool_names=available_tool_names,
"To fix this issue:\n" )
"1. Ensure the tools are passed to create_agent() via " raise ValueError(msg)
"the 'tools' parameter\n"
"2. If using custom middleware with tools, ensure "
"they're registered via middleware.tools attribute\n"
"3. Verify that tool names in ModelRequest.tools match "
"the actual tool.name values\n"
"Note: Built-in provider tools (dict format) can be added dynamically."
)
raise ValueError(msg)
# Determine effective response format (auto-detect if needed) # Determine effective response format (auto-detect if needed)
effective_response_format: ResponseFormat[Any] | None effective_response_format: ResponseFormat[Any] | None

View File

@@ -13,7 +13,7 @@ version = "1.2.6"
requires-python = ">=3.10.0,<4.0.0" requires-python = ">=3.10.0,<4.0.0"
dependencies = [ dependencies = [
"langchain-core>=1.2.7,<2.0.0", "langchain-core>=1.2.7,<2.0.0",
"langgraph>=1.0.2,<1.1.0", "langgraph>=1.0.7,<1.1.0",
"pydantic>=2.7.4,<3.0.0", "pydantic>=2.7.4,<3.0.0",
] ]

View File

@@ -0,0 +1,407 @@
"""Tests for dynamic tool registration via middleware.
These tests verify that middleware can dynamically register and handle tools
that are not declared upfront when creating the agent.
"""
from collections.abc import Awaitable, Callable
from typing import Any
import pytest
from langchain_core.messages import HumanMessage, ToolCall, ToolMessage
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from langchain.agents.factory import create_agent
from langchain.agents.middleware.types import (
AgentMiddleware,
ModelCallResult,
ModelRequest,
ModelResponse,
ToolCallRequest,
)
from tests.unit_tests.agents.model import FakeToolCallingModel
@tool
def static_tool(value: str) -> str:
"""A static tool that is always available."""
return f"Static result: {value}"
@tool
def dynamic_tool(value: str) -> str:
"""A dynamically registered tool."""
return f"Dynamic result: {value}"
@tool
def another_dynamic_tool(x: int, y: int) -> str:
"""Another dynamically registered tool for calculations."""
return f"Sum: {x + y}"
# -----------------------------------------------------------------------------
# Middleware classes
# -----------------------------------------------------------------------------
class DynamicToolMiddleware(AgentMiddleware):
"""Middleware that dynamically adds and handles a tool (sync and async)."""
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool])
return handler(updated)
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool])
return await handler(updated)
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
if request.tool_call["name"] == "dynamic_tool":
return handler(request.override(tool=dynamic_tool))
return handler(request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]:
if request.tool_call["name"] == "dynamic_tool":
return await handler(request.override(tool=dynamic_tool))
return await handler(request)
class MultipleDynamicToolsMiddleware(AgentMiddleware):
"""Middleware that dynamically adds multiple tools (sync and async)."""
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool, another_dynamic_tool])
return handler(updated)
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool, another_dynamic_tool])
return await handler(updated)
def _handle_tool(self, request: ToolCallRequest) -> ToolCallRequest | None:
"""Return updated request if this is a dynamic tool, else None."""
tool_name = request.tool_call["name"]
if tool_name == "dynamic_tool":
return request.override(tool=dynamic_tool)
if tool_name == "another_dynamic_tool":
return request.override(tool=another_dynamic_tool)
return None
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
updated = self._handle_tool(request)
return handler(updated if updated else request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]:
updated = self._handle_tool(request)
return await handler(updated if updated else request)
class DynamicToolMiddlewareWithoutHandler(AgentMiddleware):
"""Middleware that adds a dynamic tool but doesn't handle it."""
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool])
return handler(updated)
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelCallResult:
updated = request.override(tools=[*request.tools, dynamic_tool])
return await handler(updated)
class ConditionalDynamicToolMiddleware(AgentMiddleware):
"""Middleware that conditionally adds a tool based on state (sync and async)."""
def _should_add_tool(self, request: ModelRequest) -> bool:
messages = request.state.get("messages", [])
return messages and "calculator" in str(messages[-1].content).lower()
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
if self._should_add_tool(request):
request = request.override(tools=[*request.tools, another_dynamic_tool])
return handler(request)
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelCallResult:
if self._should_add_tool(request):
request = request.override(tools=[*request.tools, another_dynamic_tool])
return await handler(request)
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
if request.tool_call["name"] == "another_dynamic_tool":
return handler(request.override(tool=another_dynamic_tool))
return handler(request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]:
if request.tool_call["name"] == "another_dynamic_tool":
return await handler(request.override(tool=another_dynamic_tool))
return await handler(request)
# -----------------------------------------------------------------------------
# Helper functions
# -----------------------------------------------------------------------------
def get_tool_messages(result: dict[str, Any]) -> list[ToolMessage]:
"""Extract ToolMessage objects from agent result."""
return [m for m in result["messages"] if isinstance(m, ToolMessage)]
async def invoke_agent(agent: Any, message: str, *, use_async: bool) -> dict[str, Any]:
"""Invoke agent synchronously or asynchronously based on flag."""
input_data = {"messages": [HumanMessage(message)]}
config = {"configurable": {"thread_id": "test"}}
if use_async:
return await agent.ainvoke(input_data, config)
return agent.invoke(input_data, config)
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("use_async", [False, True])
@pytest.mark.parametrize(
"tools",
[
pytest.param([static_tool], id="with_static_tools"),
pytest.param([], id="without_static_tools"),
pytest.param(None, id="with_none_tools"),
],
)
async def test_dynamic_tool_basic(*, use_async: bool, tools: list[Any] | None) -> None:
"""Test dynamic tool registration with various static tool configurations."""
model = FakeToolCallingModel(
tool_calls=[
[ToolCall(name="dynamic_tool", args={"value": "test"}, id="1")],
[],
]
)
agent = create_agent(
model=model,
tools=tools, # type: ignore[arg-type]
middleware=[DynamicToolMiddleware()],
checkpointer=InMemorySaver(),
)
result = await invoke_agent(agent, "Use the dynamic tool", use_async=use_async)
tool_messages = get_tool_messages(result)
assert len(tool_messages) == 1
assert tool_messages[0].name == "dynamic_tool"
assert "Dynamic result: test" in tool_messages[0].content
@pytest.mark.parametrize("use_async", [False, True])
async def test_multiple_dynamic_tools_with_static(*, use_async: bool) -> None:
"""Test multiple dynamic tools and mixing with static tool calls."""
model = FakeToolCallingModel(
tool_calls=[
[
ToolCall(name="static_tool", args={"value": "static-call"}, id="1"),
ToolCall(name="dynamic_tool", args={"value": "first"}, id="2"),
ToolCall(name="another_dynamic_tool", args={"x": 5, "y": 3}, id="3"),
],
[],
]
)
agent = create_agent(
model=model,
tools=[static_tool],
middleware=[MultipleDynamicToolsMiddleware()],
checkpointer=InMemorySaver(),
)
result = await invoke_agent(agent, "Use all tools", use_async=use_async)
tool_messages = get_tool_messages(result)
assert len(tool_messages) == 3
tool_results = {m.name: m.content for m in tool_messages}
assert "Static result: static-call" in tool_results["static_tool"]
assert "Dynamic result: first" in tool_results["dynamic_tool"]
assert "Sum: 8" in tool_results["another_dynamic_tool"]
@pytest.mark.parametrize("use_async", [False, True])
@pytest.mark.parametrize(
"tools",
[
pytest.param([static_tool], id="with_static_tools"),
pytest.param([], id="without_static_tools"),
],
)
async def test_dynamic_tool_without_handler_raises_error(
*, use_async: bool, tools: list[Any]
) -> None:
"""Test that a helpful error is raised when dynamic tool is not handled."""
model = FakeToolCallingModel(
tool_calls=[
[ToolCall(name="dynamic_tool", args={"value": "test"}, id="1")],
[],
]
)
agent = create_agent(
model=model,
tools=tools,
middleware=[DynamicToolMiddlewareWithoutHandler()],
checkpointer=InMemorySaver(),
)
with pytest.raises(
ValueError,
match=r"(?s)Middleware added tools.*Unknown tools:.*dynamic_tool",
):
await invoke_agent(agent, "Use the dynamic tool", use_async=use_async)
@pytest.mark.parametrize("use_async", [False, True])
async def test_conditional_dynamic_tool(*, use_async: bool) -> None:
"""Test that dynamic tools can be conditionally added based on state."""
model = FakeToolCallingModel(
tool_calls=[
[ToolCall(name="another_dynamic_tool", args={"x": 10, "y": 20}, id="1")],
[],
]
)
agent = create_agent(
model=model,
tools=[static_tool],
middleware=[ConditionalDynamicToolMiddleware()],
checkpointer=InMemorySaver(),
)
result = await invoke_agent(agent, "I need a calculator to add numbers", use_async=use_async)
tool_messages = get_tool_messages(result)
assert len(tool_messages) == 1
assert tool_messages[0].name == "another_dynamic_tool"
assert "Sum: 30" in tool_messages[0].content
@pytest.mark.parametrize("use_async", [False, True])
async def test_dynamic_tool_chained_middleware(*, use_async: bool) -> None:
"""Test dynamic tools work with multiple middleware in chain."""
call_log: list[str] = []
class LoggingMiddleware(AgentMiddleware):
def __init__(self, label: str) -> None:
self._label = label
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelCallResult:
call_log.append(f"{self._label}_model")
return handler(request)
async def awrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
) -> ModelCallResult:
call_log.append(f"{self._label}_model")
return await handler(request)
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
call_log.append(f"{self._label}_tool")
return handler(request)
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]:
call_log.append(f"{self._label}_tool")
return await handler(request)
model = FakeToolCallingModel(
tool_calls=[
[ToolCall(name="dynamic_tool", args={"value": "chained"}, id="1")],
[],
]
)
agent = create_agent(
model=model,
tools=[static_tool],
middleware=[LoggingMiddleware("first"), DynamicToolMiddleware()],
checkpointer=InMemorySaver(),
)
result = await invoke_agent(agent, "Use the dynamic tool", use_async=use_async)
tool_messages = get_tool_messages(result)
assert len(tool_messages) == 1
assert tool_messages[0].name == "dynamic_tool"
# Verify middleware chain was called
assert "first_model" in call_log
assert "first_tool" in call_log

View File

@@ -152,7 +152,10 @@ def test_unknown_tool_raises_error() -> None:
middleware=[BadMiddleware()], middleware=[BadMiddleware()],
) )
with pytest.raises(ValueError, match="Middleware returned unknown tool names"): with pytest.raises(
ValueError,
match=r"(?s)Middleware added tools.*Unknown tools:.*unknown_tool",
):
agent.invoke({"messages": [HumanMessage("Hello")]}) agent.invoke({"messages": [HumanMessage("Hello")]})

View File

@@ -1970,7 +1970,7 @@ requires-dist = [
{ name = "langchain-perplexity", marker = "extra == 'perplexity'" }, { name = "langchain-perplexity", marker = "extra == 'perplexity'" },
{ name = "langchain-together", marker = "extra == 'together'" }, { name = "langchain-together", marker = "extra == 'together'" },
{ name = "langchain-xai", marker = "extra == 'xai'" }, { name = "langchain-xai", marker = "extra == 'xai'" },
{ name = "langgraph", specifier = ">=1.0.2,<1.1.0" }, { name = "langgraph", specifier = ">=1.0.7,<1.1.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" }, { name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
] ]
provides-extras = ["community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"] provides-extras = ["community", "anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
@@ -2119,7 +2119,7 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "jsonpatch", specifier = ">=1.33.0,<2.0.0" }, { name = "jsonpatch", specifier = ">=1.33.0,<2.0.0" },
{ name = "langsmith", specifier = ">=0.3.45,<1.0.0" }, { name = "langsmith", specifier = ">=0.3.45,<1.0.0" },
{ name = "packaging", specifier = ">=23.2.0,<26.0.0" }, { name = "packaging", specifier = ">=23.2.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" }, { name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" }, { name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" }, { name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
@@ -2131,7 +2131,7 @@ requires-dist = [
dev = [ dev = [
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" }, { name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" }, { name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" }, { name = "setuptools", specifier = ">=67.6.1,<79.0.0" },
] ]
lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }] lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }]
test = [ test = [
@@ -2303,7 +2303,7 @@ dev = [{ name = "langchain-core", editable = "../core" }]
lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }] lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }]
test = [ test = [
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, { name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "langchain", editable = "../langchain_v1" }, { name = "langchain", editable = "." },
{ name = "langchain-core", editable = "../core" }, { name = "langchain-core", editable = "../core" },
{ name = "langchain-tests", editable = "../standard-tests" }, { name = "langchain-tests", editable = "../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" }, { name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
@@ -2484,7 +2484,7 @@ wheels = [
[[package]] [[package]]
name = "langgraph" name = "langgraph"
version = "1.0.4" version = "1.0.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
@@ -2494,9 +2494,9 @@ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "xxhash" }, { name = "xxhash" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d6/3c/af87902d300c1f467165558c8966d8b1e1f896dace271d3f35a410a5c26a/langgraph-1.0.4.tar.gz", hash = "sha256:86d08e25d7244340f59c5200fa69fdd11066aa999b3164b531e2a20036fac156", size = 484397, upload-time = "2025-11-25T20:31:48.608Z" } sdist = { url = "https://files.pythonhosted.org/packages/72/5b/f72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f/langgraph-1.0.7.tar.gz", hash = "sha256:0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a", size = 497098, upload-time = "2026-01-22T16:57:47.303Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/52/4eb25a3f60399da34ba34adff1b3e324cf0d87eb7a08cebf1882a9b5e0d5/langgraph-1.0.4-py3-none-any.whl", hash = "sha256:b1a835ceb0a8d69b9db48075e1939e28b1ad70ee23fa3fa8f90149904778bacf", size = 157271, upload-time = "2025-11-25T20:31:47.518Z" }, { url = "https://files.pythonhosted.org/packages/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl", hash = "sha256:9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2", size = 157353, upload-time = "2026-01-22T16:57:45.997Z" },
] ]
[[package]] [[package]]
@@ -2514,28 +2514,28 @@ wheels = [
[[package]] [[package]]
name = "langgraph-prebuilt" name = "langgraph-prebuilt"
version = "1.0.5" version = "1.0.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
{ name = "langgraph-checkpoint" }, { name = "langgraph-checkpoint" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/46/f9/54f8891b32159e4542236817aea2ee83de0de18bce28e9bdba08c7f93001/langgraph_prebuilt-1.0.5.tar.gz", hash = "sha256:85802675ad778cc7240fd02d47db1e0b59c0c86d8369447d77ce47623845db2d", size = 144453, upload-time = "2025-11-20T16:47:39.23Z" } sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/5e/aeba4a5b39fe6e874e0dd003a82da71c7153e671312671a8dacc5cb7c1af/langgraph_prebuilt-1.0.5-py3-none-any.whl", hash = "sha256:22369563e1848862ace53fbc11b027c28dd04a9ac39314633bb95f2a7e258496", size = 35072, upload-time = "2025-11-20T16:47:38.187Z" }, { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" },
] ]
[[package]] [[package]]
name = "langgraph-sdk" name = "langgraph-sdk"
version = "0.2.9" version = "0.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "orjson" }, { name = "orjson" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/23/d8/40e01190a73c564a4744e29a6c902f78d34d43dad9b652a363a92a67059c/langgraph_sdk-0.2.9.tar.gz", hash = "sha256:b3bd04c6be4fa382996cd2be8fbc1e7cc94857d2bc6b6f4599a7f2a245975303", size = 99802, upload-time = "2025-09-20T18:49:14.734Z" } sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/05/b2d34e16638241e6f27a6946d28160d4b8b641383787646d41a3727e0896/langgraph_sdk-0.2.9-py3-none-any.whl", hash = "sha256:fbf302edadbf0fb343596f91c597794e936ef68eebc0d3e1d358b6f9f72a1429", size = 56752, upload-time = "2025-09-20T18:49:13.346Z" }, { url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" },
] ]
[[package]] [[package]]