diff --git a/libs/langgraph/langgraph/channels/any_value.py b/libs/langgraph/langgraph/channels/any_value.py index e9dfb77d6..35452084f 100644 --- a/libs/langgraph/langgraph/channels/any_value.py +++ b/libs/langgraph/langgraph/channels/any_value.py @@ -1,8 +1,9 @@ -from typing import Generic, Optional, Sequence, Type +from typing import Any, Generic, Optional, Sequence, Type from typing_extensions import Self from langgraph.channels.base import BaseChannel, Value +from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError @@ -12,6 +13,10 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): __slots__ = ("typ", "value") + def __init__(self, typ: Any, key: str = "") -> None: + super().__init__(typ, key) + self.value = MISSING + def __eq__(self, value: object) -> bool: return isinstance(value, AnyValue) @@ -34,17 +39,19 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): def update(self, values: Sequence[Value]) -> bool: if len(values) == 0: - try: - del self.value - return True - except AttributeError: + if self.value is MISSING: return False + else: + self.value = MISSING + return True self.value = values[-1] return True def get(self) -> Value: - try: - return self.value - except AttributeError: + if self.value is MISSING: raise EmptyChannelError() + return self.value + + def is_available(self) -> bool: + return self.value is not MISSING diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index c4b49c650..b9239be7a 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -3,7 +3,6 @@ from typing import Any, Generic, Optional, Sequence, TypeVar from typing_extensions import Self -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError, InvalidUpdateError Value = TypeVar("Value") @@ -65,13 +64,16 @@ class BaseChannel(Generic[Value, Update, C], ABC): """ return False - def get_catch(self) -> Value: - """Return the current value of the channel, or MISSING if the channel - is empty. Subclasses can override to skip the EmptyChannelError check.""" + def is_available(self) -> bool: + """Return True if the channel is available (not empty), False otherwise. + Subclasses should override this method to provide a more efficient + implementation than calling get() and catching EmptyChannelError. + """ try: - return self.get() + self.get() + return True except EmptyChannelError: - return MISSING + return False __all__ = [ diff --git a/libs/langgraph/langgraph/channels/binop.py b/libs/langgraph/langgraph/channels/binop.py index a2360142b..413e0b91a 100644 --- a/libs/langgraph/langgraph/channels/binop.py +++ b/libs/langgraph/langgraph/channels/binop.py @@ -10,6 +10,7 @@ from typing import ( from typing_extensions import NotRequired, Required, Self from langgraph.channels.base import BaseChannel, Value +from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError @@ -51,7 +52,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): try: self.value = typ() except Exception: - pass + self.value = MISSING def __eq__(self, value: object) -> bool: return isinstance(value, BinaryOperatorAggregate) and ( @@ -81,7 +82,7 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): def update(self, values: Sequence[Value]) -> bool: if not values: return False - if not hasattr(self, "value"): + if self.value is MISSING: self.value = values[0] values = values[1:] for value in values: @@ -89,7 +90,9 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): return True def get(self) -> Value: - try: - return self.value - except AttributeError: + if self.value is MISSING: raise EmptyChannelError() + return self.value + + def is_available(self) -> bool: + return self.value is not MISSING diff --git a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py index f64191e86..155c65446 100644 --- a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py +++ b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py @@ -85,6 +85,9 @@ class DynamicBarrierValue( raise EmptyChannelError() return None + def is_available(self) -> bool: + return self.seen == self.names + def consume(self) -> bool: if self.seen == self.names: self.seen = set() diff --git a/libs/langgraph/langgraph/channels/ephemeral_value.py b/libs/langgraph/langgraph/channels/ephemeral_value.py index 4a64c6b32..29a9a698c 100644 --- a/libs/langgraph/langgraph/channels/ephemeral_value.py +++ b/libs/langgraph/langgraph/channels/ephemeral_value.py @@ -57,5 +57,5 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): raise EmptyChannelError() return self.value - def get_catch(self) -> Value: - return self.value + def is_available(self) -> bool: + return self.value is not MISSING diff --git a/libs/langgraph/langgraph/channels/last_value.py b/libs/langgraph/langgraph/channels/last_value.py index 13fac983e..61669d390 100644 --- a/libs/langgraph/langgraph/channels/last_value.py +++ b/libs/langgraph/langgraph/channels/last_value.py @@ -59,5 +59,5 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): raise EmptyChannelError() return self.value - def get_catch(self) -> Value: - return self.value + def is_available(self) -> bool: + return self.value is not MISSING diff --git a/libs/langgraph/langgraph/channels/named_barrier_value.py b/libs/langgraph/langgraph/channels/named_barrier_value.py index 4a1d990ca..553316e19 100644 --- a/libs/langgraph/langgraph/channels/named_barrier_value.py +++ b/libs/langgraph/langgraph/channels/named_barrier_value.py @@ -60,6 +60,9 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]): raise EmptyChannelError() return None + def is_available(self) -> bool: + return self.seen == self.names + def consume(self) -> bool: if self.seen == self.names: self.seen = set() diff --git a/libs/langgraph/langgraph/channels/topic.py b/libs/langgraph/langgraph/channels/topic.py index 0430343dc..91e7027f9 100644 --- a/libs/langgraph/langgraph/channels/topic.py +++ b/libs/langgraph/langgraph/channels/topic.py @@ -75,3 +75,6 @@ class Topic( return list(self.values) else: raise EmptyChannelError + + def is_available(self) -> bool: + return bool(self.values) diff --git a/libs/langgraph/langgraph/channels/untracked_value.py b/libs/langgraph/langgraph/channels/untracked_value.py index 2560d61a3..f9168131e 100644 --- a/libs/langgraph/langgraph/channels/untracked_value.py +++ b/libs/langgraph/langgraph/channels/untracked_value.py @@ -54,5 +54,5 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]): raise EmptyChannelError() return self.value - def get_catch(self) -> Value: - return self.value + def is_available(self) -> bool: + return self.value is not MISSING diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index b2bcdcedf..e8fd76a1e 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -48,7 +48,6 @@ from langgraph.constants import ( EMPTY_SEQ, ERROR, INTERRUPT, - MISSING, NO_WRITES, NS_END, NS_SEP, @@ -650,7 +649,7 @@ def prepare_single_task( if triggers := sorted( chan for chan in proc.triggers - if channels[chan].get_catch() is not MISSING + if channels[chan].is_available() and checkpoint["channel_versions"].get(chan, null_version) # type: ignore[operator] > seen.get(chan, null_version) ):