1use std::sync::Arc;
7
8use indexmap::IndexMap;
9
10use super::{Edge, EdgeAnalysis, Graph};
11use crate::error::{BuildError, BuildErrors};
12use crate::node::NodeKind;
13use crate::state::workflow_state::{MergeStrategy, WorkflowState};
14use crate::state::{State, StateMerge};
15
16pub struct PendingEdge<'a, S: WorkflowState = State, M: MergeStrategy<S> = StateMerge> {
20 builder: &'a mut GraphBuilder<S, M>,
21 edge_index: usize,
22}
23
24impl<'a, S: WorkflowState, M: MergeStrategy<S>> PendingEdge<'a, S, M> {
25 pub fn max_visits(self, n: usize) -> &'a mut GraphBuilder<S, M> {
26 self.builder.edges[self.edge_index].analysis = Some(EdgeAnalysis {
27 max_visits: Some(n),
28 });
29 self.builder
30 }
31}
32
33pub struct GraphBuilder<S: WorkflowState = State, M: MergeStrategy<S> = StateMerge> {
37 name: String,
38 nodes: IndexMap<String, NodeKind<S, M>>,
39 edges: Vec<Edge<S>>,
40 start: Option<String>,
41 end: Option<String>,
42 canonical_hash: Option<u64>,
44}
45
46impl<S: WorkflowState, M: MergeStrategy<S>> GraphBuilder<S, M> {
47 pub fn new(name: impl Into<String>) -> Self {
49 Self {
50 name: name.into(),
51 nodes: IndexMap::new(),
52 edges: Vec::new(),
53 start: None,
54 end: None,
55 canonical_hash: None,
56 }
57 }
58
59 pub fn canonical_hash(&mut self, hash: u64) -> &mut Self {
61 self.canonical_hash = Some(hash);
62 self
63 }
64
65 pub fn start(&mut self, node: impl Into<String>) -> &mut Self {
66 self.start = Some(node.into());
67 self
68 }
69
70 pub fn end(&mut self, node: impl Into<String>) -> &mut Self {
71 self.end = Some(node.into());
72 self
73 }
74
75 pub fn node(&mut self, name: impl Into<String>, kind: NodeKind<S, M>) -> &mut Self {
76 self.nodes.insert(name.into(), kind);
77 self
78 }
79
80 pub fn subgraph<Inner: WorkflowState, IM: MergeStrategy<Inner>, L: crate::StateLens<S, Inner>>(
82 &mut self,
83 name: impl Into<String>,
84 spec: crate::SubgraphSpec<S, Inner, IM, L>,
85 ) -> &mut Self
86 where
87 S: 'static,
88 Inner: 'static,
89 IM: 'static,
90 L: 'static,
91 {
92 let compiled = spec.compile();
93 self.nodes.insert(name.into(), NodeKind::Subgraph(compiled));
94 self
95 }
96
97 pub fn edge(
98 &mut self,
99 from: impl Into<String>,
100 to: impl Into<String>,
101 ) -> PendingEdge<'_, S, M> {
102 let edge_index = self.edges.len();
103 self.edges.push(Edge {
104 from: from.into(),
105 to: to.into(),
106 condition: None,
107 analysis: None,
108 fallback: false,
109 });
110 PendingEdge {
111 builder: self,
112 edge_index,
113 }
114 }
115
116 pub fn edge_if(
117 &mut self,
118 from: impl Into<String>,
119 to: impl Into<String>,
120 condition: impl Fn(&S) -> bool + Send + Sync + 'static,
121 ) -> PendingEdge<'_, S, M> {
122 let edge_index = self.edges.len();
123 self.edges.push(Edge {
124 from: from.into(),
125 to: to.into(),
126 condition: Some(Arc::new(condition)),
127 analysis: None,
128 fallback: false,
129 });
130 PendingEdge {
131 builder: self,
132 edge_index,
133 }
134 }
135
136 pub fn edge_fallback(
137 &mut self,
138 from: impl Into<String>,
139 to: impl Into<String>,
140 ) -> PendingEdge<'_, S, M> {
141 let edge_index = self.edges.len();
142 self.edges.push(Edge {
143 from: from.into(),
144 to: to.into(),
145 condition: None,
146 analysis: None,
147 fallback: true,
148 });
149 PendingEdge {
150 builder: self,
151 edge_index,
152 }
153 }
154
155 pub fn build(self) -> Result<Graph<S, M>, BuildErrors> {
156 let mut errors = BuildErrors::new();
157
158 let start = match self.start {
159 Some(s) => s,
160 None => {
161 errors.push(BuildError::MissingEntryPoint);
162 return Err(errors);
163 }
164 };
165 let end = match self.end {
166 Some(s) => s,
167 None => {
168 errors.push(BuildError::MissingExitPoint);
169 return Err(errors);
170 }
171 };
172
173 let mut seen_nodes = std::collections::HashSet::new();
174 for name in self.nodes.keys() {
175 if !seen_nodes.insert(name.clone()) {
176 errors.push(BuildError::DuplicateNode { id: name.clone() });
177 }
178 }
179
180 for edge in &self.edges {
181 if !self.nodes.contains_key(&edge.from) {
182 errors.push(BuildError::MissingNode {
183 from: edge.from.clone(),
184 to: edge.to.clone(),
185 });
186 }
187 if !self.nodes.contains_key(&edge.to) {
188 errors.push(BuildError::MissingNode {
189 from: edge.from.clone(),
190 to: edge.to.clone(),
191 });
192 }
193 }
194
195 if !errors.is_empty() {
196 return Err(errors);
197 }
198
199 let structural_hash = compute_structural_hash(&self.nodes, &self.edges);
200
201 let graph = Graph {
202 name: self.name,
203 nodes: self.nodes,
204 edges: self.edges,
205 start,
206 end,
207 canonical_hash: self.canonical_hash.unwrap_or(structural_hash),
208 };
209
210 if let Err(e) = graph.validate() {
211 return Err(BuildErrors(vec![BuildError::InvalidEdgeDefinition {
212 from: "graph".to_string(),
213 to: "graph".to_string(),
214 reason: e.to_string(),
215 }]));
216 }
217
218 Ok(graph)
219 }
220
221 pub fn name(&self) -> &str {
222 &self.name
223 }
224
225 pub fn compile(self) -> Result<Graph<S, M>, BuildErrors> {
227 use crate::compiler::CompilerPass;
228
229 let mut graph = self.build()?;
230
231 let mut ctx = crate::compiler::CompilerContext::<S>::new();
232 let pass = crate::compiler::InlinePass::new();
233 pass.run(&mut graph, &mut ctx);
234
235 if ctx.debug {
236 tracing::debug!(
237 inlined = ctx.stats.inlined_count,
238 skipped = ctx.stats.not_inlined_count,
239 "compile passes complete"
240 );
241 }
242
243 Ok(graph)
244 }
245}
246
247pub(crate) fn fnv_hash(s: &str) -> u64 {
250 let mut hash: u64 = 0xcbf29ce484222325;
251 for &byte in s.as_bytes() {
252 hash ^= byte as u64;
253 hash = hash.wrapping_mul(0x100000001b3);
254 }
255 hash
256}
257
258fn compute_structural_hash<S: WorkflowState, M: MergeStrategy<S>>(
260 nodes: &IndexMap<String, NodeKind<S, M>>,
261 edges: &[Edge<S>],
262) -> u64 {
263 let mut s = String::new();
264 let mut names: Vec<&str> = nodes.keys().map(|k| k.as_str()).collect();
265 names.sort();
266 s.push_str(&names.join(","));
267 s.push('|');
268 let mut edge_strs: Vec<String> = edges
269 .iter()
270 .map(|e| {
271 format!(
272 "{}->{}{:?}{}",
273 e.from,
274 e.to,
275 if e.condition.is_some() { "?" } else { "" },
276 if e.fallback { "!" } else { "" }
277 )
278 })
279 .collect();
280 edge_strs.sort();
281 s.push_str(&edge_strs.join(","));
282 fnv_hash(&s)
283}