diff --git a/libs/langchain_v1/langchain/agents/factory.py b/libs/langchain_v1/langchain/agents/factory.py index 3c4cef05243..a7106fbb787 100644 --- a/libs/langchain_v1/langchain/agents/factory.py +++ b/libs/langchain_v1/langchain/agents/factory.py @@ -63,6 +63,32 @@ if TYPE_CHECKING: 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 = [ # if model profile data are not available, these models are assumed to support # structured output @@ -775,14 +801,15 @@ def create_agent( # Tools that require client-side execution (must be in ToolNode) 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 = ( ToolNode( tools=available_tools, wrap_tool_call=wrap_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 ) @@ -997,6 +1024,10 @@ def create_agent( ValueError: If `ToolStrategy` specifies tools not declared upfront. """ # 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 # (which has already converted callables) available_tools_by_name = {} @@ -1004,29 +1035,23 @@ def create_agent( available_tools_by_name = tool_node.tools_by_name.copy() # Check if any requested tools are unknown CLIENT-SIDE tools - unknown_tool_names = [] - for t in request.tools: - # Only validate BaseTool instances (skip built-in dict tools) - if isinstance(t, dict): - continue - if isinstance(t, BaseTool) and t.name not in available_tools_by_name: - unknown_tool_names.append(t.name) + # Only validate if wrap_tool_call is NOT defined (no dynamic tool handling) + if not has_wrap_tool_call: + unknown_tool_names = [] + for t in request.tools: + # Only validate BaseTool instances (skip built-in dict tools) + if isinstance(t, dict): + continue + if isinstance(t, BaseTool) and t.name not in available_tools_by_name: + unknown_tool_names.append(t.name) - if unknown_tool_names: - available_tool_names = sorted(available_tools_by_name.keys()) - msg = ( - f"Middleware returned unknown tool names: {unknown_tool_names}\n\n" - f"Available client-side tools: {available_tool_names}\n\n" - "To fix this issue:\n" - "1. Ensure the tools are passed to create_agent() via " - "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) + if unknown_tool_names: + available_tool_names = sorted(available_tools_by_name.keys()) + msg = DYNAMIC_TOOL_ERROR_TEMPLATE.format( + unknown_tool_names=unknown_tool_names, + available_tool_names=available_tool_names, + ) + raise ValueError(msg) # Determine effective response format (auto-detect if needed) effective_response_format: ResponseFormat[Any] | None diff --git a/libs/langchain_v1/pyproject.toml b/libs/langchain_v1/pyproject.toml index 70f8d7d01eb..d40d583fb94 100644 --- a/libs/langchain_v1/pyproject.toml +++ b/libs/langchain_v1/pyproject.toml @@ -13,7 +13,7 @@ version = "1.2.6" requires-python = ">=3.10.0,<4.0.0" dependencies = [ "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", ] diff --git a/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_dynamic_tools.py b/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_dynamic_tools.py new file mode 100644 index 00000000000..bfa4def5692 --- /dev/null +++ b/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_dynamic_tools.py @@ -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 diff --git a/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_tools.py b/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_tools.py index d6c3bbfd20a..53cf54ffebb 100644 --- a/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_tools.py +++ b/libs/langchain_v1/tests/unit_tests/agents/middleware/core/test_tools.py @@ -152,7 +152,10 @@ def test_unknown_tool_raises_error() -> None: 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")]}) diff --git a/libs/langchain_v1/uv.lock b/libs/langchain_v1/uv.lock index 3643e1d44e5..69cc1624ec1 100644 --- a/libs/langchain_v1/uv.lock +++ b/libs/langchain_v1/uv.lock @@ -1970,7 +1970,7 @@ requires-dist = [ { name = "langchain-perplexity", marker = "extra == 'perplexity'" }, { name = "langchain-together", marker = "extra == 'together'" }, { 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" }, ] 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 = [ { name = "jsonpatch", specifier = ">=1.33.0,<2.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 = "pyyaml", specifier = ">=5.3.0,<7.0.0" }, { name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" }, @@ -2131,7 +2131,7 @@ requires-dist = [ dev = [ { name = "grandalf", specifier = ">=0.8.0,<1.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" }] test = [ @@ -2303,7 +2303,7 @@ dev = [{ name = "langchain-core", editable = "../core" }] lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }] test = [ { name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, - { name = "langchain", editable = "../langchain_v1" }, + { name = "langchain", editable = "." }, { name = "langchain-core", editable = "../core" }, { name = "langchain-tests", editable = "../standard-tests" }, { name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" }, @@ -2484,7 +2484,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.0.4" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -2494,9 +2494,9 @@ dependencies = [ { name = "pydantic" }, { 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 = [ - { 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]] @@ -2514,28 +2514,28 @@ wheels = [ [[package]] name = "langgraph-prebuilt" -version = "1.0.5" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { 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 = [ - { 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]] name = "langgraph-sdk" -version = "0.2.9" +version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { 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 = [ - { 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]]