1use std::{fmt, sync::Arc, time::Duration};
2
3use runifold_agent::Agent;
4use runifold_core::{CapabilitySet, EffectClass, Usage};
5
6use crate::remediation::RepairableNode;
7use crate::{
8 AgentStep, StepId, WorkflowBuildError, WorkflowCondition, WorkflowInterruptRequest,
9 WorkflowRemediationPolicy, WorkflowReviewer, WorkflowSignalName, WorkflowStep,
10 WorkflowStepError, WorkflowWait,
11};
12
13pub(crate) enum WorkflowNodeKind {
14 Step(Arc<dyn WorkflowStep>),
15 Branch {
16 condition: Arc<dyn WorkflowCondition>,
17 when_true: Arc<dyn WorkflowStep>,
18 when_false: Arc<dyn WorkflowStep>,
19 },
20 Parallel(Arc<[ParallelBranch]>),
21 Race(Arc<[ParallelBranch]>),
22 Repairable(RepairableNode),
23 Timer(WorkflowWait),
24 Signal(WorkflowWait),
25 SignalOrTimeout(WorkflowWait),
26 Interrupt(String),
27}
28
29pub(crate) struct WorkflowNode {
30 pub(crate) id: StepId,
31 pub(crate) capabilities: CapabilitySet,
32 pub(crate) kind: WorkflowNodeKind,
33}
34
35impl WorkflowNode {
36 pub(crate) async fn execute(
37 &self,
38 input: serde_json::Value,
39 run: &runifold_core::RunContext,
40 ) -> Result<(serde_json::Value, Option<bool>), WorkflowStepError> {
41 match &self.kind {
42 WorkflowNodeKind::Step(step) => {
43 step.execute(input, run).await.map(|output| (output, None))
44 }
45 WorkflowNodeKind::Branch {
46 condition,
47 when_true,
48 when_false,
49 } => {
50 let selected = condition.evaluate(&input)?;
51 let step = if selected { when_true } else { when_false };
52 step.execute(input, run)
53 .await
54 .map(|output| (output, Some(selected)))
55 }
56 WorkflowNodeKind::Parallel(_)
57 | WorkflowNodeKind::Race(_)
58 | WorkflowNodeKind::Repairable(_)
59 | WorkflowNodeKind::Timer(_)
60 | WorkflowNodeKind::Signal(_)
61 | WorkflowNodeKind::SignalOrTimeout(_)
62 | WorkflowNodeKind::Interrupt(_) => {
63 unreachable!("concurrent nodes use their dedicated scheduler")
64 }
65 }
66 }
67}
68
69pub struct ParallelBranch {
71 pub(crate) id: String,
72 pub(crate) step: Arc<dyn WorkflowStep>,
73 pub(crate) capabilities: CapabilitySet,
74 pub(crate) reservation: Usage,
75}
76
77impl ParallelBranch {
78 pub fn step<S>(
80 id: impl Into<String>,
81 step: S,
82 capabilities: CapabilitySet,
83 reservation: Usage,
84 ) -> Self
85 where
86 S: WorkflowStep + 'static,
87 {
88 Self {
89 id: id.into(),
90 step: Arc::new(step),
91 capabilities,
92 reservation,
93 }
94 }
95
96 pub fn agent(
98 id: impl Into<String>,
99 agent: Arc<Agent>,
100 capabilities: CapabilitySet,
101 reservation: Usage,
102 ) -> Self {
103 Self::step(id, AgentStep::new(agent), capabilities, reservation)
104 }
105}
106
107impl fmt::Debug for ParallelBranch {
108 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109 formatter
110 .debug_struct("ParallelBranch")
111 .field("id", &self.id)
112 .field("capabilities", &self.capabilities)
113 .field("reservation", &self.reservation)
114 .finish_non_exhaustive()
115 }
116}
117
118#[derive(Clone)]
120pub struct Workflow {
121 pub(crate) name: String,
122 pub(crate) version: u32,
123 pub(crate) nodes: Arc<[WorkflowNode]>,
124}
125
126impl Workflow {
127 pub fn builder(name: impl Into<String>) -> WorkflowBuilder {
129 WorkflowBuilder::new(name)
130 }
131
132 pub fn name(&self) -> &str {
134 &self.name
135 }
136
137 pub const fn version(&self) -> u32 {
139 self.version
140 }
141
142 pub fn step_ids(&self) -> impl ExactSizeIterator<Item = &StepId> {
144 self.nodes.iter().map(|node| &node.id)
145 }
146}
147
148impl fmt::Debug for Workflow {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 formatter
151 .debug_struct("Workflow")
152 .field("name", &self.name)
153 .field("version", &self.version)
154 .field("steps", &self.step_ids().collect::<Vec<_>>())
155 .finish_non_exhaustive()
156 }
157}
158
159pub struct WorkflowBuilder {
161 name: String,
162 version: u32,
163 nodes: Vec<WorkflowNode>,
164 error: Option<WorkflowBuildError>,
165}
166
167impl WorkflowBuilder {
168 pub fn new(name: impl Into<String>) -> Self {
170 let name = name.into();
171 let error = name
172 .trim()
173 .is_empty()
174 .then_some(WorkflowBuildError::EmptyName);
175 Self {
176 name,
177 version: 1,
178 nodes: Vec::new(),
179 error,
180 }
181 }
182
183 #[must_use]
185 pub fn version(mut self, version: u32) -> Self {
186 if version == 0 && self.error.is_none() {
187 self.error = Some(WorkflowBuildError::InvalidVersion);
188 } else {
189 self.version = version;
190 }
191 self
192 }
193
194 #[must_use]
201 pub fn repairable_step<S, R>(
202 mut self,
203 id: impl Into<String>,
204 generator: S,
205 reviewer: R,
206 policy: WorkflowRemediationPolicy,
207 generator_capabilities: CapabilitySet,
208 reviewer_capabilities: CapabilitySet,
209 ) -> Self
210 where
211 S: WorkflowStep + 'static,
212 R: WorkflowReviewer + 'static,
213 {
214 self.push_node(
215 id,
216 generator_capabilities,
217 WorkflowNodeKind::Repairable(RepairableNode {
218 generator: Arc::new(generator),
219 reviewer: Arc::new(reviewer),
220 reviewer_capabilities,
221 policy,
222 }),
223 );
224 self
225 }
226
227 #[must_use]
229 pub fn repairable_agent<R>(
230 self,
231 id: impl Into<String>,
232 agent: Arc<Agent>,
233 reviewer: R,
234 policy: WorkflowRemediationPolicy,
235 generator_capabilities: CapabilitySet,
236 reviewer_capabilities: CapabilitySet,
237 ) -> Self
238 where
239 R: WorkflowReviewer + 'static,
240 {
241 self.repairable_step(
242 id,
243 AgentStep::new(agent),
244 reviewer,
245 policy,
246 generator_capabilities,
247 reviewer_capabilities,
248 )
249 }
250
251 #[must_use]
253 pub fn timer(mut self, id: impl Into<String>, delay: Duration) -> Self {
254 match WorkflowWait::timer(delay) {
255 Ok(wait) => self.push_node(id, CapabilitySet::new(), WorkflowNodeKind::Timer(wait)),
256 Err(_) if self.error.is_none() => {
257 self.error = Some(WorkflowBuildError::InvalidTimerDuration);
258 }
259 Err(_) => {}
260 }
261 self
262 }
263
264 #[must_use]
266 pub fn wait_for_signal(mut self, id: impl Into<String>, signal: impl Into<String>) -> Self {
267 match WorkflowSignalName::parse(signal) {
268 Ok(name) => self.push_node(
269 id,
270 CapabilitySet::new(),
271 WorkflowNodeKind::Signal(WorkflowWait::signal(name)),
272 ),
273 Err(_) if self.error.is_none() => {
274 self.error = Some(WorkflowBuildError::InvalidSignalName);
275 }
276 Err(_) => {}
277 }
278 self
279 }
280
281 #[must_use]
283 pub fn wait_for_signal_or_timeout(
284 mut self,
285 id: impl Into<String>,
286 signal: impl Into<String>,
287 timeout: Duration,
288 ) -> Self {
289 let wait = WorkflowSignalName::parse(signal)
290 .map_err(|_| WorkflowBuildError::InvalidSignalName)
291 .and_then(|name| {
292 WorkflowWait::signal_or_timeout(name, timeout)
293 .map_err(|_| WorkflowBuildError::InvalidTimerDuration)
294 });
295 match wait {
296 Ok(wait) => self.push_node(
297 id,
298 CapabilitySet::new(),
299 WorkflowNodeKind::SignalOrTimeout(wait),
300 ),
301 Err(error) if self.error.is_none() => self.error = Some(error),
302 Err(_) => {}
303 }
304 self
305 }
306
307 #[must_use]
313 pub fn interrupt(mut self, id: impl Into<String>, prompt: impl Into<String>) -> Self {
314 let prompt = prompt.into();
315 match WorkflowInterruptRequest::validate_prompt(&prompt) {
316 Ok(()) => self.push_node(
317 id,
318 CapabilitySet::new(),
319 WorkflowNodeKind::Interrupt(prompt),
320 ),
321 Err(_) if self.error.is_none() => {
322 self.error = Some(WorkflowBuildError::InvalidInterruptPrompt);
323 }
324 Err(_) => {}
325 }
326 self
327 }
328
329 #[must_use]
331 pub fn step<S>(mut self, id: impl Into<String>, step: S, capabilities: CapabilitySet) -> Self
332 where
333 S: WorkflowStep + 'static,
334 {
335 self.push_node(id, capabilities, WorkflowNodeKind::Step(Arc::new(step)));
336 self
337 }
338
339 #[must_use]
341 pub fn agent(
342 mut self,
343 id: impl Into<String>,
344 agent: Arc<Agent>,
345 capabilities: CapabilitySet,
346 ) -> Self {
347 self.push_node(
348 id,
349 capabilities,
350 WorkflowNodeKind::Step(Arc::new(AgentStep::new(agent))),
351 );
352 self
353 }
354
355 #[must_use]
357 pub fn branch<C, T, F>(
358 mut self,
359 id: impl Into<String>,
360 condition: C,
361 when_true: T,
362 when_false: F,
363 capabilities: CapabilitySet,
364 ) -> Self
365 where
366 C: WorkflowCondition + 'static,
367 T: WorkflowStep + 'static,
368 F: WorkflowStep + 'static,
369 {
370 self.push_node(
371 id,
372 capabilities,
373 WorkflowNodeKind::Branch {
374 condition: Arc::new(condition),
375 when_true: Arc::new(when_true),
376 when_false: Arc::new(when_false),
377 },
378 );
379 self
380 }
381
382 #[must_use]
387 pub fn parallel(
388 mut self,
389 id: impl Into<String>,
390 branches: impl IntoIterator<Item = ParallelBranch>,
391 ) -> Self {
392 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
393 if branches.len() < 2 {
394 self.error = Some(WorkflowBuildError::TooFewParallelBranches(id));
395 return self;
396 }
397 self.nodes.push(WorkflowNode {
398 id,
399 capabilities: CapabilitySet::new(),
400 kind: WorkflowNodeKind::Parallel(branches.into()),
401 });
402 }
403 self
404 }
405
406 #[must_use]
412 pub fn race(
413 mut self,
414 id: impl Into<String>,
415 branches: impl IntoIterator<Item = ParallelBranch>,
416 ) -> Self {
417 if let Some((id, branches)) = self.validate_concurrent_branches(id, branches) {
418 if branches.len() < 2 {
419 self.error = Some(WorkflowBuildError::TooFewRaceBranches(id));
420 return self;
421 }
422 for branch in &branches {
423 if let Some(capability) = branch.capabilities.iter().find(|capability| {
424 !matches!(capability.effect, EffectClass::Pure | EffectClass::ReadOnly)
425 }) {
426 let branch = match StepId::parse(branch.id.clone()) {
427 Ok(branch) => branch,
428 Err(branch) => {
429 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch));
430 return self;
431 }
432 };
433 self.error = Some(WorkflowBuildError::UnsafeRaceCapability {
434 step: id,
435 branch,
436 capability: capability.name.clone(),
437 });
438 return self;
439 }
440 }
441 self.nodes.push(WorkflowNode {
442 id,
443 capabilities: CapabilitySet::new(),
444 kind: WorkflowNodeKind::Race(branches.into()),
445 });
446 }
447 self
448 }
449
450 pub fn build(self) -> Result<Workflow, WorkflowBuildError> {
457 if let Some(error) = self.error {
458 return Err(error);
459 }
460 if self.nodes.is_empty() {
461 return Err(WorkflowBuildError::NoSteps);
462 }
463 Ok(Workflow {
464 name: self.name,
465 version: self.version,
466 nodes: self.nodes.into(),
467 })
468 }
469
470 fn push_node(
471 &mut self,
472 id: impl Into<String>,
473 capabilities: CapabilitySet,
474 kind: WorkflowNodeKind,
475 ) {
476 if self.error.is_some() {
477 return;
478 }
479 let id = match StepId::parse(id) {
480 Ok(id) => id,
481 Err(id) => {
482 self.error = Some(WorkflowBuildError::InvalidStepId(id));
483 return;
484 }
485 };
486 if self.nodes.iter().any(|node| node.id == id) {
487 self.error = Some(WorkflowBuildError::DuplicateStep(id));
488 return;
489 }
490 self.nodes.push(WorkflowNode {
491 id,
492 capabilities,
493 kind,
494 });
495 }
496
497 fn validate_concurrent_branches(
498 &mut self,
499 id: impl Into<String>,
500 branches: impl IntoIterator<Item = ParallelBranch>,
501 ) -> Option<(StepId, Vec<ParallelBranch>)> {
502 if self.error.is_some() {
503 return None;
504 }
505 let id = match StepId::parse(id) {
506 Ok(id) => id,
507 Err(id) => {
508 self.error = Some(WorkflowBuildError::InvalidStepId(id));
509 return None;
510 }
511 };
512 if self.nodes.iter().any(|node| node.id == id) {
513 self.error = Some(WorkflowBuildError::DuplicateStep(id));
514 return None;
515 }
516 let mut validated = Vec::new();
517 for branch in branches {
518 let branch_id = match StepId::parse(branch.id) {
519 Ok(branch_id) => branch_id,
520 Err(branch_id) => {
521 self.error = Some(WorkflowBuildError::InvalidParallelBranchId(branch_id));
522 return None;
523 }
524 };
525 if validated
526 .iter()
527 .any(|existing: &ParallelBranch| existing.id == branch_id.as_str())
528 {
529 self.error = Some(WorkflowBuildError::DuplicateParallelBranch {
530 step: id,
531 branch: branch_id,
532 });
533 return None;
534 }
535 validated.push(ParallelBranch {
536 id: branch_id.to_string(),
537 ..branch
538 });
539 }
540 Some((id, validated))
541 }
542}
543
544impl fmt::Debug for WorkflowBuilder {
545 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
546 formatter
547 .debug_struct("WorkflowBuilder")
548 .field("name", &self.name)
549 .field("version", &self.version)
550 .field(
551 "steps",
552 &self.nodes.iter().map(|node| &node.id).collect::<Vec<_>>(),
553 )
554 .field("error", &self.error)
555 .finish()
556 }
557}