1use std::{fmt, sync::Arc};
2
3use runifold_agent::Agent;
4use runifold_core::{CapabilitySet, EffectClass, Usage};
5
6use crate::{
7 AgentStep, StepId, WorkflowBuildError, WorkflowCondition, WorkflowStep, WorkflowStepError,
8};
9
10pub(crate) enum WorkflowNodeKind {
11 Step(Arc<dyn WorkflowStep>),
12 Branch {
13 condition: Arc<dyn WorkflowCondition>,
14 when_true: Arc<dyn WorkflowStep>,
15 when_false: Arc<dyn WorkflowStep>,
16 },
17 Parallel(Arc<[ParallelBranch]>),
18 Race(Arc<[ParallelBranch]>),
19}
20
21pub(crate) struct WorkflowNode {
22 pub(crate) id: StepId,
23 pub(crate) capabilities: CapabilitySet,
24 pub(crate) kind: WorkflowNodeKind,
25}
26
27impl WorkflowNode {
28 pub(crate) async fn execute(
29 &self,
30 input: serde_json::Value,
31 run: &runifold_core::RunContext,
32 ) -> Result<(serde_json::Value, Option<bool>), WorkflowStepError> {
33 match &self.kind {
34 WorkflowNodeKind::Step(step) => {
35 step.execute(input, run).await.map(|output| (output, None))
36 }
37 WorkflowNodeKind::Branch {
38 condition,
39 when_true,
40 when_false,
41 } => {
42 let selected = condition.evaluate(&input)?;
43 let step = if selected { when_true } else { when_false };
44 step.execute(input, run)
45 .await
46 .map(|output| (output, Some(selected)))
47 }
48 WorkflowNodeKind::Parallel(_) | WorkflowNodeKind::Race(_) => {
49 unreachable!("concurrent nodes use their dedicated scheduler")
50 }
51 }
52 }
53}
54
55pub struct ParallelBranch {
57 pub(crate) id: String,
58 pub(crate) step: Arc<dyn WorkflowStep>,
59 pub(crate) capabilities: CapabilitySet,
60 pub(crate) reservation: Usage,
61}
62
63impl ParallelBranch {
64 pub fn step<S>(
66 id: impl Into<String>,
67 step: S,
68 capabilities: CapabilitySet,
69 reservation: Usage,
70 ) -> Self
71 where
72 S: WorkflowStep + 'static,
73 {
74 Self {
75 id: id.into(),
76 step: Arc::new(step),
77 capabilities,
78 reservation,
79 }
80 }
81
82 pub fn agent(
84 id: impl Into<String>,
85 agent: Arc<Agent>,
86 capabilities: CapabilitySet,
87 reservation: Usage,
88 ) -> Self {
89 Self::step(id, AgentStep::new(agent), capabilities, reservation)
90 }
91}
92
93impl fmt::Debug for ParallelBranch {
94 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 formatter
96 .debug_struct("ParallelBranch")
97 .field("id", &self.id)
98 .field("capabilities", &self.capabilities)
99 .field("reservation", &self.reservation)
100 .finish_non_exhaustive()
101 }
102}
103
104#[derive(Clone)]
106pub struct Workflow {
107 pub(crate) name: String,
108 pub(crate) version: u32,
109 pub(crate) nodes: Arc<[WorkflowNode]>,
110}
111
112impl Workflow {
113 pub fn builder(name: impl Into<String>) -> WorkflowBuilder {
115 WorkflowBuilder::new(name)
116 }
117
118 pub fn name(&self) -> &str {
120 &self.name
121 }
122
123 pub const fn version(&self) -> u32 {
125 self.version
126 }
127
128 pub fn step_ids(&self) -> impl ExactSizeIterator<Item = &StepId> {
130 self.nodes.iter().map(|node| &node.id)
131 }
132}
133
134impl fmt::Debug for Workflow {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter
137 .debug_struct("Workflow")
138 .field("name", &self.name)
139 .field("version", &self.version)
140 .field("steps", &self.step_ids().collect::<Vec<_>>())
141 .finish_non_exhaustive()
142 }
143}
144
145pub struct WorkflowBuilder {
147 name: String,
148 version: u32,
149 nodes: Vec<WorkflowNode>,
150 error: Option<WorkflowBuildError>,
151}
152
153impl WorkflowBuilder {
154 pub fn new(name: impl Into<String>) -> Self {
156 let name = name.into();
157 let error = name
158 .trim()
159 .is_empty()
160 .then_some(WorkflowBuildError::EmptyName);
161 Self {
162 name,
163 version: 1,
164 nodes: Vec::new(),
165 error,
166 }
167 }
168
169 #[must_use]
171 pub fn version(mut self, version: u32) -> Self {
172 if version == 0 && self.error.is_none() {
173 self.error = Some(WorkflowBuildError::InvalidVersion);
174 } else {
175 self.version = version;
176 }
177 self
178 }
179
180 #[must_use]
182 pub fn step<S>(mut self, id: impl Into<String>, step: S, capabilities: CapabilitySet) -> Self
183 where
184 S: WorkflowStep + 'static,
185 {
186 self.push_node(id, capabilities, WorkflowNodeKind::Step(Arc::new(step)));
187 self
188 }
189
190 #[must_use]
192 pub fn agent(
193 mut self,
194 id: impl Into<String>,
195 agent: Arc<Agent>,
196 capabilities: CapabilitySet,
197 ) -> Self {
198 self.push_node(
199 id,
200 capabilities,
201 WorkflowNodeKind::Step(Arc::new(AgentStep::new(agent))),
202 );
203 self
204 }
205
206 #[must_use]
208 pub fn branch<C, T, F>(
209 mut self,
210 id: impl Into<String>,
211 condition: C,
212 when_true: T,
213 when_false: F,
214 capabilities: CapabilitySet,
215 ) -> Self
216 where
217 C: WorkflowCondition + 'static,
218 T: WorkflowStep + 'static,
219 F: WorkflowStep + 'static,
220 {
221 self.push_node(
222 id,
223 capabilities,
224 WorkflowNodeKind::Branch {
225 condition: Arc::new(condition),
226 when_true: Arc::new(when_true),
227 when_false: Arc::new(when_false),
228 },
229 );
230 self
231 }
232
233 #[must_use]
238 pub fn parallel(
239 mut self,
240 id: impl Into<String>,
241 branches: impl IntoIterator<Item = ParallelBranch>,
242 ) -> Self {
243 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
244 if branches.len() < 2 {
245 self.error = Some(WorkflowBuildError::TooFewParallelBranches(id));
246 return self;
247 }
248 self.nodes.push(WorkflowNode {
249 id,
250 capabilities: CapabilitySet::new(),
251 kind: WorkflowNodeKind::Parallel(branches.into()),
252 });
253 }
254 self
255 }
256
257 #[must_use]
263 pub fn race(
264 mut self,
265 id: impl Into<String>,
266 branches: impl IntoIterator<Item = ParallelBranch>,
267 ) -> Self {
268 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
269 if branches.len() < 2 {
270 self.error = Some(WorkflowBuildError::TooFewRaceBranches(id));
271 return self;
272 }
273 for branch in &branches {
274 if let Some(capability) = branch.capabilities.iter().find(|capability| {
275 !matches!(capability.effect, EffectClass::Pure | EffectClass::ReadOnly)
276 }) {
277 let branch = match StepId::parse(branch.id.clone()) {
278 Ok(branch) => branch,
279 Err(branch) => {
280 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch));
281 return self;
282 }
283 };
284 self.error = Some(WorkflowBuildError::UnsafeRaceCapability {
285 step: id,
286 branch,
287 capability: capability.name.clone(),
288 });
289 return self;
290 }
291 }
292 self.nodes.push(WorkflowNode {
293 id,
294 capabilities: CapabilitySet::new(),
295 kind: WorkflowNodeKind::Race(branches.into()),
296 });
297 }
298 self
299 }
300
301 pub fn build(self) -> Result<Workflow, WorkflowBuildError> {
308 if let Some(error) = self.error {
309 return Err(error);
310 }
311 if self.nodes.is_empty() {
312 return Err(WorkflowBuildError::NoSteps);
313 }
314 Ok(Workflow {
315 name: self.name,
316 version: self.version,
317 nodes: self.nodes.into(),
318 })
319 }
320
321 fn push_node(
322 &mut self,
323 id: impl Into<String>,
324 capabilities: CapabilitySet,
325 kind: WorkflowNodeKind,
326 ) {
327 if self.error.is_some() {
328 return;
329 }
330 let id = match StepId::parse(id) {
331 Ok(id) => id,
332 Err(id) => {
333 self.error = Some(WorkflowBuildError::InvalidStepId(id));
334 return;
335 }
336 };
337 if self.nodes.iter().any(|node| node.id == id) {
338 self.error = Some(WorkflowBuildError::DuplicateStep(id));
339 return;
340 }
341 self.nodes.push(WorkflowNode {
342 id,
343 capabilities,
344 kind,
345 });
346 }
347
348 fn validate_concurrent_branches(
349 &mut self,
350 id: impl Into<String>,
351 branches: impl IntoIterator<Item = ParallelBranch>,
352 ) -> Option<(StepId, Vec<ParallelBranch>)> {
353 if self.error.is_some() {
354 return None;
355 }
356 let id = match StepId::parse(id) {
357 Ok(id) => id,
358 Err(id) => {
359 self.error = Some(WorkflowBuildError::InvalidStepId(id));
360 return None;
361 }
362 };
363 if self.nodes.iter().any(|node| node.id == id) {
364 self.error = Some(WorkflowBuildError::DuplicateStep(id));
365 return None;
366 }
367 let mut validated = Vec::new();
368 for branch in branches {
369 let branch_id = match StepId::parse(branch.id) {
370 Ok(branch_id) => branch_id,
371 Err(branch_id) => {
372 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch_id));
373 return None;
374 }
375 };
376 if validated
377 .iter()
378 .any(|existing: &ParallelBranch| existing.id == branch_id.as_str())
379 {
380 self.error = Some(WorkflowBuildError::DuplicateParallelBranch {
381 step: id,
382 branch: branch_id,
383 });
384 return None;
385 }
386 validated.push(ParallelBranch {
387 id: branch_id.to_string(),
388 ..branch
389 });
390 }
391 Some((id, validated))
392 }
393}
394
395impl fmt::Debug for WorkflowBuilder {
396 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397 formatter
398 .debug_struct("WorkflowBuilder")
399 .field("name", &self.name)
400 .field("version", &self.version)
401 .field(
402 "steps",
403 &self.nodes.iter().map(|node| &node.id).collect::<Vec<_>>(),
404 )
405 .field("error", &self.error)
406 .finish()
407 }
408}