Skip to main content

pe_graph/
config.rs

1//! Graph execution configuration.
2//!
3//! `GraphConfig` controls how a compiled graph executes: thread identity,
4//! recursion limits, concurrency bounds, and user-defined configurable values.
5
6use crate::retry::RetryPolicy;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// Configuration for a single graph execution.
12///
13/// # Example
14///
15/// ```
16/// use pe_graph::GraphConfig;
17///
18/// let config = GraphConfig::default()
19///     .with_thread_id("my-thread")
20///     .with_recursion_limit(50);
21/// ```
22/// NOTE: `#[non_exhaustive]` — will grow with timeout, retry policy, etc.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct GraphConfig {
26    /// Conversation thread identifier. Used as the checkpoint key.
27    pub thread_id: String,
28
29    /// Resume from a specific past checkpoint (time travel).
30    pub checkpoint_id: Option<String>,
31
32    /// Maximum supersteps before `PeError::GraphRecursion`. Default: 25.
33    ///
34    /// Kept low to catch infinite loops early in agentic systems.
35    /// Users can raise this for legitimately deep graphs.
36    pub recursion_limit: u32,
37
38    /// Maximum parallel nodes per superstep. Default: unbounded.
39    ///
40    /// Tokio's thread pool naturally bounds actual parallelism.
41    /// This provides an additional application-level limit.
42    pub max_concurrency: Option<usize>,
43
44    /// User-defined values readable inside nodes.
45    pub configurable: HashMap<String, serde_json::Value>,
46
47    /// Maximum wall-clock time for the entire graph execution.
48    ///
49    /// The Pregel engine checks elapsed time at the start of each superstep.
50    /// If exceeded, returns `PeError::Timeout` before running nodes.
51    /// Default: `None` (no timeout).
52    #[serde(skip)]
53    pub max_execution_time: Option<Duration>,
54
55    /// Retry policy for failed nodes.
56    ///
57    /// When set, nodes that return `NodeResult::Error(e)` where
58    /// `e.is_retryable()` are retried up to `max_attempts` times
59    /// with backoff delays. Only the individual failed node is retried,
60    /// not the entire superstep.
61    /// Default: `None` (no retries — errors are immediate).
62    #[serde(skip)]
63    pub retry_policy: Option<RetryPolicy>,
64}
65
66impl Default for GraphConfig {
67    fn default() -> Self {
68        Self {
69            thread_id: uuid::Uuid::new_v4().to_string(),
70            checkpoint_id: None,
71            recursion_limit: 25,
72            max_concurrency: None,
73            configurable: HashMap::new(),
74            max_execution_time: None,
75            retry_policy: None,
76        }
77    }
78}
79
80impl GraphConfig {
81    /// Set the thread identifier for this execution.
82    #[must_use = "builder methods return the modified config"]
83    pub fn with_thread_id(mut self, id: impl Into<String>) -> Self {
84        self.thread_id = id.into();
85        self
86    }
87
88    /// Set the maximum number of supersteps before recursion error.
89    #[must_use = "builder methods return the modified config"]
90    pub fn with_recursion_limit(mut self, limit: u32) -> Self {
91        self.recursion_limit = limit;
92        self
93    }
94
95    /// Limit parallel node executions per superstep.
96    #[must_use = "builder methods return the modified config"]
97    pub fn with_max_concurrency(mut self, n: usize) -> Self {
98        self.max_concurrency = Some(n);
99        self
100    }
101
102    /// Resume from a specific checkpoint.
103    #[must_use = "builder methods return the modified config"]
104    pub fn with_checkpoint_id(mut self, id: impl Into<String>) -> Self {
105        self.checkpoint_id = Some(id.into());
106        self
107    }
108
109    /// Set the maximum wall-clock time for the entire execution.
110    ///
111    /// The engine checks this at the start of each superstep, before running
112    /// any nodes. Running nodes are never cancelled mid-execution.
113    #[must_use = "builder methods return the modified config"]
114    pub fn with_max_execution_time(mut self, duration: Duration) -> Self {
115        self.max_execution_time = Some(duration);
116        self
117    }
118
119    /// Set a retry policy for failed nodes.
120    ///
121    /// When set, nodes returning retryable errors are retried individually
122    /// with exponential backoff. Non-retryable errors fail immediately.
123    ///
124    /// # Example
125    ///
126    /// ```
127    /// use pe_graph::{GraphConfig, RetryPolicy};
128    ///
129    /// let config = GraphConfig::default()
130    ///     .with_retry_policy(RetryPolicy::default());
131    /// ```
132    #[must_use = "builder methods return the modified config"]
133    pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
134        self.retry_policy = Some(policy);
135        self
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_default_values() {
145        let config = GraphConfig::default();
146        assert_eq!(config.recursion_limit, 25);
147        assert!(config.max_concurrency.is_none());
148        assert!(config.checkpoint_id.is_none());
149        assert!(!config.thread_id.is_empty());
150        assert!(config.configurable.is_empty());
151        assert!(config.max_execution_time.is_none());
152        assert!(config.retry_policy.is_none());
153    }
154
155    #[test]
156    fn test_builder_methods() {
157        let config = GraphConfig::default()
158            .with_thread_id("test-thread")
159            .with_recursion_limit(100)
160            .with_max_concurrency(4)
161            .with_checkpoint_id("cp-123");
162
163        assert_eq!(config.thread_id, "test-thread");
164        assert_eq!(config.recursion_limit, 100);
165        assert_eq!(config.max_concurrency, Some(4));
166        assert_eq!(config.checkpoint_id.as_deref(), Some("cp-123"));
167    }
168
169    #[test]
170    fn test_with_retry_policy() {
171        let policy = RetryPolicy {
172            max_attempts: 5,
173            ..Default::default()
174        };
175        let config = GraphConfig::default().with_retry_policy(policy);
176        assert!(config.retry_policy.is_some());
177        assert_eq!(config.retry_policy.unwrap().max_attempts, 5);
178    }
179
180    #[test]
181    fn test_unique_thread_ids() {
182        let a = GraphConfig::default();
183        let b = GraphConfig::default();
184        assert_ne!(a.thread_id, b.thread_id);
185    }
186}