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 kill_switch: Option<KillSwitch>,
55 pub breaker: BreakerConfig,
57 pub task_typing: bool,
59 pub efficient_supports_images: bool,
61 pub confirmed_recovery_min_clean_tail: Option<u32>,
64 pub agentic_handoff: bool,
67 pub compact_handoff: bool,
69 pub efficient_turn_budget: Option<Duration>,
72 pub latch_escalation: bool,
75}
76
77impl VgrConfig {
78 pub fn new(efficient: ModelId, capable: ModelId) -> Self {
80 Self {
81 targets: Targets {
82 efficient,
83 capable,
84 judge: None,
85 capable_judge: None,
86 },
87 mode: ServingMode::Off,
88 deadline: Duration::from_secs(30),
89 kill_switch: None,
90 breaker: BreakerConfig::default(),
91 task_typing: true,
92 efficient_supports_images: false,
93 confirmed_recovery_min_clean_tail: None,
94 agentic_handoff: false,
95 compact_handoff: false,
96 efficient_turn_budget: None,
97 latch_escalation: false,
98 }
99 }
100
101 pub(super) fn validate(&self) -> Result<()> {
102 if self.compact_handoff && !self.agentic_handoff {
103 return Err(LibsyError::AlgorithmError {
104 message: "compact_handoff requires agentic_handoff".into(),
105 });
106 }
107 if self.targets.efficient == self.targets.capable {
108 return Err(LibsyError::AlgorithmError {
109 message: "vgr requires distinct efficient and capable model IDs".into(),
110 });
111 }
112 if self.deadline.is_zero() || self.breaker.threshold == 0 {
113 return Err(LibsyError::AlgorithmError {
114 message: "vgr deadline and breaker threshold must be non-zero".into(),
115 });
116 }
117 if let ServingMode::Active { approval } = &self.mode
118 && approval != ACTIVE_APPROVAL
119 {
120 return Err(LibsyError::AlgorithmError {
121 message: format!(
122 "vgr active mode requires approval attestation {ACTIVE_APPROVAL:?}"
123 ),
124 });
125 }
126 Ok(())
127 }
128
129 pub(super) fn judge(&self) -> &ModelId {
130 self.targets
131 .judge
132 .as_ref()
133 .unwrap_or(&self.targets.efficient)
134 }
135
136 pub(super) fn confirmed_min_clean_tail(&self) -> Option<i32> {
137 self.confirmed_recovery_min_clean_tail
138 .map(|tail| i32::try_from(tail.max(1)).unwrap_or(i32::MAX))
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn compaction_requires_handoff_for_direct_callers() {
148 let mut config = VgrConfig::new("efficient".into(), "capable".into());
149 assert!(!config.compact_handoff);
150 config.compact_handoff = true;
151 assert!(super::super::Vgr::new(config.clone()).is_err());
152 config.agentic_handoff = true;
153 assert!(super::super::Vgr::new(config).is_ok());
154 }
155
156 #[test]
157 fn direct_callers_must_use_distinct_efficient_and_capable_ids() {
158 assert!(super::super::Vgr::new(VgrConfig::new("same".into(), "same".into())).is_err());
159 assert!(
160 super::super::Vgr::new(VgrConfig::new("efficient".into(), "capable".into())).is_ok()
161 );
162 }
163}