switchyard_libsy/algorithms/vgr/
config.rs1use std::time::Duration;
5
6use switchyard_protocol::ModelId;
7
8use super::safety::{BreakerConfig, KillSwitch};
9use crate::{LibsyError, Result};
10
11pub const ACTIVE_APPROVAL: &str = "prospective-validation-and-canary-approved";
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub enum ServingMode {
17 #[default]
19 Off,
20 Evaluate,
22 Shadow,
24 Active {
26 approval: String,
28 },
29}
30
31#[derive(Clone, Debug)]
33pub struct Targets {
34 pub efficient: ModelId,
36 pub capable: ModelId,
38 pub judge: Option<ModelId>,
40 pub capable_judge: Option<ModelId>,
42}
43
44#[derive(Clone, Debug)]
46pub struct VgrConfig {
47 pub targets: Targets,
49 pub mode: ServingMode,
51 pub deadline: Duration,
53 pub bounded_efficient: bool,
55 pub kill_switch: Option<KillSwitch>,
57 pub breaker: BreakerConfig,
59 pub task_typing: bool,
61 pub efficient_supports_images: bool,
63 pub confirmed_recovery_min_clean_tail: Option<u32>,
66 pub agentic_handoff: bool,
69 pub compact_handoff: bool,
71 pub efficient_turn_budget: Option<Duration>,
74 pub latch_escalation: bool,
77}
78
79impl VgrConfig {
80 pub fn new(efficient: ModelId, capable: ModelId) -> Self {
82 Self {
83 targets: Targets {
84 efficient,
85 capable,
86 judge: None,
87 capable_judge: None,
88 },
89 mode: ServingMode::Off,
90 deadline: Duration::from_secs(30),
91 bounded_efficient: true,
92 kill_switch: None,
93 breaker: BreakerConfig::default(),
94 task_typing: true,
95 efficient_supports_images: false,
96 confirmed_recovery_min_clean_tail: None,
97 agentic_handoff: false,
98 compact_handoff: false,
99 efficient_turn_budget: None,
100 latch_escalation: false,
101 }
102 }
103
104 pub(super) fn validate(&self) -> Result<()> {
105 if self.compact_handoff && !self.agentic_handoff {
106 return Err(LibsyError::AlgorithmError {
107 message: "compact_handoff requires agentic_handoff".into(),
108 });
109 }
110 if self.targets.efficient == self.targets.capable {
111 return Err(LibsyError::AlgorithmError {
112 message: "vgr requires distinct efficient and capable model IDs".into(),
113 });
114 }
115 if self.deadline.is_zero() || self.breaker.threshold == 0 {
116 return Err(LibsyError::AlgorithmError {
117 message: "vgr deadline and breaker threshold must be non-zero".into(),
118 });
119 }
120 if let ServingMode::Active { approval } = &self.mode
121 && approval != ACTIVE_APPROVAL
122 {
123 return Err(LibsyError::AlgorithmError {
124 message: format!(
125 "vgr active mode requires approval attestation {ACTIVE_APPROVAL:?}"
126 ),
127 });
128 }
129 Ok(())
130 }
131
132 pub(super) fn judge(&self) -> &ModelId {
133 self.targets
134 .judge
135 .as_ref()
136 .unwrap_or(&self.targets.efficient)
137 }
138
139 pub(super) fn confirmed_min_clean_tail(&self) -> Option<i32> {
140 self.confirmed_recovery_min_clean_tail
141 .map(|tail| i32::try_from(tail.max(1)).unwrap_or(i32::MAX))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn compaction_requires_handoff_for_direct_callers() {
151 let mut config = VgrConfig::new("efficient".into(), "capable".into());
152 assert!(!config.compact_handoff);
153 config.compact_handoff = true;
154 assert!(super::super::Vgr::new(config.clone()).is_err());
155 config.agentic_handoff = true;
156 assert!(super::super::Vgr::new(config).is_ok());
157 }
158
159 #[test]
160 fn direct_callers_must_use_distinct_efficient_and_capable_ids() {
161 assert!(super::super::Vgr::new(VgrConfig::new("same".into(), "same".into())).is_err());
162 assert!(
163 super::super::Vgr::new(VgrConfig::new("efficient".into(), "capable".into())).is_ok()
164 );
165 }
166}