Skip to main content

roder_dynamic_workflows/
model.rs

1use std::fmt;
2
3use roder_api::dynamic_workflows::{WorkflowRunId, WorkflowRunLimits};
4use serde::{Deserialize, Serialize};
5
6pub const WORKFLOW_HOST_API_VERSION: u32 = 1;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub struct WorkflowDefinition {
11    pub name: String,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub description: Option<String>,
14    #[serde(default)]
15    pub arguments_schema: serde_json::Value,
16    #[serde(default)]
17    pub phases: Vec<String>,
18    pub host_api_version: u32,
19    pub limits: WorkflowRunLimits,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(rename_all = "camelCase")]
24pub struct WorkflowRuntimeOptions {
25    pub max_loop_iterations: u64,
26    pub max_promise_drains: usize,
27    pub max_report_bytes: u64,
28    pub limits: WorkflowRunLimits,
29}
30
31impl Default for WorkflowRuntimeOptions {
32    fn default() -> Self {
33        let limits = WorkflowRunLimits::default();
34        Self {
35            max_loop_iterations: 100_000,
36            max_promise_drains: 8,
37            max_report_bytes: limits.max_report_bytes,
38            limits,
39        }
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct WorkflowRunInput {
46    pub run_id: WorkflowRunId,
47    #[serde(default)]
48    pub arguments: serde_json::Value,
49    #[serde(default)]
50    pub abort_before_start: bool,
51    #[serde(default)]
52    pub checkpoints: Vec<crate::host_api::WorkflowCheckpoint>,
53}
54
55impl WorkflowRunInput {
56    pub fn new(run_id: impl Into<WorkflowRunId>) -> Self {
57        Self {
58            run_id: run_id.into(),
59            arguments: serde_json::Value::Object(Default::default()),
60            abort_before_start: false,
61            checkpoints: Vec::new(),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum WorkflowRuntimeErrorKind {
68    MissingDefinition,
69    InvalidMetadata,
70    UnsupportedHostApiVersion,
71    DeniedAmbientApi,
72    ScriptExecution,
73    LimitExceeded,
74    Aborted,
75    Store,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct WorkflowRuntimeError {
80    kind: WorkflowRuntimeErrorKind,
81    message: String,
82}
83
84impl WorkflowRuntimeError {
85    pub fn new(kind: WorkflowRuntimeErrorKind, message: impl Into<String>) -> Self {
86        Self {
87            kind,
88            message: message.into(),
89        }
90    }
91
92    pub fn kind(&self) -> WorkflowRuntimeErrorKind {
93        self.kind
94    }
95
96    pub fn message(&self) -> &str {
97        &self.message
98    }
99}
100
101impl fmt::Display for WorkflowRuntimeError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{:?}: {}", self.kind, self.message)
104    }
105}
106
107impl std::error::Error for WorkflowRuntimeError {}
108
109pub type WorkflowRuntimeResult<T> = Result<T, WorkflowRuntimeError>;
110
111#[derive(Debug, Clone, Default, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub(crate) struct RawWorkflowDefinition {
114    pub name: Option<String>,
115    #[serde(default)]
116    pub description: Option<String>,
117    #[serde(default)]
118    pub arguments_schema: serde_json::Value,
119    #[serde(default)]
120    pub phases: Vec<String>,
121    #[serde(default)]
122    pub host_api_version: Option<u32>,
123    #[serde(default)]
124    pub limits: WorkflowRunLimitsPatch,
125}
126
127impl RawWorkflowDefinition {
128    pub fn into_definition(
129        self,
130        base_limits: &WorkflowRunLimits,
131    ) -> WorkflowRuntimeResult<WorkflowDefinition> {
132        let Some(name) = self.name.filter(|name| !name.trim().is_empty()) else {
133            return Err(WorkflowRuntimeError::new(
134                WorkflowRuntimeErrorKind::InvalidMetadata,
135                "workflow metadata must include a non-empty name",
136            ));
137        };
138        let host_api_version = self.host_api_version.unwrap_or(WORKFLOW_HOST_API_VERSION);
139        if host_api_version != WORKFLOW_HOST_API_VERSION {
140            return Err(WorkflowRuntimeError::new(
141                WorkflowRuntimeErrorKind::UnsupportedHostApiVersion,
142                format!("unsupported workflow host API version {host_api_version}"),
143            ));
144        }
145
146        Ok(WorkflowDefinition {
147            name,
148            description: self.description,
149            arguments_schema: self.arguments_schema,
150            phases: self.phases,
151            host_api_version,
152            limits: self.limits.apply_to(base_limits.clone()),
153        })
154    }
155}
156
157#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
158#[serde(rename_all = "camelCase")]
159pub(crate) struct WorkflowRunLimitsPatch {
160    pub max_concurrent_agents: Option<u32>,
161    pub max_agents_per_run: Option<u32>,
162    pub default_agent_timeout_seconds: Option<u64>,
163    pub default_run_timeout_seconds: Option<u64>,
164    pub default_checkpoint_bytes: Option<u64>,
165    pub max_report_bytes: Option<u64>,
166}
167
168impl WorkflowRunLimitsPatch {
169    fn apply_to(self, mut limits: WorkflowRunLimits) -> WorkflowRunLimits {
170        if let Some(value) = self.max_concurrent_agents {
171            limits.max_concurrent_agents = value;
172        }
173        if let Some(value) = self.max_agents_per_run {
174            limits.max_agents_per_run = value;
175        }
176        if let Some(value) = self.default_agent_timeout_seconds {
177            limits.default_agent_timeout_seconds = value;
178        }
179        if let Some(value) = self.default_run_timeout_seconds {
180            limits.default_run_timeout_seconds = value;
181        }
182        if let Some(value) = self.default_checkpoint_bytes {
183            limits.default_checkpoint_bytes = value;
184        }
185        if let Some(value) = self.max_report_bytes {
186            limits.max_report_bytes = value;
187        }
188        limits
189    }
190}