Skip to main content

minion_engine/steps/
mod.rs

1pub mod agent;
2pub mod call;
3pub mod chat;
4pub mod cmd;
5pub mod gate;
6pub mod map;
7pub mod parallel;
8pub mod repeat;
9pub mod script;
10pub mod template_step;
11
12use std::sync::Arc;
13use std::time::Duration;
14
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use tokio::sync::Mutex;
18
19use crate::config::StepConfig;
20use crate::engine::context::Context;
21use crate::error::StepError;
22use crate::sandbox::docker::DockerSandbox;
23use crate::workflow::schema::StepDef;
24
25/// Shared reference to a Docker sandbox (None when sandbox is disabled)
26pub type SharedSandbox = Option<Arc<Mutex<DockerSandbox>>>;
27
28/// Typed parsed value produced by output parsing
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ParsedValue {
32    Text(String),
33    Json(serde_json::Value),
34    Integer(i64),
35    Lines(Vec<String>),
36    Boolean(bool),
37}
38
39/// Trait that each step type implements
40#[async_trait]
41pub trait StepExecutor: Send + Sync {
42    async fn execute(
43        &self,
44        step_def: &StepDef,
45        config: &StepConfig,
46        context: &Context,
47    ) -> Result<StepOutput, StepError>;
48}
49
50/// Extended trait for executors that can run inside a sandbox
51#[async_trait]
52pub trait SandboxAwareExecutor: Send + Sync {
53    async fn execute_sandboxed(
54        &self,
55        step_def: &StepDef,
56        config: &StepConfig,
57        context: &Context,
58        sandbox: &SharedSandbox,
59    ) -> Result<StepOutput, StepError>;
60}
61
62/// Result of any executed step
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum StepOutput {
66    Cmd(CmdOutput),
67    Agent(AgentOutput),
68    Chat(ChatOutput),
69    Gate(GateOutput),
70    Scope(ScopeOutput),
71    Empty,
72}
73
74impl StepOutput {
75    /// Main text of the output
76    pub fn text(&self) -> &str {
77        match self {
78            StepOutput::Cmd(o) => &o.stdout,
79            StepOutput::Agent(o) => &o.response,
80            StepOutput::Chat(o) => &o.response,
81            StepOutput::Gate(o) => o.message.as_deref().unwrap_or(""),
82            StepOutput::Scope(o) => o
83                .final_value
84                .as_ref()
85                .map(|v| v.text())
86                .unwrap_or(""),
87            StepOutput::Empty => "",
88        }
89    }
90
91    /// Exit code (only meaningful for cmd, 0 for others)
92    #[allow(dead_code)]
93    pub fn exit_code(&self) -> i32 {
94        match self {
95            StepOutput::Cmd(o) => o.exit_code,
96            _ => 0,
97        }
98    }
99
100    /// Whether the step succeeded
101    #[allow(dead_code)]
102    pub fn success(&self) -> bool {
103        match self {
104            StepOutput::Cmd(o) => o.exit_code == 0,
105            StepOutput::Gate(o) => o.passed,
106            _ => true,
107        }
108    }
109
110    /// Split text into lines
111    #[allow(dead_code)]
112    pub fn lines(&self) -> Vec<&str> {
113        self.text()
114            .lines()
115            .filter(|l| !l.is_empty())
116            .collect()
117    }
118
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CmdOutput {
123    pub stdout: String,
124    pub stderr: String,
125    pub exit_code: i32,
126    #[serde(
127        serialize_with = "serialize_duration",
128        deserialize_with = "deserialize_duration"
129    )]
130    pub duration: Duration,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct AgentOutput {
135    pub response: String,
136    pub session_id: Option<String>,
137    pub stats: AgentStats,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, Default)]
141pub struct AgentStats {
142    #[serde(
143        serialize_with = "serialize_duration",
144        deserialize_with = "deserialize_duration"
145    )]
146    pub duration: Duration,
147    pub input_tokens: u64,
148    pub output_tokens: u64,
149    pub cost_usd: f64,
150    pub turns: u32,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ChatOutput {
155    pub response: String,
156    pub model: String,
157    pub input_tokens: u64,
158    pub output_tokens: u64,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct GateOutput {
163    pub passed: bool,
164    pub message: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ScopeOutput {
169    pub iterations: Vec<IterationOutput>,
170    pub final_value: Option<Box<StepOutput>>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct IterationOutput {
175    pub index: usize,
176    pub output: StepOutput,
177}
178
179fn serialize_duration<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
180where
181    S: serde::Serializer,
182{
183    s.serialize_f64(d.as_secs_f64())
184}
185
186fn deserialize_duration<'de, D>(d: D) -> Result<Duration, D::Error>
187where
188    D: serde::Deserializer<'de>,
189{
190    let secs = f64::deserialize(d)?;
191    Ok(Duration::from_secs_f64(secs))
192}