Skip to main content

everruns_builtins/
framework_config.rs

1//! Curated Framework configuration values for portable capabilities.
2
3use everruns_capability::{CapabilityRef, CapabilitySpec, IntoCapability};
4
5/// Strategy used when a conversation outgrows the model context.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7#[non_exhaustive]
8pub enum CompactionStrategy {
9    /// Cascade through masking, provider-native compaction, and summarization.
10    #[default]
11    Auto,
12    /// Use the provider's native compaction operation.
13    Native,
14    /// Replace older tool outputs with compact summaries.
15    ObservationMasking,
16    /// Ask the configured model to summarize older turns.
17    Summarization,
18}
19
20impl CompactionStrategy {
21    fn as_str(self) -> &'static str {
22        match self {
23            Self::Auto => "auto",
24            Self::Native => "native",
25            Self::ObservationMasking => "observation_masking",
26            Self::Summarization => "summarization",
27        }
28    }
29}
30
31/// Application-facing context-compaction policy.
32///
33/// The default proactively compacts at 85% of the model's context budget and
34/// lets the runtime select the best available strategy. Durable checkpoint
35/// storage remains a host concern. This is the canonical `CompactionConfig`
36/// exported from `everruns-builtins` and `everruns`; the expanded runtime
37/// representation is named `RuntimeCompactionConfig`.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct CompactionConfig {
40    strategy: CompactionStrategy,
41    proactive: bool,
42    budget_percent: f32,
43}
44
45impl CompactionConfig {
46    /// Start with the safe automatic policy.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Choose the compaction strategy.
52    pub fn strategy(mut self, strategy: CompactionStrategy) -> Self {
53        self.strategy = strategy;
54        self
55    }
56
57    /// Enable or disable compaction before a provider rejects an oversized request.
58    pub fn proactive(mut self, enabled: bool) -> Self {
59        self.proactive = enabled;
60        self
61    }
62
63    /// Set the proactive trigger as a fraction of the model context budget.
64    ///
65    /// The capability validator accepts values from 0.1 through 1.0.
66    pub fn budget_percent(mut self, budget_percent: f32) -> Self {
67        self.budget_percent = budget_percent;
68        self
69    }
70}
71
72impl Default for CompactionConfig {
73    fn default() -> Self {
74        Self {
75            strategy: CompactionStrategy::Auto,
76            proactive: true,
77            budget_percent: 0.85,
78        }
79    }
80}
81
82impl IntoCapability for CompactionConfig {
83    fn into_capability(self) -> CapabilitySpec {
84        CapabilityRef::new(super::COMPACTION_CAPABILITY_ID)
85            .config(serde_json::json!({
86                "strategy": self.strategy.as_str(),
87                "proactive": self.proactive,
88                "budget_percent": self.budget_percent,
89            }))
90            .into()
91    }
92}
93
94/// Model-adaptive deferred tool-loading configuration.
95#[derive(Clone, Debug, Default, PartialEq, Eq)]
96pub struct ToolSearch {
97    threshold: Option<usize>,
98    never_defer: Vec<String>,
99}
100
101impl ToolSearch {
102    /// Select hosted tool search when the model supports it and the generic
103    /// client-side implementation otherwise.
104    pub fn automatic() -> Self {
105        Self::default()
106    }
107
108    /// Override the minimum total tool count at which schemas are deferred.
109    pub fn threshold(mut self, threshold: usize) -> Self {
110        self.threshold = Some(threshold);
111        self
112    }
113
114    /// Keep these tools' full schemas visible under the generic fallback.
115    pub fn never_defer<I, S>(mut self, names: I) -> Self
116    where
117        I: IntoIterator<Item = S>,
118        S: Into<String>,
119    {
120        self.never_defer.extend(names.into_iter().map(Into::into));
121        self
122    }
123}
124
125impl IntoCapability for ToolSearch {
126    fn into_capability(self) -> CapabilitySpec {
127        let mut config = serde_json::Map::new();
128        if let Some(threshold) = self.threshold {
129            config.insert("threshold".to_string(), threshold.into());
130        }
131        if !self.never_defer.is_empty() {
132            config.insert("never_defer".to_string(), self.never_defer.into());
133        }
134        CapabilityRef::new(super::AUTO_TOOL_SEARCH_CAPABILITY_ID)
135            .config(serde_json::Value::Object(config))
136            .into()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn typed_compaction_uses_the_stable_id_and_schema() {
146        let spec = CompactionConfig::new()
147            .strategy(CompactionStrategy::ObservationMasking)
148            .budget_percent(0.9)
149            .into_capability();
150        assert_eq!(spec.capability_ref().id(), "compaction");
151        let budget = spec.capability_ref().config_value()["budget_percent"]
152            .as_f64()
153            .unwrap();
154        assert!((budget - 0.9).abs() < 1e-6);
155    }
156
157    #[test]
158    fn typed_tool_search_uses_the_model_adaptive_capability() {
159        let spec = ToolSearch::automatic()
160            .threshold(12)
161            .never_defer(["read_file"])
162            .into_capability();
163        assert_eq!(spec.capability_ref().id(), "auto_tool_search");
164        assert_eq!(spec.capability_ref().config_value()["threshold"], 12);
165    }
166}