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    pub fn exit_code(&self) -> i32 {
93        match self {
94            StepOutput::Cmd(o) => o.exit_code,
95            _ => 0,
96        }
97    }
98
99    /// Whether the step succeeded
100    pub fn success(&self) -> bool {
101        match self {
102            StepOutput::Cmd(o) => o.exit_code == 0,
103            StepOutput::Gate(o) => o.passed,
104            _ => true,
105        }
106    }
107
108    /// Split text into lines
109    pub fn lines(&self) -> Vec<&str> {
110        self.text()
111            .lines()
112            .filter(|l| !l.is_empty())
113            .collect()
114    }
115
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CmdOutput {
120    pub stdout: String,
121    pub stderr: String,
122    pub exit_code: i32,
123    #[serde(
124        serialize_with = "serialize_duration",
125        deserialize_with = "deserialize_duration"
126    )]
127    pub duration: Duration,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct AgentOutput {
132    pub response: String,
133    pub session_id: Option<String>,
134    pub stats: AgentStats,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138pub struct AgentStats {
139    #[serde(
140        serialize_with = "serialize_duration",
141        deserialize_with = "deserialize_duration"
142    )]
143    pub duration: Duration,
144    pub input_tokens: u64,
145    pub output_tokens: u64,
146    pub cost_usd: f64,
147    pub turns: u32,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ChatOutput {
152    pub response: String,
153    pub model: String,
154    pub input_tokens: u64,
155    pub output_tokens: u64,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct GateOutput {
160    pub passed: bool,
161    pub message: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ScopeOutput {
166    pub iterations: Vec<IterationOutput>,
167    pub final_value: Option<Box<StepOutput>>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct IterationOutput {
172    pub index: usize,
173    pub output: StepOutput,
174}
175
176fn serialize_duration<S>(d: &Duration, s: S) -> Result<S::Ok, S::Error>
177where
178    S: serde::Serializer,
179{
180    s.serialize_f64(d.as_secs_f64())
181}
182
183fn deserialize_duration<'de, D>(d: D) -> Result<Duration, D::Error>
184where
185    D: serde::Deserializer<'de>,
186{
187    let secs = f64::deserialize(d)?;
188    Ok(Duration::from_secs_f64(secs))
189}