1use std::{collections::HashMap, ffi::OsString, time::Duration};
4
5use anyhow::{Context, Result};
6use scv_client::Layout;
7use scv_core::{AgentConfig as CoreAgentConfig, HistoryLimits};
8use scv_provider_openai::ProviderLimits;
9use scv_tools::{
10 AgentAdapterConfig, BusyConfig, ToolsConfig,
11 conversation::ConversationLimits,
12 web::{SearchBackend, WebToolsConfig},
13};
14
15use super::{AgentPermissions, AgentTransport, Config, WebSearchMode, load::ensure_private_dir};
16
17impl Config {
18 pub(crate) fn core_agent(&self, system_prompt: String) -> CoreAgentConfig {
19 CoreAgentConfig {
20 system_prompt,
21 max_steps: self.agent.max_steps,
22 history_limits: HistoryLimits {
23 max_bytes: self.session.max_history_bytes,
24 max_messages: self.session.max_messages,
25 note_max_chars: self.context.summary_max_chars,
26 },
27 }
28 }
29
30 pub(crate) fn tools(&self) -> ToolsConfig {
31 ToolsConfig {
32 command_timeout: Duration::from_secs(self.tools.command_timeout_seconds),
33 agent_timeout: Duration::from_secs(self.tools.agent_timeout_seconds),
34 max_timeout: Duration::from_secs(self.tools.max_timeout_seconds),
35 output_limit_bytes: self.tools.output_limit_bytes,
36 max_read_bytes: self.tools.max_read_bytes,
37 max_write_bytes: self.tools.max_write_bytes,
38 max_delegation_depth: self.agent.max_delegation_depth,
39 prefer: self.agent.prefer.clone(),
40 conversations: ConversationLimits {
41 max: self.agent.max_conversations,
42 idle: Duration::from_secs(self.agent.conversation_idle_seconds),
43 },
44 delegation: None,
45 max_background: self.agent.max_background,
46 background: None,
47 reviews: None,
48 chat_attach: None,
49 chat_history: None,
50 precheck_agent_models: true,
51 }
52 }
53
54 pub(crate) fn episode_gap(&self) -> Duration {
57 Duration::from_secs(self.history.episode_gap_minutes.saturating_mul(60))
58 }
59
60 pub(crate) fn archive_dir(&self) -> std::path::PathBuf {
63 self.history
64 .archive_dir
65 .clone()
66 .unwrap_or_else(|| self.layout().history())
67 }
68
69 pub(crate) fn web_tools(&self) -> Option<WebToolsConfig> {
72 if !self.web.enabled {
73 return None;
74 }
75 let search = match self.web.search {
76 WebSearchMode::Off | WebSearchMode::Provider => None,
77 WebSearchMode::Searxng => self
78 .web
79 .searxng_url
80 .clone()
81 .map(|url| SearchBackend::Searxng { url }),
82 WebSearchMode::Brave => {
83 let api_key = self
84 .web
85 .brave_api_key
86 .clone()
87 .or_else(|| {
88 self.web
89 .brave_api_key_env
90 .as_deref()
91 .and_then(|name| std::env::var(name).ok())
92 .map(scv_client::Secret::from)
93 })
94 .filter(|key| !key.trim().is_empty());
95 if api_key.is_none() {
96 tracing::warn!(
97 "web.search is \"brave\" but no Brave API key is configured; web_search is unavailable"
98 );
99 }
100 api_key.map(|api_key| SearchBackend::Brave {
101 url: self.web.brave_url.clone(),
102 api_key: api_key.into_inner(),
103 })
104 }
105 };
106 Some(WebToolsConfig {
107 fetch_max_bytes: self.web.fetch_max_bytes,
108 fetch_timeout: Duration::from_secs(self.web.fetch_timeout_seconds),
109 max_redirects: self.web.max_redirects,
110 auto_approve_domains: self.web.auto_approve_domains.clone(),
111 allow_private_addresses: self.web.allow_private_addresses,
112 search,
113 max_search_results: self.web.max_search_results,
114 output_limit: self.tools.output_limit_bytes,
115 })
116 }
117
118 pub fn hosted_web_search(&self) -> bool {
120 self.web.enabled && self.web.search == WebSearchMode::Provider
121 }
122
123 pub(crate) fn provider_limits(&self) -> ProviderLimits {
124 ProviderLimits {
125 max_sse_event_bytes: self.provider_limits.max_sse_event_bytes,
126 max_response_bytes: self.provider_limits.max_response_bytes,
127 max_assistant_bytes: self.provider_limits.max_assistant_bytes,
128 max_tool_calls: self.provider_limits.max_tool_calls,
129 max_tool_arguments_bytes: self.provider_limits.max_tool_arguments_bytes,
130 max_retries: self.provider_limits.max_retries,
131 ..ProviderLimits::default()
132 }
133 }
134
135 pub fn adapters(&self) -> HashMap<String, AgentAdapterConfig> {
136 let user_home = dirs::home_dir();
137 self.agents
138 .0
139 .iter()
140 .filter_map(|(name, config)| {
141 let descriptor = scv_tools::adapters::adapter(name)?;
142 let adapter_home = self.layout().agent_home(name);
143 let mut environment = vec![
144 (OsString::from("SCV_HOME"), adapter_home.clone().into()),
145 (OsString::from("HOME"), adapter_home.clone().into()),
146 (
147 OsString::from("XDG_CONFIG_HOME"),
148 adapter_home.join("config").into(),
149 ),
150 (
151 OsString::from("XDG_DATA_HOME"),
152 adapter_home.join("data").into(),
153 ),
154 (
155 OsString::from("XDG_STATE_HOME"),
156 adapter_home.join("state").into(),
157 ),
158 ];
159 for (variable, relative) in descriptor.home_environment {
160 let path = if relative.is_empty() {
161 adapter_home.clone()
162 } else {
163 adapter_home.join(relative)
164 };
165 environment.push((OsString::from(variable), path.into()));
166 }
167 let full = config.permissions == AgentPermissions::Full;
168 environment.extend(
169 descriptor
170 .fixed_environment
171 .iter()
172 .chain(
173 descriptor
174 .full_permission_environment
175 .iter()
176 .filter(|_| full),
177 )
178 .map(|(variable, value)| (OsString::from(variable), OsString::from(value))),
179 );
180 Some((
181 name.clone(),
182 AgentAdapterConfig {
183 command: config.command.clone(),
184 args: config.args.clone(),
185 prompt_args: config.prompt_args.clone(),
186 full_permission_args: full.then(|| {
187 descriptor
188 .full_permission_args
189 .iter()
190 .map(|arg| (*arg).to_owned())
191 .collect()
192 }),
193 model_args: config.model_args.clone(),
194 effort_args: config.effort_args.clone(),
195 model_hint: descriptor.model_hint.into(),
196 environment,
197 search_dirs: user_home
198 .as_deref()
199 .map(|home| scv_tools::adapters::adapter_search_dirs(descriptor, home))
200 .unwrap_or_default(),
201 output: descriptor.output,
202 resume: descriptor.resume,
203 home: Some(adapter_home),
204 transport: descriptor.transport,
205 acp: config.acp_launch(descriptor).map(|launch| {
206 scv_tools::AcpAgentLaunch {
207 command: launch.command.to_owned(),
208 args: scv_tools::adapters::acp_args(&launch, full),
209 full_mode: launch.full_mode.filter(|_| full).map(str::to_owned),
210 environment: launch
211 .full_environment
212 .iter()
213 .filter(|_| full)
214 .map(|(variable, value)| {
215 (OsString::from(variable), OsString::from(value))
216 })
217 .collect(),
218 required: config.transport == AgentTransport::Acp,
219 session_options: launch.session_options,
220 }
221 }),
222 use_for: config.use_for.clone(),
223 defaults: config.defaults(),
224 options_file: Some(self.layout().agent_options(name)),
225 busy: BusyConfig {
226 behavior: config.on_busy.unwrap_or(self.agent.on_busy),
227 steer_fallback: config
228 .steer_fallback
229 .unwrap_or(self.agent.steer_fallback),
230 max_queued_turns: config
231 .max_queued_turns
232 .unwrap_or(self.agent.max_queued_turns),
233 },
234 },
235 ))
236 })
237 .collect()
238 }
239
240 pub fn layout(&self) -> Layout {
244 Layout::new(&self.instance_home)
245 }
246
247 pub fn prepare_adapter_homes(&self) -> Result<()> {
248 let layout = self.layout();
249 std::fs::create_dir_all(layout.agents()).context("create the agent homes directory")?;
250 ensure_private_dir(&layout.agents())?;
251 for name in self.agents.0.keys() {
252 let path = layout.agent_home(name);
253 std::fs::create_dir_all(&path)
254 .with_context(|| format!("create isolated {name} agent home"))?;
255 ensure_private_dir(&path)
256 .with_context(|| format!("secure isolated {name} agent home"))?;
257 }
258 Ok(())
259 }
260}