Skip to main content

switchyard_libsy/algorithms/
vgr.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Verification-gated routing: fail-closed capability derivation.
5//!
6//! Capabilities are never client-declared. They come from the router's own
7//! typing of the request, the attempt it produced on the efficient tier, and its own reading
8//! of the tool results in the conversation. No client-reachable input selects
9//! a weaker regime than the default one; unsupported content and missing
10//! evidence select [`Branch::Unknown`], which never commits.
11
12use 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
38/// A verification-gated route between an efficient and a capable tier.
39pub struct Vgr {
40    route: FallThrough,
41    classifier: Arc<runtime::VgrClassifier>,
42}
43
44impl Vgr {
45    /// Validates and constructs a verification-gated route.
46    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        // The handoff note is written for the capable tier, so an efficient stand-in gets the request
80        // as it arrived.
81        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        // Efficient answers are already buffered and have no fallback.
87        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            // The efficient tier may have failed while this request was routed.
93            && 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/// The verification regime a request's capabilities license.
105#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
106enum Branch {
107    /// Code or test activity, with no sandboxed checker to appeal to.
108    Coding,
109    /// A single-turn request typed as answer-seeking.
110    Answer,
111    /// Conversational traffic.
112    Chat,
113    /// A tool-using session verified from its trajectory.
114    Agentic,
115    /// Anything else with an attempt to verify.
116    DefaultVerified,
117    /// Nothing to verify.
118    #[default]
119    Unknown,
120}
121
122/// A task type produced by the router's own typing call; `None` abstains.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124enum TaskType {
125    Coding,
126    Agentic,
127    Answer,
128    Chat,
129}
130
131/// The complete input the decision core sees.
132#[derive(Default)]
133struct Capabilities {
134    branch: Branch,
135    /// The judged view of the request and attempt.
136    transcript: Option<String>,
137    /// The router's own tool-result record; unused by the answer regime.
138    tools: Option<ToolRecord>,
139}
140
141/// Derives capabilities from the router's own evidence, failing closed.
142fn 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    // A tool trajectory is judged as agentic work unless typed conversational.
167    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}