Skip to main content

behest_runtime/
policy.rs

1//! Runtime policy configuration.
2//!
3//! Defines limits and constraints for agent execution,
4//! including iteration limits, timeouts, resource budgets,
5//! and compaction strategy.
6
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use behest_core::cache::CacheTtl;
12
13use super::doom_loop::DoomLoopConfig;
14use super::input::InputAdmissionConfig;
15use super::tool_output::ToolOutputConfig;
16use behest_provider::{ModelName, ProviderId};
17
18/// Compaction configuration for automatic context compression.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[non_exhaustive]
21pub struct CompactionConfig {
22    /// Enable automatic compaction before provider turns. Default: `true`.
23    #[serde(default = "default_true")]
24    pub auto: bool,
25    /// Enable old tool output pruning. Default: `false`.
26    #[serde(default)]
27    pub prune: bool,
28    /// Token headroom between context limit and compaction trigger. Default: `20_000`.
29    #[serde(default = "default_buffer")]
30    pub buffer_tokens: usize,
31    /// Tokens to retain as recent context after compaction. Default: `8_000`.
32    #[serde(default = "default_keep")]
33    pub keep_tokens: usize,
34    /// Number of recent turns to preserve intact. Default: `2`.
35    #[serde(default = "default_tail_turns")]
36    pub tail_turns: usize,
37    /// Model to use for compaction. Falls back to the run's model when `None`.
38    #[serde(default)]
39    pub model: Option<ModelName>,
40    /// Provider to use for compaction. Falls back to the run's provider when `None`.
41    #[serde(default)]
42    pub provider: Option<ProviderId>,
43    /// Number of consecutive compaction failures before the circuit breaker opens.
44    /// When open, proactive compaction is skipped. Default: `3`.
45    #[serde(default = "default_circuit_breaker_threshold")]
46    pub circuit_breaker_threshold: u32,
47}
48
49const fn default_true() -> bool {
50    true
51}
52
53const fn default_buffer() -> usize {
54    20_000
55}
56
57const fn default_keep() -> usize {
58    8_000
59}
60
61const fn default_tail_turns() -> usize {
62    2
63}
64
65const fn default_circuit_breaker_threshold() -> u32 {
66    3
67}
68
69const fn default_max_output_recovery() -> usize {
70    3
71}
72
73impl Default for CompactionConfig {
74    fn default() -> Self {
75        Self {
76            auto: true,
77            prune: false,
78            buffer_tokens: 20_000,
79            keep_tokens: 8_000,
80            tail_turns: 2,
81            model: None,
82            provider: None,
83            circuit_breaker_threshold: 3,
84        }
85    }
86}
87
88impl CompactionConfig {
89    /// Creates a new compaction config with defaults.
90    #[must_use]
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Disables automatic compaction.
96    #[must_use]
97    pub fn with_auto_disabled(mut self) -> Self {
98        self.auto = false;
99        self
100    }
101
102    /// Enables tool output pruning.
103    #[must_use]
104    pub fn with_prune(mut self) -> Self {
105        self.prune = true;
106        self
107    }
108
109    /// Sets the buffer token count.
110    #[must_use]
111    pub fn with_buffer_tokens(mut self, tokens: usize) -> Self {
112        self.buffer_tokens = tokens;
113        self
114    }
115
116    /// Sets the keep token count.
117    #[must_use]
118    pub fn with_keep_tokens(mut self, tokens: usize) -> Self {
119        self.keep_tokens = tokens;
120        self
121    }
122
123    /// Sets the number of recent turns to preserve.
124    #[must_use]
125    pub fn with_tail_turns(mut self, turns: usize) -> Self {
126        self.tail_turns = turns;
127        self
128    }
129
130    /// Sets the compaction model.
131    #[must_use]
132    pub fn with_model(mut self, model: ModelName) -> Self {
133        self.model = Some(model);
134        self
135    }
136
137    /// Sets the compaction provider.
138    #[must_use]
139    pub fn with_provider(mut self, provider: ProviderId) -> Self {
140        self.provider = Some(provider);
141        self
142    }
143
144    /// Sets the circuit breaker failure threshold.
145    #[must_use]
146    pub fn with_circuit_breaker_threshold(mut self, threshold: u32) -> Self {
147        self.circuit_breaker_threshold = threshold;
148        self
149    }
150}
151
152/// Prompt caching configuration.
153///
154/// Controls automatic placement of cache breakpoints in the context
155/// pipeline. Active only when the provider advertises `prompt_caching =
156/// true` in [`ProviderCapabilities`](behest_provider::ProviderCapabilities).
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[non_exhaustive]
159pub struct PromptCacheConfig {
160    /// Master switch. When `false`, the context pipeline never adds cache
161    /// markers regardless of the per-adapter settings. Default: `true`.
162    #[serde(default = "default_true")]
163    pub enabled: bool,
164    /// Default TTL for automatically placed cache breakpoints.
165    /// Default: [`CacheTtl::FiveMinutes`].
166    #[serde(default)]
167    pub default_ttl: CacheTtl,
168    /// When `true`, the context pipeline automatically inserts cache
169    /// markers at three stable regions (system end, tools end,
170    /// conversation tail). When `false`, only explicit per-`ContentPart`
171    /// markers from adapters are honored. Default: `true`.
172    #[serde(default = "default_true")]
173    pub auto_breakpoints: bool,
174    /// Minimum estimated system-prompt token count before the system-end
175    /// breakpoint is placed. Avoids wasting cache writes on tiny prompts.
176    /// Default: `1024`.
177    #[serde(default = "default_min_system_tokens")]
178    pub min_system_tokens: usize,
179}
180
181const fn default_min_system_tokens() -> usize {
182    1024
183}
184
185impl Default for PromptCacheConfig {
186    fn default() -> Self {
187        Self {
188            enabled: true,
189            default_ttl: CacheTtl::FiveMinutes,
190            auto_breakpoints: true,
191            min_system_tokens: 1024,
192        }
193    }
194}
195
196impl PromptCacheConfig {
197    /// Creates a new `PromptCacheConfig` with defaults.
198    #[must_use]
199    pub fn new() -> Self {
200        Self::default()
201    }
202
203    /// Disables prompt caching entirely.
204    #[must_use]
205    pub fn disabled() -> Self {
206        Self {
207            enabled: false,
208            ..Self::default()
209        }
210    }
211
212    /// Sets the default TTL for auto-placed cache markers.
213    #[must_use]
214    pub fn with_default_ttl(mut self, ttl: CacheTtl) -> Self {
215        self.default_ttl = ttl;
216        self
217    }
218
219    /// Sets whether to auto-place cache breakpoints.
220    #[must_use]
221    pub fn with_auto_breakpoints(mut self, enabled: bool) -> Self {
222        self.auto_breakpoints = enabled;
223        self
224    }
225
226    /// Sets the minimum system-prompt token count for the system breakpoint.
227    #[must_use]
228    pub fn with_min_system_tokens(mut self, tokens: usize) -> Self {
229        self.min_system_tokens = tokens;
230        self
231    }
232}
233
234/// Runtime policy for agent execution.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236#[non_exhaustive]
237pub struct RuntimePolicy {
238    /// Maximum number of model call iterations per run.
239    pub max_iterations: usize,
240    /// Maximum total tokens per run.
241    pub max_tokens: Option<usize>,
242    /// Maximum concurrent tool executions.
243    pub max_tool_concurrency: usize,
244    /// Timeout for individual tool execution.
245    pub tool_timeout: Duration,
246    /// Timeout for provider calls.
247    pub provider_timeout: Duration,
248    /// Whether to allow tool execution failures to continue the run.
249    pub continue_on_tool_failure: bool,
250    /// Whether to retry on retryable provider errors.
251    pub retry_on_provider_error: bool,
252    /// Maximum retries for provider calls.
253    pub max_retries: usize,
254    /// Compaction strategy configuration.
255    #[serde(default)]
256    pub compaction: CompactionConfig,
257    /// Tool output truncation configuration.
258    #[serde(default)]
259    pub tool_output: ToolOutputConfig,
260    /// Doom loop detection configuration.
261    #[serde(default)]
262    pub doom_loop: DoomLoopConfig,
263    /// Input admission pipeline configuration.
264    #[serde(default)]
265    pub input_admission: InputAdmissionConfig,
266    /// Maximum attempts to recover from model output truncation
267    /// (`FinishReason::Length`). When exceeded, the run completes with
268    /// truncated output. Default: `3`.
269    #[serde(default = "default_max_output_recovery")]
270    pub max_output_recovery_attempts: usize,
271    /// Prompt caching strategy. Default: enabled with auto-breakpoints and
272    /// 5-minute TTL.
273    #[serde(default)]
274    pub prompt_cache: PromptCacheConfig,
275}
276
277impl Default for RuntimePolicy {
278    fn default() -> Self {
279        Self {
280            max_iterations: 10,
281            max_tokens: None,
282            max_tool_concurrency: 4,
283            tool_timeout: Duration::from_secs(30),
284            provider_timeout: Duration::from_secs(60),
285            continue_on_tool_failure: true,
286            retry_on_provider_error: true,
287            max_retries: 2,
288            compaction: CompactionConfig::default(),
289            tool_output: ToolOutputConfig::default(),
290            doom_loop: DoomLoopConfig::default(),
291            input_admission: InputAdmissionConfig::default(),
292            max_output_recovery_attempts: 3,
293            prompt_cache: PromptCacheConfig::default(),
294        }
295    }
296}
297
298impl RuntimePolicy {
299    /// Creates a new policy with default values.
300    #[must_use]
301    pub fn new() -> Self {
302        Self::default()
303    }
304
305    /// Sets the maximum iterations.
306    #[must_use]
307    pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
308        self.max_iterations = max_iterations;
309        self
310    }
311
312    /// Sets the maximum tokens.
313    #[must_use]
314    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
315        self.max_tokens = Some(max_tokens);
316        self
317    }
318
319    /// Sets the maximum tool concurrency.
320    #[must_use]
321    pub fn with_max_tool_concurrency(mut self, max_tool_concurrency: usize) -> Self {
322        self.max_tool_concurrency = max_tool_concurrency.max(1);
323        self
324    }
325
326    /// Sets the tool timeout.
327    #[must_use]
328    pub fn with_tool_timeout(mut self, tool_timeout: Duration) -> Self {
329        self.tool_timeout = tool_timeout;
330        self
331    }
332
333    /// Sets the provider timeout.
334    #[must_use]
335    pub fn with_provider_timeout(mut self, provider_timeout: Duration) -> Self {
336        self.provider_timeout = provider_timeout;
337        self
338    }
339
340    /// Sets whether to continue on tool failure.
341    #[must_use]
342    pub fn with_continue_on_tool_failure(mut self, continue_on_tool_failure: bool) -> Self {
343        self.continue_on_tool_failure = continue_on_tool_failure;
344        self
345    }
346
347    /// Sets whether to retry on provider errors.
348    #[must_use]
349    pub fn with_retry_on_provider_error(mut self, retry_on_provider_error: bool) -> Self {
350        self.retry_on_provider_error = retry_on_provider_error;
351        self
352    }
353
354    /// Sets the maximum retries.
355    #[must_use]
356    pub fn with_max_retries(mut self, max_retries: usize) -> Self {
357        self.max_retries = max_retries;
358        self
359    }
360
361    /// Sets the compaction configuration.
362    #[must_use]
363    pub fn with_compaction(mut self, compaction: CompactionConfig) -> Self {
364        self.compaction = compaction;
365        self
366    }
367
368    /// Sets the tool output truncation configuration.
369    #[must_use]
370    pub fn with_tool_output(mut self, config: ToolOutputConfig) -> Self {
371        self.tool_output = config;
372        self
373    }
374
375    /// Sets the doom loop detection configuration.
376    #[must_use]
377    pub fn with_doom_loop(mut self, config: DoomLoopConfig) -> Self {
378        self.doom_loop = config;
379        self
380    }
381
382    /// Sets the input admission pipeline configuration.
383    #[must_use]
384    pub fn with_input_admission(mut self, config: InputAdmissionConfig) -> Self {
385        self.input_admission = config;
386        self
387    }
388
389    /// Sets the maximum output recovery attempts for truncated responses.
390    #[must_use]
391    pub fn with_max_output_recovery_attempts(mut self, attempts: usize) -> Self {
392        self.max_output_recovery_attempts = attempts;
393        self
394    }
395
396    /// Sets the prompt caching configuration.
397    #[must_use]
398    pub fn with_prompt_cache(mut self, config: PromptCacheConfig) -> Self {
399        self.prompt_cache = config;
400        self
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn default_policy() {
410        let policy = RuntimePolicy::default();
411        assert_eq!(policy.max_iterations, 10);
412        assert_eq!(policy.max_tool_concurrency, 4);
413        assert!(policy.max_tokens.is_none());
414        assert!(policy.continue_on_tool_failure);
415    }
416
417    #[test]
418    fn policy_builder() {
419        let policy = RuntimePolicy::new()
420            .with_max_iterations(5)
421            .with_max_tokens(1000)
422            .with_max_tool_concurrency(2);
423
424        assert_eq!(policy.max_iterations, 5);
425        assert_eq!(policy.max_tokens, Some(1000));
426        assert_eq!(policy.max_tool_concurrency, 2);
427    }
428}