switchyard_libsy/algorithms/
vgr.rs1use std::sync::Arc;
13
14use switchyard_protocol::Request;
15
16use self::text::ToolRecord;
17use crate::Result;
18use crate::algorithms::fall_through::FallThrough;
19use crate::algorithms::util::affinity::AffinityRouter;
20use crate::core::algorithm::{Algorithm, Driver, RoutingOutcome};
21
22mod config;
23mod decide;
24mod readout;
25mod render;
26mod rungs;
27mod runtime;
28mod safety;
29mod text;
30
31pub use config::{ACTIVE_APPROVAL, ServingMode, Targets, VgrConfig};
32pub use safety::{BreakerConfig, KillSwitch};
33
34#[cfg(test)]
35mod tests;
36
37pub struct Vgr {
39 route: FallThrough,
40 classifier: Arc<runtime::VgrClassifier>,
41}
42
43impl Vgr {
44 pub fn new(config: VgrConfig) -> Result<Self> {
46 config.validate()?;
47 let mut affinity = AffinityRouter::new().with_latch_only([config.targets.cloud.clone()]);
48 let reason = if config.latch_escalation && config.mode != ServingMode::Shadow {
49 "session_latched"
50 } else {
51 affinity = affinity.with_release_on_user_turn();
52 "turn_verification_latched"
53 };
54 let affinity = Arc::new(affinity);
55 let classifier = Arc::new(runtime::VgrClassifier {
56 breaker: safety::CircuitBreaker::new(config.breaker),
57 config,
58 });
59 let route = FallThrough::new_with_state()
60 .with_name("vgr")
61 .with_processor(affinity.clone())
62 .with_classifier(Arc::new(runtime::Held(affinity, reason)))
63 .with_classifier(classifier.clone());
64 Ok(Self { route, classifier })
65 }
66}
67
68#[async_trait::async_trait]
69impl Algorithm for Vgr {
70 fn name(&self) -> &str {
71 "vgr"
72 }
73
74 async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome> {
75 let arrived = self
78 .classifier
79 .local_may_stand_in(&request)
80 .then(|| request.clone());
81 let mut outcome = self.route.execute(driver, request).await?;
82 outcome.selected_model_ids.truncate(1);
84 let config = &self.classifier.config;
85 if let Some(arrived) = arrived
86 && outcome.response.is_none()
87 && outcome.selected_model_ids.first() == Some(&config.targets.cloud)
88 && self.classifier.local_may_stand_in(&arrived)
90 {
91 outcome
92 .selected_model_ids
93 .push(config.targets.local.clone());
94 outcome.fallback_request = Some(arrived);
95 }
96 Ok(outcome)
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
102enum Branch {
103 Coding,
105 Answer,
107 Chat,
109 Agentic,
111 DefaultVerified,
113 #[default]
115 Unknown,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120enum TaskType {
121 Coding,
122 Agentic,
123 Answer,
124 Chat,
125}
126
127#[derive(Default)]
129struct Capabilities {
130 branch: Branch,
131 transcript: Option<String>,
133 tools: Option<ToolRecord>,
135}
136
137fn derive_capabilities(
139 request: &Request,
140 attempt: &str,
141 task_type: Option<TaskType>,
142) -> Capabilities {
143 let (turns, unsupported) = text::turns(request);
144 if unsupported || text::user_task_text(&turns).trim().is_empty() || attempt.trim().is_empty() {
145 return Capabilities::default();
146 }
147 let caps = |branch, transcript| Capabilities {
148 branch,
149 transcript,
150 tools: Some(ToolRecord::from_request(request)),
151 };
152
153 if task_type == Some(TaskType::Answer) && !text::has_assistant_turn(&turns) {
154 return Capabilities {
155 tools: None,
156 ..caps(
157 Branch::Answer,
158 Some(render::render_session(&turns, attempt)),
159 )
160 };
161 }
162 if task_type == Some(TaskType::Agentic)
164 || (text::has_tool_trajectory(request)
165 && !matches!(task_type, Some(TaskType::Answer | TaskType::Chat)))
166 {
167 return caps(
168 Branch::Agentic,
169 Some(render::render_agentic_view(request, &turns, attempt)),
170 );
171 }
172 let transcript = Some(render::render_session(&turns, attempt));
173 if text::observed_hardening(attempt) || task_type == Some(TaskType::Coding) {
174 return caps(Branch::Coding, transcript);
175 }
176 if text::observed_tool_activity(attempt) {
177 caps(Branch::Agentic, transcript)
178 } else if matches!(task_type, Some(TaskType::Chat | TaskType::Answer)) {
179 caps(Branch::Chat, transcript)
180 } else {
181 caps(Branch::DefaultVerified, transcript)
182 }
183}