Skip to main content

deepstrike_core/scheduler/
policy.rs

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