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
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct ScheduleRunRecord {
182    pub run_id: String,
183    pub schedule_id: String,
184    pub scheduled_for: DateTime<Utc>,
185    pub claimed_at: DateTime<Utc>,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub started_at: Option<DateTime<Utc>>,
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub completed_at: Option<DateTime<Utc>>,
190    pub status: ScheduleRunStatus,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub outcome_reason: Option<String>,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub session_id: Option<String>,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub dispatch_lag_ms: Option<u64>,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub execution_duration_ms: Option<u64>,
199    #[serde(default)]
200    pub was_catch_up: bool,
201}
202
203/// Runtime configuration for schedule-executed sessions.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
205pub struct ScheduleRunConfig {
206    /// Optional system prompt override for new sessions created by this schedule.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub system_prompt: Option<String>,
209    /// Optional task message to add to the new session.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub task_message: Option<String>,
212    /// Model used when auto-executing.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub model: Option<String>,
215    /// Optional reasoning effort override used when auto-executing.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub reasoning_effort: Option<ReasoningEffort>,
218    /// Optional workspace path context.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub workspace_path: Option<String>,
221    /// Optional enhancement prompt.
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub enhance_prompt: Option<String>,
224    /// If true, immediately execute the new session (only meaningful if `task_message` exists).
225    #[serde(default)]
226    pub auto_execute: bool,
227}