Skip to main content

kimetsu_core/
config.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{KIMETSU_SCHEMA_VERSION, KimetsuResult};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ProjectConfig {
7    pub kimetsu: KimetsuSection,
8    pub model: ModelSection,
9    pub broker: BrokerSection,
10    pub shell: ShellSection,
11    pub ingestion: IngestionSection,
12    pub run: RunSection,
13}
14
15impl ProjectConfig {
16    pub fn default_for_project(project_id: impl Into<String>) -> Self {
17        Self {
18            kimetsu: KimetsuSection {
19                project_id: project_id.into(),
20                schema_version: KIMETSU_SCHEMA_VERSION,
21            },
22            model: ModelSection::default(),
23            broker: BrokerSection::default(),
24            shell: ShellSection::default(),
25            ingestion: IngestionSection::default(),
26            run: RunSection::default(),
27        }
28    }
29
30    pub fn from_toml(value: &str) -> KimetsuResult<Self> {
31        Ok(toml::from_str(value)?)
32    }
33
34    pub fn to_toml(&self) -> KimetsuResult<String> {
35        Ok(toml::to_string_pretty(self)?)
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct KimetsuSection {
41    pub project_id: String,
42    pub schema_version: i64,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ModelSection {
47    pub provider: String,
48    pub model: String,
49    pub api_key_env: String,
50    pub max_output_tokens: u32,
51    pub temperature: f32,
52    pub request_timeout_secs: u64,
53}
54
55impl Default for ModelSection {
56    fn default() -> Self {
57        Self {
58            provider: "anthropic".to_string(),
59            model: "claude-opus-4-7".to_string(),
60            api_key_env: "ANTHROPIC_API_KEY".to_string(),
61            max_output_tokens: 8192,
62            temperature: 0.2,
63            request_timeout_secs: 120,
64        }
65    }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct BrokerSection {
70    pub default_budget_tokens: u32,
71    pub weights: BrokerWeights,
72}
73
74impl Default for BrokerSection {
75    fn default() -> Self {
76        Self {
77            default_budget_tokens: 6000,
78            weights: BrokerWeights::default(),
79        }
80    }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct BrokerWeights {
85    pub relevance: f32,
86    pub confidence: f32,
87    pub freshness: f32,
88    pub scope: f32,
89    pub localization: Option<StageWeights>,
90    pub patch_plan: Option<StageWeights>,
91    pub verification: Option<StageWeights>,
92    pub review: Option<StageWeights>,
93    /// v0.5.1: half-life (in days) for the usefulness-decay
94    /// multiplier. A memory's effective usefulness contribution
95    /// decays as `exp(-ln(2) * age_days / half_life)` where age is
96    /// measured from `last_useful_at` if present, else
97    /// `created_at`. 30 days = a 6-month-old useful memory ends
98    /// up at ~1.5% of its original weight; tune lower for faster-
99    /// changing repos, higher for slow-evolving ones.
100    ///
101    /// `#[serde(default)]` keeps pre-v0.5.1 project.toml files
102    /// loading cleanly — they get the 30-day default.
103    #[serde(default = "default_decay_half_life_days")]
104    pub decay_half_life_days: f32,
105}
106
107fn default_decay_half_life_days() -> f32 {
108    30.0
109}
110
111impl Default for BrokerWeights {
112    fn default() -> Self {
113        Self {
114            relevance: 0.50,
115            confidence: 0.20,
116            freshness: 0.20,
117            scope: 0.10,
118            localization: Some(StageWeights {
119                relevance: 0.70,
120                confidence: 0.10,
121                freshness: 0.10,
122                scope: 0.10,
123            }),
124            patch_plan: Some(StageWeights {
125                relevance: 0.40,
126                confidence: 0.30,
127                freshness: 0.10,
128                scope: 0.20,
129            }),
130            verification: Some(StageWeights {
131                relevance: 0.40,
132                confidence: 0.10,
133                freshness: 0.40,
134                scope: 0.10,
135            }),
136            review: Some(StageWeights {
137                relevance: 0.50,
138                confidence: 0.20,
139                freshness: 0.20,
140                scope: 0.10,
141            }),
142            decay_half_life_days: default_decay_half_life_days(),
143        }
144    }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct StageWeights {
149    pub relevance: f32,
150    pub confidence: f32,
151    pub freshness: f32,
152    pub scope: f32,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ShellSection {
157    pub default_timeout_secs: u64,
158    pub max_timeout_secs: u64,
159    pub env_allowlist_extra: Vec<String>,
160    pub redact_secrets: bool,
161}
162
163impl Default for ShellSection {
164    fn default() -> Self {
165        Self {
166            default_timeout_secs: 60,
167            max_timeout_secs: 600,
168            env_allowlist_extra: vec!["RUSTFLAGS".to_string(), "CARGO_HOME".to_string()],
169            redact_secrets: true,
170        }
171    }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct IngestionSection {
176    pub max_file_bytes: u64,
177    pub extra_skip_dirs: Vec<String>,
178    pub max_total_files: u64,
179}
180
181impl Default for IngestionSection {
182    fn default() -> Self {
183        Self {
184            max_file_bytes: 524_288,
185            extra_skip_dirs: Vec::new(),
186            max_total_files: 50_000,
187        }
188    }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RunSection {
193    pub max_total_tool_calls: u32,
194    pub max_total_model_turns: u32,
195    pub max_total_cost_usd: f32,
196}
197
198impl Default for RunSection {
199    fn default() -> Self {
200        // `max_total_cost_usd` is treated as advisory under subscription-based
201        // providers (e.g. Claude Code OAuth). The agent loop still enforces it
202        // when it does fire, but the default is set high enough that it
203        // functions as a runaway-prevention safety net rather than a per-run
204        // budget. Tighten in `project.toml` when running against a metered
205        // provider.
206        Self {
207            max_total_tool_calls: 60,
208            max_total_model_turns: 30,
209            max_total_cost_usd: 250.0,
210        }
211    }
212}