Skip to main content

deepstrike_core/scheduler/
policy.rs

1/// Deterministic DAG scheduling policy. All weights are non-negative; setting every
2/// weight to zero reduces ordering to FIFO with node-id tie-breaking.
3#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct SchedulerPolicyConfig {
6    pub critical_path_weight: i64,
7    pub fanout_weight: i64,
8    pub age_weight: i64,
9    pub token_cost_weight: i64,
10    /// Deadline urgency, process priority, and host-reported pressure are integer-only inputs.
11    /// Zero preserves the pre-016-06 ordering when no new factor is configured.
12    pub deadline_weight: i64,
13    pub process_priority_weight: i64,
14    pub resource_pressure_weight: i64,
15    pub budget_pressure_weight: i64,
16}
17
18impl Default for SchedulerPolicyConfig {
19    fn default() -> Self {
20        Self {
21            critical_path_weight: 1_000_000,
22            fanout_weight: 10_000,
23            age_weight: 1_000,
24            token_cost_weight: 1,
25            deadline_weight: 0,
26            process_priority_weight: 0,
27            resource_pressure_weight: 0,
28            budget_pressure_weight: 0,
29        }
30    }
31}
32
33impl SchedulerPolicyConfig {
34    pub fn validate(&self) -> Result<(), String> {
35        for (name, weight) in [
36            ("critical_path_weight", self.critical_path_weight),
37            ("fanout_weight", self.fanout_weight),
38            ("age_weight", self.age_weight),
39            ("token_cost_weight", self.token_cost_weight),
40            ("deadline_weight", self.deadline_weight),
41            ("process_priority_weight", self.process_priority_weight),
42            ("resource_pressure_weight", self.resource_pressure_weight),
43            ("budget_pressure_weight", self.budget_pressure_weight),
44        ] {
45            if !(0..=1_000_000_000).contains(&weight) {
46                return Err(format!(
47                    "scheduler_policy {name} must be between 0 and 1000000000"
48                ));
49            }
50        }
51        Ok(())
52    }
53}
54
55/// OS Phase-2 unified scheduler budget: turn / token / wall-clock three axes.
56#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
57pub struct SchedulerBudget {
58    /// Context window size passed to the pressure monitor.
59    pub max_tokens: u32,
60    /// Maximum tool-call turns before the loop forces a final text response.
61    pub max_turns: u32,
62    /// Accumulated token budget across all turns.
63    pub max_total_tokens: u64,
64    /// Optional wall-clock run budget in milliseconds. Evaluated from
65    /// `started_at_ms` using accepted envelope time.
66    /// `None` means no wall-clock limit (existing behavior).
67    pub max_wall_ms: Option<u64>,
68}
69
70impl Default for SchedulerBudget {
71    fn default() -> Self {
72        Self {
73            max_tokens: 128_000,
74            max_turns: 25,
75            max_total_tokens: 1_000_000,
76            max_wall_ms: None,
77        }
78    }
79}
80
81impl SchedulerBudget {
82    /// Check whether any budget axis is exceeded.
83    /// Returns `Some(budget_name)` for the first axis that fires.
84    pub fn should_terminate(
85        &self,
86        turns: u32,
87        total_tokens: u64,
88        now_ms: Option<u64>,
89        started_at_ms: Option<u64>,
90    ) -> Option<&'static str> {
91        if turns >= self.max_turns {
92            return Some("max_turns");
93        }
94        if total_tokens >= self.max_total_tokens {
95            return Some("token_budget");
96        }
97        if let (Some(limit), Some(now), Some(start)) = (self.max_wall_ms, now_ms, started_at_ms) {
98            if now.saturating_sub(start) >= limit {
99                return Some("wall_time");
100            }
101        }
102        None
103    }
104}