1use std::{fmt, sync::Arc, time::Duration};
2
3use runifold_agent::Agent;
4use runifold_core::{CapabilitySet, EffectClass, Usage};
5
6use crate::{
7 AgentStep, StepId, WorkflowBuildError, WorkflowCondition, WorkflowInterruptRequest,
8 WorkflowSignalName, WorkflowStep, WorkflowStepError, WorkflowWait,
9};
10
11pub(crate) enum WorkflowNodeKind {
12 Step(Arc<dyn WorkflowStep>),
13 Branch {
14 condition: Arc<dyn WorkflowCondition>,
15 when_true: Arc<dyn WorkflowStep>,
16 when_false: Arc<dyn WorkflowStep>,
17 },
18 Parallel(Arc<[ParallelBranch]>),
19 Race(Arc<[ParallelBranch]>),
20 Timer(WorkflowWait),
21 Signal(WorkflowWait),
22 SignalOrTimeout(WorkflowWait),
23 Interrupt(String),
24}
25
26pub(crate) struct WorkflowNode {
27 pub(crate) id: StepId,
28 pub(crate) capabilities: CapabilitySet,
29 pub(crate) kind: WorkflowNodeKind,
30}
31
32impl WorkflowNode {
33 pub(crate) async fn execute(
34 &self,
35 input: serde_json::Value,
36 run: &runifold_core::RunContext,
37 ) -> Result<(serde_json::Value, Option<bool>), WorkflowStepError> {
38 match &self.kind {
39 WorkflowNodeKind::Step(step) => {
40 step.execute(input, run).await.map(|output| (output, None))
41 }
42 WorkflowNodeKind::Branch {
43 condition,
44 when_true,
45 when_false,
46 } => {
47 let selected = condition.evaluate(&input)?;
48 let step = if selected { when_true } else { when_false };
49 step.execute(input, run)
50 .await
51 .map(|output| (output, Some(selected)))
52 }
53 WorkflowNodeKind::Parallel(_)
54 | WorkflowNodeKind::Race(_)
55 | WorkflowNodeKind::Timer(_)
56 | WorkflowNodeKind::Signal(_)
57 | WorkflowNodeKind::SignalOrTimeout(_)
58 | WorkflowNodeKind::Interrupt(_) => {
59 unreachable!("concurrent nodes use their dedicated scheduler")
60 }
61 }
62 }
63}
64
65pub struct ParallelBranch {
67 pub(crate) id: String,
68 pub(crate) step: Arc<dyn WorkflowStep>,
69 pub(crate) capabilities: CapabilitySet,
70 pub(crate) reservation: Usage,
71}
72
73impl ParallelBranch {
74 pub fn step<S>(
76 id: impl Into<String>,
77 step: S,
78 capabilities: CapabilitySet,
79 reservation: Usage,
80 ) -> Self
81 where
82 S: WorkflowStep + 'static,
83 {
84 Self {
85 id: id.into(),
86 step: Arc::new(step),
87 capabilities,
88 reservation,
89 }
90 }
91
92 pub fn agent(
94 id: impl Into<String>,
95 agent: Arc<Agent>,
96 capabilities: CapabilitySet,
97 reservation: Usage,
98 ) -> Self {
99 Self::step(id, AgentStep::new(agent), capabilities, reservation)
100 }
101}
102
103impl fmt::Debug for ParallelBranch {
104 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
105 formatter
106 .debug_struct("ParallelBranch")
107 .field("id", &self.id)
108 .field("capabilities", &self.capabilities)
109 .field("reservation", &self.reservation)
110 .finish_non_exhaustive()
111 }
112}
113
114#[derive(Clone)]
116pub struct Workflow {
117 pub(crate) name: String,
118 pub(crate) version: u32,
119 pub(crate) nodes: Arc<[WorkflowNode]>,
120}
121
122impl Workflow {
123 pub fn builder(name: impl Into<String>) -> WorkflowBuilder {
125 WorkflowBuilder::new(name)
126 }
127
128 pub fn name(&self) -> &str {
130 &self.name
131 }
132
133 pub const fn version(&self) -> u32 {
135 self.version
136 }
137
138 pub fn step_ids(&self) -> impl ExactSizeIterator<Item = &StepId> {
140 self.nodes.iter().map(|node| &node.id)
141 }
142}
143
144impl fmt::Debug for Workflow {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter
147 .debug_struct("Workflow")
148 .field("name", &self.name)
149 .field("version", &self.version)
150 .field("steps", &self.step_ids().collect::<Vec<_>>())
151 .finish_non_exhaustive()
152 }
153}
154
155pub struct WorkflowBuilder {
157 name: String,
158 version: u32,
159 nodes: Vec<WorkflowNode>,
160 error: Option<WorkflowBuildError>,
161}
162
163impl WorkflowBuilder {
164 pub fn new(name: impl Into<String>) -> Self {
166 let name = name.into();
167 let error = name
168 .trim()
169 .is_empty()
170 .then_some(WorkflowBuildError::EmptyName);
171 Self {
172 name,
173 version: 1,
174 nodes: Vec::new(),
175 error,
176 }
177 }
178
179 #[must_use]
181 pub fn version(mut self, version: u32) -> Self {
182 if version == 0 && self.error.is_none() {
183 self.error = Some(WorkflowBuildError::InvalidVersion);
184 } else {
185 self.version = version;
186 }
187 self
188 }
189
190 #[must_use]
192 pub fn timer(mut self, id: impl Into<String>, delay: Duration) -> Self {
193 match WorkflowWait::timer(delay) {
194 Ok(wait) => self.push_node(id, CapabilitySet::new(), WorkflowNodeKind::Timer(wait)),
195 Err(_) if self.error.is_none() => {
196 self.error = Some(WorkflowBuildError::InvalidTimerDuration);
197 }
198 Err(_) => {}
199 }
200 self
201 }
202
203 #[must_use]
205 pub fn wait_for_signal(mut self, id: impl Into<String>, signal: impl Into<String>) -> Self {
206 match WorkflowSignalName::parse(signal) {
207 Ok(name) => self.push_node(
208 id,
209 CapabilitySet::new(),
210 WorkflowNodeKind::Signal(WorkflowWait::signal(name)),
211 ),
212 Err(_) if self.error.is_none() => {
213 self.error = Some(WorkflowBuildError::InvalidSignalName);
214 }
215 Err(_) => {}
216 }
217 self
218 }
219
220 #[must_use]
222 pub fn wait_for_signal_or_timeout(
223 mut self,
224 id: impl Into<String>,
225 signal: impl Into<String>,
226 timeout: Duration,
227 ) -> Self {
228 let wait = WorkflowSignalName::parse(signal)
229 .map_err(|_| WorkflowBuildError::InvalidSignalName)
230 .and_then(|name| {
231 WorkflowWait::signal_or_timeout(name, timeout)
232 .map_err(|_| WorkflowBuildError::InvalidTimerDuration)
233 });
234 match wait {
235 Ok(wait) => self.push_node(
236 id,
237 CapabilitySet::new(),
238 WorkflowNodeKind::SignalOrTimeout(wait),
239 ),
240 Err(error) if self.error.is_none() => self.error = Some(error),
241 Err(_) => {}
242 }
243 self
244 }
245
246 #[must_use]
252 pub fn interrupt(mut self, id: impl Into<String>, prompt: impl Into<String>) -> Self {
253 let prompt = prompt.into();
254 match WorkflowInterruptRequest::validate_prompt(&prompt) {
255 Ok(()) => self.push_node(
256 id,
257 CapabilitySet::new(),
258 WorkflowNodeKind::Interrupt(prompt),
259 ),
260 Err(_) if self.error.is_none() => {
261 self.error = Some(WorkflowBuildError::InvalidInterruptPrompt);
262 }
263 Err(_) => {}
264 }
265 self
266 }
267
268 #[must_use]
270 pub fn step<S>(mut self, id: impl Into<String>, step: S, capabilities: CapabilitySet) -> Self
271 where
272 S: WorkflowStep + 'static,
273 {
274 self.push_node(id, capabilities, WorkflowNodeKind::Step(Arc::new(step)));
275 self
276 }
277
278 #[must_use]
280 pub fn agent(
281 mut self,
282 id: impl Into<String>,
283 agent: Arc<Agent>,
284 capabilities: CapabilitySet,
285 ) -> Self {
286 self.push_node(
287 id,
288 capabilities,
289 WorkflowNodeKind::Step(Arc::new(AgentStep::new(agent))),
290 );
291 self
292 }
293
294 #[must_use]
296 pub fn branch<C, T, F>(
297 mut self,
298 id: impl Into<String>,
299 condition: C,
300 when_true: T,
301 when_false: F,
302 capabilities: CapabilitySet,
303 ) -> Self
304 where
305 C: WorkflowCondition + 'static,
306 T: WorkflowStep + 'static,
307 F: WorkflowStep + 'static,
308 {
309 self.push_node(
310 id,
311 capabilities,
312 WorkflowNodeKind::Branch {
313 condition: Arc::new(condition),
314 when_true: Arc::new(when_true),
315 when_false: Arc::new(when_false),
316 },
317 );
318 self
319 }
320
321 #[must_use]
326 pub fn parallel(
327 mut self,
328 id: impl Into<String>,
329 branches: impl IntoIterator<Item = ParallelBranch>,
330 ) -> Self {
331 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
332 if branches.len() < 2 {
333 self.error = Some(WorkflowBuildError::TooFewParallelBranches(id));
334 return self;
335 }
336 self.nodes.push(WorkflowNode {
337 id,
338 capabilities: CapabilitySet::new(),
339 kind: WorkflowNodeKind::Parallel(branches.into()),
340 });
341 }
342 self
343 }
344
345 #[must_use]
351 pub fn race(
352 mut self,
353 id: impl Into<String>,
354 branches: impl IntoIterator<Item = ParallelBranch>,
355 ) -> Self {
356 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
357 if branches.len() < 2 {
358 self.error = Some(WorkflowBuildError::TooFewRaceBranches(id));
359 return self;
360 }
361 for branch in &branches {
362 if let Some(capability) = branch.capabilities.iter().find(|capability| {
363 !matches!(capability.effect, EffectClass::Pure | EffectClass::ReadOnly)
364 }) {
365 let branch = match StepId::parse(branch.id.clone()) {
366 Ok(branch) => branch,
367 Err(branch) => {
368 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch));
369 return self;
370 }
371 };
372 self.error = Some(WorkflowBuildError::UnsafeRaceCapability {
373 step: id,
374 branch,
375 capability: capability.name.clone(),
376 });
377 return self;
378 }
379 }
380 self.nodes.push(WorkflowNode {
381 id,
382 capabilities: CapabilitySet::new(),
383 kind: WorkflowNodeKind::Race(branches.into()),
384 });
385 }
386 self
387 }
388
389 pub fn build(self) -> Result<Workflow, WorkflowBuildError> {
396 if let Some(error) = self.error {
397 return Err(error);
398 }
399 if self.nodes.is_empty() {
400 return Err(WorkflowBuildError::NoSteps);
401 }
402 Ok(Workflow {
403 name: self.name,
404 version: self.version,
405 nodes: self.nodes.into(),
406 })
407 }
408
409 fn push_node(
410 &mut self,
411 id: impl Into<String>,
412 capabilities: CapabilitySet,
413 kind: WorkflowNodeKind,
414 ) {
415 if self.error.is_some() {
416 return;
417 }
418 let id = match StepId::parse(id) {
419 Ok(id) => id,
420 Err(id) => {
421 self.error = Some(WorkflowBuildError::InvalidStepId(id));
422 return;
423 }
424 };
425 if self.nodes.iter().any(|node| node.id == id) {
426 self.error = Some(WorkflowBuildError::DuplicateStep(id));
427 return;
428 }
429 self.nodes.push(WorkflowNode {
430 id,
431 capabilities,
432 kind,
433 });
434 }
435
436 fn validate_concurrent_branches(
437 &mut self,
438 id: impl Into<String>,
439 branches: impl IntoIterator<Item = ParallelBranch>,
440 ) -> Option<(StepId, Vec<ParallelBranch>)> {
441 if self.error.is_some() {
442 return None;
443 }
444 let id = match StepId::parse(id) {
445 Ok(id) => id,
446 Err(id) => {
447 self.error = Some(WorkflowBuildError::InvalidStepId(id));
448 return None;
449 }
450 };
451 if self.nodes.iter().any(|node| node.id == id) {
452 self.error = Some(WorkflowBuildError::DuplicateStep(id));
453 return None;
454 }
455 let mut validated = Vec::new();
456 for branch in branches {
457 let branch_id = match StepId::parse(branch.id) {
458 Ok(branch_id) => branch_id,
459 Err(branch_id) => {
460 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch_id));
461 return None;
462 }
463 };
464 if validated
465 .iter()
466 .any(|existing: &ParallelBranch| existing.id == branch_id.as_str())
467 {
468 self.error = Some(WorkflowBuildError::DuplicateParallelBranch {
469 step: id,
470 branch: branch_id,
471 });
472 return None;
473 }
474 validated.push(ParallelBranch {
475 id: branch_id.to_string(),
476 ..branch
477 });
478 }
479 Some((id, validated))
480 }
481}
482
483impl fmt::Debug for WorkflowBuilder {
484 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
485 formatter
486 .debug_struct("WorkflowBuilder")
487 .field("name", &self.name)
488 .field("version", &self.version)
489 .field(
490 "steps",
491 &self.nodes.iter().map(|node| &node.id).collect::<Vec<_>>(),
492 )
493 .field("error", &self.error)
494 .finish()
495 }
496}