use crate::primitives::Address;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowTemplateStatus {
#[default]
Active,
Inactive,
Deprecated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum WorkflowStepSpec {
UseTool {
tool_id: String,
tool_name: String,
params: serde_json::Value,
#[serde(default)]
output_as: Option<String>,
},
UseModel {
model_id: String,
params: serde_json::Value,
#[serde(default)]
output_as: Option<String>,
},
UseKnowledge {
knowledge_id: String,
params: serde_json::Value,
#[serde(default)]
output_as: Option<String>,
},
SpawnAgent {
agent_template_id: String,
tnzo_budget: u128,
#[serde(default)]
valid_until: Option<i64>,
scope_overrides: serde_json::Value,
#[serde(default)]
output_as: Option<String>,
},
Wait {
wait_kind: String,
params: serde_json::Value,
},
Compound {
op: String,
#[serde(default)]
condition: Option<String>,
#[serde(default)]
if_true_step_ids: Vec<usize>,
#[serde(default)]
if_false_step_ids: Vec<usize>,
#[serde(default)]
step_ids: Vec<usize>,
#[serde(default)]
fail_fast: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowTemplate {
pub template_id: String,
pub name: String,
pub version: String,
pub description: String,
pub category: String,
pub capabilities: Vec<String>,
pub creator_did: Option<String>,
pub creator_wallet: Option<Address>,
pub price_per_instantiate: u128,
pub status: WorkflowTemplateStatus,
pub created_at: u64,
pub instantiation_count: u64,
#[serde(default = "default_last_seen")]
pub last_seen_at: u64,
pub required_inputs: serde_json::Value,
pub expected_outputs: serde_json::Value,
pub steps: Vec<WorkflowStepSpec>,
#[serde(default)]
pub allowed_to_subjects: Option<Vec<String>>,
}
fn default_last_seen() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
impl WorkflowTemplate {
pub fn new(
name: String,
version: String,
description: String,
category: String,
steps: Vec<WorkflowStepSpec>,
) -> Self {
let template_id = uuid::Uuid::new_v4().to_string();
let created_at = default_last_seen();
Self {
template_id,
name,
version,
description,
category,
capabilities: Vec::new(),
creator_did: None,
creator_wallet: None,
price_per_instantiate: 0,
status: WorkflowTemplateStatus::Active,
created_at,
instantiation_count: 0,
last_seen_at: created_at,
required_inputs: serde_json::json!({}),
expected_outputs: serde_json::json!({}),
steps,
allowed_to_subjects: None,
}
}
pub fn is_paid(&self) -> bool {
self.price_per_instantiate > 0
}
pub fn validate_for_registration(&self) -> Result<(), &'static str> {
if self.is_paid() && self.creator_wallet.is_none() {
return Err("Paid workflow template requires a creator_wallet");
}
if self.steps.is_empty() {
return Err("Workflow template must declare at least one step");
}
Ok(())
}
pub fn is_available(&self) -> bool {
self.status == WorkflowTemplateStatus::Active
}
pub fn touch(&mut self) {
self.last_seen_at = default_last_seen();
}
pub fn is_subject_allowed(&self, subject: Option<&str>) -> bool {
match (&self.allowed_to_subjects, subject) {
(None, _) => true,
(Some(list), Some(s)) => list.iter().any(|x| x == s),
(Some(_), None) => false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WorkflowTemplateFilter {
pub category: Option<String>,
pub status: Option<String>,
pub creator_did: Option<String>,
pub query: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowInstantiationResult {
pub template_id: String,
pub workflow_id: String,
pub started_at: u64,
pub status: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_steps_rejected() {
let t = WorkflowTemplate::new(
"x".to_string(),
"1".to_string(),
"x".to_string(),
"x".to_string(),
Vec::new(),
);
assert!(t.validate_for_registration().is_err());
}
#[test]
fn paid_without_wallet_rejected() {
let mut t = WorkflowTemplate::new(
"x".to_string(),
"1".to_string(),
"x".to_string(),
"x".to_string(),
vec![WorkflowStepSpec::Wait {
wait_kind: "duration".to_string(),
params: serde_json::json!({"seconds": 1}),
}],
);
t.price_per_instantiate = 100;
assert!(t.validate_for_registration().is_err());
}
#[test]
fn step_serialization_roundtrip() {
let step = WorkflowStepSpec::UseTool {
tool_id: "tool-123".to_string(),
tool_name: "search".to_string(),
params: serde_json::json!({"q": "test"}),
output_as: Some("search_result".to_string()),
};
let json = serde_json::to_string(&step).unwrap();
let parsed: WorkflowStepSpec = serde_json::from_str(&json).unwrap();
match parsed {
WorkflowStepSpec::UseTool { tool_id, .. } => assert_eq!(tool_id, "tool-123"),
_ => panic!("wrong variant"),
}
}
#[test]
fn wait_step_roundtrip() {
let step = WorkflowStepSpec::Wait {
wait_kind: "duration".to_string(),
params: serde_json::json!({"seconds": 1}),
};
let json = serde_json::to_string(&step).unwrap();
let _: WorkflowStepSpec = serde_json::from_str(&json).unwrap();
}
}