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}