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<runtime::VgrState>,
40}
41
42impl Vgr {
43 pub fn new(config: VgrConfig) -> Result<Self> {
45 config.validate()?;
46 let cloud = config.targets.cloud.clone();
47 let turn_affinity = Arc::new(
50 AffinityRouter::new()
51 .with_release_on_user_turn()
52 .with_latch_only([cloud.clone()]),
53 );
54 let classifier = Arc::new(runtime::VgrClassifier {
55 breaker: safety::CircuitBreaker::new(config.breaker),
56 config,
57 });
58 let route = FallThrough::new_with_state()
59 .with_name("vgr")
60 .with_processor(turn_affinity.clone())
61 .with_classifier(turn_affinity)
62 .with_classifier(classifier);
63 Ok(Self { route })
64 }
65}
66
67#[async_trait::async_trait]
68impl Algorithm for Vgr {
69 fn name(&self) -> &str {
70 "vgr"
71 }
72
73 async fn route(self: Arc<Self>, driver: Driver, request: Request) -> Result<RoutingOutcome> {
74 let mut outcome = self.route.execute(driver, request).await?;
75 outcome.selected_model_ids.truncate(1);
77 Ok(outcome)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83enum Branch {
84 Coding,
86 Answer,
88 Chat,
90 Agentic,
92 DefaultVerified,
94 #[default]
96 Unknown,
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101enum TaskType {
102 Coding,
103 Agentic,
104 Answer,
105 Chat,
106}
107
108#[derive(Default)]
110struct Capabilities {
111 branch: Branch,
112 transcript: Option<String>,
114 tools: Option<ToolRecord>,
116}
117
118fn derive_capabilities(
120 request: &Request,
121 attempt: &str,
122 task_type: Option<TaskType>,
123) -> Capabilities {
124 let (turns, unsupported) = text::turns(request);
125 if unsupported || text::user_task_text(&turns).trim().is_empty() || attempt.trim().is_empty() {
126 return Capabilities::default();
127 }
128 let caps = |branch, transcript| Capabilities {
129 branch,
130 transcript,
131 tools: Some(ToolRecord::from_request(request)),
132 };
133
134 if task_type == Some(TaskType::Answer) && !text::has_assistant_turn(&turns) {
135 return Capabilities {
136 tools: None,
137 ..caps(
138 Branch::Answer,
139 Some(render::render_session(&turns, attempt)),
140 )
141 };
142 }
143 if task_type == Some(TaskType::Agentic)
145 || (text::has_tool_trajectory(request)
146 && !matches!(task_type, Some(TaskType::Answer | TaskType::Chat)))
147 {
148 return caps(
149 Branch::Agentic,
150 Some(render::render_agentic_view(request, &turns, attempt)),
151 );
152 }
153 if text::observed_hardening(attempt) || task_type == Some(TaskType::Coding) {
154 return caps(Branch::Coding, None);
155 }
156 let transcript = Some(render::render_session(&turns, attempt));
157 if text::observed_tool_activity(attempt) {
158 caps(Branch::Agentic, transcript)
159 } else if matches!(task_type, Some(TaskType::Chat | TaskType::Answer)) {
160 caps(Branch::Chat, transcript)
161 } else {
162 caps(Branch::DefaultVerified, transcript)
163 }
164}