From 3a5b5c9821f23a1be4bd562c66d7f736e264a67c Mon Sep 17 00:00:00 2001 From: Hunter Lovell <40191806+hntrl@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:43:48 -0700 Subject: [PATCH 1/6] feat(langgraph): add graph lifecycle callback handlers (#7429) ## Summary This change adds first-class graph lifecycle callbacks to LangGraph so interrupt and resume transitions can be observed without overloading the existing LangChain custom event system. It introduces a dedicated graph callback manager and wires lifecycle emission through Pregel execution in both sync and async paths. ## Changes - **`libs/langgraph/langgraph/callbacks.py`**: Adds `GraphCallbackHandler` and `GraphCallbackManager` (built on LangChain base callback classes), plus config plumbing via `graph_callbacks` and `get_graph_callback_manager_for_config`. - **`libs/langgraph/langgraph/pregel/_loop.py`**: Introduces `GraphLifecycleEvent` and records lifecycle transitions (`resume`, `interrupt`) into an internal FIFO queue with `shift_graph_lifecycle_event()`. - **`libs/langgraph/langgraph/pregel/main.py`**: Resolves graph callback manager from config and drains lifecycle events while loop execution progresses, dispatching `on_resume` and `on_interrupt` consistently in sync and async runtimes. - **`libs/langgraph/tests/test_graph_callbacks.py`**: Adds sync and async coverage verifying lifecycle callbacks fire correctly and remain distinct from LangChain `on_custom_event` handlers. --------- Co-authored-by: Eugene Yurtsev --- libs/langgraph/langgraph/callbacks.py | 412 +++++++++++++++++++ libs/langgraph/langgraph/pregel/_loop.py | 66 ++- libs/langgraph/langgraph/pregel/main.py | 58 ++- libs/langgraph/tests/test_graph_callbacks.py | 277 +++++++++++++ 4 files changed, 806 insertions(+), 7 deletions(-) create mode 100644 libs/langgraph/langgraph/callbacks.py create mode 100644 libs/langgraph/tests/test_graph_callbacks.py diff --git a/libs/langgraph/langgraph/callbacks.py b/libs/langgraph/langgraph/callbacks.py new file mode 100644 index 000000000..d5933b1b6 --- /dev/null +++ b/libs/langgraph/langgraph/callbacks.py @@ -0,0 +1,412 @@ +"""Graph lifecycle callback interfaces and event payloads. + +This module defines the public callback surface for observing LangGraph-specific +lifecycle transitions such as interrupt and resume. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any, Literal, TypeAlias, TypeVar +from uuid import UUID + +from langchain_core.callbacks import BaseCallbackHandler, BaseCallbackManager +from langchain_core.callbacks.manager import ahandle_event, handle_event +from langchain_core.runnables import RunnableConfig + +from langgraph.types import Interrupt + +__all__ = ( + "GraphCallbackHandler", + "GraphInterruptEvent", + "GraphLifecycleEvent", + "GraphLifecycleStatus", + "GraphResumeEvent", + "get_async_graph_callback_manager_for_config", + "get_sync_graph_callback_manager_for_config", +) + + +GraphLifecycleStatus: TypeAlias = Literal[ + "input", + "pending", + "done", + "interrupt_before", + "interrupt_after", + "out_of_steps", +] +"""Allowed lifecycle statuses reported in graph lifecycle callback events.""" + + +@dataclass(frozen=True) +class GraphInterruptEvent: + """Graph lifecycle event emitted when execution pauses for interrupts.""" + + run_id: UUID | None + """Run id for the current graph execution, if available.""" + + status: GraphLifecycleStatus + """Loop status when the interrupt was captured.""" + + checkpoint_id: str + """Checkpoint id associated with the interrupted execution.""" + + checkpoint_ns: tuple[str, ...] + """Checkpoint namespace path for the current graph or subgraph.""" + + interrupts: tuple[Interrupt, ...] + """Interrupt payloads that caused the graph to pause.""" + + +@dataclass(frozen=True) +class GraphResumeEvent: + """Graph lifecycle event emitted when execution resumes from a checkpoint.""" + + run_id: UUID | None + """Run id for the current graph execution, if available.""" + + status: GraphLifecycleStatus + """Loop status when the resume was captured.""" + + checkpoint_id: str + """Checkpoint id the graph resumed from.""" + + checkpoint_ns: tuple[str, ...] + """Checkpoint namespace path for the current graph or subgraph.""" + + +GraphLifecycleEvent: TypeAlias = GraphInterruptEvent | GraphResumeEvent +"""Union of all public graph lifecycle callback event payloads. + +Use this alias when a callback or helper can receive either interrupt or resume +lifecycle events. +""" + + +class GraphCallbackHandler(BaseCallbackHandler): + """Base class for graph-level lifecycle callbacks. + + Subclass this handler to observe graph lifecycle transitions that are + specific to LangGraph execution, rather than generic LangChain runnable + callbacks. + + Instances can be passed through `config["callbacks"]` when invoking a + graph. Only handlers that inherit from `GraphCallbackHandler` receive these + lifecycle events. + """ + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + """Run when graph execution pauses due to one or more interrupts. + + Args: + event: Interrupt lifecycle event payload. + """ + + def on_resume(self, event: GraphResumeEvent) -> Any: + """Run when graph execution resumes from a persisted checkpoint. + + Args: + event: Resume lifecycle event payload. + """ + + +_MISSING = object() + + +def _filter_graph_handlers( + handlers: list[BaseCallbackHandler], +) -> list[GraphCallbackHandler]: + return [h for h in handlers if isinstance(h, GraphCallbackHandler)] + + +def _init_base_manager( + manager: BaseCallbackManager, + handlers: Sequence[GraphCallbackHandler] | None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None, + parent_run_id: UUID | None, + *, + tags: list[str] | None, + inheritable_tags: list[str] | None, + metadata: dict[str, Any] | None, + inheritable_metadata: dict[str, Any] | None, + run_id: UUID | None, +) -> None: + base_handlers: list[BaseCallbackHandler] = [] + base_inheritable_handlers: list[BaseCallbackHandler] = [] + if handlers is not None: + base_handlers.extend(handlers) + if inheritable_handlers is not None: + base_inheritable_handlers.extend(inheritable_handlers) + BaseCallbackManager.__init__( + manager, + handlers=base_handlers, + inheritable_handlers=base_inheritable_handlers, + parent_run_id=parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + ) + manager.run_id = run_id # type: ignore[attr-defined] + + +def _configure_graph_callbacks( + cls: type[_GraphManagerT], + callbacks: object | None, + *, + run_id: UUID | None, +) -> _GraphManagerT: + if callbacks is None: + return cls(run_id=run_id) + if isinstance(callbacks, cls): + return callbacks.copy(run_id=run_id) + if isinstance(callbacks, (_GraphCallbackManager, _AsyncGraphCallbackManager)): + # Cross-type: extract handlers into the requested cls. + return cls( + handlers=_filter_graph_handlers(callbacks.handlers), + inheritable_handlers=_filter_graph_handlers(callbacks.inheritable_handlers), + parent_run_id=callbacks.parent_run_id, + tags=callbacks.tags.copy(), + inheritable_tags=callbacks.inheritable_tags.copy(), + metadata=callbacks.metadata.copy(), + inheritable_metadata=callbacks.inheritable_metadata.copy(), + run_id=run_id, + ) + if isinstance(callbacks, BaseCallbackManager): + return cls( + handlers=_filter_graph_handlers(callbacks.handlers), + inheritable_handlers=_filter_graph_handlers(callbacks.inheritable_handlers), + parent_run_id=callbacks.parent_run_id, + tags=callbacks.tags.copy(), + inheritable_tags=callbacks.inheritable_tags.copy(), + metadata=callbacks.metadata.copy(), + inheritable_metadata=callbacks.inheritable_metadata.copy(), + run_id=run_id, + ) + if isinstance(callbacks, GraphCallbackHandler): + return cls((callbacks,), run_id=run_id) + if isinstance(callbacks, (str, bytes)) or not isinstance(callbacks, Sequence): + raise TypeError("callbacks must be a handler, sequence, or manager") + return cls(_filter_graph_handlers(list(callbacks)), run_id=run_id) + + +def _copy_graph_manager( + manager: _GraphCallbackManager | _AsyncGraphCallbackManager, + cls: type[_GraphManagerT], + run_id: UUID | None | object, +) -> _GraphManagerT: + resolved_run_id: UUID | None + if run_id is _MISSING: + resolved_run_id = manager.run_id + else: + if run_id is not None and not isinstance(run_id, UUID): + raise TypeError("run_id must be a UUID or None") + resolved_run_id = run_id + + return cls( + handlers=_filter_graph_handlers(manager.handlers), + inheritable_handlers=_filter_graph_handlers(manager.inheritable_handlers), + parent_run_id=manager.parent_run_id, + tags=manager.tags.copy(), + inheritable_tags=manager.inheritable_tags.copy(), + metadata=manager.metadata.copy(), + inheritable_metadata=manager.inheritable_metadata.copy(), + run_id=resolved_run_id, + ) + + +class _GraphCallbackManager(BaseCallbackManager): + """Sync dispatcher for graph lifecycle events.""" + + run_id: UUID | None + + def __init__( + self, + handlers: Sequence[GraphCallbackHandler] | None = None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None = None, + parent_run_id: UUID | None = None, + *, + tags: list[str] | None = None, + inheritable_tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + inheritable_metadata: dict[str, Any] | None = None, + run_id: UUID | None = None, + ) -> None: + _init_base_manager( + self, + handlers, + inheritable_handlers, + parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + run_id=run_id, + ) + + def add_handler( + self, + handler: BaseCallbackHandler, + inherit: bool = True, # noqa: FBT001,FBT002 + ) -> None: + if not isinstance(handler, GraphCallbackHandler): + raise TypeError("handlers must inherit GraphCallbackHandler") + super().add_handler(handler, inherit=inherit) + + def copy( + self, + *, + run_id: UUID | None | object = _MISSING, + ) -> _GraphCallbackManager: + return _copy_graph_manager(self, _GraphCallbackManager, run_id) + + @classmethod + def configure( + cls, + callbacks: object | None = None, + *, + run_id: UUID | None = None, + ) -> _GraphCallbackManager: + return _configure_graph_callbacks(cls, callbacks, run_id=run_id) + + def on_interrupt(self, event: GraphInterruptEvent) -> None: + handle_event( + self.handlers, + "on_interrupt", + None, + event, + ) + + def on_resume(self, event: GraphResumeEvent) -> None: + handle_event( + self.handlers, + "on_resume", + None, + event, + ) + + +class _AsyncGraphCallbackManager(BaseCallbackManager): + """Async dispatcher for graph lifecycle events.""" + + run_id: UUID | None + + @property + def is_async(self) -> bool: + """Return whether the manager is async.""" + return True + + def __init__( + self, + handlers: Sequence[GraphCallbackHandler] | None = None, + inheritable_handlers: Sequence[GraphCallbackHandler] | None = None, + parent_run_id: UUID | None = None, + *, + tags: list[str] | None = None, + inheritable_tags: list[str] | None = None, + metadata: dict[str, Any] | None = None, + inheritable_metadata: dict[str, Any] | None = None, + run_id: UUID | None = None, + ) -> None: + _init_base_manager( + self, + handlers, + inheritable_handlers, + parent_run_id, + tags=tags, + inheritable_tags=inheritable_tags, + metadata=metadata, + inheritable_metadata=inheritable_metadata, + run_id=run_id, + ) + + def add_handler( + self, + handler: BaseCallbackHandler, + inherit: bool = True, # noqa: FBT001,FBT002 + ) -> None: + if not isinstance(handler, GraphCallbackHandler): + raise TypeError("handlers must inherit GraphCallbackHandler") + super().add_handler(handler, inherit=inherit) + + def copy( + self, + *, + run_id: UUID | None | object = _MISSING, + ) -> _AsyncGraphCallbackManager: + return _copy_graph_manager(self, _AsyncGraphCallbackManager, run_id) + + @classmethod + def configure( + cls, + callbacks: object | None = None, + *, + run_id: UUID | None = None, + ) -> _AsyncGraphCallbackManager: + return _configure_graph_callbacks(cls, callbacks, run_id=run_id) + + async def on_interrupt(self, event: GraphInterruptEvent) -> None: + await ahandle_event( + self.handlers, + "on_interrupt", + None, + event, + ) + + async def on_resume(self, event: GraphResumeEvent) -> None: + await ahandle_event( + self.handlers, + "on_resume", + None, + event, + ) + + +_GraphManagerT = TypeVar( + "_GraphManagerT", _GraphCallbackManager, _AsyncGraphCallbackManager +) + +GraphCallbacks: TypeAlias = ( + _GraphCallbackManager + | _AsyncGraphCallbackManager + | BaseCallbackManager + | GraphCallbackHandler + | Sequence[BaseCallbackHandler] + | Sequence[GraphCallbackHandler] + | None +) + + +def get_sync_graph_callback_manager_for_config( + config: RunnableConfig, + *, + run_id: UUID | None = None, +) -> _GraphCallbackManager: + """Build a sync graph lifecycle callback manager from a runnable config. + + This helper filters `config["callbacks"]` down to handlers that inherit + from `GraphCallbackHandler` and binds the provided `run_id` onto the + returned manager. + """ + return _GraphCallbackManager.configure( + config.get("callbacks"), + run_id=run_id, + ) + + +def get_async_graph_callback_manager_for_config( + config: RunnableConfig, + *, + run_id: UUID | None = None, +) -> _AsyncGraphCallbackManager: + """Build an async graph lifecycle callback manager from a runnable config. + + This helper filters `config["callbacks"]` down to handlers that inherit + from `GraphCallbackHandler` and binds the provided `run_id` onto the + returned manager. + """ + return _AsyncGraphCallbackManager.configure( + config.get("callbacks"), + run_id=run_id, + ) diff --git a/libs/langgraph/langgraph/pregel/_loop.py b/libs/langgraph/langgraph/pregel/_loop.py index 4f9c55d2d..aec4b74cd 100644 --- a/libs/langgraph/langgraph/pregel/_loop.py +++ b/libs/langgraph/langgraph/pregel/_loop.py @@ -62,6 +62,11 @@ from langgraph._internal._constants import ( from langgraph._internal._replay import ReplayState from langgraph._internal._scratchpad import PregelScratchpad from langgraph._internal._typing import EMPTY_SEQ, MISSING +from langgraph.callbacks import ( + GraphInterruptEvent, + GraphLifecycleEvent, + GraphResumeEvent, +) from langgraph.channels.base import BaseChannel from langgraph.channels.untracked_value import UntrackedValue from langgraph.constants import TAG_HIDDEN @@ -117,6 +122,7 @@ from langgraph.types import ( CachePolicy, Command, Durability, + Interrupt, PregelExecutableTask, RetryPolicy, Send, @@ -203,6 +209,8 @@ class PregelLoop: tasks: dict[str, PregelExecutableTask] output: None | dict[str, Any] | Any = None updated_channels: set[str] | None = None + _graph_lifecycle_events: deque[GraphLifecycleEvent] + _has_graph_lifecycle_callbacks: bool # public @@ -228,6 +236,7 @@ class PregelLoop: migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: self.stream = stream self.config = config @@ -252,6 +261,8 @@ class PregelLoop: self.retry_policy = retry_policy self.cache_policy = cache_policy self.durability = durability + self._has_graph_lifecycle_callbacks = has_graph_lifecycle_callbacks + self._graph_lifecycle_events = deque() if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]: self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM]) scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD) @@ -303,6 +314,40 @@ class PregelLoop: ) self.prev_checkpoint_config = None + def _push_graph_lifecycle_event( + self, + kind: Literal["resume", "interrupt"], + *, + interrupts: tuple[Interrupt, ...] = (), + ) -> None: + if kind == "resume": + self._graph_lifecycle_events.append( + GraphResumeEvent( + run_id=None, + status=self.status, + checkpoint_id=self.checkpoint["id"], + checkpoint_ns=self.checkpoint_ns, + ) + ) + elif kind == "interrupt": + self._graph_lifecycle_events.append( + GraphInterruptEvent( + run_id=None, + status=self.status, + checkpoint_id=self.checkpoint["id"], + checkpoint_ns=self.checkpoint_ns, + interrupts=interrupts, + ) + ) + else: + msg = f"Unknown graph lifecycle event type: {kind}" + raise AssertionError(msg) + + def _pop_lifecycle_event(self) -> GraphLifecycleEvent | None: + if not self._graph_lifecycle_events: + return None + return self._graph_lifecycle_events.popleft() + def put_writes(self, task_id: str, writes: WritesT) -> None: """Put writes for a task, to be read by the next tick.""" if not writes: @@ -785,6 +830,8 @@ class PregelLoop: ) # set flag self.status = "pending" + if is_resuming: + self._push_graph_lifecycle_event("resume") return updated_channels def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: @@ -885,8 +932,10 @@ class PregelLoop: self._put_checkpoint(self.checkpoint_metadata) self._put_pending_writes() # suppress interrupt - suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested - if suppress: + if isinstance(exc_value, GraphInterrupt) and not self.is_nested: + interrupt = exc_value + interrupts = tuple(interrupt.args[0]) if interrupt.args else () + self._push_graph_lifecycle_event("interrupt", interrupts=interrupts) # emit one last "values" event, with pending writes applied if ( hasattr(self, "tasks") @@ -913,12 +962,11 @@ class PregelLoop: self.channels, ) # emit INTERRUPT if exception is empty (otherwise emitted by put_writes) - if exc_value is not None and (not exc_value.args or not exc_value.args[0]): + if not interrupt.args or not interrupt.args[0]: + interrupt_payload = interrupt.args[0] if interrupt.args else () self._emit( "updates", - lambda: iter( - [{INTERRUPT: cast(GraphInterrupt, exc_value).args[0]}] - ), + lambda: iter([{INTERRUPT: interrupt_payload}]), ) # save final output self.output = read_channels(self.channels, self.output_keys) @@ -1040,6 +1088,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: super().__init__( input, @@ -1061,6 +1110,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): retry_policy=retry_policy, cache_policy=cache_policy, durability=durability, + has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks, ) self.stack = ExitStack() if checkpointer: @@ -1136,6 +1186,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): # context manager def __enter__(self) -> Self: + self._graph_lifecycle_events = deque() if not self.checkpointer: saved = None elif self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID): @@ -1236,6 +1287,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, + has_graph_lifecycle_callbacks: bool = False, ) -> None: super().__init__( input, @@ -1257,6 +1309,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): retry_policy=retry_policy, cache_policy=cache_policy, durability=durability, + has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks, ) self.stack = AsyncExitStack() if checkpointer: @@ -1335,6 +1388,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): # context manager async def __aenter__(self) -> Self: + self._graph_lifecycle_events = deque() if not self.checkpointer: saved = None elif self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID): diff --git a/libs/langgraph/langgraph/pregel/main.py b/libs/langgraph/langgraph/pregel/main.py index d754db391..02d0ee11e 100644 --- a/libs/langgraph/langgraph/pregel/main.py +++ b/libs/langgraph/langgraph/pregel/main.py @@ -16,7 +16,7 @@ from collections.abc import ( Mapping, Sequence, ) -from dataclasses import is_dataclass +from dataclasses import is_dataclass, replace from functools import partial from inspect import isclass from typing import ( @@ -96,6 +96,12 @@ from langgraph._internal._runnable import ( coerce_to_runnable, ) from langgraph._internal._typing import MISSING, DeprecatedKwargs +from langgraph.callbacks import ( + GraphInterruptEvent, + GraphResumeEvent, + get_async_graph_callback_manager_for_config, + get_sync_graph_callback_manager_for_config, +) from langgraph.channels.base import BaseChannel from langgraph.channels.topic import Topic from langgraph.config import get_config @@ -2585,6 +2591,10 @@ class Pregel( name=config.get("run_name", self.get_name()), run_id=config.get("run_id"), ) + graph_callback_manager = get_sync_graph_callback_manager_for_config( + config, + run_id=run_manager.run_id, + ) try: # assign defaults ( @@ -2669,6 +2679,17 @@ class Pregel( _output_mapper = self._output_mapper if version == "v2" else None _state_mapper = self._state_mapper if version == "v2" else None + def emit_graph_lifecycle_events(loop: SyncPregelLoop) -> None: + while (event := loop._pop_lifecycle_event()) is not None: + if isinstance(event, GraphResumeEvent): + graph_callback_manager.on_resume( + replace(event, run_id=graph_callback_manager.run_id) + ) + else: + graph_callback_manager.on_interrupt( + replace(event, run_id=graph_callback_manager.run_id) + ) + with SyncPregelLoop( input, stream=StreamProtocol(stream.put, stream_modes), @@ -2689,7 +2710,9 @@ class Pregel( migrate_checkpoint=self._migrate_checkpoint, retry_policy=self.retry_policy, cache_policy=self.cache_policy, + has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers), ) as loop: + emit_graph_lifecycle_events(loop) # create runner runner = PregelRunner( submit=config[CONF].get( @@ -2751,9 +2774,11 @@ class Pregel( _state_mapper, ) loop.after_tick() + emit_graph_lifecycle_events(loop) # wait for checkpoint if durability_ == "sync": loop._put_checkpoint_fut.result() + emit_graph_lifecycle_events(loop) # emit output yield from _output( stream_mode, @@ -2928,6 +2953,10 @@ class Pregel( name=config.get("run_name", self.get_name()), run_id=config.get("run_id"), ) + graph_callback_manager = get_async_graph_callback_manager_for_config( + config, + run_id=run_manager.run_id, + ) # if running from astream_log() run each proc with streaming do_stream = ( next( @@ -3042,6 +3071,28 @@ class Pregel( _output_mapper = self._output_mapper if version == "v2" else None _state_mapper = self._state_mapper if version == "v2" else None + async def aemit_graph_lifecycle_events(loop: AsyncPregelLoop) -> None: + while (event := loop._pop_lifecycle_event()) is not None: + if isinstance(event, GraphResumeEvent): + await graph_callback_manager.on_resume( + GraphResumeEvent( + run_id=graph_callback_manager.run_id, + status=event.status, + checkpoint_id=event.checkpoint_id, + checkpoint_ns=event.checkpoint_ns, + ) + ) + else: + await graph_callback_manager.on_interrupt( + GraphInterruptEvent( + run_id=graph_callback_manager.run_id, + status=event.status, + checkpoint_id=event.checkpoint_id, + checkpoint_ns=event.checkpoint_ns, + interrupts=event.interrupts, + ) + ) + async with AsyncPregelLoop( input, stream=StreamProtocol(stream.put_nowait, stream_modes), @@ -3062,7 +3113,9 @@ class Pregel( migrate_checkpoint=self._migrate_checkpoint, retry_policy=self.retry_policy, cache_policy=self.cache_policy, + has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers), ) as loop: + await aemit_graph_lifecycle_events(loop) # create runner runner = PregelRunner( submit=config[CONF].get( @@ -3144,6 +3197,7 @@ class Pregel( ): yield o loop.after_tick() + await aemit_graph_lifecycle_events(loop) # wait for checkpoint if durability_ == "sync": await cast(asyncio.Future, loop._put_checkpoint_fut) @@ -3152,6 +3206,8 @@ class Pregel( if _cleanup_waiter is not None: await _cleanup_waiter() + await aemit_graph_lifecycle_events(loop) + # emit output for o in _output( stream_mode, diff --git a/libs/langgraph/tests/test_graph_callbacks.py b/libs/langgraph/tests/test_graph_callbacks.py new file mode 100644 index 000000000..ca825f904 --- /dev/null +++ b/libs/langgraph/tests/test_graph_callbacks.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +import sys +from typing import Any + +import pytest +from langchain_core.callbacks.base import BaseCallbackHandler +from langchain_core.callbacks.manager import CallbackManager +from langgraph.checkpoint.memory import InMemorySaver +from typing_extensions import TypedDict + +from langgraph.callbacks import ( + GraphCallbackHandler, + GraphInterruptEvent, + GraphResumeEvent, +) +from langgraph.graph import START, StateGraph +from langgraph.types import Command, Interrupt, interrupt + +NEEDS_CONTEXTVARS = pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Python 3.11+ is required for async contextvars support", +) + + +class _GraphEventHandler(GraphCallbackHandler): + def __init__(self) -> None: + self.interrupt_events: list[GraphInterruptEvent] = [] + self.resume_events: list[GraphResumeEvent] = [] + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + self.interrupt_events.append(event) + + def on_resume(self, event: GraphResumeEvent) -> Any: + self.resume_events.append(event) + + +class _LangChainCustomEventHandler(BaseCallbackHandler): + run_inline = True + + def __init__(self) -> None: + self.events: list[str] = [] + + def on_custom_event(self, name: str, data: Any, **kwargs: Any) -> Any: + self.events.append(name) + + +class _RaisingGraphEventHandler(GraphCallbackHandler): + def __init__( + self, + *, + raise_on_interrupt: bool = False, + raise_on_resume: bool = False, + raise_error: bool = False, + ) -> None: + self.raise_on_interrupt = raise_on_interrupt + self.raise_on_resume = raise_on_resume + self.raise_error = raise_error + + def on_interrupt(self, event: GraphInterruptEvent) -> Any: + if self.raise_on_interrupt: + raise ValueError("boom-interrupt") + + def on_resume(self, event: GraphResumeEvent) -> Any: + if self.raise_on_resume: + raise ValueError("boom-resume") + + +class _AsyncRaisingGraphEventHandler(GraphCallbackHandler): + def __init__( + self, + *, + raise_on_interrupt: bool = False, + raise_on_resume: bool = False, + raise_error: bool = False, + ) -> None: + self.raise_on_interrupt = raise_on_interrupt + self.raise_on_resume = raise_on_resume + self.raise_error = raise_error + + async def on_interrupt(self, event: GraphInterruptEvent) -> Any: + if self.raise_on_interrupt: + raise ValueError("boom-interrupt") + + async def on_resume(self, event: GraphResumeEvent) -> Any: + if self.raise_on_resume: + raise ValueError("boom-resume") + + +class _State(TypedDict): + answer: str | None + + +def _build_interrupt_graph() -> Any: + def ask(state: _State) -> _State: + answer = interrupt("Provide value") + return {"answer": answer} + + builder = StateGraph(_State) + builder.add_node("ask", ask) + builder.add_edge(START, "ask") + return builder.compile(checkpointer=InMemorySaver()) + + +def test_graph_callbacks_interrupt_and_resume_sync() -> None: + graph = _build_interrupt_graph() + handler = _GraphEventHandler() + langchain_handler = _LangChainCustomEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-sync"}, + "callbacks": [langchain_handler, handler], + } + + first = graph.invoke({"answer": None}, config) + assert "__interrupt__" in first + + assert len(handler.interrupt_events) == 1 + assert handler.interrupt_events[0].interrupts + assert isinstance(handler.interrupt_events[0].interrupts[0], Interrupt) + assert handler.interrupt_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + handler.resume_events.clear() + resumed = graph.invoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + + assert len(handler.resume_events) == 1 + assert handler.resume_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_interrupt_and_resume_async() -> None: + graph = _build_interrupt_graph() + handler = _GraphEventHandler() + langchain_handler = _LangChainCustomEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async"}, + "callbacks": [langchain_handler, handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + + assert len(handler.interrupt_events) == 1 + assert handler.interrupt_events[0].interrupts + assert isinstance(handler.interrupt_events[0].interrupts[0], Interrupt) + assert handler.interrupt_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + handler.resume_events.clear() + resumed = await graph.ainvoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + + assert len(handler.resume_events) == 1 + assert handler.resume_events[0].checkpoint_ns == () + assert langchain_handler.events == [] + + +def test_graph_callbacks_continue_when_interrupt_handler_raises_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler(raise_on_interrupt=True) + recording_handler = _GraphEventHandler() + + first = graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-sync-raises"}, + "callbacks": [raising_handler, recording_handler], + }, + ) + + assert "__interrupt__" in first + assert len(recording_handler.interrupt_events) == 1 + + +def test_graph_callbacks_continue_when_resume_handler_raises_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler(raise_on_resume=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-sync-raises-resume"}, + "callbacks": [raising_handler, recording_handler], + } + + first = graph.invoke({"answer": None}, config) + assert "__interrupt__" in first + + resumed = graph.invoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + assert len(recording_handler.resume_events) == 1 + + +def test_graph_callbacks_raise_error_propagates_sync() -> None: + graph = _build_interrupt_graph() + raising_handler = _RaisingGraphEventHandler( + raise_on_interrupt=True, + raise_error=True, + ) + + with pytest.raises(ValueError, match="boom-interrupt"): + graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-sync-raise-error"}, + "callbacks": [raising_handler], + }, + ) + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_continue_when_handler_raises_async() -> None: + graph = _build_interrupt_graph() + raising_interrupt_handler = _AsyncRaisingGraphEventHandler(raise_on_interrupt=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async-raises-interrupt"}, + "callbacks": [raising_interrupt_handler, recording_handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + assert len(recording_handler.interrupt_events) == 1 + + graph = _build_interrupt_graph() + raising_resume_handler = _AsyncRaisingGraphEventHandler(raise_on_resume=True) + recording_handler = _GraphEventHandler() + config = { + "configurable": {"thread_id": "graph-callback-async-raises-resume"}, + "callbacks": [raising_resume_handler, recording_handler], + } + + first = await graph.ainvoke({"answer": None}, config) + assert "__interrupt__" in first + resumed = await graph.ainvoke(Command(resume="done"), config) + assert resumed == {"answer": "done"} + assert len(recording_handler.resume_events) == 1 + + +@pytest.mark.anyio +@NEEDS_CONTEXTVARS +async def test_graph_callbacks_raise_error_propagates_async() -> None: + graph = _build_interrupt_graph() + raising_handler = _AsyncRaisingGraphEventHandler( + raise_on_interrupt=True, + raise_error=True, + ) + + with pytest.raises(ValueError, match="boom-interrupt"): + await graph.ainvoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-async-raise-error"}, + "callbacks": [raising_handler], + }, + ) + + +def test_graph_callbacks_accept_base_callback_manager() -> None: + graph = _build_interrupt_graph() + graph_handler = _GraphEventHandler() + custom_handler = _LangChainCustomEventHandler() + manager = CallbackManager.configure(inheritable_callbacks=[custom_handler]) + manager.add_handler(graph_handler) + + first = graph.invoke( + {"answer": None}, + { + "configurable": {"thread_id": "graph-callback-base-manager"}, + "callbacks": manager, + }, + ) + + assert "__interrupt__" in first + assert len(graph_handler.interrupt_events) == 1 From bede0b7acf4175f992dbdcafe9357798cd96f6a6 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Fri, 10 Apr 2026 17:05:37 -0400 Subject: [PATCH 2/6] test(langgraph): use monotonic clock in flaky streaming test (#7477) Switches test_sync_streaming_with_functional_api to time.monotonic() for both emitted task timestamps and observed arrival times so the assertion is based on a monotonic clock instead of wall time. This makes the streaming timing check less flaky on systems where time.time() can jump or lack sufficient precision. Created with [Deep Agents CLI](https://docs.langchain.com/oss/python/deepagents/cli/overview) using gpt-5.4 (provider: openai). --- libs/langgraph/tests/test_pregel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 95afe3e6e..ad18f7843 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -6271,7 +6271,7 @@ def test_sync_streaming_with_functional_api() -> None: @task() def slow() -> dict: time.sleep(time_delay) # Simulate a delay of 10 ms - return {"tic": time.time()} + return {"tic": time.monotonic()} @entrypoint() def graph(inputs: dict) -> list: @@ -6284,7 +6284,7 @@ def test_sync_streaming_with_functional_api() -> None: for chunk in graph.stream({}): if "slow" not in chunk: # We'll just look at the updates from `slow` continue - arrival_times.append(time.time()) + arrival_times.append(time.monotonic()) assert len(arrival_times) == 2 delta = arrival_times[1] - arrival_times[0] From b442bf802aa1a1a3c5c2d5e9b91b569083779523 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Fri, 10 Apr 2026 17:12:03 -0400 Subject: [PATCH 3/6] release(langgraph): 1.1.7a1 (#7476) Adding graph life cycle callbacks --- libs/langgraph/pyproject.toml | 2 +- libs/langgraph/uv.lock | 2 +- libs/prebuilt/uv.lock | 2 +- libs/sdk-py/uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 6c1745fe2..6e4378c0c 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" description = "Building stateful, multi-actor applications with LLMs" authors = [] requires-python = ">=3.10" diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 7554e2765..bfb933280 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1367,7 +1367,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "." } dependencies = [ { name = "langchain-core" }, diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index 88bdeb623..f6ce058cc 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -268,7 +268,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "../langgraph" } dependencies = [ { name = "langchain-core" }, diff --git a/libs/sdk-py/uv.lock b/libs/sdk-py/uv.lock index 5194368d1..bb29f8686 100644 --- a/libs/sdk-py/uv.lock +++ b/libs/sdk-py/uv.lock @@ -281,7 +281,7 @@ wheels = [ [[package]] name = "langgraph" -version = "1.1.6" +version = "1.1.7a1" source = { editable = "../langgraph" } dependencies = [ { name = "langchain-core" }, From 742d165acb430151625d30d160d2fa732116a40d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:28:46 -0700 Subject: [PATCH 4/6] chore(deps): bump uv from 0.11.3 to 0.11.6 in /libs/cli (#7472) Bumps [uv](https://github.com/astral-sh/uv) from 0.11.3 to 0.11.6.
Release notes

Sourced from uv's releases.

0.11.6

Release Notes

Released on 2026-04-09.

This release resolves a low severity security advisory in which wheels with malformed RECORD entries could delete arbitrary files on uninstall. See GHSA-pjjw-68hj-v9mw for details.

Bug fixes

  • Do not remove files outside the venv on uninstall (#18942)
  • Validate and heal wheel RECORD during installation (#18943)
  • Avoid uv cache clean errors due to Win32 path normalization (#18856)

Install uv 0.11.6

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf
https://releases.astral.sh/github/uv/releases/download/0.11.6/uv-installer.sh
| sh

Install prebuilt binaries via powershell script

powershell -ExecutionPolicy Bypass -c "irm
https://releases.astral.sh/github/uv/releases/download/0.11.6/uv-installer.ps1
| iex"

Download uv 0.11.6

File Platform Checksum
uv-aarch64-apple-darwin.tar.gz Apple Silicon macOS checksum
uv-x86_64-apple-darwin.tar.gz Intel macOS checksum
uv-aarch64-pc-windows-msvc.zip ARM64 Windows checksum
uv-i686-pc-windows-msvc.zip x86 Windows checksum
uv-x86_64-pc-windows-msvc.zip x64 Windows checksum
uv-aarch64-unknown-linux-gnu.tar.gz ARM64 Linux checksum
uv-i686-unknown-linux-gnu.tar.gz x86 Linux checksum
uv-powerpc64le-unknown-linux-gnu.tar.gz PPC64LE Linux checksum
uv-riscv64gc-unknown-linux-gnu.tar.gz RISCV Linux checksum
uv-s390x-unknown-linux-gnu.tar.gz S390x Linux checksum
uv-x86_64-unknown-linux-gnu.tar.gz x64 Linux checksum
uv-armv7-unknown-linux-gnueabihf.tar.gz ARMv7 Linux checksum
uv-aarch64-unknown-linux-musl.tar.gz ARM64 MUSL Linux checksum
uv-i686-unknown-linux-musl.tar.gz x86 MUSL Linux checksum
uv-riscv64gc-unknown-linux-musl.tar.gz RISCV MUSL Linux checksum
uv-x86_64-unknown-linux-musl.tar.gz x64 MUSL Linux checksum
uv-arm-unknown-linux-musleabihf.tar.gz ARMv6 MUSL Linux (Hardfloat) checksum
uv-armv7-unknown-linux-musleabihf.tar.gz ARMv7 MUSL Linux checksum

... (truncated)

Changelog

Sourced from uv's changelog.

0.11.6

Released on 2026-04-09.

This release resolves a low severity security advisory in which wheels with malformed RECORD entries could delete arbitrary files on uninstall. See GHSA-pjjw-68hj-v9mw for details.

Bug fixes

  • Do not remove files outside the venv on uninstall (#18942)
  • Validate and heal wheel RECORD during installation (#18943)
  • Avoid uv cache clean errors due to Win32 path normalization (#18856)

0.11.5

Released on 2026-04-08.

Python

  • Add CPython 3.13.13, 3.14.4, and 3.15.0a8 (#18908)

Enhancements

  • Fix build_system.requires error message (#18911)
  • Remove trailing path separators in path normalization (#18915)
  • Improve error messages for unsupported or invalid TLS certificates (#18924)

Preview features

  • Add exclude-newer to [[tool.uv.index]] (#18839)
  • uv audit: add context/warnings for ignored vulnerabilities (#18905)

Bug fixes

  • Normalize persisted fork markers before lock equality checks (#18612)
  • Clear junction properly when uninstalling Python versions on Windows (#18815)
  • Report error cleanly instead of panicking on TLS certificate error (#18904)

Documentation

  • Remove the legacy PIP_COMPATIBILITY.md redirect file (#18928)
  • Fix uv init example-bare --bare examples (#18822, #18925)

0.11.4

Released on 2026-04-07.

Enhancements

  • Add support for --upgrade-group (#18266)
  • Merge repeated archive URL hashes by version ID (#18841)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uv&package-manager=uv&previous-version=0.11.3&new-version=0.11.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/cli/uv.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/libs/cli/uv.lock b/libs/cli/uv.lock index 8c9392d32..1113fce67 100644 --- a/libs/cli/uv.lock +++ b/libs/cli/uv.lock @@ -2318,28 +2318,28 @@ wheels = [ [[package]] name = "uv" -version = "0.11.3" +version = "0.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/ed/f11c558e8d2e02fba6057dacd9e92a71557359a80bd5355452310b89f40f/uv-0.11.3.tar.gz", hash = "sha256:6a6fcaf1fec28bbbdf0dfc5a0a6e34be4cea08c6287334b08c24cf187300f20d", size = 4027684, upload-time = "2026-04-01T21:47:22.096Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/93/4f04c49fd6046a18293de341d795ded3b9cbd95db261d687e26db0f11d1e/uv-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:deb533e780e8181e0859c68c84f546620072cd1bd827b38058cb86ebfba9bb7d", size = 23337334, upload-time = "2026-04-01T21:46:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4b/c44fd3fbc80ac2f81e2ad025d235c820aac95b228076da85be3f5d509781/uv-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d2b3b0fa1693880ca354755c216ae1c65dd938a4f1a24374d0c3f4b9538e0ee6", size = 22940169, upload-time = "2026-04-01T21:47:32.72Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c7/7d01be259a47d42fa9e80adcb7a829d81e7c376aa8fa1b714f31d7dfc226/uv-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71f5d0b9e73daa5d8a7e2db3fa2e22a4537d24bb4fe78130db797280280d4edc", size = 21473579, upload-time = "2026-04-01T21:47:25.063Z" }, - { url = "https://files.pythonhosted.org/packages/9a/71/fffcd890290a4639a3799cf3f3e87947c10d1b0de19eba3cf837cb418dd8/uv-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:55ba578752f29a3f2b22879b22a162edad1454e3216f3ca4694fdbd4093a6822", size = 23132691, upload-time = "2026-04-01T21:47:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7b/1ac9e1f753a19b6252434f0bbe96efdcc335cd74677f4c6f431a7c916114/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3b1fe09d5e1d8e19459cd28d7825a3b66ef147b98328345bad6e17b87c4fea48", size = 22955764, upload-time = "2026-04-01T21:46:51.721Z" }, - { url = "https://files.pythonhosted.org/packages/ff/51/1a6010a681a3c3e0a8ec99737ba2d0452194dc372a5349a9267873261c02/uv-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:088165b9eed981d2c2a58566cc75dd052d613e47c65e2416842d07308f793a6f", size = 22966245, upload-time = "2026-04-01T21:47:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/38/74/1a1b0712daead7e85f56d620afe96fe166a04b615524c14027b4edd39b82/uv-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef0ae8ee2988928092616401ec7f473612b8e9589fe1567452c45dbc56840f85", size = 24623370, upload-time = "2026-04-01T21:47:03.59Z" }, - { url = "https://files.pythonhosted.org/packages/b6/62/5c3aa5e7bd2744810e50ad72a5951386ec84a513e109b1b5cb7ec442f3b6/uv-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6708827ecb846d00c5512a7e4dc751c2e27b92e9bd55a0be390561ac68930c32", size = 25142735, upload-time = "2026-04-01T21:46:55.756Z" }, - { url = "https://files.pythonhosted.org/packages/88/ab/6266a04980e0877af5518762adfe23a0c1ab0b801ae3099a2e7b74e34411/uv-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df030ea7563e99c09854e1bc82ab743dfa2d0ba18976e6861979cb40d04dba7", size = 24512083, upload-time = "2026-04-01T21:46:43.531Z" }, - { url = "https://files.pythonhosted.org/packages/4e/be/7c66d350f833eb437f9aa0875655cc05e07b441e3f4a770f8bced56133f7/uv-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fde893b5ab9f6997fe357138e794bac09d144328052519fbbe2e6f72145e457", size = 24589293, upload-time = "2026-04-01T21:47:11.379Z" }, - { url = "https://files.pythonhosted.org/packages/18/4f/22ada41564a8c8c36653fc86f89faae4c54a4cdd5817bda53764a3eb352d/uv-0.11.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:45006bcd9e8718248a23ab81448a5beb46a72a9dd508e3212d6f3b8c63aeb88a", size = 23214854, upload-time = "2026-04-01T21:46:59.491Z" }, - { url = "https://files.pythonhosted.org/packages/aa/18/8669840657fea9fd668739dec89643afe1061c023c1488228b02f79a2399/uv-0.11.3-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:089b9d338a64463956b6fee456f03f73c9a916479bdb29009600781dc1e1d2a7", size = 23914434, upload-time = "2026-04-01T21:47:29.164Z" }, - { url = "https://files.pythonhosted.org/packages/08/0d/c59f24b3a1ae5f377aa6fd9653562a0968ea6be946fe35761871a0072919/uv-0.11.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3ff461335888336467402cc5cb792c911df95dd0b52e369182cfa4c902bb21f4", size = 23971481, upload-time = "2026-04-01T21:47:48.551Z" }, - { url = "https://files.pythonhosted.org/packages/66/7d/f83ed79921310ef216ed6d73fcd3822dff4b66749054fb97e09b7bd5901e/uv-0.11.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:a62e29277efd39c35caf4a0fe739c4ebeb14d4ce4f02271f3f74271d608061ff", size = 23784797, upload-time = "2026-04-01T21:47:40.588Z" }, - { url = "https://files.pythonhosted.org/packages/35/19/3ff3539c44ca7dc2aa87b021d4a153ba6a72866daa19bf91c289e4318f95/uv-0.11.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ebccdcdebd2b288925f0f7c18c39705dc783175952eacaf94912b01d3b381b86", size = 24794606, upload-time = "2026-04-01T21:47:36.814Z" }, - { url = "https://files.pythonhosted.org/packages/79/e5/e676454bb7cc5dcf5c4637ed3ef0ff97309d84a149b832a4dea53f04c0ab/uv-0.11.3-py3-none-win32.whl", hash = "sha256:794aae3bab141eafbe37c51dc5dd0139658a755a6fa9cc74d2dbd7c71dcc4826", size = 22573432, upload-time = "2026-04-01T21:47:15.143Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a0/95d22d524bd3b4708043d65035f02fc9656e5fb6e0aaef73510313b1641b/uv-0.11.3-py3-none-win_amd64.whl", hash = "sha256:68fda574f2e5e7536a2b747dcea88329a71aad7222317e8f4717d0af8f99fbd4", size = 24969508, upload-time = "2026-04-01T21:47:19.515Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6d/3f0b90a06e8c4594e11f813651756d6896de6dd4461f554fd7e4984a1c4f/uv-0.11.3-py3-none-win_arm64.whl", hash = "sha256:92ffc4d521ab2c4738ef05d8ef26f2750e26d31f3ad5611cdfefc52445be9ace", size = 23488911, upload-time = "2026-04-01T21:47:52.427Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" }, + { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" }, + { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" }, + { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" }, + { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" }, + { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" }, + { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" }, + { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" }, + { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" }, ] [[package]] From d27d4b2d9895a4aa83170b4aaa135878f99c7fd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:29:04 -0700 Subject: [PATCH 5/6] chore(deps): bump langsmith from 0.5.4 to 0.5.18 in /libs/cli/js-monorepo-example (#7475) Bumps [langsmith](https://github.com/langchain-ai/langsmith-sdk) from 0.5.4 to 0.5.18.
Commits
Install script changes

This version modifies prepublish script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=langsmith&package-manager=npm_and_yarn&previous-version=0.5.4&new-version=0.5.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/cli/js-monorepo-example/yarn.lock | 79 +++----------------------- 1 file changed, 7 insertions(+), 72 deletions(-) diff --git a/libs/cli/js-monorepo-example/yarn.lock b/libs/cli/js-monorepo-example/yarn.lock index eeeef98e3..83cb3dd06 100644 --- a/libs/cli/js-monorepo-example/yarn.lock +++ b/libs/cli/js-monorepo-example/yarn.lock @@ -217,11 +217,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/uuid@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" - integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== - "@typescript-eslint/eslint-plugin@^8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz#ad40e492f1931f46da1bd888e52b9e56df9063aa" @@ -343,13 +338,6 @@ ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - ansi-styles@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" @@ -508,38 +496,11 @@ camelcase@6: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -console-table-printer@^2.12.1: - version "2.14.6" - resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.14.6.tgz#edfe0bf311fa2701922ed509443145ab51e06436" - integrity sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw== - dependencies: - simple-wcswidth "^1.0.1" - cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1059,11 +1020,6 @@ has-bigints@^1.0.2: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -1372,16 +1328,12 @@ keyv@^4.5.4: json-buffer "3.0.1" "langsmith@>=0.5.0 <1.0.0": - version "0.5.4" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.4.tgz#f75b82b08e30db72a7d1d595b341e9666bd525e5" - integrity sha512-qYkNIoKpf0ZYt+cYzrDV+XI3FCexApmZmp8EMs3eDTMv0OvrHMLoxJ9IpkeoXJSX24+GPk0/jXjKx2hWerpy9w== + version "0.5.18" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.5.18.tgz#c691ad23614f0b46eaf07d982e0ac988e1f43880" + integrity sha512-3zuZUWffTHQ+73EAwnodADtf534VNEZUpXr9jC12qyG8/IQuJET7PRsCpTb9wX2lmBspakwLUpqpj3tNm/0bVA== dependencies: - "@types/uuid" "^10.0.0" - chalk "^4.1.2" - console-table-printer "^2.12.1" - p-queue "^6.6.2" - semver "^7.6.3" - uuid "^10.0.0" + p-queue "6.6.2" + uuid "10.0.0" levn@^0.4.1: version "0.4.1" @@ -1528,7 +1480,7 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-queue@^6.6.2: +p-queue@6.6.2, p-queue@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== @@ -1690,11 +1642,6 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.6.3: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== - semver@^7.7.3: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" @@ -1783,11 +1730,6 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -simple-wcswidth@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b" - integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw== - stop-iteration-iterator@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" @@ -1838,13 +1780,6 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -1966,7 +1901,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -uuid@^10.0.0: +uuid@10.0.0, uuid@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== From 2c98c59fca6c99b696988b97dfaeb885f652920c Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" <215916821+open-swe[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:08:09 -0400 Subject: [PATCH 6/6] fix: populate assistant_id from config configurable instead of metadata (#7468) ## Description The `_build_server_info` function was reading `assistant_id` and `graph_id` from `config["metadata"]`, but the server puts these values in `config["configurable"]`. This updates the source to read from `configurable` consistently. ## Test Plan - [ ] Verify `server_info.assistant_id` and `server_info.graph_id` are correctly populated from `config["configurable"]` _Opened collaboratively by Sydney Runkle and open-swe._ Co-authored-by: open-swe[bot] Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> --- libs/langgraph/langgraph/pregel/main.py | 9 ++++----- libs/langgraph/tests/test_runtime.py | 17 ++++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/main.py b/libs/langgraph/langgraph/pregel/main.py index 02d0ee11e..c440e77f9 100644 --- a/libs/langgraph/langgraph/pregel/main.py +++ b/libs/langgraph/langgraph/pregel/main.py @@ -3715,15 +3715,14 @@ def _coerce_checkpoint_values(payload: Any, mapper: Callable[[Any], Any]) -> Non def _build_server_info( config: RunnableConfig, parent_runtime: Runtime[Any] ) -> ServerInfo | None: - """Build ServerInfo from config metadata and configurable. + """Build ServerInfo from config configurable. - The server puts assistant_id/graph_id in config metadata and the + The server puts assistant_id/graph_id in config configurable and the authenticated user dict in configurable["langgraph_auth_user"]. """ - metadata = config.get("metadata") or {} configurable = config.get(CONF) or {} - assistant_id = metadata.get("assistant_id") - graph_id = metadata.get("graph_id") + assistant_id = configurable.get("assistant_id") + graph_id = configurable.get("graph_id") # Read authenticated user from configurable (set by LangGraph Server). # We prefer isinstance(BaseUser) but fall back to hasattr("identity") diff --git a/libs/langgraph/tests/test_runtime.py b/libs/langgraph/tests/test_runtime.py index 0796ddf9d..a85abe45f 100644 --- a/libs/langgraph/tests/test_runtime.py +++ b/libs/langgraph/tests/test_runtime.py @@ -501,13 +501,13 @@ async def test_execution_info_populated_in_graph_async() -> None: assert isinstance(info.node_first_attempt_time, float) -def test_server_info_from_metadata() -> None: - """server_info is built from assistant_id/graph_id in config metadata.""" +def test_server_info_from_configurable() -> None: + """server_info is built from assistant_id/graph_id in config configurable.""" captured: dict[str, Any] = {} compiled = _make_capture_graph(captured) compiled.invoke( {"message": "hi"}, - config={"metadata": {"assistant_id": "asst-abc", "graph_id": "my-graph"}}, + config={"configurable": {"assistant_id": "asst-abc", "graph_id": "my-graph"}}, ) si = captured["server_info"] assert si is not None @@ -516,8 +516,8 @@ def test_server_info_from_metadata() -> None: assert si.user is None -def test_server_info_none_without_metadata() -> None: - """server_info is None when no assistant_id/graph_id in metadata.""" +def test_server_info_none_without_configurable() -> None: + """server_info is None when no assistant_id/graph_id in configurable.""" captured: dict[str, Any] = {} compiled = _make_capture_graph(captured) compiled.invoke({"message": "hi"}) @@ -579,8 +579,11 @@ def test_server_info_user_from_auth_user() -> None: compiled.invoke( {"message": "hi"}, config={ - "configurable": {"langgraph_auth_user": proxy}, - "metadata": {"assistant_id": "asst-proxy", "graph_id": "graph-proxy"}, + "configurable": { + "langgraph_auth_user": proxy, + "assistant_id": "asst-proxy", + "graph_id": "graph-proxy", + }, }, ) si = captured["server_info"]