Skip to main content

forge_guardrails/core/
steps.rs

1use indexmap::{IndexMap, IndexSet};
2use serde_json::Value;
3
4/// Result of checking prerequisites against prior tool executions.
5#[derive(Debug, Clone, PartialEq)]
6pub struct PrerequisiteCheck {
7    /// Whether the prerequisites are satisfied.
8    pub satisfied: bool,
9    /// List of missing prerequisite step names or identifiers.
10    pub missing: Vec<String>,
11}
12
13impl PrerequisiteCheck {
14    /// Returns a satisfied prerequisite check.
15    pub fn satisfied() -> Self {
16        Self {
17            satisfied: true,
18            missing: Vec::new(),
19        }
20    }
21
22    /// Returns an unsatisfied prerequisite check containing the missing steps.
23    pub fn unsatisfied(missing: Vec<String>) -> Self {
24        Self {
25            satisfied: false,
26            missing,
27        }
28    }
29}
30
31/// A prerequisite specification that can be checked against prior executions.
32#[derive(Debug, Clone, PartialEq)]
33pub enum Prerequisite {
34    /// Prerequisite satisfied solely by the occurrence of a tool call.
35    NameOnly(String),
36    /// Prerequisite satisfied by a tool call only when specific arguments match.
37    ArgMatched {
38        /// Name of the tool.
39        tool: String,
40        /// Description or key matching parameter criteria.
41        match_arg: String,
42    },
43}
44
45/// Tracks required steps and executed tools in a workflow run.
46pub struct StepTracker {
47    required_steps: Vec<String>,
48    completed_steps: IndexSet<String>,
49    executed_tools: IndexMap<String, Vec<IndexMap<String, Value>>>,
50}
51
52impl StepTracker {
53    /// Creates a new `StepTracker` with the given required steps.
54    pub fn new(required_steps: Vec<String>) -> Self {
55        Self {
56            required_steps,
57            completed_steps: IndexSet::new(),
58            executed_tools: IndexMap::new(),
59        }
60    }
61
62    /// Record that a tool was executed with the given arguments.
63    ///
64    /// completed_steps is idempotent: recording the same tool twice does not
65    /// add a duplicate. executed_tools accumulates all invocations.
66    pub fn record(&mut self, tool_name: &str, args: Option<&IndexMap<String, Value>>) {
67        self.completed_steps.insert(tool_name.to_string());
68        let empty = IndexMap::new();
69        let args = args.unwrap_or(&empty);
70        self.executed_tools
71            .entry(tool_name.to_string())
72            .or_default()
73            .push(args.clone());
74    }
75
76    /// Returns true when all required steps have been recorded.
77    /// An empty required_steps list is always satisfied.
78    pub fn is_satisfied(&self) -> bool {
79        self.required_steps
80            .iter()
81            .all(|step| self.completed_steps.contains(step))
82    }
83
84    /// Returns required steps not yet completed, in declaration order.
85    pub fn pending(&self) -> Vec<String> {
86        self.required_steps
87            .iter()
88            .filter(|step| !self.completed_steps.contains(step.as_str()))
89            .cloned()
90            .collect()
91    }
92
93    /// Check whether the prerequisites for a tool are satisfied given current args.
94    pub fn check_prerequisites(
95        &self,
96        _tool_name: &str,
97        args: &IndexMap<String, Value>,
98        prerequisites: &[Prerequisite],
99    ) -> PrerequisiteCheck {
100        let mut missing = Vec::new();
101
102        for prereq in prerequisites {
103            match prereq {
104                Prerequisite::NameOnly(tool) => {
105                    if !self.completed_steps.contains(tool.as_str()) {
106                        missing.push(tool.clone());
107                    }
108                }
109                Prerequisite::ArgMatched { tool, match_arg } => {
110                    let current_val = args.get(match_arg).cloned().unwrap_or(Value::Null);
111                    let found = self
112                        .executed_tools
113                        .get(tool.as_str())
114                        .map(|invocations| {
115                            invocations.iter().any(|inv| {
116                                inv.get(match_arg).cloned().unwrap_or(Value::Null) == current_val
117                            })
118                        })
119                        .unwrap_or(false);
120                    if !found {
121                        missing.push(tool.clone());
122                    }
123                }
124            }
125        }
126
127        if missing.is_empty() {
128            PrerequisiteCheck::satisfied()
129        } else {
130            PrerequisiteCheck::unsatisfied(missing)
131        }
132    }
133
134    /// Returns a human-readable summary of completed steps.
135    ///
136    /// If no steps are completed, returns `"[No steps completed yet]"`.
137    /// Otherwise returns `"[Steps completed: names]"` in execution order.
138    pub fn summary_hint(&self) -> String {
139        if self.completed_steps.is_empty() {
140            "[No steps completed yet]".to_string()
141        } else {
142            let names: Vec<&str> = self.completed_steps.iter().map(|s| s.as_str()).collect();
143            format!("[Steps completed: {}]", names.join(", "))
144        }
145    }
146
147    /// Number of completed steps (unique tool names).
148    pub fn completed_count(&self) -> usize {
149        self.completed_steps.len()
150    }
151
152    /// Returns a reference to the required steps list.
153    pub fn required_steps(&self) -> &[String] {
154        &self.required_steps
155    }
156}