1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4use crate::events::{ThreadId, TurnId};
5use crate::inference::TokenUsage;
6use crate::subagents::{SubagentExitReason, SubagentLane};
7
8pub type WorkflowRunId = String;
9pub type WorkflowScriptId = String;
10pub type WorkflowScriptHash = String;
11pub type WorkflowPhaseId = String;
12pub type WorkflowAgentRunId = String;
13pub type WorkflowApprovalId = String;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "camelCase")]
17pub enum WorkflowScriptSourceKind {
18 Generated,
19 BuiltIn,
20 User,
21 Workspace,
22 Extension,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "camelCase")]
27pub struct WorkflowScriptSource {
28 pub kind: WorkflowScriptSourceKind,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub path: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub command_name: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub extension_id: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38#[serde(rename_all = "camelCase")]
39pub struct WorkflowScript {
40 pub script_id: WorkflowScriptId,
41 pub name: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub description: Option<String>,
44 pub source: WorkflowScriptSource,
45 pub hash: WorkflowScriptHash,
46 pub host_api_version: u32,
47 #[serde(default)]
48 pub arguments_schema: serde_json::Value,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub body: Option<String>,
51 pub limits: WorkflowRunLimits,
52 #[serde(with = "time::serde::rfc3339")]
53 pub created_at: OffsetDateTime,
54 #[serde(with = "time::serde::rfc3339")]
55 pub updated_at: OffsetDateTime,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
59#[serde(rename_all = "camelCase")]
60pub enum WorkflowRunStatus {
61 Drafted,
62 AwaitingApproval,
63 Queued,
64 #[default]
65 Running,
66 Paused,
67 ApprovalWait,
68 Completed,
69 Failed,
70 Stopped,
71}
72
73#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
74#[serde(rename_all = "camelCase")]
75pub enum WorkflowPhaseStatus {
76 #[default]
77 Queued,
78 Running,
79 Completed,
80 Failed,
81 Skipped,
82}
83
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
85#[serde(rename_all = "camelCase")]
86pub enum WorkflowAgentStatus {
87 #[default]
88 Queued,
89 Running,
90 Completed,
91 Failed,
92 Timeout,
93 Cancelled,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97#[serde(rename_all = "camelCase")]
98pub struct WorkflowRunLimits {
99 pub max_concurrent_agents: u32,
100 pub max_agents_per_run: u32,
101 pub default_agent_timeout_seconds: u64,
102 pub default_run_timeout_seconds: u64,
103 pub default_checkpoint_bytes: u64,
104 pub max_report_bytes: u64,
105}
106
107impl Default for WorkflowRunLimits {
108 fn default() -> Self {
109 Self {
110 max_concurrent_agents: 16,
111 max_agents_per_run: 1000,
112 default_agent_timeout_seconds: 180,
113 default_run_timeout_seconds: 14_400,
114 default_checkpoint_bytes: 1_048_576,
115 max_report_bytes: 65_536,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct WorkflowCostEstimate {
123 #[serde(default)]
124 pub min_child_agents: u32,
125 #[serde(default)]
126 pub max_child_agents: u32,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub estimated_prompt_tokens: Option<u64>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub estimated_completion_tokens: Option<u64>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub warning: Option<String>,
133}
134
135#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
136#[serde(rename_all = "camelCase")]
137pub enum WorkflowApprovalDecision {
138 RunOnce,
139 AlwaysForScriptAndWorkspace,
140 Deny,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144#[serde(rename_all = "camelCase")]
145pub struct WorkflowApproval {
146 pub approval_id: WorkflowApprovalId,
147 pub run_id: WorkflowRunId,
148 pub script_hash: WorkflowScriptHash,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub workspace: Option<String>,
151 pub decision: WorkflowApprovalDecision,
152 #[serde(default)]
153 pub approved_capabilities: Vec<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub reason: Option<String>,
156 #[serde(with = "time::serde::rfc3339")]
157 pub decided_at: OffsetDateTime,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
161#[serde(rename_all = "camelCase")]
162pub struct WorkflowConsent {
163 pub script_hash: WorkflowScriptHash,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub workspace: Option<String>,
166 pub decision: WorkflowApprovalDecision,
167 #[serde(default)]
168 pub approved_capabilities: Vec<String>,
169 #[serde(with = "time::serde::rfc3339")]
170 pub decided_at: OffsetDateTime,
171 #[serde(default, with = "time::serde::rfc3339::option")]
172 pub expires_at: Option<OffsetDateTime>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176#[serde(rename_all = "camelCase")]
177pub struct WorkflowPhase {
178 pub phase_id: WorkflowPhaseId,
179 pub name: String,
180 pub status: WorkflowPhaseStatus,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub description: Option<String>,
183 #[serde(default)]
184 pub queued_agents: u32,
185 #[serde(default)]
186 pub completed_agents: u32,
187 #[serde(default)]
188 pub failed_agents: u32,
189 #[serde(default, with = "time::serde::rfc3339::option")]
190 pub started_at: Option<OffsetDateTime>,
191 #[serde(default, with = "time::serde::rfc3339::option")]
192 pub completed_at: Option<OffsetDateTime>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
196#[serde(rename_all = "camelCase")]
197pub struct WorkflowAgentRun {
198 pub agent_id: WorkflowAgentRunId,
199 pub phase_id: WorkflowPhaseId,
200 pub description: String,
201 pub status: WorkflowAgentStatus,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub lane: Option<SubagentLane>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub model: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub thread_id: Option<ThreadId>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub turn_id: Option<TurnId>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub usage: Option<TokenUsage>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub exit_reason: Option<SubagentExitReason>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub error: Option<String>,
216 #[serde(default, with = "time::serde::rfc3339::option")]
217 pub started_at: Option<OffsetDateTime>,
218 #[serde(default, with = "time::serde::rfc3339::option")]
219 pub completed_at: Option<OffsetDateTime>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223#[serde(rename_all = "camelCase")]
224pub struct WorkflowRunSummary {
225 pub run_id: WorkflowRunId,
226 pub status: WorkflowRunStatus,
227 pub title: String,
228 #[serde(default)]
229 pub phase_count: u32,
230 #[serde(default)]
231 pub completed_phase_count: u32,
232 #[serde(default)]
233 pub agent_count: u32,
234 #[serde(default)]
235 pub completed_agent_count: u32,
236 #[serde(default)]
237 pub failed_agent_count: u32,
238 #[serde(default)]
239 pub concurrency_peak: u32,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub usage: Option<TokenUsage>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub elapsed_ms: Option<u64>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub report_preview: Option<String>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
249#[serde(rename_all = "camelCase")]
250pub struct WorkflowRun {
251 pub run_id: WorkflowRunId,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub thread_id: Option<ThreadId>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub turn_id: Option<TurnId>,
256 pub script: WorkflowScript,
257 pub status: WorkflowRunStatus,
258 pub limits: WorkflowRunLimits,
259 #[serde(default)]
260 pub phases: Vec<WorkflowPhase>,
261 #[serde(default)]
262 pub agents: Vec<WorkflowAgentRun>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub approval: Option<WorkflowApproval>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub cost_estimate: Option<WorkflowCostEstimate>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub summary: Option<WorkflowRunSummary>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub error: Option<String>,
271 #[serde(with = "time::serde::rfc3339")]
272 pub created_at: OffsetDateTime,
273 #[serde(with = "time::serde::rfc3339")]
274 pub updated_at: OffsetDateTime,
275 #[serde(default, with = "time::serde::rfc3339::option")]
276 pub started_at: Option<OffsetDateTime>,
277 #[serde(default, with = "time::serde::rfc3339::option")]
278 pub completed_at: Option<OffsetDateTime>,
279}
280
281macro_rules! workflow_event {
282 ($name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
283 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284 #[serde(rename_all = "camelCase")]
285 pub struct $name {
286 pub run_id: WorkflowRunId,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub thread_id: Option<ThreadId>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub turn_id: Option<TurnId>,
291 $(pub $field: $ty,)*
292 #[serde(with = "time::serde::rfc3339")]
293 pub timestamp: OffsetDateTime,
294 }
295 };
296}
297
298workflow_event!(WorkflowRunDrafted { run: WorkflowRun });
299workflow_event!(WorkflowApprovalRequested {
300 approval_id: WorkflowApprovalId,
301 run: WorkflowRun
302});
303workflow_event!(WorkflowRunApproved {
304 approval: WorkflowApproval
305});
306workflow_event!(WorkflowRunDenied {
307 approval: WorkflowApproval
308});
309workflow_event!(WorkflowRunQueued {
310 status: WorkflowRunStatus
311});
312workflow_event!(WorkflowRunStarted {
313 status: WorkflowRunStatus
314});
315workflow_event!(WorkflowPhaseStarted {
316 phase: WorkflowPhase
317});
318workflow_event!(WorkflowPhaseCompleted {
319 phase: WorkflowPhase
320});
321workflow_event!(WorkflowAgentQueued {
322 agent: WorkflowAgentRun
323});
324workflow_event!(WorkflowAgentStarted {
325 agent: WorkflowAgentRun
326});
327workflow_event!(WorkflowAgentCompleted {
328 agent: WorkflowAgentRun
329});
330workflow_event!(WorkflowAgentFailed {
331 agent: WorkflowAgentRun,
332 error: String
333});
334workflow_event!(WorkflowOutputRecorded { phase_id: Option<WorkflowPhaseId>, output: String, truncated: bool });
335workflow_event!(WorkflowCheckpointRecorded { phase_id: Option<WorkflowPhaseId>, key: String, byte_count: u64 });
336workflow_event!(WorkflowRunPaused { reason: Option<String> });
337workflow_event!(WorkflowRunResumed {
338 status: WorkflowRunStatus
339});
340workflow_event!(WorkflowRunStopped { reason: Option<String> });
341workflow_event!(WorkflowRunCompleted {
342 summary: WorkflowRunSummary
343});
344workflow_event!(WorkflowRunFailed { error: String, summary: Option<WorkflowRunSummary> });
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn script() -> WorkflowScript {
351 WorkflowScript {
352 script_id: "script-1".to_string(),
353 name: "audit".to_string(),
354 description: Some("Audit the repo".to_string()),
355 source: WorkflowScriptSource {
356 kind: WorkflowScriptSourceKind::Generated,
357 path: None,
358 command_name: None,
359 extension_id: None,
360 },
361 hash: "sha256:abc".to_string(),
362 host_api_version: 1,
363 arguments_schema: serde_json::json!({"type": "object"}),
364 body: None,
365 limits: WorkflowRunLimits::default(),
366 created_at: OffsetDateTime::UNIX_EPOCH,
367 updated_at: OffsetDateTime::UNIX_EPOCH,
368 }
369 }
370
371 #[test]
372 fn workflow_run_uses_camel_case_wire_shape() {
373 let run = WorkflowRun {
374 run_id: "run-1".to_string(),
375 thread_id: Some("thread-1".to_string()),
376 turn_id: Some("turn-1".to_string()),
377 script: script(),
378 status: WorkflowRunStatus::AwaitingApproval,
379 limits: WorkflowRunLimits::default(),
380 phases: vec![WorkflowPhase {
381 phase_id: "phase-1".to_string(),
382 name: "Scout".to_string(),
383 status: WorkflowPhaseStatus::Queued,
384 description: None,
385 queued_agents: 2,
386 completed_agents: 0,
387 failed_agents: 0,
388 started_at: None,
389 completed_at: None,
390 }],
391 agents: vec![WorkflowAgentRun {
392 agent_id: "agent-1".to_string(),
393 phase_id: "phase-1".to_string(),
394 description: "Inspect crate".to_string(),
395 status: WorkflowAgentStatus::Queued,
396 lane: Some(SubagentLane::Scout),
397 model: Some("mock".to_string()),
398 thread_id: None,
399 turn_id: None,
400 usage: None,
401 exit_reason: None,
402 error: None,
403 started_at: None,
404 completed_at: None,
405 }],
406 approval: None,
407 cost_estimate: Some(WorkflowCostEstimate {
408 min_child_agents: 2,
409 max_child_agents: 4,
410 estimated_prompt_tokens: Some(1000),
411 estimated_completion_tokens: Some(500),
412 warning: Some("workflow may fan out".to_string()),
413 }),
414 summary: None,
415 error: None,
416 created_at: OffsetDateTime::UNIX_EPOCH,
417 updated_at: OffsetDateTime::UNIX_EPOCH,
418 started_at: None,
419 completed_at: None,
420 };
421
422 let value = serde_json::to_value(&run).unwrap();
423
424 assert_eq!(value["runId"], "run-1");
425 assert_eq!(value["threadId"], "thread-1");
426 assert_eq!(value["status"], "awaitingApproval");
427 assert_eq!(value["script"]["hostApiVersion"], 1);
428 assert_eq!(value["phases"][0]["queuedAgents"], 2);
429 assert_eq!(value["agents"][0]["lane"], "scout");
430 assert_eq!(value["costEstimate"]["maxChildAgents"], 4);
431
432 let decoded: WorkflowRun = serde_json::from_value(value).unwrap();
433 assert_eq!(decoded, run);
434 }
435}