Skip to main content

tinyagents/graph/compiled/
types.rs

1//! Compiled graph and execution-result types.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use crate::graph::builder::START;
7use crate::graph::builder::{BarrierRelief, Branch, BuilderNode, NodeMeta};
8use crate::graph::checkpoint::{
9    CheckpointConfig, CheckpointMetadata, Checkpointer, DurabilityMode,
10};
11use crate::graph::command::Interrupt;
12use crate::graph::observability::{GraphEventJournal, GraphStatusStore};
13use crate::graph::recursion::{ChildRun, RunTree};
14use crate::graph::reducer::StateReducer;
15use crate::graph::status::GraphRunStatus;
16use crate::graph::stream::GraphEventSink;
17use crate::harness::ids::{CheckpointId, GraphId, NodeId, RunId};
18
19/// An immutable, validated graph ready to execute.
20///
21/// Cheap to clone (all heavy fields are `Arc`-shared) and safe to run
22/// concurrently. Construct one with [`crate::graph::GraphBuilder::compile`].
23pub struct CompiledGraph<State, Update> {
24    pub(crate) graph_id: GraphId,
25    /// Optional human-readable graph name surfaced by the topology export.
26    pub(crate) name: Option<String>,
27    pub(crate) nodes: Arc<HashMap<NodeId, BuilderNode<State, Update>>>,
28    pub(crate) edges: Arc<HashMap<NodeId, NodeId>>,
29    pub(crate) branches: Arc<HashMap<NodeId, Branch<State>>>,
30    #[allow(dead_code)]
31    pub(crate) command_nodes: Arc<HashSet<NodeId>>,
32    /// Barrier/waiting edges: target -> the predecessor set that must all
33    /// complete (across steps) before the target activates.
34    pub(crate) waiting: Arc<HashMap<NodeId, HashSet<NodeId>>>,
35    /// Mixed fan-in barrier relief registrations; see [`BarrierRelief`].
36    pub(crate) barrier_reliefs: Arc<Vec<BarrierRelief>>,
37    /// Behavior-free per-node markers/metadata surfaced by the topology export.
38    pub(crate) node_meta: Arc<HashMap<NodeId, NodeMeta>>,
39    pub(crate) entry: NodeId,
40    pub(crate) reducer: Arc<dyn StateReducer<State, Update>>,
41    pub(crate) recursion_limit: usize,
42    /// Explicit recursion caps (run-tree depth, per-node visits, total steps)
43    /// enforced by the executor; configured via
44    /// [`CompiledGraph::with_recursion_policy`](crate::graph::CompiledGraph::with_recursion_policy).
45    pub(crate) recursion_policy: crate::graph::recursion::RecursionPolicy,
46    /// Inherited recursion frames of an enclosing run (empty for a top-level
47    /// graph). A subgraph/sub-agent wrapper seeds these so a nested run extends
48    /// the parent's recursion stack rather than starting a fresh tree.
49    pub(crate) recursion_frames: Vec<crate::graph::recursion::RecursionFrame>,
50    /// The hosting node this graph runs under when embedded as a subgraph; used
51    /// as the `node_id` of this run's root recursion frame so the run tree names
52    /// the embedding node. `None` for a top-level run.
53    pub(crate) recursion_node: Option<NodeId>,
54    pub(crate) checkpointer: Option<Arc<dyn Checkpointer<State>>>,
55    pub(crate) event_sink: Option<Arc<dyn GraphEventSink>>,
56    /// Optional durable observation journal (opt-in via
57    /// [`CompiledGraph::with_event_journal`]).
58    pub(crate) journal: Option<Arc<dyn GraphEventJournal>>,
59    /// Optional run-status surface (opt-in via
60    /// [`CompiledGraph::with_status_store`]).
61    pub(crate) status_store: Option<Arc<dyn GraphStatusStore>>,
62    pub(crate) namespace: Vec<String>,
63    /// When true, the active node set of a superstep is executed concurrently.
64    pub(crate) parallel: bool,
65    /// Upper bound on concurrently-running branches per step (`None` = unbounded).
66    pub(crate) max_concurrency: Option<usize>,
67    /// Default per-node handler timeout (`None` = no timeout).
68    pub(crate) node_timeout: Option<std::time::Duration>,
69    /// Optional whole-run wall-clock deadline (`None` = no deadline). Checked at
70    /// every super-step boundary: when the elapsed run time reaches it the run
71    /// stops *between* super-steps with a [`TinyAgentsError::Timeout`], leaving
72    /// the last committed boundary checkpoint intact and resumable. Configured
73    /// via [`CompiledGraph::with_run_deadline`](crate::graph::CompiledGraph::with_run_deadline).
74    pub(crate) run_deadline: Option<std::time::Duration>,
75    /// When checkpoints are persisted relative to execution (default
76    /// [`DurabilityMode::Sync`]).
77    pub(crate) durability: DurabilityMode,
78    /// Optional per-node retry policy applied around each node handler. When
79    /// set, a handler that fails with a
80    /// [retryable][crate::harness::retry::is_retryable] error is re-run from its
81    /// start (with opt-in backoff) up to the policy's attempt cap before the
82    /// failure escalates. `None` (default) preserves the original
83    /// abort-on-first-error behavior. Configured via
84    /// [`CompiledGraph::with_node_retry`](crate::graph::CompiledGraph::with_node_retry).
85    pub(crate) node_retry: Option<crate::harness::retry::RetryPolicy>,
86}
87
88impl<State, Update> std::fmt::Debug for CompiledGraph<State, Update> {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("CompiledGraph")
91            .field("graph_id", &self.graph_id)
92            .field("nodes", &self.nodes.len())
93            .field("entry", &self.entry)
94            .field("recursion_limit", &self.recursion_limit)
95            .field("namespace", &self.namespace)
96            .field("parallel", &self.parallel)
97            .finish_non_exhaustive()
98    }
99}
100
101impl<State, Update> Clone for CompiledGraph<State, Update> {
102    fn clone(&self) -> Self {
103        Self {
104            graph_id: self.graph_id.clone(),
105            name: self.name.clone(),
106            nodes: self.nodes.clone(),
107            edges: self.edges.clone(),
108            branches: self.branches.clone(),
109            command_nodes: self.command_nodes.clone(),
110            waiting: self.waiting.clone(),
111            barrier_reliefs: self.barrier_reliefs.clone(),
112            node_meta: self.node_meta.clone(),
113            entry: self.entry.clone(),
114            reducer: self.reducer.clone(),
115            recursion_limit: self.recursion_limit,
116            recursion_policy: self.recursion_policy,
117            recursion_frames: self.recursion_frames.clone(),
118            recursion_node: self.recursion_node.clone(),
119            checkpointer: self.checkpointer.clone(),
120            event_sink: self.event_sink.clone(),
121            journal: self.journal.clone(),
122            status_store: self.status_store.clone(),
123            namespace: self.namespace.clone(),
124            parallel: self.parallel,
125            max_concurrency: self.max_concurrency,
126            node_timeout: self.node_timeout,
127            run_deadline: self.run_deadline,
128            durability: self.durability,
129            node_retry: self.node_retry.clone(),
130        }
131    }
132}
133
134/// Tracks checkpoint writes handed to background tasks under
135/// [`DurabilityMode::Async`].
136///
137/// # Failure semantics
138///
139/// Async durability is fire-and-forget for *latency*, never for *outcome*: a
140/// background `put` error is recorded here rather than dropped, and the
141/// executor surfaces it at the next durability boundary
142/// ([`Self::take_failure`]) or — at the latest — when the run drains all
143/// in-flight writes at its terminal/interrupt boundary ([`Self::drain`]). A
144/// run therefore never reports success while one of its checkpoints silently
145/// failed to persist.
146#[derive(Default)]
147pub(crate) struct AsyncCheckpointWrites {
148    /// In-flight (or finished-but-unharvested) background writes, in the order
149    /// they were spawned.
150    handles: Vec<tokio::task::JoinHandle<crate::error::Result<CheckpointId>>>,
151}
152
153impl AsyncCheckpointWrites {
154    /// Tracks one spawned background write.
155    pub(crate) fn push(
156        &mut self,
157        handle: tokio::task::JoinHandle<crate::error::Result<CheckpointId>>,
158    ) {
159        self.handles.push(handle);
160    }
161
162    /// Harvests writes that have already finished, without blocking on the
163    /// ones still in flight. Returns the first error found (a failed `put` or
164    /// a panicked task), or `None` when every finished write succeeded.
165    pub(crate) async fn take_failure(&mut self) -> Option<crate::error::TinyAgentsError> {
166        let mut failure = None;
167        let mut remaining = Vec::with_capacity(self.handles.len());
168        for handle in self.handles.drain(..) {
169            if !handle.is_finished() {
170                remaining.push(handle);
171                continue;
172            }
173            // Finished, so this await is immediate.
174            if let Some(err) = Self::harvest(handle.await)
175                && failure.is_none()
176            {
177                failure = Some(err);
178            }
179        }
180        self.handles = remaining;
181        failure
182    }
183
184    /// Awaits every in-flight write. Returns the first error (spawn order),
185    /// after all writes have settled. This is the "final await at run end":
186    /// terminal, interrupt, and failure boundaries drain before returning so
187    /// the run result reflects persistence failures.
188    pub(crate) async fn drain(&mut self) -> crate::error::Result<()> {
189        let mut failure = None;
190        for handle in self.handles.drain(..) {
191            if let Some(err) = Self::harvest(handle.await)
192                && failure.is_none()
193            {
194                failure = Some(err);
195            }
196        }
197        match failure {
198            Some(err) => Err(err),
199            None => Ok(()),
200        }
201    }
202
203    /// Maps one settled join result onto its error, if any.
204    fn harvest(
205        joined: std::result::Result<crate::error::Result<CheckpointId>, tokio::task::JoinError>,
206    ) -> Option<crate::error::TinyAgentsError> {
207        match joined {
208            Ok(Ok(_)) => None,
209            Ok(Err(err)) => Some(err),
210            Err(join_err) => Some(crate::error::TinyAgentsError::Checkpoint(format!(
211                "background checkpoint write task failed: {join_err}"
212            ))),
213        }
214    }
215}
216
217/// The result of a durable graph run.
218///
219/// Carries the final committed state, the visited node history, the superstep
220/// count, any pending interrupts, the run status snapshot, and the latest
221/// checkpoint id (when checkpointing was enabled).
222#[derive(Clone, Debug)]
223pub struct GraphExecution<State> {
224    /// Final committed state.
225    pub state: State,
226    /// This run's own id.
227    pub run_id: RunId,
228    /// The graph that produced this run.
229    pub graph_id: GraphId,
230    /// The root run id of the recursion tree (equals `run_id` for a top-level
231    /// run; the shared ancestor for a subgraph/sub-agent child run).
232    pub root_run_id: RunId,
233    /// The enclosing run's id, when this run was spawned as a child.
234    pub parent_run_id: Option<RunId>,
235    /// Child runs spawned from subgraph nodes during this run, in completion
236    /// order, keyed by the embedding node.
237    pub child_runs: Vec<ChildRun>,
238    /// Ordered list of executed nodes (may repeat across supersteps).
239    pub visited: Vec<NodeId>,
240    /// Number of supersteps executed.
241    pub steps: usize,
242    /// Interrupts that paused the run, if any.
243    pub interrupts: Vec<Interrupt>,
244    /// Compact status snapshot at the final boundary.
245    pub status: GraphRunStatus,
246    /// The latest persisted checkpoint id, if checkpointing was enabled.
247    pub checkpoint_id: Option<CheckpointId>,
248}
249
250/// One external input used to seed a graph run.
251///
252/// Most runs should target [`START`] via [`GraphInput::start`], which delivers
253/// the payload to the graph's compiled entry node. More complex orchestrations
254/// can seed additional nodes in the same first superstep, letting independent
255/// loops receive user input, tool results, webhook payloads, or other external
256/// events without forcing every input through one entry handler.
257#[derive(Clone, Debug, PartialEq)]
258pub struct GraphInput {
259    /// Target node for this input. `__start__` is accepted as the virtual entry
260    /// target and is resolved to the graph's compiled entry node at run time.
261    pub node: NodeId,
262    /// Optional per-input payload delivered as [`NodeContext::send_arg`].
263    pub payload: Option<serde_json::Value>,
264}
265
266impl GraphInput {
267    /// Delivers `payload` to the graph's compiled entry node.
268    pub fn start(payload: serde_json::Value) -> Self {
269        Self {
270            node: NodeId::from(START),
271            payload: Some(payload),
272        }
273    }
274
275    /// Delivers `payload` directly to `node` in the first superstep.
276    pub fn new(node: impl Into<NodeId>, payload: serde_json::Value) -> Self {
277        Self {
278            node: node.into(),
279            payload: Some(payload),
280        }
281    }
282
283    /// Activates `node` without a per-input payload.
284    pub fn node(node: impl Into<NodeId>) -> Self {
285        Self {
286            node: node.into(),
287            payload: None,
288        }
289    }
290}
291
292impl<State> GraphExecution<State> {
293    /// Returns true when the run paused on an interrupt rather than completing.
294    pub fn is_interrupted(&self) -> bool {
295        !self.interrupts.is_empty()
296    }
297
298    /// Builds the parent/child run-lineage view for this run.
299    ///
300    /// This is the run-id counterpart to the live recursion stack: it reports
301    /// this run's id, the shared root, the enclosing parent (when this was a
302    /// child run), and every child run spawned from a subgraph node.
303    pub fn run_tree(&self) -> RunTree {
304        RunTree {
305            run_id: self.run_id.clone(),
306            root_run_id: self.root_run_id.clone(),
307            parent_run_id: self.parent_run_id.clone(),
308            children: self.child_runs.clone(),
309        }
310    }
311}
312
313/// A point-in-time view of a thread's checkpointed state, returned by
314/// [`CompiledGraph::get_state`](crate::graph::CompiledGraph::get_state) and
315/// [`CompiledGraph::get_state_history`](crate::graph::CompiledGraph::get_state_history).
316///
317/// This is the state-inspection / time-travel surface: it bundles the committed
318/// channel values at a checkpoint with everything a caller needs to reason about
319/// or resume from that point — the next nodes that would run, the config that
320/// addresses this snapshot, the config of its parent (for walking the lineage),
321/// the checkpoint metadata (source/step/run id), and any pending interrupts.
322#[derive(Clone, Debug)]
323pub struct StateSnapshot<State> {
324    /// Committed graph state (channel values) at the snapshot's checkpoint.
325    pub values: State,
326    /// Nodes that would run if execution resumed from this snapshot.
327    pub next_nodes: Vec<NodeId>,
328    /// Tasks scheduled for the next superstep (mirrors `next_nodes`).
329    pub tasks: Vec<NodeId>,
330    /// Config that addresses this snapshot's checkpoint.
331    pub config: CheckpointConfig,
332    /// Lightweight metadata for the snapshot's checkpoint (source, step, run id).
333    pub metadata: CheckpointMetadata,
334    /// Config addressing the parent checkpoint, when one exists.
335    pub parent_config: Option<CheckpointConfig>,
336    /// Interrupts that paused the run at this checkpoint, if any.
337    pub pending_interrupts: Vec<Interrupt>,
338}
339
340/// Selects which checkpoint a time-travel resume starts from.
341///
342/// [`CompiledGraph::resume`](crate::graph::CompiledGraph::resume) is shorthand
343/// for [`ResumeTarget::Latest`]; [`CompiledGraph::resume_from`](crate::graph::CompiledGraph::resume_from)
344/// accepts an explicit target so a run can be replayed from an older checkpoint
345/// config (time travel) without mutating the original record.
346#[derive(Clone, Debug, PartialEq, Eq)]
347pub enum ResumeTarget {
348    /// Resume from the thread's most recent checkpoint.
349    Latest,
350    /// Resume from a specific checkpoint id within the thread.
351    Checkpoint(String),
352}