Skip to main content

selfware/profiles/
mod.rs

1//! Configuration Profiles
2//!
3//! Pre-configured settings for different use cases
4
5use crate::config::Config;
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Profile definition
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Profile {
13    pub name: String,
14    pub description: String,
15    pub config_overrides: ConfigOverrides,
16}
17
18/// Configuration overrides for a profile
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct ConfigOverrides {
21    pub max_tokens: Option<usize>,
22    pub temperature: Option<f32>,
23    pub max_iterations: Option<usize>,
24    pub step_timeout_secs: Option<u64>,
25    pub concurrency: Option<ConcurrencyOverrides>,
26    pub agent: Option<AgentOverrides>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ConcurrencyOverrides {
31    pub max_parallel_requests: Option<usize>,
32    pub timeout_secs: Option<u64>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AgentOverrides {
37    pub streaming: Option<bool>,
38    pub native_function_calling: Option<bool>,
39    pub enable_thinking: Option<bool>,
40}
41
42/// Profile manager
43pub struct ProfileManager {
44    profiles: HashMap<String, Profile>,
45}
46
47impl ProfileManager {
48    /// Create with built-in profiles
49    pub fn new() -> Self {
50        let mut profiles = HashMap::new();
51
52        // Single powerful agent (architect mode)
53        profiles.insert(
54            "architect".to_string(),
55            Profile {
56                name: "Architect".to_string(),
57                description: "Deep thinking, single agent for complex design tasks".to_string(),
58                config_overrides: ConfigOverrides {
59                    max_tokens: Some(8192),
60                    temperature: Some(0.7),
61                    max_iterations: Some(100),
62                    step_timeout_secs: Some(900),
63                    concurrency: Some(ConcurrencyOverrides {
64                        max_parallel_requests: Some(4),
65                        timeout_secs: Some(120),
66                    }),
67                    agent: Some(AgentOverrides {
68                        streaming: Some(false),
69                        native_function_calling: Some(true),
70                        enable_thinking: Some(true),
71                    }),
72                },
73            },
74        );
75
76        // 8-agent swarm
77        profiles.insert(
78            "swarm-8".to_string(),
79            Profile {
80                name: "Swarm-8".to_string(),
81                description: "8 concurrent agents for collaborative tasks".to_string(),
82                config_overrides: ConfigOverrides {
83                    max_tokens: Some(4096),
84                    temperature: Some(0.6),
85                    max_iterations: Some(50),
86                    step_timeout_secs: Some(600),
87                    concurrency: Some(ConcurrencyOverrides {
88                        max_parallel_requests: Some(16),
89                        timeout_secs: Some(300),
90                    }),
91                    agent: Some(AgentOverrides {
92                        streaming: Some(false),
93                        native_function_calling: Some(true),
94                        enable_thinking: Some(false),
95                    }),
96                },
97            },
98        );
99
100        // 16-agent batch (sweet spot for 2x4090)
101        profiles.insert(
102            "batch-16".to_string(),
103            Profile {
104                name: "Batch-16".to_string(),
105                description: "16 concurrent agents - optimal for 2x RTX 4090".to_string(),
106                config_overrides: ConfigOverrides {
107                    max_tokens: Some(4096),
108                    temperature: Some(0.6),
109                    max_iterations: Some(30),
110                    step_timeout_secs: Some(900),
111                    concurrency: Some(ConcurrencyOverrides {
112                        max_parallel_requests: Some(24),
113                        timeout_secs: Some(600),
114                    }),
115                    agent: Some(AgentOverrides {
116                        streaming: Some(false),
117                        native_function_calling: Some(true),
118                        enable_thinking: Some(false),
119                    }),
120                },
121            },
122        );
123
124        // 32-way batch (direct API only)
125        profiles.insert(
126            "batch-32".to_string(),
127            Profile {
128                name: "Batch-32".to_string(),
129                description: "Maximum throughput for simple tasks".to_string(),
130                config_overrides: ConfigOverrides {
131                    max_tokens: Some(2048),
132                    temperature: Some(0.5),
133                    max_iterations: Some(10),
134                    step_timeout_secs: Some(300),
135                    concurrency: Some(ConcurrencyOverrides {
136                        max_parallel_requests: Some(32),
137                        timeout_secs: Some(120),
138                    }),
139                    agent: Some(AgentOverrides {
140                        streaming: Some(false),
141                        native_function_calling: Some(false),
142                        enable_thinking: Some(false),
143                    }),
144                },
145            },
146        );
147
148        // Visual validation mode
149        profiles.insert(
150            "visual".to_string(),
151            Profile {
152                name: "Visual".to_string(),
153                description: "Website building with visual validation".to_string(),
154                config_overrides: ConfigOverrides {
155                    max_tokens: Some(4096),
156                    temperature: Some(0.6),
157                    max_iterations: Some(50),
158                    step_timeout_secs: Some(600),
159                    concurrency: Some(ConcurrencyOverrides {
160                        max_parallel_requests: Some(8),
161                        timeout_secs: Some(300),
162                    }),
163                    agent: Some(AgentOverrides {
164                        streaming: Some(false),
165                        native_function_calling: Some(true),
166                        enable_thinking: Some(false),
167                    }),
168                },
169            },
170        );
171
172        // Quick mode (fastest)
173        profiles.insert(
174            "quick".to_string(),
175            Profile {
176                name: "Quick".to_string(),
177                description: "Fast responses, minimal verification".to_string(),
178                config_overrides: ConfigOverrides {
179                    max_tokens: Some(2048),
180                    temperature: Some(0.5),
181                    max_iterations: Some(10),
182                    step_timeout_secs: Some(120),
183                    concurrency: Some(ConcurrencyOverrides {
184                        max_parallel_requests: Some(32),
185                        timeout_secs: Some(60),
186                    }),
187                    agent: Some(AgentOverrides {
188                        streaming: Some(false),
189                        native_function_calling: Some(false),
190                        enable_thinking: Some(false),
191                    }),
192                },
193            },
194        );
195
196        Self { profiles }
197    }
198
199    /// Get a profile by name
200    pub fn get(&self, name: &str) -> Option<&Profile> {
201        self.profiles.get(name)
202    }
203
204    /// List all available profiles
205    pub fn list(&self) -> Vec<&Profile> {
206        self.profiles.values().collect()
207    }
208
209    /// Apply profile to config
210    pub fn apply_profile(&self, config: &mut Config, profile_name: &str) -> Result<()> {
211        let profile = self
212            .get(profile_name)
213            .ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
214
215        // Apply every override that has a concrete Config target. (Previously
216        // only max_tokens/temperature were applied, so --profile silently
217        // dropped max_iterations, step_timeout, streaming, etc.)
218        let o = &profile.config_overrides;
219
220        if let Some(max_tokens) = o.max_tokens {
221            config.max_tokens = max_tokens;
222        }
223        if let Some(temperature) = o.temperature {
224            config.temperature = temperature;
225        }
226        if let Some(max_iterations) = o.max_iterations {
227            config.agent.max_iterations = max_iterations;
228        }
229        if let Some(step_timeout_secs) = o.step_timeout_secs {
230            config.agent.step_timeout_secs = step_timeout_secs;
231        }
232        if let Some(agent) = &o.agent {
233            if let Some(streaming) = agent.streaming {
234                config.agent.streaming = streaming;
235            }
236            if let Some(native_fc) = agent.native_function_calling {
237                config.agent.native_function_calling = native_fc;
238            }
239            // `enable_thinking` has no direct Config field — it's expressed as
240            // chat_template_kwargs in extra_body — so it's intentionally not
241            // mapped here rather than silently faked. (Follow-up: wire it into
242            // extra_body if profile-level thinking control is wanted.)
243        }
244        if let Some(concurrency) = &o.concurrency {
245            if let Some(max_parallel) = concurrency.max_parallel_requests {
246                config.concurrency.max_streams = max_parallel;
247            }
248            // `timeout_secs` has no ConcurrencyConfig field; not mapped (see above).
249        }
250
251        Ok(())
252    }
253
254    /// Get profile description
255    pub fn describe(&self, name: &str) -> Option<String> {
256        self.get(name)
257            .map(|p| format!("{}: {}", p.name, p.description))
258    }
259}
260
261impl Default for ProfileManager {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267#[cfg(test)]
268#[path = "../../tests/unit/profiles/mod_test.rs"]
269mod tests;