use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use devboy_core::ToolValueModel;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::token_counter::Tokenizer;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("adaptive-config I/O: {0}")]
Io(#[from] std::io::Error),
#[error("adaptive-config parse: {0}")]
Parse(#[from] toml::de::Error),
#[error("adaptive-config serialize: {0}")]
Serialize(#[from] toml::ser::Error),
#[error("adaptive-config unsupported schema version {0} (expected 1)")]
UnsupportedSchemaVersion(u32),
}
pub type Result<T> = std::result::Result<T, ConfigError>;
pub const CURRENT_SCHEMA_VERSION: u32 = 4;
pub const MIN_SUPPORTED_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveConfig {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub dedup: DedupConfig,
#[serde(default)]
pub templates: TemplatesConfig,
#[serde(default)]
pub mckp: MckpConfig,
#[serde(default)]
pub telemetry: TelemetryConfig,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub endpoint_overrides: BTreeMap<String, EndpointOverride>,
#[serde(default)]
pub profiles: ProfilesConfig,
#[serde(default)]
pub hints: HintsConfig,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tools: BTreeMap<String, ToolValueModel>,
#[serde(default)]
pub enrichment: EnrichmentConfig,
}
fn default_schema_version() -> u32 {
CURRENT_SCHEMA_VERSION
}
impl Default for AdaptiveConfig {
fn default() -> Self {
Self {
schema_version: CURRENT_SCHEMA_VERSION,
dedup: DedupConfig::default(),
templates: TemplatesConfig::default(),
mckp: MckpConfig::default(),
telemetry: TelemetryConfig::default(),
endpoint_overrides: BTreeMap::new(),
profiles: ProfilesConfig::default(),
hints: HintsConfig::default(),
tools: BTreeMap::new(),
enrichment: EnrichmentConfig::default(),
}
}
}
impl AdaptiveConfig {
pub fn load_or_default(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
return Ok(Self::default());
}
let s = fs::read_to_string(path)?;
let mut cfg: AdaptiveConfig = toml::from_str(&s)?;
cfg.upgrade_in_place()?;
Ok(cfg)
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let s = fs::read_to_string(path)?;
let mut cfg: AdaptiveConfig = toml::from_str(&s)?;
cfg.upgrade_in_place()?;
Ok(cfg)
}
fn upgrade_in_place(&mut self) -> Result<()> {
if self.schema_version > CURRENT_SCHEMA_VERSION {
return Err(ConfigError::UnsupportedSchemaVersion(self.schema_version));
}
if self.schema_version < MIN_SUPPORTED_SCHEMA_VERSION {
return Err(ConfigError::UnsupportedSchemaVersion(self.schema_version));
}
if self.schema_version < CURRENT_SCHEMA_VERSION {
self.schema_version = CURRENT_SCHEMA_VERSION;
}
Ok(())
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let s = toml::to_string_pretty(self)?;
let tmp = path.with_extension("toml.tmp");
fs::write(&tmp, s)?;
fs::rename(&tmp, path)?;
Ok(())
}
pub fn effective_dedup_enabled(&self, endpoint: &str) -> bool {
if let Some(o) = self.endpoint_overrides.get(endpoint)
&& let Some(v) = o.dedup_enabled
{
return v;
}
self.dedup.enabled_for(endpoint)
}
pub fn effective_min_body_chars(&self, endpoint: &str) -> usize {
self.endpoint_overrides
.get(endpoint)
.and_then(|o| o.min_body_chars)
.unwrap_or(self.dedup.min_body_chars)
}
pub fn effective_lru_size(&self, endpoint: &str) -> usize {
let per_ep = self
.endpoint_overrides
.get(endpoint)
.and_then(|o| o.lru_size);
match per_ep {
Some(n) => n.max(self.dedup.lru_size),
None => self.dedup.lru_size,
}
}
pub fn max_lru_size(&self) -> usize {
let mut n = self.dedup.lru_size;
for o in self.endpoint_overrides.values() {
if let Some(v) = o.lru_size {
n = n.max(v);
}
}
n.max(1)
}
pub fn effective_tokenizer_profile(&self) -> &TokenizerProfile {
let active = self.profiles.tokenizer.active.as_str();
let id = if active == "auto" || active.is_empty() {
"anthropic_class"
} else {
active
};
self.profiles
.tokenizer
.variants
.get(id)
.or_else(|| self.profiles.tokenizer.variants.get("anthropic_class"))
.unwrap_or_else(|| {
static FALLBACK: std::sync::OnceLock<TokenizerProfile> = std::sync::OnceLock::new();
FALLBACK.get_or_init(TokenizerProfile::default)
})
}
pub fn effective_token_count(&self, text: &str) -> usize {
self.effective_tokenizer_profile().count_tokens(text)
}
pub fn effective_template(&self, endpoint: &str) -> Option<&str> {
if let Some(o) = self.endpoint_overrides.get(endpoint)
&& let Some(t) = o.template_id.as_deref()
{
return Some(t);
}
self.templates.template_for(endpoint)
}
pub fn effective_tool_value_model(&self, tool_name: &str) -> Option<&ToolValueModel> {
if let Some(m) = self.tools.get(tool_name) {
return Some(m);
}
self.tools.get("*")
}
pub fn merge_right_wins(&mut self, other: AdaptiveConfig) {
self.dedup = other.dedup;
self.templates = other.templates;
self.mckp = other.mckp;
self.telemetry = other.telemetry;
self.profiles = other.profiles;
self.hints = other.hints;
for (k, v) in other.endpoint_overrides {
self.endpoint_overrides.insert(k, v);
}
for (k, v) in other.tools {
self.tools.insert(k, v);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DedupConfig {
#[serde(default = "default_lru_size")]
pub lru_size: usize,
#[serde(default)]
pub hint_verbosity: HintVerbosity,
#[serde(default)]
pub near_ref_enabled: bool,
#[serde(default = "default_min_body_chars")]
pub min_body_chars: usize,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub enabled_per_endpoint: BTreeMap<String, bool>,
}
fn default_lru_size() -> usize {
5
}
fn default_min_body_chars() -> usize {
200
}
impl Default for DedupConfig {
fn default() -> Self {
Self {
lru_size: default_lru_size(),
hint_verbosity: HintVerbosity::Standard,
near_ref_enabled: false,
min_body_chars: default_min_body_chars(),
enabled_per_endpoint: BTreeMap::new(),
}
}
}
impl DedupConfig {
pub fn enabled_for(&self, endpoint: &str) -> bool {
self.enabled_per_endpoint
.get(endpoint)
.copied()
.unwrap_or(true)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HintVerbosity {
Terse,
#[default]
Standard,
Verbose,
}
impl HintVerbosity {
pub fn to_runtime(self) -> crate::dedup::HintVerbosity {
match self {
Self::Terse => crate::dedup::HintVerbosity::Terse,
Self::Standard => crate::dedup::HintVerbosity::Standard,
Self::Verbose => crate::dedup::HintVerbosity::Verbose,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplatesConfig {
#[serde(default = "default_active_templates")]
pub active: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub endpoint_overrides: BTreeMap<String, String>,
}
fn default_active_templates() -> Vec<String> {
vec![
"csv_from_md".to_string(),
"pipeline_deep_mckp".to_string(),
"mr_diff_fence".to_string(),
]
}
impl Default for TemplatesConfig {
fn default() -> Self {
Self {
active: default_active_templates(),
endpoint_overrides: BTreeMap::new(),
}
}
}
impl TemplatesConfig {
pub fn is_template_active(&self, id: &str) -> bool {
self.active.iter().any(|s| s == id)
}
pub fn template_for(&self, endpoint: &str) -> Option<&str> {
self.endpoint_overrides.get(endpoint).map(String::as_str)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MckpConfig {
#[serde(default = "default_recursion_depth")]
pub recursion_depth: usize,
#[serde(default = "default_formats_enabled")]
pub formats_enabled: Vec<String>,
#[serde(default)]
pub shape_thresholds: ShapeThresholds,
}
fn default_recursion_depth() -> usize {
5
}
fn default_formats_enabled() -> Vec<String> {
vec![
"csv_from_md".to_string(),
"deep_mckp".to_string(),
"kv".to_string(),
"csv".to_string(),
"json_compact".to_string(),
]
}
impl Default for MckpConfig {
fn default() -> Self {
Self {
recursion_depth: default_recursion_depth(),
formats_enabled: default_formats_enabled(),
shape_thresholds: ShapeThresholds::default(),
}
}
}
impl MckpConfig {
pub fn format_enabled(&self, id: &str) -> bool {
self.formats_enabled.iter().any(|s| s == id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShapeThresholds {
#[serde(default = "thr_md_cols")]
pub markdown_table_min_cols: usize,
#[serde(default = "thr_arr_items")]
pub array_of_objects_min_items: usize,
#[serde(default = "thr_key_stability")]
pub array_of_objects_min_key_stability: f32,
#[serde(default = "thr_flat_fields")]
pub flat_object_min_fields: usize,
}
fn thr_md_cols() -> usize {
2
}
fn thr_arr_items() -> usize {
4
}
fn thr_key_stability() -> f32 {
0.7
}
fn thr_flat_fields() -> usize {
8
}
impl Default for ShapeThresholds {
fn default() -> Self {
Self {
markdown_table_min_cols: thr_md_cols(),
array_of_objects_min_items: thr_arr_items(),
array_of_objects_min_key_stability: thr_key_stability(),
flat_object_min_fields: thr_flat_fields(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
#[serde(default = "default_telemetry_enabled")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default = "default_rotate_mib")]
pub rotate_mib: u32,
#[serde(default = "default_sample_rate")]
pub sample_rate: f32,
#[serde(default = "default_flush_every")]
pub flush_every_n: usize,
}
fn default_telemetry_enabled() -> bool {
false
}
fn default_rotate_mib() -> u32 {
100
}
fn default_sample_rate() -> f32 {
1.0
}
fn default_flush_every() -> usize {
25
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: default_telemetry_enabled(),
path: None,
rotate_mib: default_rotate_mib(),
sample_rate: default_sample_rate(),
flush_every_n: default_flush_every(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichmentConfig {
#[serde(default = "default_enrichment_enabled")]
pub enabled: bool,
#[serde(default = "default_max_parallel_prefetches")]
pub max_parallel_prefetches: u32,
#[serde(default = "default_prefetch_budget_tokens")]
pub prefetch_budget_tokens: u32,
#[serde(default = "default_prefetch_timeout_ms")]
pub prefetch_timeout_ms: u32,
#[serde(default = "default_respect_rate_limits")]
pub respect_rate_limits: bool,
}
fn default_enrichment_enabled() -> bool {
false
}
fn default_max_parallel_prefetches() -> u32 {
3
}
fn default_prefetch_budget_tokens() -> u32 {
8000
}
fn default_prefetch_timeout_ms() -> u32 {
1000
}
fn default_respect_rate_limits() -> bool {
true
}
impl Default for EnrichmentConfig {
fn default() -> Self {
Self {
enabled: default_enrichment_enabled(),
max_parallel_prefetches: default_max_parallel_prefetches(),
prefetch_budget_tokens: default_prefetch_budget_tokens(),
prefetch_timeout_ms: default_prefetch_timeout_ms(),
respect_rate_limits: default_respect_rate_limits(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EndpointOverride {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dedup_enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lru_size: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_body_chars: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProfilesConfig {
#[serde(default)]
pub tokenizer: TokenizerProfilesConfig,
#[serde(default)]
pub llm: LlmProfilesConfig,
#[serde(default)]
pub agent: AgentProfilesConfig,
#[serde(default)]
pub data: DataProfilesConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenizerProfile {
pub chars_per_token: f32,
#[serde(default)]
pub bpe: Tokenizer,
#[serde(default = "default_inline_json_cost")]
pub inline_json_cost: f32,
#[serde(default = "default_toon_overhead")]
pub toon_overhead: f32,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub format_factors: BTreeMap<String, f32>,
}
fn default_inline_json_cost() -> f32 {
1.0
}
fn default_toon_overhead() -> f32 {
1.0
}
impl Default for TokenizerProfile {
fn default() -> Self {
Self {
chars_per_token: 4.0,
bpe: Tokenizer::Heuristic,
inline_json_cost: default_inline_json_cost(),
toon_overhead: default_toon_overhead(),
format_factors: BTreeMap::new(),
}
}
}
impl TokenizerProfile {
pub fn count_tokens(&self, text: &str) -> usize {
if text.is_empty() {
return 0;
}
match self.bpe {
Tokenizer::Heuristic => {
let cpt = if self.chars_per_token > 0.0 {
self.chars_per_token as f64
} else {
3.5
};
(text.len() as f64 / cpt).ceil() as usize
}
tk => tk.count(text),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenizerProfilesConfig {
#[serde(default = "default_active_auto")]
pub active: String,
#[serde(default = "default_tokenizer_variants")]
pub variants: BTreeMap<String, TokenizerProfile>,
}
fn default_active_auto() -> String {
"auto".to_string()
}
fn default_tokenizer_variants() -> BTreeMap<String, TokenizerProfile> {
let mut m = BTreeMap::new();
m.insert(
"anthropic_class".into(),
TokenizerProfile {
chars_per_token: 3.5,
bpe: Tokenizer::O200kBase,
inline_json_cost: 2.2,
toon_overhead: 1.13,
format_factors: BTreeMap::new(),
},
);
m.insert(
"openai_o200k".into(),
TokenizerProfile {
chars_per_token: 4.0,
bpe: Tokenizer::O200kBase,
inline_json_cost: 1.0,
toon_overhead: 0.60,
format_factors: BTreeMap::new(),
},
);
m.insert(
"openai_cl100k".into(),
TokenizerProfile {
chars_per_token: 3.7,
bpe: Tokenizer::Cl100kBase,
inline_json_cost: 1.0,
toon_overhead: 0.60,
format_factors: BTreeMap::new(),
},
);
m.insert(
"ollama_bpe".into(),
TokenizerProfile {
chars_per_token: 3.8,
bpe: Tokenizer::Heuristic,
inline_json_cost: 1.0,
toon_overhead: 1.00,
format_factors: BTreeMap::new(),
},
);
m
}
impl Default for TokenizerProfilesConfig {
fn default() -> Self {
Self {
active: default_active_auto(),
variants: default_tokenizer_variants(),
}
}
}
impl TokenizerProfilesConfig {
pub fn get(&self, id: &str) -> Option<&TokenizerProfile> {
self.variants.get(id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmProfile {
pub tokenizer: String,
#[serde(default = "default_prefer_explicit_keys")]
pub prefer_explicit_keys: bool,
#[serde(default = "default_context_window")]
pub context_window: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_inline_nested: Option<u32>,
}
fn default_prefer_explicit_keys() -> bool {
true
}
fn default_context_window() -> u32 {
32_000
}
impl Default for LlmProfile {
fn default() -> Self {
Self {
tokenizer: "ollama_bpe".to_string(),
prefer_explicit_keys: default_prefer_explicit_keys(),
context_window: default_context_window(),
max_inline_nested: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmProfilesConfig {
#[serde(default = "default_active_auto")]
pub active: String,
#[serde(default = "default_llm_variants")]
pub variants: BTreeMap<String, LlmProfile>,
}
fn default_llm_variants() -> BTreeMap<String, LlmProfile> {
let mut m = BTreeMap::new();
m.insert(
"default".into(),
LlmProfile {
tokenizer: "openai_o200k".into(),
prefer_explicit_keys: true,
context_window: 32_000,
max_inline_nested: Some(256),
},
);
m.insert(
"glm-5.1".into(),
LlmProfile {
tokenizer: "anthropic_class".into(),
prefer_explicit_keys: true,
context_window: 128_000,
max_inline_nested: Some(128),
},
);
m.insert(
"claude-sonnet-4.6".into(),
LlmProfile {
tokenizer: "anthropic_class".into(),
prefer_explicit_keys: true,
context_window: 200_000,
max_inline_nested: Some(64),
},
);
m.insert(
"gpt-oss:20b".into(),
LlmProfile {
tokenizer: "ollama_bpe".into(),
prefer_explicit_keys: false,
context_window: 8_192,
max_inline_nested: Some(512),
},
);
m.insert(
"gemma4:26b".into(),
LlmProfile {
tokenizer: "ollama_bpe".into(),
prefer_explicit_keys: false,
context_window: 8_192,
max_inline_nested: Some(512),
},
);
m
}
impl Default for LlmProfilesConfig {
fn default() -> Self {
Self {
active: default_active_auto(),
variants: default_llm_variants(),
}
}
}
impl LlmProfilesConfig {
pub fn resolve<'a>(&'a self, session_model_id: Option<&str>) -> &'a LlmProfile {
let key: &str = if self.active == "auto" {
session_model_id.unwrap_or("default")
} else {
self.active.as_str()
};
self.variants
.get(key)
.or_else(|| self.variants.get("default"))
.unwrap_or_else(|| {
static FALLBACK: std::sync::OnceLock<LlmProfile> = std::sync::OnceLock::new();
FALLBACK.get_or_init(LlmProfile::default)
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Latency,
#[default]
Balanced,
Accuracy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProfile {
#[serde(default)]
pub priority: Priority,
#[serde(default = "default_recursion_depth")]
pub mckp_recursion_depth: usize,
#[serde(default = "default_hint_aggressiveness")]
pub hint_aggressiveness: f32,
#[serde(default)]
pub near_ref_enabled: bool,
}
fn default_hint_aggressiveness() -> f32 {
0.5
}
impl Default for AgentProfile {
fn default() -> Self {
Self {
priority: Priority::Balanced,
mckp_recursion_depth: default_recursion_depth(),
hint_aggressiveness: default_hint_aggressiveness(),
near_ref_enabled: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProfilesConfig {
#[serde(default = "default_active_auto")]
pub active: String,
#[serde(default = "default_auto_window")]
pub auto_detect_window: usize,
#[serde(default = "default_agent_variants")]
pub variants: BTreeMap<String, AgentProfile>,
}
fn default_auto_window() -> usize {
50
}
fn default_agent_variants() -> BTreeMap<String, AgentProfile> {
let mut m = BTreeMap::new();
m.insert("default".into(), AgentProfile::default());
m.insert(
"file_search_heavy".into(),
AgentProfile {
priority: Priority::Latency,
mckp_recursion_depth: 3,
hint_aggressiveness: 0.3,
near_ref_enabled: false,
},
);
m.insert(
"marathon_refactor".into(),
AgentProfile {
priority: Priority::Accuracy,
mckp_recursion_depth: 7,
hint_aggressiveness: 0.7,
near_ref_enabled: true,
},
);
m
}
impl Default for AgentProfilesConfig {
fn default() -> Self {
Self {
active: default_active_auto(),
auto_detect_window: default_auto_window(),
variants: default_agent_variants(),
}
}
}
impl AgentProfilesConfig {
pub fn resolve<'a>(&'a self, stats: &SessionStats) -> &'a AgentProfile {
let key: &str = if self.active == "auto" {
classify_agent(stats)
} else {
self.active.as_str()
};
self.variants
.get(key)
.or_else(|| self.variants.get("default"))
.unwrap_or_else(|| {
static FALLBACK: std::sync::OnceLock<AgentProfile> = std::sync::OnceLock::new();
FALLBACK.get_or_init(AgentProfile::default)
})
}
}
fn classify_agent(stats: &SessionStats) -> &'static str {
if stats.event_count >= 500 && stats.compaction_count >= 3 {
"marathon_refactor"
} else if stats.event_count <= 200 && stats.read_share >= 0.5 {
"file_search_heavy"
} else {
"default"
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataProfile {
pub endpoint_pattern: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preferred_format: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hint_set: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataProfilesConfig {
#[serde(default = "default_active_auto")]
pub active: String,
#[serde(default = "default_data_variants")]
pub variants: BTreeMap<String, DataProfile>,
}
fn default_data_variants() -> BTreeMap<String, DataProfile> {
let mut m = BTreeMap::new();
m.insert(
"gitlab_issues".into(),
DataProfile {
endpoint_pattern: "mcp__gitlab__get_issues".into(),
preferred_format: Some("csv_from_md".into()),
hint_set: vec!["near_ref".into()],
},
);
m.insert(
"github_pulls".into(),
DataProfile {
endpoint_pattern: "mcp__github__list_pulls".into(),
preferred_format: Some("csv_from_md".into()),
hint_set: vec!["near_ref".into()],
},
);
m.insert(
"k8s_logs".into(),
DataProfile {
endpoint_pattern: "mcp__k8s__get_logs".into(),
preferred_format: Some("pipeline_deep_mckp".into()),
hint_set: vec!["timestamp_ref".into()],
},
);
m.insert(
"mr_diffs".into(),
DataProfile {
endpoint_pattern: "mcp__gitlab__get_mr_diff".into(),
preferred_format: Some("mr_diff_fence".into()),
hint_set: Vec::new(),
},
);
m
}
impl Default for DataProfilesConfig {
fn default() -> Self {
Self {
active: default_active_auto(),
variants: default_data_variants(),
}
}
}
impl DataProfilesConfig {
pub fn match_endpoint(&self, endpoint: &str) -> Option<&DataProfile> {
if self.active != "auto" {
return self.variants.get(&self.active);
}
self.variants
.values()
.find(|v| endpoint == v.endpoint_pattern || endpoint.starts_with(&v.endpoint_pattern))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HintTypeRule {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_per_session: Option<u32>,
#[serde(default = "default_any_model")]
pub applies_to_models: Vec<String>,
}
fn default_true() -> bool {
true
}
fn default_any_model() -> Vec<String> {
vec!["*".to_string()]
}
impl Default for HintTypeRule {
fn default() -> Self {
Self {
enabled: true,
max_per_session: None,
applies_to_models: default_any_model(),
}
}
}
impl HintTypeRule {
pub fn applies_to(&self, model_id: &str) -> bool {
if !self.enabled {
return false;
}
self.applies_to_models
.iter()
.any(|m| m == "*" || m == model_id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HintsConfig {
#[serde(default)]
pub default_verbosity: HintVerbosity,
#[serde(default = "default_hint_types")]
pub types: BTreeMap<String, HintTypeRule>,
}
fn default_hint_types() -> BTreeMap<String, HintTypeRule> {
let mut m = BTreeMap::new();
m.insert(
"near_ref".into(),
HintTypeRule {
enabled: true,
max_per_session: Some(50),
applies_to_models: default_any_model(),
},
);
m.insert(
"timestamp_ref".into(),
HintTypeRule {
enabled: true,
max_per_session: Some(100),
applies_to_models: default_any_model(),
},
);
m.insert(
"delta".into(),
HintTypeRule {
enabled: false, max_per_session: Some(20),
applies_to_models: default_any_model(),
},
);
m.insert(
"schema_explainer".into(),
HintTypeRule {
enabled: false,
max_per_session: None,
applies_to_models: default_any_model(),
},
);
m.insert(
"inline_format_hint".into(),
HintTypeRule {
enabled: true,
max_per_session: Some(10),
applies_to_models: vec!["gpt-oss:20b".into(), "gemma4:26b".into()],
},
);
m
}
impl Default for HintsConfig {
fn default() -> Self {
Self {
default_verbosity: HintVerbosity::Standard,
types: default_hint_types(),
}
}
}
impl HintsConfig {
pub fn allow(&self, type_id: &str, model_id: &str) -> bool {
match self.types.get(type_id) {
Some(rule) => rule.applies_to(model_id),
None => false, }
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionStats {
pub event_count: usize,
pub compaction_count: usize,
pub read_share: f32,
}
#[derive(Debug, Clone, Default)]
pub struct SessionContext {
pub model_id: Option<String>,
pub stats: SessionStats,
}
#[derive(Debug, Clone)]
pub struct EffectiveConfig {
pub tokenizer: TokenizerProfile,
pub llm: LlmProfile,
pub agent: AgentProfile,
pub hints: HintsConfig,
pub mckp: MckpConfig,
}
impl EffectiveConfig {
pub fn resolve(cfg: &AdaptiveConfig, ctx: &SessionContext) -> Self {
let llm = cfg.profiles.llm.resolve(ctx.model_id.as_deref()).clone();
let tokenizer_id = if cfg.profiles.tokenizer.active == "auto" {
llm.tokenizer.as_str()
} else {
cfg.profiles.tokenizer.active.as_str()
};
let tokenizer = cfg
.profiles
.tokenizer
.get(tokenizer_id)
.cloned()
.unwrap_or_default();
let agent = cfg.profiles.agent.resolve(&ctx.stats).clone();
let mut mckp = cfg.mckp.clone();
mckp.recursion_depth = agent.mckp_recursion_depth;
Self {
tokenizer,
llm,
agent,
hints: cfg.hints.clone(),
mckp,
}
}
pub fn preferred_format_for<'a>(
&self,
cfg: &'a AdaptiveConfig,
endpoint: &str,
) -> Option<&'a str> {
if let Some(dp) = cfg.profiles.data.match_endpoint(endpoint)
&& let Some(f) = dp.preferred_format.as_deref()
{
return Some(f);
}
cfg.effective_template(endpoint)
}
pub fn allow_hint(&self, type_id: &str) -> bool {
let model_id = "default"; let _ = model_id;
self.hints.allow(type_id, "*")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_valid() {
let cfg = AdaptiveConfig::default();
assert_eq!(cfg.schema_version, CURRENT_SCHEMA_VERSION);
assert_eq!(cfg.dedup.lru_size, 5);
assert!(cfg.dedup.enabled_for("anything"));
assert!(cfg.templates.is_template_active("csv_from_md"));
assert!(cfg.mckp.format_enabled("deep_mckp"));
}
#[test]
fn roundtrip_toml() {
let mut cfg = AdaptiveConfig::default();
cfg.dedup.lru_size = 7;
cfg.dedup.near_ref_enabled = true;
cfg.dedup
.enabled_per_endpoint
.insert("mcp__test__get".into(), false);
cfg.templates
.endpoint_overrides
.insert("mcp__test__get".into(), "csv_from_md".into());
cfg.endpoint_overrides.insert(
"Bash:git_log".into(),
EndpointOverride {
dedup_enabled: Some(false),
..Default::default()
},
);
let s = toml::to_string_pretty(&cfg).unwrap();
let parsed: AdaptiveConfig = toml::from_str(&s).unwrap();
assert_eq!(parsed.dedup.lru_size, 7);
assert!(parsed.dedup.near_ref_enabled);
assert!(!parsed.dedup.enabled_for("mcp__test__get"));
assert_eq!(
parsed.templates.template_for("mcp__test__get"),
Some("csv_from_md")
);
}
#[test]
fn unknown_schema_version_is_rejected() {
let cfg = AdaptiveConfig {
schema_version: 99,
..Default::default()
};
let s = toml::to_string(&cfg).unwrap();
let err = toml::from_str::<AdaptiveConfig>(&s).ok().and_then(|c| {
if c.schema_version != CURRENT_SCHEMA_VERSION {
Some(c.schema_version)
} else {
None
}
});
assert_eq!(err, Some(99));
}
#[test]
fn load_or_default_handles_missing_file() {
let p = std::env::temp_dir().join("definitely_does_not_exist_12345.toml");
let cfg = AdaptiveConfig::load_or_default(&p).unwrap();
assert_eq!(cfg.schema_version, CURRENT_SCHEMA_VERSION);
}
#[test]
fn save_and_load_roundtrip() {
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("devboy_cfg_test_{pid}.toml"));
let mut cfg = AdaptiveConfig::default();
cfg.dedup.lru_size = 10;
cfg.mckp.recursion_depth = 7;
cfg.save(&p).unwrap();
let loaded = AdaptiveConfig::load(&p).unwrap();
assert_eq!(loaded.dedup.lru_size, 10);
assert_eq!(loaded.mckp.recursion_depth, 7);
std::fs::remove_file(&p).ok();
}
#[test]
fn default_profiles_have_expected_variants() {
let cfg = AdaptiveConfig::default();
assert!(cfg.profiles.tokenizer.get("anthropic_class").is_some());
assert!(cfg.profiles.tokenizer.get("openai_o200k").is_some());
assert!(cfg.profiles.tokenizer.get("ollama_bpe").is_some());
assert!(cfg.profiles.llm.variants.contains_key("default"));
assert!(cfg.profiles.llm.variants.contains_key("glm-5.1"));
assert!(cfg.profiles.llm.variants.contains_key("gpt-oss:20b"));
assert!(cfg.profiles.agent.variants.contains_key("default"));
assert!(
cfg.profiles
.agent
.variants
.contains_key("file_search_heavy")
);
assert!(
cfg.profiles
.agent
.variants
.contains_key("marathon_refactor")
);
assert!(cfg.profiles.data.variants.contains_key("gitlab_issues"));
assert!(cfg.profiles.data.variants.contains_key("k8s_logs"));
}
#[test]
fn anthropic_tokenizer_has_inline_json_penalty() {
let cfg = AdaptiveConfig::default();
let p = cfg.profiles.tokenizer.get("anthropic_class").unwrap();
assert!(p.inline_json_cost > 2.0);
assert!((p.toon_overhead - 1.13).abs() < 0.001);
}
#[test]
fn llm_resolve_picks_exact_model_match() {
let cfg = AdaptiveConfig::default();
let p = cfg.profiles.llm.resolve(Some("glm-5.1"));
assert_eq!(p.tokenizer, "anthropic_class");
assert_eq!(p.context_window, 128_000);
}
#[test]
fn llm_resolve_falls_back_to_default_for_unknown() {
let cfg = AdaptiveConfig::default();
let p = cfg.profiles.llm.resolve(Some("unknown-model-xyz"));
assert_eq!(p.tokenizer, "openai_o200k");
}
#[test]
fn agent_classifier_picks_marathon_for_long_session() {
let cfg = AdaptiveConfig::default();
let stats = SessionStats {
event_count: 800,
compaction_count: 5,
read_share: 0.3,
};
let p = cfg.profiles.agent.resolve(&stats);
assert_eq!(p.priority, Priority::Accuracy);
assert_eq!(p.mckp_recursion_depth, 7);
assert!(p.near_ref_enabled);
}
#[test]
fn agent_classifier_picks_file_search_for_short_read_heavy() {
let cfg = AdaptiveConfig::default();
let stats = SessionStats {
event_count: 80,
compaction_count: 0,
read_share: 0.7,
};
let p = cfg.profiles.agent.resolve(&stats);
assert_eq!(p.priority, Priority::Latency);
assert_eq!(p.mckp_recursion_depth, 3);
}
#[test]
fn agent_classifier_default_for_balanced_session() {
let cfg = AdaptiveConfig::default();
let stats = SessionStats {
event_count: 300,
compaction_count: 0,
read_share: 0.4,
};
let p = cfg.profiles.agent.resolve(&stats);
assert_eq!(p.priority, Priority::Balanced);
}
#[test]
fn data_profile_matches_endpoint_prefix() {
let cfg = AdaptiveConfig::default();
let dp = cfg.profiles.data.match_endpoint("mcp__gitlab__get_issues");
assert!(dp.is_some());
assert_eq!(dp.unwrap().preferred_format.as_deref(), Some("csv_from_md"));
}
#[test]
fn data_profile_returns_none_for_unmatched() {
let cfg = AdaptiveConfig::default();
let dp = cfg.profiles.data.match_endpoint("Bash:git_log");
assert!(dp.is_none());
}
#[test]
fn hint_policy_disables_schema_explainer_by_default() {
let cfg = AdaptiveConfig::default();
assert!(!cfg.hints.allow("schema_explainer", "glm-5.1"));
assert!(!cfg.hints.allow("schema_explainer", "gpt-oss:20b"));
}
#[test]
fn hint_policy_inline_format_hint_only_for_local_models() {
let cfg = AdaptiveConfig::default();
assert!(cfg.hints.allow("inline_format_hint", "gpt-oss:20b"));
assert!(cfg.hints.allow("inline_format_hint", "gemma4:26b"));
assert!(!cfg.hints.allow("inline_format_hint", "glm-5.1"));
assert!(!cfg.hints.allow("inline_format_hint", "claude-sonnet-4.6"));
}
#[test]
fn hint_policy_unknown_type_fails_closed() {
let cfg = AdaptiveConfig::default();
assert!(!cfg.hints.allow("never_seen_hint_type", "anything"));
}
#[test]
fn effective_config_resolves_glm_to_anthropic_tokenizer() {
let cfg = AdaptiveConfig::default();
let ctx = SessionContext {
model_id: Some("glm-5.1".to_string()),
stats: SessionStats::default(),
};
let eff = EffectiveConfig::resolve(&cfg, &ctx);
assert_eq!(eff.llm.tokenizer, "anthropic_class");
assert!(eff.tokenizer.inline_json_cost > 2.0);
assert_eq!(eff.llm.context_window, 128_000);
}
#[test]
fn effective_config_recursion_depth_from_agent_profile() {
let cfg = AdaptiveConfig::default();
let ctx = SessionContext {
model_id: Some("gpt-oss:20b".to_string()),
stats: SessionStats {
event_count: 1000,
compaction_count: 5,
read_share: 0.2,
},
};
let eff = EffectiveConfig::resolve(&cfg, &ctx);
assert_eq!(eff.mckp.recursion_depth, 7);
assert_eq!(eff.agent.priority, Priority::Accuracy);
}
#[test]
fn effective_config_preferred_format_from_data_profile() {
let cfg = AdaptiveConfig::default();
let ctx = SessionContext::default();
let eff = EffectiveConfig::resolve(&cfg, &ctx);
let f = eff.preferred_format_for(&cfg, "mcp__gitlab__get_issues");
assert_eq!(f, Some("csv_from_md"));
}
#[test]
fn schema_v1_file_upgrades_to_v2_in_memory() {
let v1 = r#"
schema_version = 1
[dedup]
lru_size = 7
[mckp]
recursion_depth = 6
"#;
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("devboy_cfg_v1_{pid}.toml"));
std::fs::write(&p, v1).unwrap();
let loaded = AdaptiveConfig::load(&p).unwrap();
assert_eq!(loaded.schema_version, CURRENT_SCHEMA_VERSION);
assert_eq!(loaded.dedup.lru_size, 7);
assert_eq!(loaded.mckp.recursion_depth, 6);
assert!(loaded.profiles.tokenizer.get("anthropic_class").is_some());
assert!(loaded.hints.types.contains_key("near_ref"));
std::fs::remove_file(&p).ok();
}
#[test]
fn future_schema_version_is_rejected_on_load() {
let s = format!("schema_version = {}\n[dedup]\n", CURRENT_SCHEMA_VERSION + 1);
let pid = std::process::id();
let p = std::env::temp_dir().join(format!("devboy_cfg_future_{pid}.toml"));
std::fs::write(&p, s).unwrap();
let err = AdaptiveConfig::load(&p);
assert!(matches!(err, Err(ConfigError::UnsupportedSchemaVersion(_))));
std::fs::remove_file(&p).ok();
}
#[test]
fn profiles_roundtrip_through_toml() {
let mut cfg = AdaptiveConfig::default();
cfg.profiles.llm.active = "claude-sonnet-4.6".to_string();
cfg.profiles.agent.active = "marathon_refactor".to_string();
cfg.hints.types.get_mut("near_ref").unwrap().max_per_session = Some(99);
let s = toml::to_string_pretty(&cfg).unwrap();
let parsed: AdaptiveConfig = toml::from_str(&s).unwrap();
assert_eq!(parsed.profiles.llm.active, "claude-sonnet-4.6");
assert_eq!(parsed.profiles.agent.active, "marathon_refactor");
assert_eq!(parsed.hints.types["near_ref"].max_per_session, Some(99));
}
#[test]
fn endpoint_override_roundtrip() {
let mut cfg = AdaptiveConfig::default();
cfg.endpoint_overrides.insert(
"mcp__xxx__yyy".into(),
EndpointOverride {
dedup_enabled: Some(true),
lru_size: Some(10),
template_id: Some("custom".into()),
min_body_chars: Some(50),
},
);
let s = toml::to_string_pretty(&cfg).unwrap();
let parsed: AdaptiveConfig = toml::from_str(&s).unwrap();
let o = parsed.endpoint_overrides.get("mcp__xxx__yyy").unwrap();
assert_eq!(o.lru_size, Some(10));
assert_eq!(o.template_id.as_deref(), Some("custom"));
}
#[test]
fn effective_dedup_enabled_falls_back_correctly() {
let mut cfg = AdaptiveConfig::default();
assert!(cfg.effective_dedup_enabled("anything"));
cfg.dedup.enabled_per_endpoint.insert("a".into(), false);
assert!(!cfg.effective_dedup_enabled("a"));
cfg.endpoint_overrides.insert(
"a".into(),
EndpointOverride {
dedup_enabled: Some(true),
..Default::default()
},
);
assert!(cfg.effective_dedup_enabled("a"));
}
#[test]
fn effective_min_body_chars_uses_override() {
let mut cfg = AdaptiveConfig::default();
assert_eq!(cfg.effective_min_body_chars("x"), cfg.dedup.min_body_chars);
cfg.endpoint_overrides.insert(
"x".into(),
EndpointOverride {
min_body_chars: Some(42),
..Default::default()
},
);
assert_eq!(cfg.effective_min_body_chars("x"), 42);
}
#[test]
fn effective_lru_size_uses_override_when_larger() {
let mut cfg = AdaptiveConfig::default();
cfg.dedup.lru_size = 5;
cfg.endpoint_overrides.insert(
"big".into(),
EndpointOverride {
lru_size: Some(15),
..Default::default()
},
);
assert_eq!(cfg.effective_lru_size("big"), 15);
cfg.endpoint_overrides.insert(
"small".into(),
EndpointOverride {
lru_size: Some(2),
..Default::default()
},
);
assert_eq!(cfg.effective_lru_size("small"), 5);
}
#[test]
fn max_lru_size_across_all_overrides() {
let mut cfg = AdaptiveConfig::default();
cfg.dedup.lru_size = 5;
cfg.endpoint_overrides.insert(
"a".into(),
EndpointOverride {
lru_size: Some(12),
..Default::default()
},
);
cfg.endpoint_overrides.insert(
"b".into(),
EndpointOverride {
lru_size: Some(8),
..Default::default()
},
);
assert_eq!(cfg.max_lru_size(), 12);
}
#[test]
fn effective_template_prefers_endpoint_override() {
let mut cfg = AdaptiveConfig::default();
cfg.templates
.endpoint_overrides
.insert("x".into(), "csv_from_md".into());
assert_eq!(cfg.effective_template("x"), Some("csv_from_md"));
cfg.endpoint_overrides.insert(
"x".into(),
EndpointOverride {
template_id: Some("custom_tpl".into()),
..Default::default()
},
);
assert_eq!(cfg.effective_template("x"), Some("custom_tpl"));
}
#[test]
fn merge_right_wins_overwrites_sections() {
let mut a = AdaptiveConfig::default();
a.endpoint_overrides.insert(
"keep".into(),
EndpointOverride {
dedup_enabled: Some(false),
..Default::default()
},
);
let mut b = AdaptiveConfig::default();
b.dedup.lru_size = 42;
b.endpoint_overrides.insert(
"keep".into(),
EndpointOverride {
dedup_enabled: Some(true),
..Default::default()
},
);
b.endpoint_overrides.insert(
"new".into(),
EndpointOverride {
dedup_enabled: Some(true),
..Default::default()
},
);
a.merge_right_wins(b);
assert_eq!(a.dedup.lru_size, 42);
assert_eq!(a.endpoint_overrides["keep"].dedup_enabled, Some(true));
assert!(a.endpoint_overrides.contains_key("new"));
}
#[test]
fn hint_verbosity_to_runtime_mapping() {
assert_eq!(
HintVerbosity::Terse.to_runtime(),
crate::dedup::HintVerbosity::Terse
);
assert_eq!(
HintVerbosity::Standard.to_runtime(),
crate::dedup::HintVerbosity::Standard
);
assert_eq!(
HintVerbosity::Verbose.to_runtime(),
crate::dedup::HintVerbosity::Verbose
);
}
#[test]
fn mckp_config_format_disabled_is_respected() {
let mut cfg = MckpConfig::default();
assert!(cfg.format_enabled("csv"));
cfg.formats_enabled = vec![];
assert!(!cfg.format_enabled("csv"));
}
#[test]
fn templates_is_template_active_false_for_unknown() {
let t = TemplatesConfig::default();
assert!(!t.is_template_active("not_a_real_template"));
assert!(t.is_template_active("csv_from_md"));
}
#[test]
fn tokenizer_profile_heuristic_uses_chars_per_token() {
let p = TokenizerProfile {
chars_per_token: 4.0,
bpe: Tokenizer::Heuristic,
..Default::default()
};
assert_eq!(p.count_tokens("abcdefgh"), 2);
assert_eq!(p.count_tokens(""), 0);
}
#[test]
fn tokenizer_profile_bpe_overrides_heuristic() {
let p = TokenizerProfile {
chars_per_token: 1.0,
bpe: Tokenizer::O200kBase,
..Default::default()
};
let n = p.count_tokens("hello world");
assert!(n > 0 && n < 5, "BPE should win, got {n}");
}
#[test]
fn default_tokenizer_variants_have_real_bpe_for_modern_models() {
let variants = default_tokenizer_variants();
assert_eq!(
variants.get("anthropic_class").unwrap().bpe,
Tokenizer::O200kBase
);
assert_eq!(
variants.get("openai_o200k").unwrap().bpe,
Tokenizer::O200kBase
);
assert_eq!(
variants.get("openai_cl100k").unwrap().bpe,
Tokenizer::Cl100kBase
);
assert_eq!(
variants.get("ollama_bpe").unwrap().bpe,
Tokenizer::Heuristic
);
}
#[test]
fn schema_v3_default_carries_empty_tools_map() {
let cfg = AdaptiveConfig::default();
assert_eq!(cfg.schema_version, CURRENT_SCHEMA_VERSION);
assert!(cfg.tools.is_empty());
}
#[test]
fn schema_v1_v2_v3_files_upgrade_to_current_with_empty_tools() {
for raw in [
"schema_version = 1\n",
"schema_version = 2\n[profiles.tokenizer]\nactive = \"auto\"\n",
"schema_version = 3\n[tools.Read]\nvalue_class = \"critical\"\n",
] {
let mut cfg: AdaptiveConfig = toml::from_str(raw).unwrap();
cfg.upgrade_in_place().unwrap();
assert_eq!(cfg.schema_version, CURRENT_SCHEMA_VERSION);
assert!(!cfg.enrichment.enabled);
}
}
#[test]
fn enrichment_config_round_trips_with_overrides() {
let raw = r#"
schema_version = 4
[enrichment]
enabled = true
max_parallel_prefetches = 5
prefetch_budget_tokens = 12000
prefetch_timeout_ms = 1500
respect_rate_limits = false
"#;
let cfg: AdaptiveConfig = toml::from_str(raw).unwrap();
assert!(cfg.enrichment.enabled);
assert_eq!(cfg.enrichment.max_parallel_prefetches, 5);
assert_eq!(cfg.enrichment.prefetch_budget_tokens, 12000);
assert_eq!(cfg.enrichment.prefetch_timeout_ms, 1500);
assert!(!cfg.enrichment.respect_rate_limits);
let s = toml::to_string_pretty(&cfg).unwrap();
let back: AdaptiveConfig = toml::from_str(&s).unwrap();
assert!(back.enrichment.enabled);
assert_eq!(back.enrichment.prefetch_timeout_ms, 1500);
}
#[test]
fn enrichment_defaults_are_safe() {
let cfg = AdaptiveConfig::default();
assert!(!cfg.enrichment.enabled);
assert_eq!(cfg.enrichment.max_parallel_prefetches, 3);
assert_eq!(cfg.enrichment.prefetch_budget_tokens, 8000);
assert_eq!(cfg.enrichment.prefetch_timeout_ms, 1000);
assert!(cfg.enrichment.respect_rate_limits);
}
#[test]
fn effective_tool_value_model_exact_match_wins() {
let mut cfg = AdaptiveConfig::default();
cfg.tools.insert(
"Read".into(),
devboy_core::ToolValueModel::critical_with_size(2.5),
);
let m = cfg.effective_tool_value_model("Read").unwrap();
assert_eq!(m.cost_model.typical_kb, 2.5);
assert_eq!(m.value_class, devboy_core::ValueClass::Critical);
}
#[test]
fn effective_tool_value_model_falls_back_to_wildcard() {
let mut cfg = AdaptiveConfig::default();
cfg.tools
.insert("*".into(), devboy_core::ToolValueModel::audit_only());
let m = cfg.effective_tool_value_model("UnknownTool").unwrap();
assert_eq!(m.value_class, devboy_core::ValueClass::AuditOnly);
}
#[test]
fn effective_tool_value_model_none_when_unconfigured() {
let cfg = AdaptiveConfig::default();
assert!(cfg.effective_tool_value_model("Read").is_none());
}
#[test]
fn round_trip_via_toml_with_tools_block() {
let mut cfg = AdaptiveConfig::default();
cfg.tools.insert(
"Read".into(),
devboy_core::ToolValueModel::critical_with_size(2.5),
);
cfg.tools.insert(
"TaskUpdate".into(),
devboy_core::ToolValueModel::audit_only(),
);
let s = toml::to_string_pretty(&cfg).unwrap();
assert!(s.contains("[tools.Read]"));
assert!(s.contains("[tools.TaskUpdate]"));
let back: AdaptiveConfig = toml::from_str(&s).unwrap();
assert_eq!(back.tools.len(), 2);
assert_eq!(
back.effective_tool_value_model("Read")
.unwrap()
.cost_model
.typical_kb,
2.5
);
}
#[test]
fn merge_right_wins_unions_tools_blocks() {
let mut left = AdaptiveConfig::default();
left.tools.insert(
"Read".into(),
devboy_core::ToolValueModel::critical_with_size(2.5),
);
left.tools
.insert("Bash".into(), devboy_core::ToolValueModel::default());
let mut right = AdaptiveConfig::default();
right.tools.insert(
"Read".into(),
devboy_core::ToolValueModel::critical_with_size(99.0),
);
right.tools.insert(
"WebFetch".into(),
devboy_core::ToolValueModel::critical_with_size(1.2),
);
left.merge_right_wins(right);
assert_eq!(
left.effective_tool_value_model("Read")
.unwrap()
.cost_model
.typical_kb,
99.0
);
assert!(left.effective_tool_value_model("Bash").is_some());
assert!(left.effective_tool_value_model("WebFetch").is_some());
}
}