Skip to main content

vtcode_config/constants/
tool_limits.rs

1/// Default maximum number of tool calls in one agent turn.
2pub const DEFAULT_MAX_TOOL_CALLS_PER_TURN: usize = 120;
3/// Default safety-gateway session fuse when no run-specific budget is loaded.
4pub const DEFAULT_SAFETY_MAX_TOOL_CALLS_PER_SESSION: usize = 100;
5/// Default maximum number of tool-loop iterations in one agent turn.
6pub const DEFAULT_MAX_TOOL_LOOPS: usize = 60;
7/// Default maximum number of turns in full-auto execution.
8pub const DEFAULT_FULL_AUTO_MAX_TURNS: usize = 100;
9/// Default conversation-turn retention limit. This is a context-retention
10/// limit, not a tool-execution budget.
11pub const DEFAULT_MAX_CONVERSATION_TURNS: usize = 150;
12
13/// Minimum per-turn tool-call budget while planning research is active.
14pub const PLANNING_WORKFLOW_MIN_TOOL_CALLS_PER_TURN: usize = 120;
15/// Minimum per-turn tool-call budget for executing an approved plan.
16pub const APPROVED_PLAN_MIN_TOOL_CALLS_PER_TURN: usize = 120;
17
18/// Hard cap for ordinary tool-loop extensions.
19pub const MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP: usize = 120;
20/// Multiplier used to derive the ordinary extension hard cap.
21pub const MAX_TOOL_LOOP_CAP_MULTIPLIER: usize = 3;
22/// Maximum ordinary tool-loop extension accepted from one prompt.
23pub const MAX_TOOL_LOOP_INCREMENT_PER_PROMPT: usize = 50;
24/// One-time internal allowance applied when an approved plan enters execution.
25/// This is not a user-facing configuration field and remains bounded by the
26/// ordinary tool-loop hard cap.
27pub const APPROVED_PLAN_TOOL_LOOP_INCREMENT: usize = 50;
28/// Minimum tool-loop budget while planning research is active.
29pub const PLANNING_WORKFLOW_MIN_TOOL_LOOPS: usize = 60;
30/// Hard cap for planning tool-loop extensions.
31pub const PLANNING_WORKFLOW_MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP: usize = 240;
32/// Multiplier used to derive the planning extension hard cap.
33pub const PLANNING_WORKFLOW_TOOL_LOOP_CAP_MULTIPLIER: usize = 6;
34/// Maximum planning tool-loop extension accepted from one prompt.
35pub const PLANNING_WORKFLOW_MAX_TOOL_LOOP_INCREMENT_PER_PROMPT: usize = 80;
36
37/// Total auto-granted session headroom per session (tool calls).
38///
39/// Bounds full-auto session growth without blocking normal harness runs:
40/// the default `100` base reaches `2100` total (`20` auto-grants of `+100`),
41/// while a derived unified base such as `18_000` reaches `20_000`. Manual
42/// grants are uncapped; only the automatic path consults this budget.
43pub const MAX_SESSION_AUTO_GRANT_TOTAL_HEADROOM: usize = 2000;
44
45/// Per-turn cap on budget-exempt control-plane exec calls (blocking `wait`
46/// and bounded `inspect` of running sessions or spooled output).
47///
48/// These calls coordinate long-running commands without producing new
49/// execution work, so they are exempt from `max_tool_calls_per_turn` — a
50/// long build must not consume the budget its follow-up work needs. The
51/// exemption is still bounded so a model cannot replace all work with
52/// exempt calls; the ordinary loop detector and rate limits keep applying.
53pub const MAX_CONTROL_PLANE_TOOL_CALLS_PER_TURN: usize = 64;
54
55/// Increment a full-auto run grants itself against the session headroom cap.
56///
57/// Pure so the grant arithmetic stays unit tested without standing up a
58/// session. Returns `0` once the headroom is exhausted so the caller can
59/// fail closed instead of growing the session limit further.
60#[inline]
61pub const fn session_auto_grant_increment(already_auto_granted: usize, requested: usize) -> usize {
62    let remaining = MAX_SESSION_AUTO_GRANT_TOTAL_HEADROOM.saturating_sub(already_auto_granted);
63    if remaining == 0 {
64        return 0;
65    }
66    if requested > remaining {
67        return remaining;
68    }
69    requested
70}
71
72/// Absolute ceiling for per-turn tool-loop extensions derived from a
73/// configured base limit. Values at or above the absolute cap are returned
74/// unchanged so an already-generous configuration is never shrunk.
75pub const fn tool_loop_hard_cap(base_limit: usize, planning_active: bool) -> usize {
76    if planning_active {
77        if base_limit >= PLANNING_WORKFLOW_MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP {
78            return base_limit;
79        }
80        let scaled = base_limit.saturating_mul(PLANNING_WORKFLOW_TOOL_LOOP_CAP_MULTIPLIER);
81        if scaled > PLANNING_WORKFLOW_MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP {
82            return PLANNING_WORKFLOW_MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP;
83        }
84        return scaled;
85    }
86    if base_limit >= MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP {
87        return base_limit;
88    }
89    let scaled = base_limit.saturating_mul(MAX_TOOL_LOOP_CAP_MULTIPLIER);
90    if scaled > MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP {
91        return MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP;
92    }
93    scaled
94}
95
96// Control-plane exemption invariants, checked at compile time: the exempt
97// bucket must stay strictly below the ordinary per-turn budget (so it can
98// never become the dominant spend path) and generous enough that a
99// multi-hour build's wait/poll/inspect cadence fits.
100const _: () = {
101    assert!(MAX_CONTROL_PLANE_TOOL_CALLS_PER_TURN < DEFAULT_MAX_TOOL_CALLS_PER_TURN);
102    assert!(MAX_CONTROL_PLANE_TOOL_CALLS_PER_TURN >= 32);
103};
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn default_execution_loop_budgets_are_moderate_and_bounded() {
111        assert_eq!(DEFAULT_MAX_TOOL_CALLS_PER_TURN, 120);
112        assert_eq!(DEFAULT_MAX_TOOL_LOOPS, 60);
113        assert_eq!(DEFAULT_FULL_AUTO_MAX_TURNS, 100);
114        assert_eq!(DEFAULT_MAX_CONVERSATION_TURNS, 150);
115    }
116
117    #[test]
118    fn planning_and_approved_plan_budgets_are_explicitly_separate() {
119        assert_eq!(PLANNING_WORKFLOW_MIN_TOOL_CALLS_PER_TURN, 120);
120        assert_eq!(APPROVED_PLAN_MIN_TOOL_CALLS_PER_TURN, 120);
121        assert_eq!(PLANNING_WORKFLOW_MIN_TOOL_LOOPS, 60);
122        assert_eq!(MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP, 120);
123        assert_eq!(PLANNING_WORKFLOW_MAX_TOOL_LOOP_LIMIT_ABSOLUTE_CAP, 240);
124        assert_eq!(APPROVED_PLAN_TOOL_LOOP_INCREMENT, 50);
125    }
126
127    #[test]
128    fn control_plane_exempt_budget_is_bounded_but_generous() {
129        // The compile-time invariants live in the `const _` block above; this
130        // test documents the intent and keeps the guard-rail visible to test
131        // runners.
132        assert_eq!(MAX_CONTROL_PLANE_TOOL_CALLS_PER_TURN, 64);
133    }
134
135    #[test]
136    fn tool_loop_hard_cap_scales_and_bounds() {
137        assert_eq!(tool_loop_hard_cap(20, false), 60);
138        assert_eq!(tool_loop_hard_cap(40, false), 120);
139        assert_eq!(tool_loop_hard_cap(60, false), 120);
140        assert_eq!(tool_loop_hard_cap(120, false), 120);
141        assert_eq!(tool_loop_hard_cap(200, false), 200);
142        assert_eq!(tool_loop_hard_cap(0, false), 0);
143        assert_eq!(tool_loop_hard_cap(40, true), 240);
144        assert_eq!(tool_loop_hard_cap(120, true), 240);
145        assert_eq!(tool_loop_hard_cap(300, true), 300);
146    }
147
148    #[test]
149    fn session_auto_grant_increment_bounds_total_headroom() {
150        assert_eq!(session_auto_grant_increment(0, 100), 100);
151        assert_eq!(session_auto_grant_increment(1950, 100), 50);
152        assert_eq!(session_auto_grant_increment(2000, 100), 0);
153        assert_eq!(session_auto_grant_increment(2500, 100), 0);
154        assert_eq!(session_auto_grant_increment(0, 0), 0);
155    }
156}