use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowDef {
pub name: String,
#[serde(default)]
pub title: Option<String>,
pub description: String,
pub trigger: WorkflowTrigger,
#[serde(default)]
pub targets: Vec<String>,
#[serde(default)]
pub group: Option<String>,
pub inputs: Vec<InputDecl>,
pub body: Vec<WorkflowNode>,
pub always: Vec<WorkflowNode>,
pub source_path: String,
}
impl WorkflowDef {
pub fn display_name(&self) -> &str {
self.title.as_deref().unwrap_or(&self.name)
}
pub fn total_nodes(&self) -> usize {
count_nodes(&self.body) + count_nodes(&self.always)
}
pub fn top_level_steps(&self) -> usize {
self.body.len() + self.always.len()
}
pub fn max_iterations_for_step(&self, step_name: &str) -> Option<u32> {
fn search(nodes: &[WorkflowNode], name: &str) -> Option<u32> {
for node in nodes {
match node {
WorkflowNode::DoWhile(n) => {
if n.step == name {
return Some(n.max_iterations);
}
if let Some(v) = search(&n.body, name) {
return Some(v);
}
}
WorkflowNode::While(n) => {
if n.step == name {
return Some(n.max_iterations);
}
if let Some(v) = search(&n.body, name) {
return Some(v);
}
}
_ => {
if let Some(body) = node.body() {
if let Some(v) = search(body, name) {
return Some(v);
}
}
}
}
}
None
}
search(&self.body, step_name).or_else(|| search(&self.always, step_name))
}
pub fn collect_all_snippet_refs(&self) -> Vec<String> {
let mut refs = collect_snippet_refs(&self.body);
refs.extend(collect_snippet_refs(&self.always));
refs.sort();
refs.dedup();
refs
}
pub fn collect_all_schema_refs(&self) -> Vec<String> {
let mut refs = collect_schema_refs(&self.body);
refs.extend(collect_schema_refs(&self.always));
refs.sort();
refs.dedup();
refs
}
pub fn collect_all_agent_refs(&self) -> Vec<AgentRef> {
let mut refs = collect_agent_names(&self.body);
refs.extend(collect_agent_names(&self.always));
refs.sort();
refs.dedup();
refs
}
pub fn collect_all_bot_names(&self) -> Vec<String> {
let mut names = collect_bot_names(&self.body);
names.extend(collect_bot_names(&self.always));
names.sort();
names.dedup();
names
}
pub fn collect_all_plugin_dirs(&self) -> Vec<String> {
let mut dirs = collect_plugin_dirs(&self.body);
dirs.extend(collect_plugin_dirs(&self.always));
dirs.sort();
dirs.dedup();
dirs
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowWarning {
pub file: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowTrigger {
Manual,
Pr,
Scheduled,
}
impl std::fmt::Display for WorkflowTrigger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Manual => write!(f, "manual"),
Self::Pr => write!(f, "pr"),
Self::Scheduled => write!(f, "scheduled"),
}
}
}
impl std::str::FromStr for WorkflowTrigger {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"manual" => Ok(Self::Manual),
"pr" => Ok(Self::Pr),
"scheduled" => Ok(Self::Scheduled),
_ => Err(format!("unknown trigger: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum InputType {
#[default]
String,
Boolean,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputDecl {
pub name: String,
pub required: bool,
pub default: Option<String>,
pub description: Option<String>,
#[serde(default)]
pub input_type: InputType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WorkflowNode {
Call(CallNode),
CallWorkflow(CallWorkflowNode),
If(IfNode),
Unless(UnlessNode),
While(WhileNode),
DoWhile(DoWhileNode),
Do(DoNode),
Parallel(ParallelNode),
Gate(GateNode),
Always(AlwaysNode),
Script(ScriptNode),
ForEach(ForEachNode),
}
impl WorkflowNode {
pub fn body(&self) -> Option<&[WorkflowNode]> {
match self {
WorkflowNode::If(n) => Some(&n.body),
WorkflowNode::Unless(n) => Some(&n.body),
WorkflowNode::While(n) => Some(&n.body),
WorkflowNode::DoWhile(n) => Some(&n.body),
WorkflowNode::Do(n) => Some(&n.body),
WorkflowNode::Always(n) => Some(&n.body),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForEachNode {
pub name: String,
pub over: ForeachOver,
pub scope: Option<HashMap<String, String>>,
#[serde(default)]
pub filter: HashMap<String, String>,
pub ordered: bool,
pub on_cycle: OnCycle,
pub max_parallel: u32,
pub workflow: String,
#[serde(default)]
pub inputs: HashMap<String, String>,
pub on_child_fail: OnChildFail,
}
pub type ForeachOver = String;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnChildFail {
Halt,
Continue,
SkipDependents,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnCycle {
Fail,
Warn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptNode {
pub name: String,
pub run: String,
#[serde(default)]
pub env: HashMap<String, String>,
pub timeout: Option<u64>,
#[serde(default)]
pub retries: u32,
pub on_fail: Option<OnFail>,
pub bot_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum OnFail {
Agent(AgentRef),
Continue,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum AgentRef {
Name(String),
Path(String),
}
impl AgentRef {
pub fn label(&self) -> &str {
match self {
Self::Name(s) | Self::Path(s) => s.as_str(),
}
}
pub fn step_key(&self) -> String {
match self {
Self::Name(s) => s.clone(),
Self::Path(s) => Path::new(s)
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or(s.as_str())
.to_string(),
}
}
}
impl std::fmt::Display for AgentRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallNode {
pub agent: AgentRef,
#[serde(default)]
pub retries: u32,
pub on_fail: Option<OnFail>,
pub output: Option<String>,
#[serde(default)]
pub with: Vec<String>,
pub bot_name: Option<String>,
#[serde(default)]
pub plugin_dirs: Vec<String>,
pub timeout: Option<String>,
#[serde(default)]
pub max_turns: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallWorkflowNode {
pub workflow: String,
#[serde(default)]
pub inputs: HashMap<String, String>,
#[serde(default)]
pub retries: u32,
pub on_fail: Option<OnFail>,
pub bot_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Condition {
StepMarker { step: String, marker: String },
BoolInput { input: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IfNode {
pub condition: Condition,
pub body: Vec<WorkflowNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlessNode {
pub condition: Condition,
pub body: Vec<WorkflowNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhileNode {
pub step: String,
pub marker: String,
pub max_iterations: u32,
pub stuck_after: Option<u32>,
pub on_max_iter: OnMaxIter,
pub body: Vec<WorkflowNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoWhileNode {
pub step: String,
pub marker: String,
pub max_iterations: u32,
pub stuck_after: Option<u32>,
pub on_max_iter: OnMaxIter,
pub body: Vec<WorkflowNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoNode {
pub output: Option<String>,
#[serde(default)]
pub with: Vec<String>,
pub body: Vec<WorkflowNode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnMaxIter {
Fail,
Continue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelNode {
#[serde(default = "default_true")]
pub fail_fast: bool,
pub min_success: Option<u32>,
pub calls: Vec<AgentRef>,
pub output: Option<String>,
#[serde(default)]
pub call_outputs: HashMap<String, String>,
#[serde(default)]
pub with: Vec<String>,
#[serde(default)]
pub call_with: HashMap<String, Vec<String>>,
#[serde(default)]
pub call_if: HashMap<String, (String, String)>,
#[serde(default)]
pub call_retries: HashMap<String, u32>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalMode {
#[default]
MinApprovals,
ReviewDecision,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnFailAction {
Fail,
Continue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateConfig {
pub source: String,
pub threshold: u32,
#[serde(default = "default_on_fail")]
pub on_fail_action: OnFailAction,
}
fn default_on_fail() -> OnFailAction {
OnFailAction::Fail
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GateOptions {
Static(HashMap<String, String>),
StepRef(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateNode {
pub name: String,
pub gate_type: GateType,
pub prompt: Option<String>,
#[serde(default = "default_one")]
pub min_approvals: u32,
#[serde(default)]
pub approval_mode: ApprovalMode,
pub timeout_secs: u64,
pub on_timeout: OnTimeout,
pub bot_name: Option<String>,
#[serde(flatten)]
pub quality_gate: Option<QualityGateConfig>,
pub options: Option<GateOptions>,
}
fn default_one() -> u32 {
1
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GateType {
HumanApproval,
HumanReview,
PrApproval,
PrChecks,
QualityGate,
}
impl std::fmt::Display for GateType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HumanApproval => write!(f, "human_approval"),
Self::HumanReview => write!(f, "human_review"),
Self::PrApproval => write!(f, "pr_approval"),
Self::PrChecks => write!(f, "pr_checks"),
Self::QualityGate => write!(f, "quality_gate"),
}
}
}
impl std::str::FromStr for GateType {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"human_approval" => Ok(Self::HumanApproval),
"human_review" => Ok(Self::HumanReview),
"pr_approval" => Ok(Self::PrApproval),
"pr_checks" => Ok(Self::PrChecks),
"quality_gate" => Ok(Self::QualityGate),
_ => Err(format!("unknown gate type: {s}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnTimeout {
Fail,
Continue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlwaysNode {
pub body: Vec<WorkflowNode>,
}
pub(crate) fn count_nodes(nodes: &[WorkflowNode]) -> usize {
let mut count = 0;
for node in nodes {
count += 1;
match node {
WorkflowNode::Parallel(n) => count += n.calls.len(),
_ => {
if let Some(body) = node.body() {
count += count_nodes(body);
}
}
}
}
count
}
pub fn collect_agent_names(nodes: &[WorkflowNode]) -> Vec<AgentRef> {
let mut refs = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(n) => {
refs.push(n.agent.clone());
if let Some(OnFail::Agent(ref a)) = n.on_fail {
refs.push(a.clone());
}
}
WorkflowNode::CallWorkflow(n) => {
if let Some(OnFail::Agent(ref a)) = n.on_fail {
refs.push(a.clone());
}
}
WorkflowNode::Script(n) => {
if let Some(OnFail::Agent(ref a)) = n.on_fail {
refs.push(a.clone());
}
}
WorkflowNode::Parallel(n) => refs.extend(n.calls.iter().cloned()),
_ => {
if let Some(body) = node.body() {
refs.extend(collect_agent_names(body));
}
}
}
}
refs
}
pub(crate) fn collect_snippet_refs(nodes: &[WorkflowNode]) -> Vec<String> {
let mut refs = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(n) => refs.extend(n.with.iter().cloned()),
WorkflowNode::Parallel(n) => {
refs.extend(n.with.iter().cloned());
for extra in n.call_with.values() {
refs.extend(extra.iter().cloned());
}
}
WorkflowNode::Do(n) => {
refs.extend(n.with.iter().cloned());
refs.extend(collect_snippet_refs(&n.body));
}
_ => {
if let Some(body) = node.body() {
refs.extend(collect_snippet_refs(body));
}
}
}
}
refs
}
pub fn collect_workflow_refs(nodes: &[WorkflowNode]) -> Vec<String> {
let mut refs = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(_) | WorkflowNode::Gate(_) | WorkflowNode::Script(_) => {}
WorkflowNode::CallWorkflow(n) => refs.push(n.workflow.clone()),
WorkflowNode::If(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::Unless(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::While(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::DoWhile(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::Do(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::Parallel(_) => {} WorkflowNode::Always(n) => refs.extend(collect_workflow_refs(&n.body)),
WorkflowNode::ForEach(n) => refs.push(n.workflow.clone()),
}
}
refs
}
pub(crate) fn collect_schema_refs(nodes: &[WorkflowNode]) -> Vec<String> {
let mut refs = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(n) => {
if let Some(ref s) = n.output {
refs.push(s.clone());
}
}
WorkflowNode::Do(n) => {
if let Some(ref s) = n.output {
refs.push(s.clone());
}
refs.extend(collect_schema_refs(&n.body));
}
WorkflowNode::Parallel(n) => {
if let Some(ref s) = n.output {
refs.push(s.clone());
}
refs.extend(n.call_outputs.values().cloned());
}
_ => {
if let Some(body) = node.body() {
refs.extend(collect_schema_refs(body));
}
}
}
}
refs
}
pub(crate) fn collect_bot_names(nodes: &[WorkflowNode]) -> Vec<String> {
let mut names = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(n) => {
if let Some(ref b) = n.bot_name {
names.push(b.clone());
}
}
WorkflowNode::CallWorkflow(n) => {
if let Some(ref b) = n.bot_name {
names.push(b.clone());
}
}
WorkflowNode::Gate(n) => {
if let Some(ref b) = n.bot_name {
names.push(b.clone());
}
}
WorkflowNode::Script(n) => {
if let Some(ref b) = n.bot_name {
names.push(b.clone());
}
}
_ => {
if let Some(body) = node.body() {
names.extend(collect_bot_names(body));
}
}
}
}
names
}
pub(crate) fn collect_plugin_dirs(nodes: &[WorkflowNode]) -> Vec<String> {
let mut dirs = Vec::new();
for node in nodes {
match node {
WorkflowNode::Call(n) => dirs.extend(n.plugin_dirs.iter().cloned()),
_ => {
if let Some(body) = node.body() {
dirs.extend(collect_plugin_dirs(body));
}
}
}
}
dirs
}