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