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, AnswerSummary, Driver, RoutingOutcome};
21
22mod compaction;
23mod config;
24mod decide;
25mod readout;
26mod render;
27mod rungs;
28mod runtime;
29mod safety;
30mod text;
31
32pub use config::{ACTIVE_APPROVAL, ServingMode, Targets, VgrConfig};
33pub use safety::{BreakerConfig, KillSwitch};
34
35#[cfg(test)]
36mod tests;
37
38pub struct Vgr {
40 route: FallThrough,
41 classifier: Arc<runtime::VgrClassifier>,
42}
43
44impl Vgr {
45 pub fn new(config: VgrConfig) -> Result<Self> {
47 config.validate()?;
48 let mut affinity = AffinityRouter::new().with_latch_only([config.targets.capable.clone()]);
49 let reason = if config.latch_escalation && config.mode != ServingMode::Shadow {
50 "session_latched"
51 } else {
52 affinity = affinity.with_release_on_user_turn();
53 "turn_verification_latched"
54 };
55 let affinity = Arc::new(affinity);
56 let mut route = FallThrough::new_with_state().with_name("vgr");
57 if config.compact_handoff {
58 route = route.with_classifier(Arc::new(compaction::Compactor));
59 }
60 let classifier = Arc::new(runtime::VgrClassifier {
61 breaker: safety::CircuitBreaker::new(config.breaker),
62 config,
63 });
64 if classifier.config.mode != ServingMode::Shadow {
66 route = route
67 .with_processor(affinity.clone())
68 .with_classifier(Arc::new(runtime::Held(affinity, reason)));
69 }
70 let route = route.with_classifier(classifier.clone());
71 Ok(Self { route, classifier })
72 }
73}
74
75#[async_trait::async_trait]
76impl Algorithm for Vgr {
77 fn name(&self) -> &str {
78 "vgr"
79 }
80
81 async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome> {
82 let arrived = self
85 .classifier
86 .efficient_may_stand_in(&request)
87 .then(|| request.clone());
88 let mut outcome = self.route.execute(driver, request).await?;
89 outcome.selected_model_ids.truncate(1);
91 let config = &self.classifier.config;
92 if let Some(arrived) = arrived
93 && outcome.response.is_none()
94 && outcome.selected_model_ids.first() == Some(&config.targets.capable)
95 && self.classifier.efficient_may_stand_in(&arrived)
97 {
98 outcome
99 .selected_model_ids
100 .push(config.targets.efficient.clone());
101 outcome.fallback_request = Some(arrived);
102 }
103 Ok(outcome)
104 }
105
106 fn tracks_answers(&self) -> bool {
107 self.route.tracks_answers()
108 }
109
110 async fn observe_answer(&self, session_id: &str, answer: AnswerSummary) {
111 self.route.observe_answer(session_id, answer).await;
112 }
113}
114
115#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
117enum Branch {
118 Coding,
120 Answer,
122 Chat,
124 Agentic,
126 DefaultVerified,
128 #[default]
130 Unknown,
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135enum TaskType {
136 Coding,
137 Agentic,
138 Answer,
139 Chat,
140}
141
142#[derive(Default)]
144struct Capabilities {
145 branch: Branch,
146 transcript: Option<String>,
148 tools: Option<ToolRecord>,
150}
151
152fn derive_capabilities(
154 request: &Request,
155 attempt: &str,
156 task_type: Option<TaskType>,
157) -> Capabilities {
158 let (turns, unsupported) = text::turns(request);
159 if unsupported || text::user_task_text(&turns).trim().is_empty() || attempt.trim().is_empty() {
160 return Capabilities::default();
161 }
162 let caps = |branch, transcript| Capabilities {
163 branch,
164 transcript,
165 tools: Some(ToolRecord::from_request(request)),
166 };
167
168 if task_type == Some(TaskType::Answer) && !text::turn_has_tool_trajectory(request) {
171 return Capabilities {
172 tools: None,
173 ..caps(
174 Branch::Answer,
175 Some(render::render_session(&turns, attempt)),
176 )
177 };
178 }
179 if task_type == Some(TaskType::Agentic)
181 || (text::has_tool_trajectory(request)
182 && !matches!(task_type, Some(TaskType::Answer | TaskType::Chat)))
183 {
184 return caps(
185 Branch::Agentic,
186 Some(render::render_agentic_view(request, &turns, attempt)),
187 );
188 }
189 let transcript = Some(render::render_session(&turns, attempt));
190 if text::observed_hardening(attempt) || task_type == Some(TaskType::Coding) {
191 return caps(Branch::Coding, transcript);
192 }
193 if text::observed_tool_activity(attempt) {
194 caps(Branch::Agentic, transcript)
195 } else if matches!(task_type, Some(TaskType::Chat | TaskType::Answer)) {
196 caps(Branch::Chat, transcript)
197 } else {
198 caps(Branch::DefaultVerified, transcript)
199 }
200}