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}