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 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 let route = route
65 .with_processor(affinity.clone())
66 .with_classifier(Arc::new(runtime::Held(affinity, reason)))
67 .with_classifier(classifier.clone());
68 Ok(Self { route, classifier })
69 }
70}
71
72#[async_trait::async_trait]
73impl Algorithm for Vgr {
74 fn name(&self) -> &str {
75 "vgr"
76 }
77
78 async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome> {
79 let arrived = self
82 .classifier
83 .efficient_may_stand_in(&request)
84 .then(|| request.clone());
85 let mut outcome = self.route.execute(driver, request).await?;
86 outcome.selected_model_ids.truncate(1);
88 let config = &self.classifier.config;
89 if let Some(arrived) = arrived
90 && outcome.response.is_none()
91 && outcome.selected_model_ids.first() == Some(&config.targets.capable)
92 && self.classifier.efficient_may_stand_in(&arrived)
94 {
95 outcome
96 .selected_model_ids
97 .push(config.targets.efficient.clone());
98 outcome.fallback_request = Some(arrived);
99 }
100 Ok(outcome)
101 }
102}
103
104#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
106enum Branch {
107 Coding,
109 Answer,
111 Chat,
113 Agentic,
115 DefaultVerified,
117 #[default]
119 Unknown,
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124enum TaskType {
125 Coding,
126 Agentic,
127 Answer,
128 Chat,
129}
130
131#[derive(Default)]
133struct Capabilities {
134 branch: Branch,
135 transcript: Option<String>,
137 tools: Option<ToolRecord>,
139}
140
141fn derive_capabilities(
143 request: &Request,
144 attempt: &str,
145 task_type: Option<TaskType>,
146) -> Capabilities {
147 let (turns, unsupported) = text::turns(request);
148 if unsupported || text::user_task_text(&turns).trim().is_empty() || attempt.trim().is_empty() {
149 return Capabilities::default();
150 }
151 let caps = |branch, transcript| Capabilities {
152 branch,
153 transcript,
154 tools: Some(ToolRecord::from_request(request)),
155 };
156
157 if task_type == Some(TaskType::Answer) && !text::has_assistant_turn(&turns) {
158 return Capabilities {
159 tools: None,
160 ..caps(
161 Branch::Answer,
162 Some(render::render_session(&turns, attempt)),
163 )
164 };
165 }
166 if task_type == Some(TaskType::Agentic)
168 || (text::has_tool_trajectory(request)
169 && !matches!(task_type, Some(TaskType::Answer | TaskType::Chat)))
170 {
171 return caps(
172 Branch::Agentic,
173 Some(render::render_agentic_view(request, &turns, attempt)),
174 );
175 }
176 let transcript = Some(render::render_session(&turns, attempt));
177 if text::observed_hardening(attempt) || task_type == Some(TaskType::Coding) {
178 return caps(Branch::Coding, transcript);
179 }
180 if text::observed_tool_activity(attempt) {
181 caps(Branch::Agentic, transcript)
182 } else if matches!(task_type, Some(TaskType::Chat | TaskType::Answer)) {
183 caps(Branch::Chat, transcript)
184 } else {
185 caps(Branch::DefaultVerified, transcript)
186 }
187}