Skip to main content

bamboo_domain/schedule/
domain.rs

1use crate::reasoning::ReasoningEffort;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5/// Scheduler-owned schedule definition used by the redesigned scheduling domain.
6///
7/// This coexists with the legacy `ScheduleEntry` model during the transition away
8/// from interval-only schedules.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct ScheduleSpec {
11    pub id: String,
12    pub name: String,
13    pub enabled: bool,
14    pub trigger: ScheduleTrigger,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub timezone: Option<String>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub start_at: Option<DateTime<Utc>>,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub end_at: Option<DateTime<Utc>>,
21    #[serde(default)]
22    pub misfire_policy: MisFirePolicy,
23    #[serde(default)]
24    pub overlap_policy: OverlapPolicy,
25    #[serde(default)]
26    pub run_config: ScheduleRunConfig,
27    pub created_at: DateTime<Utc>,
28    pub updated_at: DateTime<Utc>,
29}
30
31impl ScheduleSpec {
32    pub fn window(&self) -> ScheduleWindow {
33        ScheduleWindow {
34            start_at: self.start_at,
35            end_at: self.end_at,
36        }
37    }
38}
39
40/// Canonical trigger definition for future schedule implementations.
41///
42/// Kept intentionally independent from any concrete recurrence library so Bamboo
43/// can swap trigger engines without changing the persisted domain model.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(tag = "type", rename_all = "snake_case")]
46pub enum ScheduleTrigger {
47    Interval {
48        every_seconds: u64,
49        #[serde(default, skip_serializing_if = "Option::is_none")]
50        anchor_at: Option<DateTime<Utc>>,
51    },
52    Daily {
53        hour: u8,
54        minute: u8,
55        #[serde(default)]
56        second: u8,
57    },
58    Weekly {
59        weekdays: Vec<ScheduleWeekday>,
60        hour: u8,
61        minute: u8,
62        #[serde(default)]
63        second: u8,
64    },
65    Monthly {
66        days: Vec<u8>,
67        hour: u8,
68        minute: u8,
69        #[serde(default)]
70        second: u8,
71    },
72    Cron {
73        expr: String,
74    },
75}
76
77impl ScheduleTrigger {
78    pub fn legacy_interval(every_seconds: u64, anchor_at: Option<DateTime<Utc>>) -> Self {
79        Self::Interval {
80            every_seconds,
81            anchor_at,
82        }
83    }
84
85    pub fn kind_name(&self) -> &'static str {
86        match self {
87            Self::Interval { .. } => "interval",
88            Self::Daily { .. } => "daily",
89            Self::Weekly { .. } => "weekly",
90            Self::Monthly { .. } => "monthly",
91            Self::Cron { .. } => "cron",
92        }
93    }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum ScheduleWeekday {
99    Mon,
100    Tue,
101    Wed,
102    Thu,
103    Fri,
104    Sat,
105    Sun,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
109#[serde(tag = "type", rename_all = "snake_case")]
110pub enum MisFirePolicy {
111    #[default]
112    RunOnce,
113    Skip,
114    CatchUpAll,
115    CatchUpWindow {
116        max_catch_up_runs: u32,
117        max_lateness_seconds: u64,
118    },
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
122#[serde(rename_all = "snake_case")]
123pub enum OverlapPolicy {
124    Allow,
125    Skip,
126    #[default]
127    QueueOne,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
131pub struct ScheduleWindow {
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub start_at: Option<DateTime<Utc>>,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub end_at: Option<DateTime<Utc>>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
139pub struct ScheduleState {
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub next_fire_at: Option<DateTime<Utc>>,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub last_scheduled_at: Option<DateTime<Utc>>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub last_started_at: Option<DateTime<Utc>>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub last_finished_at: Option<DateTime<Utc>>,
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub last_success_at: Option<DateTime<Utc>>,
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub last_failure_at: Option<DateTime<Utc>>,
152    #[serde(default)]
153    pub queued_run_count: u32,
154    #[serde(default)]
155    pub running_run_count: u32,
156    #[serde(default)]
157    pub consecutive_failures: u32,
158    #[serde(default)]
159    pub total_run_count: u64,
160    #[serde(default)]
161    pub total_success_count: u64,
162    #[serde(default)]
163    pub total_failure_count: u64,
164    #[serde(default)]
165    pub total_missed_count: u64,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum ScheduleRunStatus {
171    Queued,
172    Running,
173    Success,
174    Failed,
175    Skipped,
176    Missed,
177    Cancelled,
178}
179
180impl ScheduleRunStatus {
181    /// Whether the run has reached a final state (no further transitions). Used
182    /// to decide which run records are safe to prune from history.
183    pub fn is_terminal(self) -> bool {
184        matches!(
185            self,
186            Self::Success | Self::Failed | Self::Skipped | Self::Missed | Self::Cancelled
187        )
188    }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct ScheduleRunRecord {
193    pub run_id: String,
194    pub schedule_id: String,
195    pub scheduled_for: DateTime<Utc>,
196    pub claimed_at: DateTime<Utc>,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub started_at: Option<DateTime<Utc>>,
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub completed_at: Option<DateTime<Utc>>,
201    pub status: ScheduleRunStatus,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub outcome_reason: Option<String>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub session_id: Option<String>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub dispatch_lag_ms: Option<u64>,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub execution_duration_ms: Option<u64>,
210    #[serde(default)]
211    pub was_catch_up: bool,
212}
213
214/// Runtime configuration for schedule-executed sessions.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
216pub struct ScheduleRunConfig {
217    /// Optional system prompt override for new sessions created by this schedule.
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub system_prompt: Option<String>,
220    /// Optional task message to add to the new session.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub task_message: Option<String>,
223    /// Model used when auto-executing.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub model: Option<String>,
226    /// Optional reasoning effort override used when auto-executing.
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub reasoning_effort: Option<ReasoningEffort>,
229    /// Optional workspace path context.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub workspace_path: Option<String>,
232    /// Optional enhancement prompt.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub enhance_prompt: Option<String>,
235    /// If true, immediately execute the new session (only meaningful if `task_message` exists).
236    #[serde(default)]
237    pub auto_execute: bool,
238}