-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubgraph.py
More file actions
133 lines (118 loc) · 6.27 KB
/
Copy pathsubgraph.py
File metadata and controls
133 lines (118 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""Subgraphs as nodes.
A compiled graph is used as a node inside another graph. The
subgraph runs against its own state schema; projection between parent
and subgraph is delegated to a ``ProjectionStrategy`` (default:
``FieldNameMatching``; ``ExplicitMapping`` is also available).
When a subgraph runs as part of a parent invocation, its inner-node
events bubble up to outer observers (in addition to the subgraph's
own attached observers), the step counter spans the
subgraph boundary, and the namespace extends. SubgraphNode.run accepts an
optional `_InvocationContext` so the engine can thread that context through;
called without it (e.g., direct test invocation), SubgraphNode falls back to
a fresh subgraph-only invocation.
Parameterized on both the parent's state type (`ParentT`) and the subgraph's
state type (`ChildT`). The outer graph only ever sees `run(state: ParentT)`; the
`ChildT` lives on the `compiled` and `projection` fields and is
invisible at the outer graph's node dispatch site.
"""
from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, cast
from pydantic import ValidationError
from .middleware import Middleware
from .projection import FieldNameMatching, ProjectionStrategy
from .state import State
if TYPE_CHECKING:
from .compiled import CompiledGraph
from .observer import _InvocationContext
@dataclass(frozen=True)
class SubgraphNode[ParentT: State, ChildT: State]:
"""A node backed by a compiled subgraph.
The parent's per-node middleware on a SubgraphNode wraps the
subgraph dispatch as a single atomic call; parent middleware
does NOT cross into the subgraph's internal nodes (those are
wrapped by the subgraph's own middleware independently).
"""
name: str
compiled: "CompiledGraph[ChildT]"
projection: ProjectionStrategy[ParentT, ChildT] = field(
default_factory=FieldNameMatching[ParentT, ChildT]
)
middleware: tuple[Middleware, ...] = field(default_factory=tuple[Middleware, ...])
# The compiled subgraph's identity (the registry key under which
# the subgraph is declared, distinct from the wrapper node's
# ``name`` in the parent graph). Optional and BC-preserving:
# callers that don't pass it get an empty string emitted as the
# observability §5.3 ``subgraph_name`` attribute (matching the
# spec's "if the implementation tracks one" fallback). Setting
# it lets dashboards filter or aggregate across observations from
# the same compiled subgraph wrapped under different node names
# (e.g., a ``validator`` subgraph used as both ``validate_input``
# and ``validate_output``).
subgraph_identity: str | None = None
async def run(
self,
state: ParentT,
context: "_InvocationContext | None" = None,
) -> Mapping[str, Any]:
"""Execute the subgraph and project its result back into the parent.
When `context` is None (e.g., direct invocation in tests, or a parent
call that doesn't thread a context), the subgraph runs via its own
public `invoke()`; a fresh root invocation with no parent observer
chain.
When `context` is provided (the engine's normal path during
a parent run), the subgraph descends into a child context
that shares the parent's queue + step counter and extends the
namespace and parent-state stack. Observer events from inner
nodes bubble up to outer observers.
"""
# Resume-with-saved-inner-state (spec pipeline-utilities §10.4):
# if the loaded record's latest save fired from inside this
# subgraph (or a deeper nested one we'll re-enter), the engine
# threads the saved inner state through ``pending_resume_states``
# keyed by descent depth. Consume the matching depth here
# before falling back to the normal projection — this is what
# makes "skip step_one, run step_two with its post-merge inner
# state" work without re-running step_one.
saved: Any = None
if context is not None:
# Descent depth of THIS subgraph's inner = current
# depth + 1. Outer is depth 0; first subgraph is depth 1.
target_depth = len(context.parent_states_prefix) + 1
saved = context.pending_resume_states.pop(target_depth, None)
if saved is not None:
# Coerce dict → typed instance if the backend stored JSON;
# in-memory backends preserve the live instance. A
# ValidationError on the JSON path means the persisted
# inner state is incompatible with the current subgraph's
# state class — surface as CheckpointRecordInvalid per
# §10.10 rather than a raw pydantic ValidationError.
sub_initial: ChildT
if isinstance(saved, dict):
try:
sub_initial = self.compiled.state_cls.model_validate(saved)
except ValidationError as exc:
# Local imports to avoid an import cycle between
# graph and checkpoint at module load time.
from openarmature.checkpoint.errors import CheckpointRecordInvalid
assert context is not None # saved is non-None only when context is set
raise CheckpointRecordInvalid(
context.resume_invocation or context.invocation_id,
f"saved inner state for subgraph {self.name!r} does not "
f"validate against {self.compiled.state_cls.__name__}: {exc}",
) from exc
else:
sub_initial = cast("ChildT", saved)
else:
sub_initial = self.projection.project_in(state, self.compiled.state_cls)
if context is None:
sub_final = await self.compiled.invoke(sub_initial)
else:
child_context = context.descend_into_subgraph(
subgraph_node_name=self.name,
parent_state=state,
sub_attached=tuple(self.compiled._attached_observers),
subgraph_identity=self.subgraph_identity,
)
sub_final = await self.compiled._invoke(sub_initial, child_context)
return self.projection.project_out(sub_final, state, self.compiled.state_cls)