Skip to main content

vtcode_config/core/agent/
approval.rs

1//! Approval, confidence-escalation, and skeptic-panel configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for async (out-of-band) approval of tool execution requests.
6///
7/// When enabled, approval requests that exceed the auto-approve threshold are
8/// written to a blocker file instead of blocking on terminal input.  A
9/// notification command (e.g. email, Slack webhook) can be configured to
10/// alert the user.  The user can then approve or reject via the CLI.
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct AsyncApprovalConfig {
14    /// Master switch.  Default: false (opt-in).
15    #[serde(default = "default_async_approval_enabled")]
16    enabled: bool,
17
18    /// Maximum time in seconds before a deferred approval is auto-denied.
19    #[serde(default = "default_async_approval_timeout_secs")]
20    timeout_secs: u64,
21
22    /// Optional command invoked with the blocker file path as argument when
23    /// an approval request is deferred (e.g. `/usr/local/bin/notify-approval`).
24    #[serde(default)]
25    notify_command: Option<String>,
26
27    /// Auto-approve tool calls whose estimated cost is below this threshold (USD).
28    /// Set to 0.0 to require approval for everything.
29    #[serde(default = "default_async_approval_auto_approve_below")]
30    auto_approve_below_usd: f64,
31
32    /// When set, auto-approve after this many seconds if no explicit answer.
33    /// The blocker file is updated with the auto-approval decision.
34    #[serde(default)]
35    auto_approve_timeout_secs: Option<u64>,
36}
37
38impl Default for AsyncApprovalConfig {
39    fn default() -> Self {
40        Self {
41            enabled: default_async_approval_enabled(),
42            timeout_secs: default_async_approval_timeout_secs(),
43            notify_command: None,
44            auto_approve_below_usd: default_async_approval_auto_approve_below(),
45            auto_approve_timeout_secs: None,
46        }
47    }
48}
49
50/// Configuration for the adversarial multi-model evaluator panel.
51///
52/// When enabled, the evaluator phase runs against every listed model in
53/// parallel and aggregates the strictest verdict/scorecard across the panel.
54/// This is opt-in because each additional skeptic adds latency and cost.
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56#[derive(Debug, Clone, Deserialize, Serialize)]
57pub struct SkepticPanelConfig {
58    /// Master switch. Default: false (opt-in).
59    #[serde(default = "default_skeptic_panel_enabled")]
60    pub enabled: bool,
61
62    /// Model identifiers to run as skeptic evaluators (in addition to the
63    /// primary evaluator). Empty when `enabled = false`.
64    #[serde(default)]
65    pub models: Vec<String>,
66}
67
68impl Default for SkepticPanelConfig {
69    fn default() -> Self {
70        Self {
71            enabled: default_skeptic_panel_enabled(),
72            models: Vec::new(),
73        }
74    }
75}
76
77const fn default_skeptic_panel_enabled() -> bool {
78    false
79}
80
81const fn default_async_approval_enabled() -> bool {
82    false
83}
84const fn default_async_approval_timeout_secs() -> u64 {
85    3600
86}
87const fn default_async_approval_auto_approve_below() -> f64 {
88    0.10
89}
90///
91/// Implements the escalation decision rule from "The Hitchhiker's Guide to Agentic AI"
92/// (Eq. 18.12):
93///   Escalate iff p_success < tau_conf OR action in A_irreversible OR cost > B_auto
94///
95/// When enabled, the harness evaluates tool calls before execution and escalates
96/// high-risk or low-confidence actions to a blocked-handoff (requiring human review).
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct ConfidenceEscalationConfig {
100    /// Master switch.  Default: false (opt-in).
101    #[serde(default = "default_escalation_enabled")]
102    pub enabled: bool,
103
104    /// Minimum p_success threshold (0.0–1.0).  Tools whose estimated success
105    /// probability falls below this threshold trigger escalation.
106    #[serde(default = "default_escalation_confidence_threshold")]
107    pub confidence_threshold: f64,
108
109    /// Tool names that always trigger escalation regardless of confidence.
110    /// These are matched against the tool call's function name.
111    #[serde(default = "default_escalation_always_escalate_tools")]
112    pub always_escalate_tools: Vec<String>,
113
114    /// Maximum estimated cost in USD before escalation.  Tool calls whose
115    /// estimated cost exceeds this threshold trigger escalation.
116    #[serde(default = "default_escalation_cost_threshold")]
117    pub cost_threshold_usd: f64,
118
119    /// Whether to solicit LLM-based confidence estimation alongside heuristics.
120    /// When false (default), only heuristic signals (error history, tool class)
121    /// are used to estimate p_success.
122    #[serde(default = "default_escalation_llm_confidence")]
123    pub use_llm_confidence: bool,
124
125    /// When true, only escalate during PlanBuildEvaluate orchestration mode.
126    /// During single-mode or other orchestration modes, escalation is skipped.
127    #[serde(default)]
128    pub plan_mode_only: bool,
129
130    /// Maximum number of re-plan attempts via conversation injection before
131    /// prompting the user.  The agent sees a structured message explaining
132    /// which tools were blocked and is asked to try a different approach.
133    #[serde(default = "default_escalation_max_replan")]
134    pub max_replan_attempts: u32,
135
136    /// Maximum total escalations (across all chain steps) before aborting
137    /// with partial results.  Must be >= max_replan_attempts.
138    #[serde(default = "default_escalation_max_total")]
139    pub max_total_escalations: u32,
140
141    /// Whether to prompt the user when the escalation chain is exhausted.
142    /// When false, the chain falls through directly to abort-with-partial-results.
143    #[serde(default = "default_escalation_prompt_user")]
144    pub prompt_user_on_exhaust: bool,
145}
146
147impl ConfidenceEscalationConfig {
148    /// Returns true if any escalation rule would trigger for the given tool name
149    /// and estimated cost.  Used as a fast pre-check before running the full gate.
150    pub fn would_escalate(&self, tool_name: &str, estimated_cost_usd: Option<f64>) -> bool {
151        if self.always_escalate_tools.iter().any(|t| t == tool_name) {
152            return true;
153        }
154        if let Some(cost) = estimated_cost_usd
155            && cost > self.cost_threshold_usd
156        {
157            return true;
158        }
159        false
160    }
161}
162
163impl Default for ConfidenceEscalationConfig {
164    fn default() -> Self {
165        Self {
166            enabled: default_escalation_enabled(),
167            confidence_threshold: default_escalation_confidence_threshold(),
168            always_escalate_tools: default_escalation_always_escalate_tools(),
169            cost_threshold_usd: default_escalation_cost_threshold(),
170            use_llm_confidence: default_escalation_llm_confidence(),
171            plan_mode_only: false,
172            max_replan_attempts: default_escalation_max_replan(),
173            max_total_escalations: default_escalation_max_total(),
174            prompt_user_on_exhaust: default_escalation_prompt_user(),
175        }
176    }
177}
178
179const fn default_escalation_enabled() -> bool {
180    false
181}
182const fn default_escalation_confidence_threshold() -> f64 {
183    0.7
184}
185fn default_escalation_always_escalate_tools() -> Vec<String> {
186    vec!["delete_file".to_string(), "remove".to_string(), "rm".to_string()]
187}
188const fn default_escalation_cost_threshold() -> f64 {
189    0.05
190}
191const fn default_escalation_llm_confidence() -> bool {
192    false
193}
194const fn default_escalation_max_replan() -> u32 {
195    3
196}
197const fn default_escalation_max_total() -> u32 {
198    5
199}
200const fn default_escalation_prompt_user() -> bool {
201    true
202}