diff --git a/libs/langchain_v1/langchain/agents/_subagent_transformer.py b/libs/langchain_v1/langchain/agents/_subagent_transformer.py new file mode 100644 index 00000000000..aa98f86c170 --- /dev/null +++ b/libs/langchain_v1/langchain/agents/_subagent_transformer.py @@ -0,0 +1,304 @@ +"""Surface nested named agents as typed `run.subagents` handles. + +Detects subagents via the `lc_agent_name` transition that langgraph's base +`_TasksLifecycleBase` now computes. `create_agent(name=...)` binds +`lc_agent_name` into the run config; the base transformer records, per +namespace, the `lc_agent_name` seen on each task start (first-write-wins). + +A subagent boundary is a nested run whose `lc_agent_name` is set *and* differs +from its parent namespace's `lc_agent_name`. Plain subgraphs inherit the +parent's name (so they compare equal and are excluded); unnamed agents have +`lc_agent_name == None` (also excluded). For genuine subagents the base also +recovers the originating tool call and passes it through as a `cause` +(`{"type": "toolCall", "tool_call_id": ...}`), joined from the parent task's +pending tool calls. + +This transformer gates on that boundary and surfaces a typed handle on +`run.subagents`, then forwards child-scope events into the handle's mux so the +nested run can be consumed independently. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, ClassVar + +from langgraph.stream.run_stream import ( + AsyncSubgraphRunStream, + SubgraphRunStream, +) +from langgraph.stream.stream_channel import StreamChannel +from langgraph.stream.transformers import ( + SubgraphStatus, + _TasksLifecycleBase, +) + +if TYPE_CHECKING: + from langchain_protocol.protocol import LifecycleCause + from langgraph.stream._mux import StreamMux + from langgraph.stream._types import ProtocolEvent + +logger = logging.getLogger(__name__) + + +class SubagentRunStream(SubgraphRunStream): + """Typed sync handle for a nested named-agent execution. + + Surfaces on `run.subagents` when a nested run's `lc_agent_name` differs + from its parent's (i.e., a `create_agent(name=...)` dispatched from a tool). + """ + + def __init__( + self, + mux: StreamMux, + *, + path: tuple[str, ...], + graph_name: str | None = None, + trigger_call_id: str | None = None, + cause: LifecycleCause | None = None, + ) -> None: + super().__init__( + mux, + path=path, + graph_name=graph_name, + trigger_call_id=trigger_call_id, + ) + self._cause = cause + + @property + def name(self) -> str | None: + """Subagent name (the nested run's `lc_agent_name`).""" + return self.graph_name + + @property + def cause(self) -> LifecycleCause | None: + """Causation edge — the tool call that triggered this subagent. + + Returns the `LifecycleCause` recovered by the base transformer (a + `{"type": "toolCall", "tool_call_id": ...}` dict) when the originating + tool call could be joined, else `None`. + """ + return self._cause + + +class AsyncSubagentRunStream(AsyncSubgraphRunStream): + """Typed async handle for a nested named-agent execution.""" + + def __init__( + self, + mux: StreamMux, + *, + path: tuple[str, ...], + graph_name: str | None = None, + trigger_call_id: str | None = None, + cause: LifecycleCause | None = None, + ) -> None: + super().__init__( + mux, + path=path, + graph_name=graph_name, + trigger_call_id=trigger_call_id, + ) + self._cause = cause + + @property + def name(self) -> str | None: + """Subagent name (the nested run's `lc_agent_name`).""" + return self.graph_name + + @property + def cause(self) -> LifecycleCause | None: + """Causation edge — the tool call that triggered this subagent. + + Returns the `LifecycleCause` recovered by the base transformer (a + `{"type": "toolCall", "tool_call_id": ...}` dict) when the originating + tool call could be joined, else `None`. + """ + return self._cause + + +class SubagentTransformer(_TasksLifecycleBase): + """Promote nested named agents into typed handles on `run.subagents`. + + The base `_TasksLifecycleBase` records each namespace's `lc_agent_name` + (set by `create_agent(name=...)`) and, on every task start, fires + `_on_started` with the resolved `graph_name` and a `cause` for genuine + subagent boundaries. This transformer gates on that boundary using the + inherited `_lc_by_ns` map: a nested run is a subagent when it carries an + `lc_agent_name`. Same-named nested agents (e.g. a subagent that invokes + itself) are surfaced; unnamed agents (`None`) are excluded. Trade-off: a + non-agent subgraph that inherited the parent's name will also surface. + + On the first matching task start it builds a child mux and emits a typed + handle on `run.subagents`, then forwards subsequent child-scope events into + that handle so the nested run can be consumed independently. + """ + + _native: ClassVar[bool] = True + # Overrides `aprocess` but also runs unchanged on the sync lane via + # `process`, so it must not be forced into an async-only run. + supports_sync: ClassVar[bool] = True + + def __init__(self, scope: tuple[str, ...] = ()) -> None: + super().__init__(scope) + self._log: StreamChannel[SubagentRunStream | AsyncSubagentRunStream] = StreamChannel() + self._handles: dict[tuple[str, ...], SubagentRunStream | AsyncSubagentRunStream] = {} + self._mux: StreamMux | None = None + + def init(self) -> dict[str, Any]: + return {"subagents": self._log} + + def _on_register(self, mux: StreamMux) -> None: + self._mux = mux + + def _should_track(self, ns: tuple[str, ...]) -> bool: + depth = len(self.scope) + return len(ns) == depth + 1 and ns[:depth] == self.scope + + def _on_started( + self, + ns: tuple[str, ...], + graph_name: str | None, + trigger_call_id: str | None, + *, + cause: LifecycleCause | None = None, + ) -> None: + child_lc = self._lc_by_ns.get(ns) + # Surface any nested run carrying an lc_agent_name (set by create_agent). + # A same-named nested agent — e.g. a subagent that invokes itself — + # re-asserts its own name and is surfaced. Unnamed runs (None) are + # excluded. Trade-off: a non-agent subgraph that inherited the parent's + # name also surfaces; null lc_agent_name when invoking such a graph to + # exclude it. + if child_lc is None: + return + if self._mux is None or ns in self._handles: + return + try: + child_mux = self._mux._make_child(ns) # noqa: SLF001 + except RuntimeError: + logger.debug("SubagentTransformer: could not create child mux for %s", ns) + return + + handle_cls = AsyncSubagentRunStream if child_mux.is_async else SubagentRunStream + handle = handle_cls( + mux=child_mux, + path=ns, + graph_name=graph_name, + trigger_call_id=trigger_call_id, + cause=cause, + ) + self._handles[ns] = handle + self._log.push(handle) + + def _on_terminal( + self, + ns: tuple[str, ...], + status: SubgraphStatus, + error: str | None, + ) -> None: + handle = self._handles.get(ns) + if handle is None or not self._mark_terminal(handle, status, error): + return + self._close_or_fail_handle(handle, status, error) + + async def _aon_terminal( + self, + ns: tuple[str, ...], + status: SubgraphStatus, + error: str | None, + ) -> None: + handle = self._handles.get(ns) + if handle is None or not self._mark_terminal(handle, status, error): + return + await self._aclose_or_fail_handle(handle, status, error) + + def _mark_terminal( + self, + handle: SubagentRunStream | AsyncSubagentRunStream, + status: SubgraphStatus, + error: str | None, + ) -> bool: + """Mark a handle terminal once. Returns True on the first transition.""" + if handle._seen_terminal: # noqa: SLF001 + return False + handle.status = status + if error is not None and handle.error is None: + handle.error = error + handle._seen_terminal = True # noqa: SLF001 + return True + + def _close_or_fail_handle( + self, + handle: SubagentRunStream | AsyncSubagentRunStream, + status: SubgraphStatus, + error: str | None, + ) -> None: + if handle._mux is None or handle._mux._events._closed: # noqa: SLF001 + return + if status == "failed": + handle._mux.fail(RuntimeError(error or "Subagent failed")) # noqa: SLF001 + else: + handle._mux.close() # noqa: SLF001 + + async def _aclose_or_fail_handle( + self, + handle: SubagentRunStream | AsyncSubagentRunStream, + status: SubgraphStatus, + error: str | None, + ) -> None: + if handle._mux is None or handle._mux._events._closed: # noqa: SLF001 + return + if status == "failed": + await handle._mux.afail(RuntimeError(error or "Subagent failed")) # noqa: SLF001 + else: + await handle._mux.aclose() # noqa: SLF001 + + def _handle_for_event( + self, event: ProtocolEvent + ) -> SubagentRunStream | AsyncSubagentRunStream | None: + ns = tuple(event["params"]["namespace"]) + depth = len(self.scope) + if len(ns) < depth + 1: + return None + handle = self._handles.get(ns[: depth + 1]) + if handle is None or handle._mux is None or handle._mux._events._closed: # noqa: SLF001 + return None + return handle + + def process(self, event: ProtocolEvent) -> bool: + # Run tasks bookkeeping first so a `started` handle exists by the + # time we forward the event into the child mini-mux. + keep = super().process(event) + handle = self._handle_for_event(event) + if handle is not None: + handle._observe_event(event) # noqa: SLF001 + handle._mux.push(event) # noqa: SLF001 + return keep + + async def aprocess(self, event: ProtocolEvent) -> bool: + # Async counterpart: repeat the tasks bookkeeping here and forward into + # the child mini-mux through its async lane so the subagent's own + # transformers are driven on the correct (async) lane instead of being + # double-driven via the sync `process`/`push` path. + if event["method"] == "tasks": + ns = tuple(event["params"]["namespace"]) + data = event["params"]["data"] + if "result" in data: + for child_ns, status, error in self._pop_terminal_transitions(ns, data): + await self._aon_terminal(child_ns, status, error) + else: + # Mirror the sync bookkeeping so the async lane observes parent + # identity / pending tool calls before discriminating a + # subagent boundary in `_handle_task_start` -> `_on_started`. + self._record_identity(ns, data) + self._record_pending_tool_calls(data) + self._handle_task_start(ns, data) + keep = False + else: + keep = True + handle = self._handle_for_event(event) + if handle is not None: + handle._observe_event(event) # noqa: SLF001 + await handle._mux.apush(event) # noqa: SLF001 + return keep diff --git a/libs/langchain_v1/langchain/agents/factory.py b/libs/langchain_v1/langchain/agents/factory.py index de69ef505c2..ffa23d910fa 100644 --- a/libs/langchain_v1/langchain/agents/factory.py +++ b/libs/langchain_v1/langchain/agents/factory.py @@ -28,6 +28,7 @@ from langgraph.types import Command, Send from langsmith import traceable from typing_extensions import NotRequired, Required, TypedDict +from langchain.agents._subagent_transformer import SubagentTransformer from langchain.agents.middleware.types import ( AgentMiddleware, AgentState, @@ -1677,6 +1678,7 @@ def create_agent( cache=cache, transformers=[ ToolCallTransformer, + SubagentTransformer, *middleware_transformers, *(transformers or ()), ], diff --git a/libs/langchain_v1/pyproject.toml b/libs/langchain_v1/pyproject.toml index a2b65be8e32..7a7351bf594 100644 --- a/libs/langchain_v1/pyproject.toml +++ b/libs/langchain_v1/pyproject.toml @@ -25,7 +25,7 @@ version = "1.3.2" requires-python = ">=3.10.0,<4.0.0" dependencies = [ "langchain-core>=1.4.0,<2.0.0", - "langgraph>=1.2.2,<1.3.0", + "langgraph>=1.2.3,<1.3.0", "pydantic>=2.7.4,<3.0.0", ] diff --git a/libs/langchain_v1/tests/unit_tests/agents/test_subagent_transformer.py b/libs/langchain_v1/tests/unit_tests/agents/test_subagent_transformer.py new file mode 100644 index 00000000000..d5c3202e350 --- /dev/null +++ b/libs/langchain_v1/tests/unit_tests/agents/test_subagent_transformer.py @@ -0,0 +1,239 @@ +"""Tests for langchain.agents._subagent_transformer. + +The transformer surfaces, on `run.subagents`, any nested run that carries an +`lc_agent_name` (set by `create_agent(name=...)`), tracked via langgraph's base +`_TasksLifecycleBase`. These tests drive a real supervisor `create_agent` that +dispatches a nested `create_agent` from a tool, giving true end-to-end coverage. +""" + +from __future__ import annotations + +import sys + +import pytest +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langgraph.prebuilt import ToolCallTransformer + +from langchain.agents import create_agent +from langchain.agents._subagent_transformer import SubagentTransformer +from tests.unit_tests.agents.model import FakeToolCallingModel + + +def _supervisor_model() -> FakeToolCallingModel: + """Supervisor emits one tool call (id `call_w`) then a final message.""" + return FakeToolCallingModel( + tool_calls=[ + [{"args": {"city": "SF"}, "id": "call_w", "name": "call_weather"}], + [], + ] + ) + + +def test_subagents_surfaces_named_subagent() -> None: + """A nested named `create_agent` dispatched from a tool surfaces a handle.""" + weather_agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]]), name="weather_agent") + + @tool("call_weather") + def call_weather(city: str) -> str: + """Call the weather agent.""" + result = weather_agent.invoke({"messages": [HumanMessage(f"weather in {city}")]}) + return result["messages"][-1].text + + supervisor = create_agent( + model=_supervisor_model(), + tools=[call_weather], + name="supervisor", + ) + + run = supervisor.stream_events({"messages": [HumanMessage("weather?")]}, version="v3") + + handles = [] + for handle in run.subagents: + handles.append(handle) + # Drain the nested run so it completes. + for _ in handle: + pass + + assert len(handles) == 1 + assert handles[0].name == "weather_agent" + assert handles[0].cause == {"type": "toolCall", "tool_call_id": "call_w"} + + +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason=( + "On Python <= 3.10, langchain-core does not automatically propagate " + "RunnableConfig into an async tool body (contextvar propagation across " + "`await` in tool execution requires 3.11+). Without it the nested " + "`ainvoke` is disconnected from the parent stream and never surfaces; " + "forwarding the parent config explicitly would instead overwrite the " + "subagent's bound `lc_agent_name` with the parent's. The async-lane " + "logic this test exercises is version-agnostic; the sync test covers " + "3.10." + ), +) +async def test_subagents_surfaces_named_subagent_async() -> None: + """Async counterpart: the handle surfaces and reaches a terminal state. + + Drives the run through `astream_events`, so events flow via the async + `aprocess`/`apush` lane and the child mini-mux is closed via `aclose`. + """ + weather_agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]]), name="weather_agent") + + @tool("call_weather") + async def call_weather(city: str) -> str: + """Call the weather agent.""" + result = await weather_agent.ainvoke({"messages": [HumanMessage(f"weather in {city}")]}) + return result["messages"][-1].text + + supervisor = create_agent( + model=_supervisor_model(), + tools=[call_weather], + name="supervisor", + ) + + run = await supervisor.astream_events({"messages": [HumanMessage("weather?")]}, version="v3") + + handles = [] + async for handle in run.subagents: + handles.append(handle) + # Drain the nested run so it completes. + async for _ in handle: + pass + + assert len(handles) == 1 + assert handles[0].name == "weather_agent" + assert handles[0].cause == {"type": "toolCall", "tool_call_id": "call_w"} + assert handles[0]._seen_terminal is True + assert handles[0].status == "completed" + + +def test_plain_tool_not_surfaced() -> None: + """A tool that returns a string (spawns no nested run) surfaces nothing.""" + + @tool("call_weather") + def call_weather(city: str) -> str: + """Return weather directly without invoking a subagent.""" + return f"Sunny in {city}" + + supervisor = create_agent( + model=_supervisor_model(), + tools=[call_weather], + name="supervisor", + ) + + run = supervisor.stream_events({"messages": [HumanMessage("weather?")]}, version="v3") + + handles = list(run.subagents) + # Drain the main run to completion. + for _ in run: + pass + + assert handles == [] + + +def test_unnamed_inner_agent_surfaces_with_inherited_name() -> None: + """An unnamed inner `create_agent` inherits the parent's name and surfaces. + + This is the accepted trade-off of detecting subagents by "carries an + lc_agent_name": a nested run that didn't set its own name (an unnamed + `create_agent`, or a plain `StateGraph`) inherits the parent's name and is + surfaced under it. A caller can null `lc_agent_name` in the config it + invokes such a graph with to exclude it. + """ + inner_agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]])) + + @tool("call_weather") + def call_weather(city: str) -> str: + """Call an unnamed inner agent.""" + result = inner_agent.invoke({"messages": [HumanMessage(f"weather in {city}")]}) + return result["messages"][-1].text + + supervisor = create_agent( + model=_supervisor_model(), + tools=[call_weather], + name="supervisor", + ) + + run = supervisor.stream_events({"messages": [HumanMessage("weather?")]}, version="v3") + + handles = [] + for handle in run.subagents: + handles.append(handle) + for _ in handle: + pass + + assert len(handles) == 1 + # Inherited the parent's name (the trade-off). + assert handles[0].name == "supervisor" + + +def test_same_name_nested_agent_surfaced() -> None: + """A nested agent with the SAME name as its parent is still surfaced. + + A subagent that dispatches a same-named agent (or invokes itself) re-asserts + its own `lc_agent_name`, so child lc == parent lc. The discriminator surfaces + any nested run that carries a name, so the same-named run is reported instead + of being folded into the parent (which the old `!= parent` rule did). + """ + inner_agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]]), name="weather_agent") + + @tool("call_weather") + def call_weather(city: str) -> str: + """Call a same-named inner agent.""" + result = inner_agent.invoke({"messages": [HumanMessage(f"weather in {city}")]}) + return result["messages"][-1].text + + # The parent agent shares the inner agent's name. + supervisor = create_agent( + model=_supervisor_model(), + tools=[call_weather], + name="weather_agent", + ) + + run = supervisor.stream_events({"messages": [HumanMessage("weather?")]}, version="v3") + + handles = [] + for handle in run.subagents: + handles.append(handle) + for _ in handle: + pass + + assert len(handles) == 1 + assert handles[0].name == "weather_agent" + assert handles[0].cause == {"type": "toolCall", "tool_call_id": "call_w"} + + +def test_transformer_init_exposes_subagents_channel() -> None: + """The transformer publishes a `subagents` channel from `init()`.""" + transformer = SubagentTransformer(scope=()) + state = transformer.init() + assert "subagents" in state + + +def test_create_agent_registers_subagent_transformer() -> None: + """`create_agent` registers `SubagentTransformer` in `stream_transformers`.""" + + @tool("call_weather") + def call_weather(city: str) -> str: + """Call the weather agent.""" + return f"Sunny in {city}" + + agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]]), tools=[call_weather]) + + assert SubagentTransformer in agent.stream_transformers + + +def test_create_agent_subagent_transformer_precedes_user_transformers() -> None: + """`ToolCallTransformer` precedes `SubagentTransformer` in registration order.""" + + @tool("call_weather") + def call_weather(city: str) -> str: + """Call the weather agent.""" + return f"Sunny in {city}" + + agent = create_agent(model=FakeToolCallingModel(tool_calls=[[]]), tools=[call_weather]) + + transformers = list(agent.stream_transformers) + assert transformers.index(ToolCallTransformer) < transformers.index(SubagentTransformer) diff --git a/libs/langchain_v1/uv.lock b/libs/langchain_v1/uv.lock index 3eb92a4bd6f..2c863e37a35 100644 --- a/libs/langchain_v1/uv.lock +++ b/libs/langchain_v1/uv.lock @@ -1417,7 +1417,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, @@ -1428,7 +1427,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -1439,7 +1437,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -1450,7 +1447,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -1461,7 +1457,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -2030,7 +2025,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.2.2,<1.3.0" }, + { name = "langgraph", specifier = ">=1.2.3,<1.3.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", "baseten", "deepseek", "xai", "perplexity"] @@ -2067,7 +2062,7 @@ typing = [ [[package]] name = "langchain-anthropic" -version = "1.4.3" +version = "1.4.4" source = { editable = "../partners/anthropic" } dependencies = [ { name = "anthropic" }, @@ -2608,7 +2603,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -2618,22 +2613,22 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/55/e42f59b5e4cce75085f68abb77114c4d1152628157bd60c2587881bca559/langgraph-1.2.3.tar.gz", hash = "sha256:7f89cd5f0946fe29bd7ca048e2a84d3c14e7f652e38bb98e00f0ba8b7004b9d0", size = 718812, upload-time = "2026-06-01T18:55:54.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/9b/287f11be799f2f4dd289ab19d23788df22ac20ce884a010d0696f79016db/langgraph-1.2.3-py3-none-any.whl", hash = "sha256:22e6c89084adb3edb7855f6ffa692af3d75925f9c70deb43b86da14db1b02c78", size = 244841, upload-time = "2026-06-01T18:55:52.945Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.1.0" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/b4/6005c5dd88ad484fe6235d4c43a0d2cee7e91b08ad85a180985c2662df87/langgraph_checkpoint-4.1.0.tar.gz", hash = "sha256:e5bb304e30fc1363ac8fcb5f7dee5ca2185d77fe475b0d01de2c5f91324c2c21", size = 181942, upload-time = "2026-05-12T03:33:49.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/74/d3be2b41955e20ccd624dba5f6fe9d38dcee385ba470a6e13ed86732fc86/langgraph_checkpoint-4.1.0-py3-none-any.whl", hash = "sha256:8bc2a0466a20c38b865ce6671b42093fd5c041133f32351cae4222e0eeaf7fb5", size = 56047, upload-time = "2026-05-12T03:33:48.548Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, ] [[package]] @@ -2651,15 +2646,18 @@ wheels = [ [[package]] name = "langgraph-sdk" -version = "0.3.3" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, ] [[package]]