Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
transformers.py929 linesDownload Raw Back to stream
1from __future__ import annotations2 3import logging4from typing import TYPE_CHECKING, Any, Literal, cast5 6from langchain_core.language_models._compat_bridge import message_to_events7from langchain_core.language_models.chat_model_stream import (8    AsyncChatModelStream,9    ChatModelStream,10)11from langchain_core.messages import AIMessageChunk, BaseMessage12from langchain_protocol.protocol import MessagesData13from typing_extensions import NotRequired, TypedDict14 15from langgraph.errors import GraphDrained, GraphInterrupt16from langgraph.stream._types import ProtocolEvent, StreamTransformer17from langgraph.stream.run_stream import AsyncSubgraphRunStream, SubgraphRunStream18from langgraph.stream.stream_channel import StreamChannel19 20if TYPE_CHECKING:21    from collections.abc import Awaitable, Callable22 23    from langgraph.stream._mux import StreamMux24 25_logger = logging.getLogger(__name__)26 27 28class ValuesTransformer(StreamTransformer):29    """Capture values events as a drainable stream of state snapshots.30 31    Provides the `run.values` projection. `run.output`,32    `run.interrupted` and `run.interrupts` are tracked directly33    by the run stream and do not depend on this transformer.34 35    Native transformer — projection keys are exposed as direct36    attributes on the run stream (e.g. `run.values`).37 38    Only values events at the run's own level are captured; snapshots39    from deeper subgraphs are left in the main event log but excluded40    from the projection. "Own level" is defined by `scope`, which41    `stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's42    checkpoint namespace so that a nested `stream_events(version="v3")` call still43    sees its own root snapshots.44    """45 46    _native = True47    required_stream_modes = ("values",)48 49    def __init__(self, scope: tuple[str, ...] = ()) -> None:50        super().__init__(scope)51        self._log: StreamChannel[dict[str, Any]] = StreamChannel()52        self._latest: dict[str, Any] | None = None53        self._interrupted = False54        self._interrupts: list[Any] = []55        # Cached as a list once for cheap equality with the protocol56        # event's `namespace` field, which is `list[str]`.57        self._scope_list: list[str] = list(scope)58 59    def init(self) -> dict[str, Any]:60        return {"values": self._log}61 62    @property63    def error(self) -> BaseException | None:64        """The error that ended the run, or `None` if it succeeded.65 66        Set by the mux when it auto-fails the projection log.67        """68        return self._log._error69 70    def process(self, event: ProtocolEvent) -> bool:71        if event["method"] != "values":72            return True73        params = event["params"]74        if params["namespace"] != self._scope_list:75            return True76        self._latest = params["data"]77        interrupts = params.get("interrupts", ())78        if interrupts:79            self._interrupted = True80            self._interrupts.extend(interrupts)81        self._log.push(params["data"])82        return True83 84 85class CustomTransformer(StreamTransformer):86    """Capture custom events as a drainable stream of arbitrary payloads.87 88    Nodes emit custom data via `get_stream_writer()`. This transformer89    surfaces those events on `run.custom` as a `StreamChannel[Any]`,90    preserving payloads in arrival order.91 92    Only events at the run's own scope are captured; custom data from93    deeper subgraphs is available on the respective subgraph handle's94    `.custom` projection.95 96    Native transformer — `run.custom` is a direct attribute.97    """98 99    _native = True100    required_stream_modes = ("custom",)101 102    def __init__(self, scope: tuple[str, ...] = ()) -> None:103        super().__init__(scope)104        self._log: StreamChannel[Any] = StreamChannel()105        self._scope_list: list[str] = list(scope)106 107    def init(self) -> dict[str, Any]:108        return {"custom": self._log}109 110    def process(self, event: ProtocolEvent) -> bool:111        if event["method"] != "custom":112            return True113        params = event["params"]114        if params["namespace"] != self._scope_list:115            return True116        self._log.push(params["data"])117        return True118 119 120class UpdatesTransformer(StreamTransformer):121    """Capture updates events as a drainable stream of node outputs.122 123    Surfaces `stream_mode="updates"` data on `run.updates` as a124    `StreamChannel[dict[str, Any]]`. Each item is a dict mapping a node125    (or task) name to the update it returned after a step.126 127    Only events at the run's own scope are captured; updates from deeper128    subgraphs are available on the respective subgraph handle's129    `.updates` projection.130 131    Native transformer — `run.updates` is a direct attribute.132    """133 134    _native = True135    required_stream_modes = ("updates",)136 137    def __init__(self, scope: tuple[str, ...] = ()) -> None:138        super().__init__(scope)139        self._log: StreamChannel[dict[str, Any]] = StreamChannel()140        self._scope_list: list[str] = list(scope)141 142    def init(self) -> dict[str, Any]:143        return {"updates": self._log}144 145    def process(self, event: ProtocolEvent) -> bool:146        if event["method"] != "updates":147            return True148        params = event["params"]149        if params["namespace"] != self._scope_list:150            return True151        self._log.push(params["data"])152        return True153 154 155class MessagesTransformer(StreamTransformer):156    """Capture messages events as ChatModelStream objects.157 158    The messages projection yields one `ChatModelStream` (or159    `AsyncChatModelStream`) per LLM call. Consumers iterate160    `run.messages` to get stream handles, then use each handle's typed161    projections (`.text`, `.reasoning`, `.tool_calls`, `.usage`,162    `.output`) for per-message content.163 164    Two input shapes are handled (via `params["data"] = (payload,165    metadata)` from `StreamMessagesHandler`):166 167    1. Protocol event (dict with `"event"` key) — emitted by168       `stream_events(version="v3")` / `astream_events(version="v3")` via the `on_stream_event`169       callback. Routed to an existing `ChatModelStream` by170       `metadata["run_id"]`. A `message-start` event creates a new171       stream; `message-finish` closes it.172    2. Whole `AIMessage` — emitted from `on_chain_end` when a node173       returns a finalized message. Replayed as a synthetic protocol174       event lifecycle via `message_to_events`, then the175       already-complete stream is pushed to the log.176 177    V1 `AIMessageChunk` tuples (from `on_llm_new_token`) are not178    streamed into this projection: chat models that want to populate179    `run.messages` with content-block streaming must use180    `stream_events(version="v3")` / `astream_events(version="v3")`. Models called via the legacy181    `stream()` method still surface their final `AIMessage` via182    `on_chain_end` when a node returns it as state.183 184    Only events at the run's own level are projected; tokens from185    deeper subgraphs are left in the main event log but excluded from186    `.messages`. "Own level" is defined by `scope`, which187    `stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's checkpoint188    namespace so that a `stream_events(version="v3")` call inside a node still sees its189    own root chat model streams on `.messages`. Consumers that need190    subgraph tokens should iterate the raw event stream or register a191    custom transformer.192 193    Native transformer — the `messages` projection is exposed as a194    direct attribute on the run stream.195    """196 197    _native = True198    required_stream_modes = ("messages",)199 200    def __init__(self, scope: tuple[str, ...] = ()) -> None:201        super().__init__(scope)202        self._log: StreamChannel[ChatModelStream] = StreamChannel()203        # Correlate protocol events back to a ChatModelStream by run_id204        # (attached to the event's metadata by StreamMessagesHandler).205        self._by_run: dict[str, ChatModelStream] = {}206        self._pump_fn: Callable[[], bool] | None = None207        self._apump_fn: Callable[[], Awaitable[bool]] | None = None208        # Cached as a list once for cheap equality with the protocol209        # event's `namespace` field, which is `list[str]`.210        self._scope_list: list[str] = list(scope)211 212    def init(self) -> dict[str, Any]:213        return {"messages": self._log}214 215    def _bind_pump(self, fn: Callable[[], bool]) -> None:216        """Wire the sync pull callback. Called by GraphRunStream._wire_request_more."""217        self._pump_fn = fn218 219    def _bind_apump(self, fn: Callable[[], Awaitable[bool]]) -> None:220        """Wire the async pull callback.221 222        Called by `AsyncGraphRunStream._wire_arequest_more` so each223        `AsyncChatModelStream` this transformer creates can drive the224        shared graph pump from its projection cursors.225        """226        self._apump_fn = fn227 228    def _make_stream(229        self,230        *,231        namespace: list[str],232        node: str | None,233        message_id: str | None,234    ) -> ChatModelStream:235        """Create a ChatModelStream (sync) or AsyncChatModelStream (async).236 237        Wires whichever pump is bound. Prefers the async pump so nested238        iteration under `AsyncGraphRunStream` drives the graph forward239        without a background task. The unwired fallback (no pump bound)240        is used by unit tests that dispatch events manually.241        """242        if self._apump_fn is not None:243            astream = AsyncChatModelStream(244                namespace=namespace,245                node=node,246                message_id=message_id,247            )248            astream.set_arequest_more(self._apump_fn)249            return astream250        if self._pump_fn is not None:251            stream: ChatModelStream = ChatModelStream(252                namespace=namespace,253                node=node,254                message_id=message_id,255            )256            stream.set_request_more(self._pump_fn)257            return stream258        return AsyncChatModelStream(259            namespace=namespace,260            node=node,261            message_id=message_id,262        )263 264    def process(self, event: ProtocolEvent) -> bool:265        if event["method"] != "messages":266            return True267        params = event["params"]268        if params["namespace"] != self._scope_list:269            return True270 271        payload, metadata = params["data"]272        node: str | None = metadata.get("langgraph_node")273        run_id = str(metadata.get("run_id", "")) if metadata else ""274 275        if isinstance(payload, dict) and "event" in payload:276            self._route_protocol_event(277                cast("MessagesData", payload), run_id=run_id, node=node278            )279        elif isinstance(payload, BaseMessage) and not isinstance(280            payload, AIMessageChunk281        ):282            self._route_whole_message(payload, node=node)283        # Legacy AIMessageChunk tuples (from on_llm_new_token) are ignored;284        # v1 streaming callers must switch to stream_events(version="v3") to populate this285        # projection.286 287        return True288 289    def _route_protocol_event(290        self,291        event: MessagesData,292        *,293        run_id: str,294        node: str | None,295    ) -> None:296        event_type = event.get("event")297        if event_type == "message-start":298            message_id = event.get("message_id")299            stream = self._make_stream(300                namespace=[],301                node=node,302                message_id=str(message_id) if message_id is not None else None,303            )304            self._by_run[run_id] = stream305            self._log.push(stream)306            stream.dispatch(event)307        elif run_id in self._by_run:308            stream = self._by_run[run_id]309            stream.dispatch(event)310            if event_type == "message-finish":311                del self._by_run[run_id]312 313    def _route_whole_message(self, message: BaseMessage, *, node: str | None) -> None:314        stream = self._make_stream(namespace=[], node=node, message_id=message.id)315        for evt in message_to_events(message, message_id=message.id):316            stream.dispatch(evt)317        self._log.push(stream)318 319    def finalize(self) -> None:320        """Clear any routing state — streams close themselves via `message-finish`."""321        self._by_run.clear()322 323    def fail(self, err: BaseException) -> None:324        """Propagate run error to any streams still open when the graph fails."""325        for stream in list(self._by_run.values()):326            stream.fail(err)327        self._by_run.clear()328 329 330SubgraphStatus = Literal["started", "completed", "failed", "interrupted", "drained"]331 332 333def _parse_ns_segment(segment: str) -> tuple[str, str | None]:334    """Split a namespace segment into `(graph_name, trigger_call_id)`.335 336    Segments are formatted `node_name:task_id` by `prepare_next_tasks`.337    Returns `(segment, None)` if no `:` is present.338    """339    name, sep, task_id = segment.partition(":")340    return name, task_id if sep else None341 342 343class LifecyclePayload(TypedDict, total=False):344    """Payload of a lifecycle event surfaced on the `lifecycle` channel.345 346    Auto-forwarded as `lifecycle` protocol events (no `custom:` prefix347    because `LifecycleTransformer` is a native transformer) so remote348    SDK clients receive the same data in-process consumers see via349    `run.lifecycle`.350    """351 352    event: SubgraphStatus353    namespace: list[str]354    graph_name: NotRequired[str]355    trigger_call_id: NotRequired[str]356    error: NotRequired[str]357 358 359class _TasksLifecycleBase(StreamTransformer):360    """Shared bookkeeping for `tasks`-event-driven lifecycle inference.361 362    Both `LifecycleTransformer` (wire-serializable channel) and363    `SubgraphTransformer` (in-process navigation handles) discover364    subgraphs by watching the same `tasks` stream — `started` on the365    first event at a tracked namespace, terminal status when the366    parent's `TaskResultPayload` arrives. Centralizing the dispatch367    + open-set bookkeeping here keeps the inference rules from368    drifting between the two surfaces.369 370    Subclasses provide three template-method hooks:371 372    - `_should_track(ns)` — scope filter (e.g. multi-depth vs373      direct-children-only).374    - `_on_started(ns, graph_name, trigger_call_id)` — first sighting375      action (push payload / build handle / etc.). Called once per376      discovered namespace.377    - `_on_terminal(ns, status, error)` — terminal action (push378      terminal payload / mark handle status). Called once per379      tracked namespace at result time, or via `finalize` / `fail`380      sweeps if no parent result arrived.381 382    Tasks events are suppressed from the main event log (`process`383    returns False) — they're folded into whichever projection the384    subclass populates; consumers iterating the raw protocol stream385    see the higher-level view.386    """387 388    required_stream_modes = ("tasks",)389 390    def __init__(self, scope: tuple[str, ...] = ()) -> None:391        super().__init__(scope)392        self._seen: set[tuple[str, ...]] = set()393        # Maps tracked namespace -> task_id of the parent task whose394        # `TaskResultPayload` will close it.395        self._open: dict[tuple[str, ...], str] = {}396 397    # --- Template-method hooks (subclass overrides) ---398 399    def _should_track(self, ns: tuple[str, ...]) -> bool:400        """Scope filter — return True iff `ns` is in this transformer's region."""401        raise NotImplementedError402 403    def _on_started(404        self,405        ns: tuple[str, ...],406        graph_name: str | None,407        trigger_call_id: str | None,408    ) -> None:409        """Fired once per discovered namespace (first observed task event)."""410        raise NotImplementedError411 412    def _on_terminal(413        self,414        ns: tuple[str, ...],415        status: SubgraphStatus,416        error: str | None,417    ) -> None:418        """Fired once per tracked namespace when its parent's result arrives,419        or via finalize/fail safety-net sweeps.420        """421        raise NotImplementedError422 423    # --- Dispatch + bookkeeping (shared) ---424 425    def process(self, event: ProtocolEvent) -> bool:426        if event["method"] != "tasks":427            return True428        ns = tuple(event["params"]["namespace"])429        data = event["params"]["data"]430        if "result" in data:431            self._handle_task_result(ns, data)432        else:433            self._handle_task_start(ns)434        # Tasks events are folded into the synthesized projections;435        # suppress from the main event log so iterators don't double-see436        # the same information in two shapes.437        return False438 439    def _handle_task_start(self, ns: tuple[str, ...]) -> None:440        if not self._should_track(ns) or ns in self._seen:441            return442        self._seen.add(ns)443        graph_name, trigger_call_id = _parse_ns_segment(ns[-1])444        self._on_started(ns, graph_name or None, trigger_call_id)445        if trigger_call_id is not None:446            self._open[ns] = trigger_call_id447 448    def _pop_terminal_transitions(449        self, ns: tuple[str, ...], data: dict[str, Any]450    ) -> list[tuple[tuple[str, ...], SubgraphStatus, str | None]]:451        """Return and remove tracked children closed by this task result."""452        result_id = data.get("id")453        if not result_id:454            return []455        transitions: list[tuple[tuple[str, ...], SubgraphStatus, str | None]] = []456        for child_ns, parent_task_id in list(self._open.items()):457            if child_ns[:-1] != ns or parent_task_id != result_id:458                continue459            status, error = _terminal_from_result(data)460            transitions.append((child_ns, status, error))461            del self._open[child_ns]462        return transitions463 464    def _handle_task_result(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:465        for child_ns, status, error in self._pop_terminal_transitions(ns, data):466            self._on_terminal(child_ns, status, error)467 468    def finalize(self) -> None:469        """Emit `completed` for any tracked namespace still open at run end."""470        for ns in list(self._open):471            self._on_terminal(ns, "completed", None)472        self._open.clear()473 474    def fail(self, err: BaseException) -> None:475        """Emit terminal status for any tracked namespace still open."""476        status, error_str = _status_from_exception(err)477        for ns in list(self._open):478            self._on_terminal(ns, status, error_str)479        self._open.clear()480 481 482def _status_from_exception(err: BaseException) -> tuple[SubgraphStatus, str | None]:483    """Map a run exception to a subgraph terminal status and error string."""484    if isinstance(err, GraphDrained):485        return "drained", None486    if isinstance(err, GraphInterrupt):487        return "interrupted", None488    return "failed", str(err)489 490 491def _terminal_from_result(492    payload: dict[str, Any],493) -> tuple[SubgraphStatus, str | None]:494    """Map a `TaskResultPayload` to a `(status, error)` pair.495 496    Order matters: a result with both `error` and `interrupts` prefers497    the interrupt classification, since `GraphInterrupt` manifests as498    a populated `interrupts` list, not as `error`.499    """500    if payload.get("interrupts"):501        return "interrupted", None502    error = payload.get("error")503    if error:504        return "failed", str(error)505    return "completed", None506 507 508class LifecycleTransformer(_TasksLifecycleBase):509    """Surface subgraph lifecycle as `lifecycle` protocol events.510 511    Pushes `LifecyclePayload` to a `StreamChannel` named `lifecycle`.512    The channel is auto-forwarded by the mux so payloads land in the513    main event log under `method = "lifecycle"` (native transformer —514    no `custom:` prefix) — visible to remote SDK clients over the515    wire and to in-process consumers via `run.lifecycle`.516 517    Tracks subgraphs at every depth strictly below the transformer's518    scope, so a graph → subgraph → subgraph chain produces lifecycle519    events for both nested levels in a flat stream.520 521    Native transformer — projection key `lifecycle` is exposed as522    `run.lifecycle`.523    """524 525    _native = True526 527    def __init__(self, scope: tuple[str, ...] = ()) -> None:528        super().__init__(scope)529        self._channel: StreamChannel[LifecyclePayload] = StreamChannel("lifecycle")530 531    def init(self) -> dict[str, Any]:532        return {"lifecycle": self._channel}533 534    def _should_track(self, ns: tuple[str, ...]) -> bool:535        depth = len(self.scope)536        return len(ns) > depth and ns[:depth] == self.scope537 538    def _on_started(539        self,540        ns: tuple[str, ...],541        graph_name: str | None,542        trigger_call_id: str | None,543    ) -> None:544        if trigger_call_id is None:545            # Without a task id we can't correlate a parent-result546            # event back to this namespace — skip the started payload547            # and rely on finalize/fail to close.548            return549        payload: LifecyclePayload = {"event": "started", "namespace": list(ns)}550        if graph_name:551            payload["graph_name"] = graph_name552        payload["trigger_call_id"] = trigger_call_id553        self._channel.push(payload)554 555    def _on_terminal(556        self,557        ns: tuple[str, ...],558        status: SubgraphStatus,559        error: str | None,560    ) -> None:561        payload: LifecyclePayload = {"event": status, "namespace": list(ns)}562        if error is not None:563            payload["error"] = error564        self._channel.push(payload)565 566 567class SubgraphTransformer(_TasksLifecycleBase):568    """Discover subgraph invocations as in-process navigation handles.569 570    Per discovered direct-child subgraph, builds a `SubgraphRunStream`571    (or `AsyncSubgraphRunStream`) wrapping a child mini-mux scoped to572    the subgraph's namespace. Consumers iterate `run.subgraphs` to573    receive handles, then drill into `handle.values` / `handle.messages`574    / `handle.subgraphs` (recursive grandchildren) / `handle.lifecycle`.575 576    Each mini-mux owns its own scope and uses its own577    `SubgraphTransformer` to discover its direct children, so578    grandchildren live on the child handle — never on the root's579    `subgraphs` log. Forwarding events into the matching child mini-mux580    is what keeps the child's projections populated.581 582    Native transformer — `subgraphs` is exposed as `run.subgraphs`.583    """584 585    _native = True586    supports_sync = True587 588    def __init__(self, scope: tuple[str, ...] = ()) -> None:589        super().__init__(scope)590        self._log: StreamChannel[SubgraphRunStream | AsyncSubgraphRunStream] = (591            StreamChannel()592        )593        self._handles: dict[594            tuple[str, ...], SubgraphRunStream | AsyncSubgraphRunStream595        ] = {}596        self._mux: StreamMux | None = None597 598    def init(self) -> dict[str, Any]:599        return {"subgraphs": self._log}600 601    def _on_register(self, mux: Any) -> None:602        self._mux = mux603 604    def _should_track(self, ns: tuple[str, ...]) -> bool:605        # Direct children only — grandchildren are picked up by the606        # child mini-mux's own SubgraphTransformer.607        depth = len(self.scope)608        return len(ns) == depth + 1 and ns[:depth] == self.scope609 610    def _on_started(611        self,612        ns: tuple[str, ...],613        graph_name: str | None,614        trigger_call_id: str | None,615    ) -> None:616        if self._mux is None:617            return618        try:619            child_mux = self._mux._make_child(ns)620        except RuntimeError:621            return622        handle_cls = AsyncSubgraphRunStream if child_mux.is_async else SubgraphRunStream623        handle = handle_cls(624            mux=child_mux,625            path=ns,626            graph_name=graph_name,627            trigger_call_id=trigger_call_id,628        )629        self._handles[ns] = handle630        self._log.push(handle)631 632    def _on_terminal(633        self,634        ns: tuple[str, ...],635        status: SubgraphStatus,636        error: str | None,637    ) -> None:638        handle = self._handles.get(ns)639        if handle is None or not self._mark_terminal(handle, status, error):640            return641        self._close_or_fail_handle(handle, status, error)642 643    async def _aon_terminal(644        self,645        ns: tuple[str, ...],646        status: SubgraphStatus,647        error: str | None,648    ) -> None:649        handle = self._handles.get(ns)650        if handle is None or not self._mark_terminal(handle, status, error):651            return652        await self._aclose_or_fail_handle(handle, status, error)653 654    def _mark_terminal(655        self,656        handle: SubgraphRunStream | AsyncSubgraphRunStream,657        status: SubgraphStatus,658        error: str | None,659    ) -> bool:660        """Mark a handle terminal once. Returns True on first transition."""661        if handle._seen_terminal:662            return False663        handle.status = status664        if error is not None and handle.error is None:665            handle.error = error666        handle._seen_terminal = True667        return True668 669    def _close_or_fail_handle(670        self,671        handle: SubgraphRunStream | AsyncSubgraphRunStream,672        status: SubgraphStatus,673        error: str | None,674    ) -> None:675        if handle._mux is None or handle._mux._events._closed:676            return677        if status == "failed":678            handle._mux.fail(RuntimeError(error or "Subgraph failed"))679        else:680            handle._mux.close()681 682    async def _aclose_or_fail_handle(683        self,684        handle: SubgraphRunStream | AsyncSubgraphRunStream,685        status: SubgraphStatus,686        error: str | None,687    ) -> None:688        if handle._mux is None or handle._mux._events._closed:689            return690        if status == "failed":691            await handle._mux.afail(RuntimeError(error or "Subgraph failed"))692        else:693            await handle._mux.aclose()694 695    def _handle_for_event(696        self, event: ProtocolEvent697    ) -> SubgraphRunStream | AsyncSubgraphRunStream | None:698        ns = tuple(event["params"]["namespace"])699        depth = len(self.scope)700        if len(ns) < depth + 1:701            return None702        handle = self._handles.get(ns[: depth + 1])703        if handle is None or handle._mux is None or handle._mux._events._closed:704            return None705        return handle706 707    def process(self, event: ProtocolEvent) -> bool:708        # Run tasks bookkeeping first so a `started` handle exists709        # by the time we forward the event to the child mini-mux.710        keep = super().process(event)711        handle = self._handle_for_event(event)712        if handle is not None:713            handle._observe_event(event)714            handle._mux.push(event)715        return keep716 717    async def aprocess(self, event: ProtocolEvent) -> bool:718        # Async counterpart: repeats the tasks bookkeeping here so719        # child mini-muxes receive events through their async lane.720        if event["method"] == "tasks":721            ns = tuple(event["params"]["namespace"])722            data = event["params"]["data"]723            if "result" in data:724                for child_ns, status, error in self._pop_terminal_transitions(ns, data):725                    await self._aon_terminal(child_ns, status, error)726            else:727                self._handle_task_start(ns)728            keep = False729        else:730            keep = True731        handle = self._handle_for_event(event)732        if handle is not None:733            handle._observe_event(event)734            await handle._mux.apush(event)735        return keep736 737    def _complete_open_handles(self) -> BaseException | None:738        first_error: BaseException | None = None739        for ns in list(self._open):740            try:741                self._on_terminal(ns, "completed", None)742            except BaseException as e:743                if first_error is None:744                    first_error = e745        self._open.clear()746        for handle in self._handles.values():747            if self._mark_terminal(handle, "completed", None):748                try:749                    self._close_or_fail_handle(handle, "completed", None)750                except BaseException as e:751                    if first_error is None:752                        first_error = e753        return first_error754 755    async def _acomplete_open_handles(self) -> BaseException | None:756        first_error: BaseException | None = None757        for ns in list(self._open):758            try:759                await self._aon_terminal(ns, "completed", None)760            except BaseException as e:761                if first_error is None:762                    first_error = e763        self._open.clear()764        for handle in self._handles.values():765            if self._mark_terminal(handle, "completed", None):766                try:767                    await self._aclose_or_fail_handle(handle, "completed", None)768                except BaseException as e:769                    if first_error is None:770                        first_error = e771        return first_error772 773    def finalize(self) -> None:774        first_error = self._complete_open_handles()775        if first_error is not None:776            raise first_error777 778    async def afinalize(self) -> None:779        first_error = await self._acomplete_open_handles()780        if first_error is not None:781            raise first_error782 783    def fail(self, err: BaseException) -> None:784        status, error_str = _status_from_exception(err)785        self._open.clear()786        for handle in self._handles.values():787            self._mark_terminal(handle, status, error_str)788            if handle._mux is not None and not handle._mux._events._closed:789                try:790                    handle._mux.fail(err)791                except Exception:792                    _logger.warning(793                        "Error failing subgraph mini-mux at %s; "794                        "subscribers may not see the terminal error.",795                        handle.path,796                        exc_info=True,797                    )798 799    async def afail(self, err: BaseException) -> None:800        status, error_str = _status_from_exception(err)801        self._open.clear()802        for handle in self._handles.values():803            self._mark_terminal(handle, status, error_str)804            if handle._mux is not None and not handle._mux._events._closed:805                try:806                    await handle._mux.afail(err)807                except Exception:808                    _logger.warning(809                        "Error failing subgraph mini-mux at %s; "810                        "subscribers may not see the terminal error.",811                        handle.path,812                        exc_info=True,813                    )814 815 816class CheckpointsTransformer(StreamTransformer):817    """Capture checkpoint events as a drainable stream.818 819    Surfaces `stream_mode="checkpoints"` data on `run.checkpoints` as820    a `StreamChannel[dict[str, Any]]`. Each item is in the same format821    as returned by `get_state()`.822 823    Checkpoint events are only emitted when a checkpointer is configured824    on the graph. When no checkpointer is present, the projection exists825    but receives no events.826 827    Only events at the run's own scope are captured; checkpoint data from828    deeper subgraphs is available on the respective subgraph handle's829    `.checkpoints` projection.830 831    Native transformer — `run.checkpoints` is a direct attribute.832    """833 834    _native = True835    required_stream_modes = ("checkpoints",)836 837    def __init__(self, scope: tuple[str, ...] = ()) -> None:838        super().__init__(scope)839        self._log: StreamChannel[dict[str, Any]] = StreamChannel()840        self._scope_list: list[str] = list(scope)841 842    def init(self) -> dict[str, Any]:843        return {"checkpoints": self._log}844 845    def process(self, event: ProtocolEvent) -> bool:846        if event["method"] != "checkpoints":847            return True848        params = event["params"]849        if params["namespace"] != self._scope_list:850            return True851        self._log.push(params["data"])852        return True853 854 855class DebugTransformer(StreamTransformer):856    """Capture debug events as a drainable stream.857 858    Surfaces `stream_mode="debug"` data on `run.debug` as a859    `StreamChannel[dict[str, Any]]`. Each item is a debug event with860    step-level detail (checkpoint snapshots, task payloads, and861    task results wrapped with step number and timestamp).862 863    Only events at the run's own scope are captured; debug data from864    deeper subgraphs is available on the respective subgraph handle's865    `.debug` projection.866 867    Native transformer — `run.debug` is a direct attribute.868    """869 870    _native = True871    required_stream_modes = ("debug",)872 873    def __init__(self, scope: tuple[str, ...] = ()) -> None:874        super().__init__(scope)875        self._log: StreamChannel[dict[str, Any]] = StreamChannel()876        self._scope_list: list[str] = list(scope)877 878    def init(self) -> dict[str, Any]:879        return {"debug": self._log}880 881    def process(self, event: ProtocolEvent) -> bool:882        if event["method"] != "debug":883            return True884        params = event["params"]885        if params["namespace"] != self._scope_list:886            return True887        self._log.push(params["data"])888        return True889 890 891class TasksTransformer(StreamTransformer):892    """Capture raw task events as a drainable stream.893 894    Surfaces `stream_mode="tasks"` data on `run.tasks` as a895    `StreamChannel[dict[str, Any]]`. Each item is a task payload896    (start or result).897 898    `LifecycleTransformer` and `SubgraphTransformer` also consume899    `tasks` events for subgraph discovery and lifecycle tracking.900    This transformer captures the raw payloads independently for901    consumers who need task-level detail.902 903    Only events at the run's own scope are captured; task data from904    deeper subgraphs is available on the respective subgraph handle's905    `.tasks` projection.906 907    Native transformer — `run.tasks` is a direct attribute.908    """909 910    _native = True911    required_stream_modes = ("tasks",)912 913    def __init__(self, scope: tuple[str, ...] = ()) -> None:914        super().__init__(scope)915        self._log: StreamChannel[dict[str, Any]] = StreamChannel()916        self._scope_list: list[str] = list(scope)917 918    def init(self) -> dict[str, Any]:919        return {"tasks": self._log}920 921    def process(self, event: ProtocolEvent) -> bool:922        if event["method"] != "tasks":923            return True924        params = event["params"]925        if params["namespace"] != self._scope_list:926            return True927        self._log.push(params["data"])928        return True929 
codekingpro/portable-devtools · Team Ai