mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-20 06:35:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a55365f3f9 | ||
|
|
2cd7ecc81e | ||
|
|
f6746b39cc |
@@ -8,6 +8,7 @@ from langchain_core.runnables import Runnable, RunnableConfig
|
|||||||
from langgraph.store.base import BaseStore
|
from langgraph.store.base import BaseStore
|
||||||
|
|
||||||
from langgraph._internal._typing import EMPTY_SEQ
|
from langgraph._internal._typing import EMPTY_SEQ
|
||||||
|
from langgraph.errors import NodeError
|
||||||
from langgraph.runtime import Runtime
|
from langgraph.runtime import Runtime
|
||||||
from langgraph.types import CachePolicy, RetryPolicy, StreamWriter, TimeoutPolicy
|
from langgraph.types import CachePolicy, RetryPolicy, StreamWriter, TimeoutPolicy
|
||||||
from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra
|
from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra
|
||||||
@@ -64,6 +65,22 @@ class _NodeWithRuntime(Protocol[NodeInputT_contra, ContextT]):
|
|||||||
) -> Any: ...
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _NodeWithNodeError(Protocol[NodeInputT_contra]):
|
||||||
|
def __call__(self, state: NodeInputT_contra, *, error: NodeError) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _NodeWithConfigNodeError(Protocol[NodeInputT_contra]):
|
||||||
|
def __call__(
|
||||||
|
self, state: NodeInputT_contra, *, config: RunnableConfig, error: NodeError
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _NodeWithRuntimeNodeError(Protocol[NodeInputT_contra, ContextT]):
|
||||||
|
def __call__(
|
||||||
|
self, state: NodeInputT_contra, *, runtime: Runtime[ContextT], error: NodeError
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
# TODO: we probably don't want to explicitly support the config / store signatures once
|
# TODO: we probably don't want to explicitly support the config / store signatures once
|
||||||
# we move to adding a context arg. Maybe what we do is we add support for kwargs with param spec
|
# we move to adding a context arg. Maybe what we do is we add support for kwargs with param spec
|
||||||
# this is purely for typing purposes though, so can easily change in the coming weeks.
|
# this is purely for typing purposes though, so can easily change in the coming weeks.
|
||||||
@@ -80,6 +97,13 @@ StateNode: TypeAlias = (
|
|||||||
| Runnable[NodeInputT, Any]
|
| Runnable[NodeInputT, Any]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ErrorHandlerNode: TypeAlias = (
|
||||||
|
StateNode[NodeInputT, ContextT]
|
||||||
|
| _NodeWithNodeError[NodeInputT]
|
||||||
|
| _NodeWithConfigNodeError[NodeInputT]
|
||||||
|
| _NodeWithRuntimeNodeError[NodeInputT, ContextT]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class StateNodeSpec(Generic[NodeInputT, ContextT]):
|
class StateNodeSpec(Generic[NodeInputT, ContextT]):
|
||||||
@@ -88,8 +112,7 @@ class StateNodeSpec(Generic[NodeInputT, ContextT]):
|
|||||||
input_schema: type[NodeInputT]
|
input_schema: type[NodeInputT]
|
||||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None
|
||||||
cache_policy: CachePolicy | None
|
cache_policy: CachePolicy | None
|
||||||
is_error_handler: bool = False
|
error_handler: Runnable[Any, Any] | None = None
|
||||||
error_handler_node: str | None = None
|
|
||||||
ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
|
ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
|
||||||
defer: bool = False
|
defer: bool = False
|
||||||
timeout: TimeoutPolicy | None = None
|
timeout: TimeoutPolicy | None = None
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ from langgraph.errors import (
|
|||||||
create_error_message,
|
create_error_message,
|
||||||
)
|
)
|
||||||
from langgraph.graph._branch import BranchSpec
|
from langgraph.graph._branch import BranchSpec
|
||||||
from langgraph.graph._node import StateNode, StateNodeSpec
|
from langgraph.graph._node import ErrorHandlerNode, StateNode, StateNodeSpec
|
||||||
from langgraph.managed.base import (
|
from langgraph.managed.base import (
|
||||||
ManagedValueSpec,
|
ManagedValueSpec,
|
||||||
is_managed_value,
|
is_managed_value,
|
||||||
@@ -772,24 +772,15 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
if destinations is not None:
|
if destinations is not None:
|
||||||
ends = destinations
|
ends = destinations
|
||||||
|
|
||||||
resolved_input_schema: type[Any] = (
|
coerced_error_handler: Runnable[Any, Any] | None = (
|
||||||
input_schema or inferred_input_schema or self.state_schema
|
coerce_to_runnable( # type: ignore[arg-type]
|
||||||
)
|
error_handler,
|
||||||
handler_node_name: str | None = None
|
name=f"__error_handler__{node}",
|
||||||
if error_handler is not None:
|
trace=False,
|
||||||
handler_node_name = f"__error_handler__{node}"
|
|
||||||
if handler_node_name in self.nodes:
|
|
||||||
raise ValueError(
|
|
||||||
f"Auto-generated error handler node `{handler_node_name}` already exists."
|
|
||||||
)
|
|
||||||
self.nodes[handler_node_name] = StateNodeSpec[Any, ContextT](
|
|
||||||
coerce_to_runnable(error_handler, name=handler_node_name, trace=False), # type: ignore[arg-type]
|
|
||||||
metadata=None,
|
|
||||||
input_schema=resolved_input_schema,
|
|
||||||
retry_policy=None,
|
|
||||||
cache_policy=None,
|
|
||||||
is_error_handler=True,
|
|
||||||
)
|
)
|
||||||
|
if error_handler is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
if input_schema is not None:
|
if input_schema is not None:
|
||||||
self.nodes[node] = StateNodeSpec[NodeInputT, ContextT](
|
self.nodes[node] = StateNodeSpec[NodeInputT, ContextT](
|
||||||
@@ -798,7 +789,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
input_schema=input_schema,
|
input_schema=input_schema,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
error_handler_node=handler_node_name,
|
error_handler=coerced_error_handler,
|
||||||
ends=ends,
|
ends=ends,
|
||||||
defer=defer,
|
defer=defer,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
@@ -810,7 +801,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
input_schema=inferred_input_schema,
|
input_schema=inferred_input_schema,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
error_handler_node=handler_node_name,
|
error_handler=coerced_error_handler,
|
||||||
ends=ends,
|
ends=ends,
|
||||||
defer=defer,
|
defer=defer,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
@@ -822,7 +813,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
input_schema=self.state_schema,
|
input_schema=self.state_schema,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
error_handler_node=handler_node_name,
|
error_handler=coerced_error_handler,
|
||||||
ends=ends,
|
ends=ends,
|
||||||
defer=defer,
|
defer=defer,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
@@ -1079,7 +1070,17 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
if interrupt:
|
if interrupt:
|
||||||
for node in interrupt:
|
for node in interrupt:
|
||||||
if node not in self.nodes:
|
if node not in self.nodes:
|
||||||
raise ValueError(f"Interrupt node `{node}` not found")
|
# __error_handler__<name> is a valid virtual task name when the
|
||||||
|
# base node has an error_handler configured.
|
||||||
|
if node.startswith("__error_handler__"):
|
||||||
|
base = node[len("__error_handler__"):]
|
||||||
|
if (
|
||||||
|
base not in self.nodes
|
||||||
|
or self.nodes[base].error_handler is None
|
||||||
|
):
|
||||||
|
raise ValueError(f"Interrupt node `{node}` not found")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Interrupt node `{node}` not found")
|
||||||
self.compiled = True
|
self.compiled = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -1094,6 +1095,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
|
transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
|
||||||
|
error_handler: ErrorHandlerNode[Any, ContextT] | None = None,
|
||||||
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
|
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
|
||||||
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
|
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
|
||||||
|
|
||||||
@@ -1193,11 +1195,15 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
key for key, val in self.channels.items() if not is_managed_value(val)
|
key for key, val in self.channels.items() if not is_managed_value(val)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
node_error_handler_map = {
|
error_handler: Runnable[Any, Any] | None = (
|
||||||
node_name: spec.error_handler_node
|
coerce_to_runnable( # type: ignore[arg-type]
|
||||||
for node_name, spec in self.nodes.items()
|
error_handler,
|
||||||
if spec.error_handler_node is not None
|
name="__graph_error_handler__",
|
||||||
}
|
trace=False,
|
||||||
|
)
|
||||||
|
if error_handler is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
|
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
|
||||||
builder=self,
|
builder=self,
|
||||||
@@ -1220,7 +1226,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
|||||||
debug=debug,
|
debug=debug,
|
||||||
store=store,
|
store=store,
|
||||||
cache=cache,
|
cache=cache,
|
||||||
node_error_handler_map=node_error_handler_map,
|
error_handler=error_handler,
|
||||||
name=name or "LangGraph",
|
name=name or "LangGraph",
|
||||||
stream_transformers=transformers,
|
stream_transformers=transformers,
|
||||||
)
|
)
|
||||||
@@ -1395,8 +1401,7 @@ class CompiledStateGraph(
|
|||||||
metadata=node.metadata,
|
metadata=node.metadata,
|
||||||
retry_policy=node.retry_policy,
|
retry_policy=node.retry_policy,
|
||||||
cache_policy=node.cache_policy,
|
cache_policy=node.cache_policy,
|
||||||
is_error_handler=node.is_error_handler,
|
error_handler=node.error_handler,
|
||||||
error_handler_node=node.error_handler_node,
|
|
||||||
bound=node.runnable, # type: ignore[arg-type]
|
bound=node.runnable, # type: ignore[arg-type]
|
||||||
timeout=node.timeout,
|
timeout=node.timeout,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from typing import (
|
|||||||
|
|
||||||
from langchain_core.callbacks import Callbacks
|
from langchain_core.callbacks import Callbacks
|
||||||
from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
|
from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
|
||||||
|
from langchain_core.runnables import Runnable
|
||||||
from langchain_core.runnables.config import RunnableConfig
|
from langchain_core.runnables.config import RunnableConfig
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
BaseCheckpointSaver,
|
BaseCheckpointSaver,
|
||||||
@@ -32,6 +33,7 @@ from langgraph.store.base import BaseStore
|
|||||||
from xxhash import xxh3_128_hexdigest
|
from xxhash import xxh3_128_hexdigest
|
||||||
|
|
||||||
from langgraph._internal._config import merge_configs, patch_config
|
from langgraph._internal._config import merge_configs, patch_config
|
||||||
|
from langgraph._internal._runnable import RunnableSeq
|
||||||
from langgraph._internal._constants import (
|
from langgraph._internal._constants import (
|
||||||
CACHE_NS_WRITES,
|
CACHE_NS_WRITES,
|
||||||
CONF,
|
CONF,
|
||||||
@@ -71,6 +73,7 @@ from langgraph.constants import TAG_HIDDEN
|
|||||||
from langgraph.errors import NodeError
|
from langgraph.errors import NodeError
|
||||||
from langgraph.managed.base import ManagedValueMapping
|
from langgraph.managed.base import ManagedValueMapping
|
||||||
from langgraph.pregel._call import get_runnable_for_task, identifier
|
from langgraph.pregel._call import get_runnable_for_task, identifier
|
||||||
|
from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
|
||||||
from langgraph.pregel._io import read_channels
|
from langgraph.pregel._io import read_channels
|
||||||
from langgraph.pregel._log import logger
|
from langgraph.pregel._log import logger
|
||||||
from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
|
from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
|
||||||
@@ -407,6 +410,7 @@ def prepare_next_tasks(
|
|||||||
updated_channels: set[str] | None = None,
|
updated_channels: set[str] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
|
) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
|
||||||
"""Prepare the set of tasks that will make up the next Pregel step.
|
"""Prepare the set of tasks that will make up the next Pregel step.
|
||||||
|
|
||||||
@@ -462,6 +466,7 @@ def prepare_next_tasks(
|
|||||||
input_cache=input_cache,
|
input_cache=input_cache,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
|
error_handler=error_handler,
|
||||||
):
|
):
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
@@ -508,6 +513,7 @@ def prepare_next_tasks(
|
|||||||
input_cache=input_cache,
|
input_cache=input_cache,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
|
error_handler=error_handler,
|
||||||
):
|
):
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
return {t.id: t for t in tasks}
|
return {t.id: t for t in tasks}
|
||||||
@@ -542,6 +548,7 @@ def prepare_single_task(
|
|||||||
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
|
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
) -> None | PregelTask | PregelExecutableTask:
|
) -> None | PregelTask | PregelExecutableTask:
|
||||||
"""Prepares a single task for the next Pregel step, given a task path, which
|
"""Prepares a single task for the next Pregel step, given a task path, which
|
||||||
uniquely identifies a PUSH or PULL task within the graph."""
|
uniquely identifies a PUSH or PULL task within the graph."""
|
||||||
@@ -756,6 +763,7 @@ def prepare_single_task(
|
|||||||
writers=proc.flat_writers,
|
writers=proc.flat_writers,
|
||||||
subgraphs=proc.subgraphs,
|
subgraphs=proc.subgraphs,
|
||||||
timeout=proc.timeout,
|
timeout=proc.timeout,
|
||||||
|
error_handler=proc.error_handler or error_handler,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return PregelTask(task_id, name, task_path[:3])
|
return PregelTask(task_id, name, task_path[:3])
|
||||||
@@ -1110,11 +1118,10 @@ def prepare_push_task_send(
|
|||||||
def prepare_node_error_handler_task(
|
def prepare_node_error_handler_task(
|
||||||
failed_task: PregelExecutableTask,
|
failed_task: PregelExecutableTask,
|
||||||
*,
|
*,
|
||||||
handler_node_name: str,
|
handler: Runnable,
|
||||||
failed_error: BaseException,
|
failed_error: BaseException,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
pending_writes: list[PendingWrite],
|
pending_writes: list[PendingWrite],
|
||||||
processes: Mapping[str, PregelNode],
|
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
managed: ManagedValueMapping,
|
managed: ManagedValueMapping,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
@@ -1123,17 +1130,14 @@ def prepare_node_error_handler_task(
|
|||||||
store: BaseStore | None = None,
|
store: BaseStore | None = None,
|
||||||
checkpointer: BaseCheckpointSaver | None = None,
|
checkpointer: BaseCheckpointSaver | None = None,
|
||||||
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
||||||
cache_policy: CachePolicy | None = None,
|
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
) -> PregelExecutableTask | None:
|
) -> PregelExecutableTask:
|
||||||
"""Prepare an immediate node-level error handler task for a failed task."""
|
"""Prepare an error handler task for a failed task.
|
||||||
if handler_node_name not in processes:
|
|
||||||
return None
|
|
||||||
proc = processes[handler_node_name]
|
|
||||||
proc_node = proc.node
|
|
||||||
if proc_node is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
The handler borrows the failed task's write pipeline (same state channels),
|
||||||
|
so no separate node registration is needed.
|
||||||
|
"""
|
||||||
|
handler_node_name = f"__error_handler__{failed_task.name}"
|
||||||
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
||||||
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
|
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
|
||||||
configurable = config.get(CONF, {})
|
configurable = config.get(CONF, {})
|
||||||
@@ -1159,27 +1163,17 @@ def prepare_node_error_handler_task(
|
|||||||
"langgraph_path": translated_task_path,
|
"langgraph_path": translated_task_path,
|
||||||
"langgraph_checkpoint_ns": task_checkpoint_ns,
|
"langgraph_checkpoint_ns": task_checkpoint_ns,
|
||||||
}
|
}
|
||||||
if proc.metadata:
|
|
||||||
metadata.update(proc.metadata)
|
|
||||||
writes: deque[tuple[str, Any]] = deque()
|
writes: deque[tuple[str, Any]] = deque()
|
||||||
|
# Mirror how regular node procs are built: combine handler with a write pipeline
|
||||||
effective_retry_policy = proc.retry_policy or retry_policy
|
# so run_with_retry invokes the full pipeline in one shot.
|
||||||
effective_cache_policy = proc.cache_policy or cache_policy
|
# - PULL node tasks: writers are in failed_task.writers → reuse them
|
||||||
if effective_cache_policy:
|
# - PUSH functional tasks: writers are embedded in proc (empty writers list) →
|
||||||
args_key = effective_cache_policy.key_func(failed_task.input)
|
# add a RETURN write so the handler's result becomes the future's value.
|
||||||
cache_key = CacheKey(
|
handler_writers = failed_task.writers
|
||||||
(
|
if handler_writers:
|
||||||
CACHE_NS_WRITES,
|
handler_proc: Runnable = RunnableSeq(handler, *handler_writers)
|
||||||
(identifier(proc) or "__dynamic__"),
|
|
||||||
handler_node_name,
|
|
||||||
),
|
|
||||||
xxh3_128_hexdigest(
|
|
||||||
args_key.encode() if isinstance(args_key, str) else args_key
|
|
||||||
),
|
|
||||||
effective_cache_policy.ttl,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
cache_key = None
|
handler_proc = RunnableSeq(handler, ChannelWrite([ChannelWriteEntry(RETURN)]))
|
||||||
|
|
||||||
scratchpad = _scratchpad(
|
scratchpad = _scratchpad(
|
||||||
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
|
||||||
@@ -1194,14 +1188,11 @@ def prepare_node_error_handler_task(
|
|||||||
runtime = runtime.override(
|
runtime = runtime.override(
|
||||||
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
|
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
|
||||||
)
|
)
|
||||||
additional_config: RunnableConfig = {
|
additional_config: RunnableConfig = {"metadata": metadata}
|
||||||
"metadata": metadata,
|
|
||||||
"tags": proc.tags,
|
|
||||||
}
|
|
||||||
return PregelExecutableTask(
|
return PregelExecutableTask(
|
||||||
handler_node_name,
|
handler_node_name,
|
||||||
failed_task.input,
|
failed_task.input,
|
||||||
proc_node,
|
handler_proc,
|
||||||
writes,
|
writes,
|
||||||
patch_config(
|
patch_config(
|
||||||
merge_configs(config, additional_config),
|
merge_configs(config, additional_config),
|
||||||
@@ -1239,12 +1230,11 @@ def prepare_node_error_handler_task(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
PUSH_TRIGGER,
|
PUSH_TRIGGER,
|
||||||
effective_retry_policy,
|
retry_policy,
|
||||||
cache_key,
|
None, # handlers don't cache
|
||||||
task_id,
|
task_id,
|
||||||
translated_task_path,
|
translated_task_path,
|
||||||
writers=proc.flat_writers,
|
writers=handler_writers, # for ParentCommand / subgraph routing
|
||||||
subgraphs=proc.subgraphs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
|
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import Runnable, RunnableConfig
|
||||||
|
|
||||||
from langgraph.cache.base import BaseCache
|
from langgraph.cache.base import BaseCache
|
||||||
from langgraph.checkpoint.base import (
|
from langgraph.checkpoint.base import (
|
||||||
WRITES_IDX_MAP,
|
WRITES_IDX_MAP,
|
||||||
@@ -279,6 +280,7 @@ class PregelLoop:
|
|||||||
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
has_graph_lifecycle_callbacks: bool = False,
|
has_graph_lifecycle_callbacks: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
@@ -303,6 +305,7 @@ class PregelLoop:
|
|||||||
self.trigger_to_nodes = trigger_to_nodes
|
self.trigger_to_nodes = trigger_to_nodes
|
||||||
self.retry_policy = retry_policy
|
self.retry_policy = retry_policy
|
||||||
self.cache_policy = cache_policy
|
self.cache_policy = cache_policy
|
||||||
|
self.error_handler = error_handler
|
||||||
self.durability = durability
|
self.durability = durability
|
||||||
self._has_graph_lifecycle_callbacks = has_graph_lifecycle_callbacks
|
self._has_graph_lifecycle_callbacks = has_graph_lifecycle_callbacks
|
||||||
self._graph_lifecycle_events = deque()
|
self._graph_lifecycle_events = deque()
|
||||||
@@ -545,6 +548,7 @@ class PregelLoop:
|
|||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
cache_policy=self.cache_policy,
|
||||||
|
error_handler=self.error_handler,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
# produce debug output
|
# produce debug output
|
||||||
@@ -597,6 +601,7 @@ class PregelLoop:
|
|||||||
updated_channels=self.updated_channels,
|
updated_channels=self.updated_channels,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
cache_policy=self.cache_policy,
|
||||||
|
error_handler=self.error_handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
# produce debug output
|
# produce debug output
|
||||||
@@ -1368,6 +1373,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
has_graph_lifecycle_callbacks: bool = False,
|
has_graph_lifecycle_callbacks: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -1389,6 +1395,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
trigger_to_nodes=trigger_to_nodes,
|
trigger_to_nodes=trigger_to_nodes,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
|
error_handler=error_handler,
|
||||||
durability=durability,
|
durability=durability,
|
||||||
has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks,
|
has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks,
|
||||||
)
|
)
|
||||||
@@ -1451,22 +1458,18 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
def schedule_error_handler(
|
def schedule_error_handler(
|
||||||
self, failed_task: PregelExecutableTask, error: BaseException
|
self, failed_task: PregelExecutableTask, error: BaseException
|
||||||
) -> PregelExecutableTask | None:
|
) -> PregelExecutableTask | None:
|
||||||
handler_node = self.nodes[failed_task.name].error_handler_node
|
handler = failed_task.error_handler or self.error_handler
|
||||||
if not handler_node:
|
if handler is None:
|
||||||
return None
|
return None
|
||||||
writes = list(failed_task.writes)
|
writes = list(failed_task.writes)
|
||||||
writes.append((ERROR_SOURCE_NODE, failed_task.name))
|
writes.append((ERROR_SOURCE_NODE, failed_task.name))
|
||||||
self.put_writes(
|
self.put_writes(failed_task.id, writes)
|
||||||
failed_task.id,
|
|
||||||
writes,
|
|
||||||
)
|
|
||||||
handler_task = prepare_node_error_handler_task(
|
handler_task = prepare_node_error_handler_task(
|
||||||
failed_task,
|
failed_task,
|
||||||
handler_node_name=handler_node,
|
handler=handler,
|
||||||
failed_error=error,
|
failed_error=error,
|
||||||
checkpoint=self.checkpoint,
|
checkpoint=self.checkpoint,
|
||||||
pending_writes=self.checkpoint_pending_writes,
|
pending_writes=self.checkpoint_pending_writes,
|
||||||
processes=self.nodes,
|
|
||||||
channels=self.channels,
|
channels=self.channels,
|
||||||
managed=self.managed,
|
managed=self.managed,
|
||||||
config=failed_task.config,
|
config=failed_task.config,
|
||||||
@@ -1476,10 +1479,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
checkpointer=self.checkpointer,
|
checkpointer=self.checkpointer,
|
||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
|
||||||
)
|
)
|
||||||
if handler_task is None:
|
|
||||||
return None
|
|
||||||
self.tasks[handler_task.id] = handler_task
|
self.tasks[handler_task.id] = handler_task
|
||||||
if not self.is_replaying:
|
if not self.is_replaying:
|
||||||
self._match_writes({handler_task.id: handler_task})
|
self._match_writes({handler_task.id: handler_task})
|
||||||
@@ -1487,6 +1487,8 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
self.output_writes(task.id, task.writes, cached=True)
|
self.output_writes(task.id, task.writes, cached=True)
|
||||||
return handler_task
|
return handler_task
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def put_writes(self, task_id: str, writes: WritesT) -> None:
|
def put_writes(self, task_id: str, writes: WritesT) -> None:
|
||||||
"""Put writes for a task, to be read by the next tick."""
|
"""Put writes for a task, to be read by the next tick."""
|
||||||
super().put_writes(task_id, writes)
|
super().put_writes(task_id, writes)
|
||||||
@@ -1621,6 +1623,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
has_graph_lifecycle_callbacks: bool = False,
|
has_graph_lifecycle_callbacks: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -1642,6 +1645,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
trigger_to_nodes=trigger_to_nodes,
|
trigger_to_nodes=trigger_to_nodes,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
|
error_handler=error_handler,
|
||||||
durability=durability,
|
durability=durability,
|
||||||
has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks,
|
has_graph_lifecycle_callbacks=has_graph_lifecycle_callbacks,
|
||||||
)
|
)
|
||||||
@@ -1706,22 +1710,18 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
async def aschedule_error_handler(
|
async def aschedule_error_handler(
|
||||||
self, failed_task: PregelExecutableTask, error: BaseException
|
self, failed_task: PregelExecutableTask, error: BaseException
|
||||||
) -> PregelExecutableTask | None:
|
) -> PregelExecutableTask | None:
|
||||||
handler_node = self.nodes[failed_task.name].error_handler_node
|
handler = failed_task.error_handler or self.error_handler
|
||||||
if not handler_node:
|
if handler is None:
|
||||||
return None
|
return None
|
||||||
writes = list(failed_task.writes)
|
writes = list(failed_task.writes)
|
||||||
writes.append((ERROR_SOURCE_NODE, failed_task.name))
|
writes.append((ERROR_SOURCE_NODE, failed_task.name))
|
||||||
self.put_writes(
|
self.put_writes(failed_task.id, writes)
|
||||||
failed_task.id,
|
|
||||||
writes,
|
|
||||||
)
|
|
||||||
handler_task = prepare_node_error_handler_task(
|
handler_task = prepare_node_error_handler_task(
|
||||||
failed_task,
|
failed_task,
|
||||||
handler_node_name=handler_node,
|
handler=handler,
|
||||||
failed_error=error,
|
failed_error=error,
|
||||||
checkpoint=self.checkpoint,
|
checkpoint=self.checkpoint,
|
||||||
pending_writes=self.checkpoint_pending_writes,
|
pending_writes=self.checkpoint_pending_writes,
|
||||||
processes=self.nodes,
|
|
||||||
channels=self.channels,
|
channels=self.channels,
|
||||||
managed=self.managed,
|
managed=self.managed,
|
||||||
config=failed_task.config,
|
config=failed_task.config,
|
||||||
@@ -1731,10 +1731,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
checkpointer=self.checkpointer,
|
checkpointer=self.checkpointer,
|
||||||
manager=self.manager,
|
manager=self.manager,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
|
||||||
)
|
)
|
||||||
if handler_task is None:
|
|
||||||
return None
|
|
||||||
self.tasks[handler_task.id] = handler_task
|
self.tasks[handler_task.id] = handler_task
|
||||||
if not self.is_replaying:
|
if not self.is_replaying:
|
||||||
self._match_writes({handler_task.id: handler_task})
|
self._match_writes({handler_task.id: handler_task})
|
||||||
|
|||||||
@@ -138,11 +138,8 @@ class PregelNode:
|
|||||||
metadata: Mapping[str, Any] | None
|
metadata: Mapping[str, Any] | None
|
||||||
"""Metadata to attach to the node for tracing."""
|
"""Metadata to attach to the node for tracing."""
|
||||||
|
|
||||||
is_error_handler: bool
|
error_handler: Runnable[Any, Any] | None
|
||||||
"""Whether this node is registered as an error handler node."""
|
"""Callable invoked after retries are exhausted; receives same input as the node."""
|
||||||
|
|
||||||
error_handler_node: str | None
|
|
||||||
"""Optional handler node name for failures from this node."""
|
|
||||||
|
|
||||||
subgraphs: Sequence[PregelProtocol]
|
subgraphs: Sequence[PregelProtocol]
|
||||||
"""Subgraphs used by the node."""
|
"""Subgraphs used by the node."""
|
||||||
@@ -159,8 +156,7 @@ class PregelNode:
|
|||||||
bound: Runnable[Any, Any] | None = None,
|
bound: Runnable[Any, Any] | None = None,
|
||||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: CachePolicy | None = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
is_error_handler: bool = False,
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
error_handler_node: str | None = None,
|
|
||||||
subgraphs: Sequence[PregelProtocol] | None = None,
|
subgraphs: Sequence[PregelProtocol] | None = None,
|
||||||
timeout: float | timedelta | TimeoutPolicy | None = None,
|
timeout: float | timedelta | TimeoutPolicy | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -177,8 +173,7 @@ class PregelNode:
|
|||||||
self.timeout = coerce_timeout_policy(timeout)
|
self.timeout = coerce_timeout_policy(timeout)
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.is_error_handler = is_error_handler
|
self.error_handler = error_handler
|
||||||
self.error_handler_node = error_handler_node
|
|
||||||
if subgraphs is not None:
|
if subgraphs is not None:
|
||||||
self.subgraphs = subgraphs
|
self.subgraphs = subgraphs
|
||||||
elif self.bound is not DEFAULT_BOUND:
|
elif self.bound is not DEFAULT_BOUND:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from collections.abc import (
|
|||||||
Collection,
|
Collection,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
Mapping,
|
|
||||||
Sequence,
|
Sequence,
|
||||||
)
|
)
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -143,7 +142,6 @@ class PregelRunner:
|
|||||||
put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
|
put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
|
||||||
use_astream: bool = False,
|
use_astream: bool = False,
|
||||||
node_finished: Callable[[str], None] | None = None,
|
node_finished: Callable[[str], None] | None = None,
|
||||||
node_error_handler_map: Mapping[str, str] | None = None,
|
|
||||||
schedule_error_handler: Callable[
|
schedule_error_handler: Callable[
|
||||||
[PregelExecutableTask, BaseException], PregelExecutableTask | None
|
[PregelExecutableTask, BaseException], PregelExecutableTask | None
|
||||||
]
|
]
|
||||||
@@ -158,20 +156,10 @@ class PregelRunner:
|
|||||||
self.put_writes = put_writes
|
self.put_writes = put_writes
|
||||||
self.use_astream = use_astream
|
self.use_astream = use_astream
|
||||||
self.node_finished = node_finished
|
self.node_finished = node_finished
|
||||||
self.node_error_handler_map = dict(node_error_handler_map or {})
|
|
||||||
self.error_handler_nodes = set(self.node_error_handler_map.values())
|
|
||||||
self.schedule_error_handler = schedule_error_handler
|
self.schedule_error_handler = schedule_error_handler
|
||||||
self.aschedule_error_handler = aschedule_error_handler
|
self.aschedule_error_handler = aschedule_error_handler
|
||||||
# Exception object ids that are already routed to graph-level error handler.
|
|
||||||
# These ids are consulted by stop/panic checks to avoid re-raising handled
|
|
||||||
# exceptions via the normal fatal path in the same run.
|
|
||||||
self._handled_exception_ids: set[int] = set()
|
self._handled_exception_ids: set[int] = set()
|
||||||
|
|
||||||
def _should_route_to_error_handler(self, task: PregelExecutableTask) -> bool:
|
|
||||||
if task.name in self.error_handler_nodes:
|
|
||||||
return False
|
|
||||||
return task.name in self.node_error_handler_map
|
|
||||||
|
|
||||||
def tick(
|
def tick(
|
||||||
self,
|
self,
|
||||||
tasks: Iterable[PregelExecutableTask],
|
tasks: Iterable[PregelExecutableTask],
|
||||||
@@ -222,7 +210,7 @@ class PregelRunner:
|
|||||||
self.commit(t, exc)
|
self.commit(t, exc)
|
||||||
if (
|
if (
|
||||||
not isinstance(exc, GraphBubbleUp)
|
not isinstance(exc, GraphBubbleUp)
|
||||||
and self._should_route_to_error_handler(t)
|
and t.error_handler is not None
|
||||||
and self.schedule_error_handler is not None
|
and self.schedule_error_handler is not None
|
||||||
):
|
):
|
||||||
self._handled_exception_ids.add(id(exc))
|
self._handled_exception_ids.add(id(exc))
|
||||||
@@ -295,7 +283,7 @@ class PregelRunner:
|
|||||||
futures[get_waiter()] = None
|
futures[get_waiter()] = None
|
||||||
elif (
|
elif (
|
||||||
(task_exc := _exception(fut))
|
(task_exc := _exception(fut))
|
||||||
and self._should_route_to_error_handler(task)
|
and task.error_handler is not None
|
||||||
and not isinstance(task_exc, GraphBubbleUp)
|
and not isinstance(task_exc, GraphBubbleUp)
|
||||||
):
|
):
|
||||||
self._handled_exception_ids.add(id(task_exc))
|
self._handled_exception_ids.add(id(task_exc))
|
||||||
@@ -414,7 +402,7 @@ class PregelRunner:
|
|||||||
self.commit(t, exc)
|
self.commit(t, exc)
|
||||||
if (
|
if (
|
||||||
not isinstance(exc, GraphBubbleUp)
|
not isinstance(exc, GraphBubbleUp)
|
||||||
and self._should_route_to_error_handler(t)
|
and t.error_handler is not None
|
||||||
and self.aschedule_error_handler is not None
|
and self.aschedule_error_handler is not None
|
||||||
):
|
):
|
||||||
self._handled_exception_ids.add(id(exc))
|
self._handled_exception_ids.add(id(exc))
|
||||||
@@ -494,7 +482,7 @@ class PregelRunner:
|
|||||||
futures[get_waiter()] = None
|
futures[get_waiter()] = None
|
||||||
elif (
|
elif (
|
||||||
(task_exc := _exception(fut))
|
(task_exc := _exception(fut))
|
||||||
and self._should_route_to_error_handler(task)
|
and task.error_handler is not None
|
||||||
and not isinstance(task_exc, GraphBubbleUp)
|
and not isinstance(task_exc, GraphBubbleUp)
|
||||||
):
|
):
|
||||||
self._handled_exception_ids.add(id(task_exc))
|
self._handled_exception_ids.add(id(task_exc))
|
||||||
@@ -594,7 +582,7 @@ class PregelRunner:
|
|||||||
else:
|
else:
|
||||||
# save error to checkpointer
|
# save error to checkpointer
|
||||||
task.writes.append((ERROR, exception))
|
task.writes.append((ERROR, exception))
|
||||||
if self._should_route_to_error_handler(task) and not isinstance(
|
if task.error_handler is not None and not isinstance(
|
||||||
exception, GraphBubbleUp
|
exception, GraphBubbleUp
|
||||||
):
|
):
|
||||||
# Mark early in commit path; loop-side routing may happen later.
|
# Mark early in commit path; loop-side routing may happen later.
|
||||||
|
|||||||
@@ -99,14 +99,21 @@ def validate_graph(
|
|||||||
|
|
||||||
if interrupt_after_nodes != "*":
|
if interrupt_after_nodes != "*":
|
||||||
for n in interrupt_after_nodes:
|
for n in interrupt_after_nodes:
|
||||||
if n not in nodes:
|
if n not in nodes and not _is_valid_error_handler_interrupt(n, nodes):
|
||||||
raise ValueError(f"Node {n} not in nodes")
|
raise ValueError(f"Node {n} not in nodes")
|
||||||
if interrupt_before_nodes != "*":
|
if interrupt_before_nodes != "*":
|
||||||
for n in interrupt_before_nodes:
|
for n in interrupt_before_nodes:
|
||||||
if n not in nodes:
|
if n not in nodes and not _is_valid_error_handler_interrupt(n, nodes):
|
||||||
raise ValueError(f"Node {n} not in nodes")
|
raise ValueError(f"Node {n} not in nodes")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_error_handler_interrupt(name: str, nodes: Mapping[str, PregelNode]) -> bool:
|
||||||
|
if not name.startswith("__error_handler__"):
|
||||||
|
return False
|
||||||
|
base = name[len("__error_handler__"):]
|
||||||
|
return base in nodes and nodes[base].error_handler is not None
|
||||||
|
|
||||||
|
|
||||||
def validate_keys(
|
def validate_keys(
|
||||||
keys: str | Sequence[str] | None,
|
keys: str | Sequence[str] | None,
|
||||||
channels: Mapping[str, Any],
|
channels: Mapping[str, Any],
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from uuid import UUID, uuid5
|
|||||||
from langchain_core._api import beta
|
from langchain_core._api import beta
|
||||||
from langchain_core.globals import get_debug
|
from langchain_core.globals import get_debug
|
||||||
from langchain_core.runnables import (
|
from langchain_core.runnables import (
|
||||||
|
Runnable,
|
||||||
RunnableSequence,
|
RunnableSequence,
|
||||||
)
|
)
|
||||||
from langchain_core.runnables.base import Input, Output
|
from langchain_core.runnables.base import Input, Output
|
||||||
@@ -751,7 +752,7 @@ class Pregel(
|
|||||||
name: str = "LangGraph"
|
name: str = "LangGraph"
|
||||||
|
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]]
|
trigger_to_nodes: Mapping[str, Sequence[str]]
|
||||||
node_error_handler_map: Mapping[str, str]
|
error_handler: Runnable[Any, Any] | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -776,7 +777,7 @@ class Pregel(
|
|||||||
context_schema: type[ContextT] | None = None,
|
context_schema: type[ContextT] | None = None,
|
||||||
config: RunnableConfig | None = None,
|
config: RunnableConfig | None = None,
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
||||||
node_error_handler_map: Mapping[str, str] | None = None,
|
error_handler: Runnable[Any, Any] | None = None,
|
||||||
name: str = "LangGraph",
|
name: str = "LangGraph",
|
||||||
stream_transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
|
stream_transformers: Sequence[Callable[[tuple[str, ...]], Any]] | None = None,
|
||||||
**deprecated_kwargs: Unpack[DeprecatedKwargs],
|
**deprecated_kwargs: Unpack[DeprecatedKwargs],
|
||||||
@@ -824,7 +825,7 @@ class Pregel(
|
|||||||
self.context_schema = context_schema
|
self.context_schema = context_schema
|
||||||
self.config = config
|
self.config = config
|
||||||
self.trigger_to_nodes = trigger_to_nodes or {}
|
self.trigger_to_nodes = trigger_to_nodes or {}
|
||||||
self.node_error_handler_map = node_error_handler_map or {}
|
self.error_handler = error_handler
|
||||||
self.name = name
|
self.name = name
|
||||||
self.stream_transformers: tuple[Callable[[tuple[str, ...]], Any], ...] = tuple(
|
self.stream_transformers: tuple[Callable[[tuple[str, ...]], Any], ...] = tuple(
|
||||||
stream_transformers or ()
|
stream_transformers or ()
|
||||||
@@ -2885,6 +2886,7 @@ class Pregel(
|
|||||||
migrate_checkpoint=self._migrate_checkpoint,
|
migrate_checkpoint=self._migrate_checkpoint,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
cache_policy=self.cache_policy,
|
||||||
|
error_handler=self.error_handler,
|
||||||
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
|
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
|
||||||
) as loop:
|
) as loop:
|
||||||
emit_graph_lifecycle_events(loop)
|
emit_graph_lifecycle_events(loop)
|
||||||
@@ -2895,7 +2897,6 @@ class Pregel(
|
|||||||
),
|
),
|
||||||
put_writes=weakref.WeakMethod(loop.put_writes),
|
put_writes=weakref.WeakMethod(loop.put_writes),
|
||||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||||
node_error_handler_map=self.node_error_handler_map,
|
|
||||||
schedule_error_handler=loop.schedule_error_handler,
|
schedule_error_handler=loop.schedule_error_handler,
|
||||||
)
|
)
|
||||||
# enable subgraph streaming
|
# enable subgraph streaming
|
||||||
@@ -3337,6 +3338,7 @@ class Pregel(
|
|||||||
migrate_checkpoint=self._migrate_checkpoint,
|
migrate_checkpoint=self._migrate_checkpoint,
|
||||||
retry_policy=self.retry_policy,
|
retry_policy=self.retry_policy,
|
||||||
cache_policy=self.cache_policy,
|
cache_policy=self.cache_policy,
|
||||||
|
error_handler=self.error_handler,
|
||||||
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
|
has_graph_lifecycle_callbacks=bool(graph_callback_manager.handlers),
|
||||||
) as loop:
|
) as loop:
|
||||||
await aemit_graph_lifecycle_events(loop)
|
await aemit_graph_lifecycle_events(loop)
|
||||||
@@ -3348,7 +3350,6 @@ class Pregel(
|
|||||||
put_writes=weakref.WeakMethod(loop.put_writes),
|
put_writes=weakref.WeakMethod(loop.put_writes),
|
||||||
use_astream=do_stream,
|
use_astream=do_stream,
|
||||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||||
node_error_handler_map=self.node_error_handler_map,
|
|
||||||
aschedule_error_handler=loop.aschedule_error_handler,
|
aschedule_error_handler=loop.aschedule_error_handler,
|
||||||
)
|
)
|
||||||
# enable subgraph streaming
|
# enable subgraph streaming
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ class PregelExecutableTask:
|
|||||||
writers: Sequence[Runnable] = ()
|
writers: Sequence[Runnable] = ()
|
||||||
subgraphs: Sequence[PregelProtocol] = ()
|
subgraphs: Sequence[PregelProtocol] = ()
|
||||||
timeout: TimeoutPolicy | None = None
|
timeout: TimeoutPolicy | None = None
|
||||||
|
error_handler: Runnable | None = None
|
||||||
|
|
||||||
|
|
||||||
class StateSnapshot(NamedTuple):
|
class StateSnapshot(NamedTuple):
|
||||||
|
|||||||
@@ -2280,3 +2280,139 @@ def test_node_without_error_handler_still_fails_run():
|
|||||||
|
|
||||||
with pytest.raises(ValueError, match="no handler"):
|
with pytest.raises(ValueError, match="no handler"):
|
||||||
graph.invoke({"foo": ""})
|
graph.invoke({"foo": ""})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Structural invariants from the policy-style refactor
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_not_registered_as_node():
|
||||||
|
"""After compile, no hidden __error_handler__* nodes should exist in the graph."""
|
||||||
|
|
||||||
|
class State(TypedDict):
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
def failing_node(state: State) -> State:
|
||||||
|
raise ValueError("boom")
|
||||||
|
|
||||||
|
def handler(state: State, error: NodeError) -> State:
|
||||||
|
return {"foo": "handled"}
|
||||||
|
|
||||||
|
graph = (
|
||||||
|
StateGraph(State)
|
||||||
|
.add_node("failing_node", failing_node, error_handler=handler)
|
||||||
|
.add_edge(START, "failing_node")
|
||||||
|
.compile()
|
||||||
|
)
|
||||||
|
|
||||||
|
hidden = [k for k in graph.nodes if k.startswith("__error_handler__")]
|
||||||
|
assert hidden == [], f"unexpected hidden nodes: {hidden}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_stored_on_pregel_node():
|
||||||
|
"""The error_handler callable should be a Runnable field on PregelNode, not a name pointer."""
|
||||||
|
|
||||||
|
class State(TypedDict):
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
def failing_node(state: State) -> State:
|
||||||
|
raise ValueError("boom")
|
||||||
|
|
||||||
|
def handler(state: State) -> State:
|
||||||
|
return {"foo": "handled"}
|
||||||
|
|
||||||
|
graph = (
|
||||||
|
StateGraph(State)
|
||||||
|
.add_node("failing_node", failing_node, error_handler=handler)
|
||||||
|
.add_edge(START, "failing_node")
|
||||||
|
.compile()
|
||||||
|
)
|
||||||
|
|
||||||
|
pregel_node = graph.nodes["failing_node"]
|
||||||
|
assert pregel_node.error_handler is not None, "error_handler should be set on PregelNode"
|
||||||
|
assert not hasattr(pregel_node, "error_handler_node"), "old string-pointer field should be gone"
|
||||||
|
assert not hasattr(pregel_node, "is_error_handler"), "is_error_handler flag should be gone"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_handler_dispatched_from_task_field():
|
||||||
|
"""error_handler on PregelExecutableTask drives dispatch — no node-map lookup needed."""
|
||||||
|
|
||||||
|
class State(TypedDict):
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
def failing_node(state: State) -> State:
|
||||||
|
raise ValueError("boom")
|
||||||
|
|
||||||
|
def handler(state: State) -> State:
|
||||||
|
return {"foo": "handled"}
|
||||||
|
|
||||||
|
graph = (
|
||||||
|
StateGraph(State)
|
||||||
|
.add_node("failing_node", failing_node, error_handler=handler)
|
||||||
|
.add_edge(START, "failing_node")
|
||||||
|
.compile()
|
||||||
|
)
|
||||||
|
result = graph.invoke({"foo": ""})
|
||||||
|
assert result["foo"] == "handled"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Graph-level error handler
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_graph_level_error_handler_used_when_no_per_node_handler():
|
||||||
|
"""compile(error_handler=fallback) should catch failures from nodes without their own handler."""
|
||||||
|
|
||||||
|
class State(TypedDict):
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
def failing_node(state: State) -> State:
|
||||||
|
raise RuntimeError("node failed")
|
||||||
|
|
||||||
|
def graph_handler(state: State, error: NodeError) -> State:
|
||||||
|
return {"foo": f"graph_handler_caught:{error.node}"}
|
||||||
|
|
||||||
|
graph = (
|
||||||
|
StateGraph(State)
|
||||||
|
.add_node("failing_node", failing_node)
|
||||||
|
.add_edge(START, "failing_node")
|
||||||
|
.compile(error_handler=graph_handler)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = graph.invoke({"foo": ""})
|
||||||
|
assert result["foo"] == "graph_handler_caught:failing_node"
|
||||||
|
|
||||||
|
|
||||||
|
def test_per_node_handler_takes_precedence_over_graph_level():
|
||||||
|
"""When a node has its own error_handler, it should win over the graph-level fallback."""
|
||||||
|
|
||||||
|
class State(TypedDict):
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
def failing_node(state: State) -> State:
|
||||||
|
raise RuntimeError("node failed")
|
||||||
|
|
||||||
|
def node_handler(state: State, error: NodeError) -> State:
|
||||||
|
return {"foo": "node_handler"}
|
||||||
|
|
||||||
|
def graph_handler(state: State, error: NodeError) -> State:
|
||||||
|
return {"foo": "graph_handler"}
|
||||||
|
|
||||||
|
graph = (
|
||||||
|
StateGraph(State)
|
||||||
|
.add_node("failing_node", failing_node, error_handler=node_handler)
|
||||||
|
.add_edge(START, "failing_node")
|
||||||
|
.compile(error_handler=graph_handler)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = graph.invoke({"foo": ""})
|
||||||
|
assert result["foo"] == "node_handler"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Functional API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user