scv_tools/config.rs
1//! What a session's built-in tools are configured with: limits, the
2//! delegation context, and each delegated agent's adapter settings.
3
4use std::{collections::HashMap, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
5
6use crate::{
7 builtin::{chat_attach, chat_history},
8 delegate::{
9 adapters::{OutputFormat, Resume, Transport},
10 background,
11 conversation::ConversationLimits,
12 records::DelegationRegistry,
13 },
14};
15
16/// Limits and shared state for one session's tools.
17#[derive(Debug, Clone)]
18pub struct ToolsConfig {
19 /// Default `bash` timeout when a call does not choose one.
20 pub command_timeout: Duration,
21 /// Default native-agent timeout when a call does not choose one.
22 pub agent_timeout: Duration,
23 /// The longest timeout any single call may request.
24 pub max_timeout: Duration,
25 pub output_limit_bytes: usize,
26 pub max_read_bytes: usize,
27 pub max_write_bytes: usize,
28 /// The `agent` tool is offered only below this delegation depth.
29 pub max_delegation_depth: u32,
30 /// Agents the user prefers, in order (`[agent] prefer`); the first one
31 /// offered runs an `agent` call that names none.
32 pub prefer: Vec<String>,
33 /// How many delegated conversations a session remembers, and for how long.
34 pub conversations: ConversationLimits,
35 /// Records delegated runs for listing and cleanup; `None` runs them untracked.
36 pub delegation: Option<DelegationContext>,
37 /// Background jobs `agent` calls may run at once (`background: true`);
38 /// 0 turns background calls and `agent_wait` / `agent_status` /
39 /// `agent_cancel` off.
40 pub max_background: usize,
41 /// The session's background job store, when the server reports finished
42 /// jobs; otherwise the registry makes its own.
43 pub background: Option<Arc<background::BackgroundJobs>>,
44 /// Offers `chat_attach` when the session answers on a chat channel.
45 pub chat_attach: Option<chat_attach::ChatAttachConfig>,
46 /// Offers `chat_history` and `chat_keep` when the session answers a
47 /// conversation that has a chat log.
48 pub chat_history: Option<chat_history::ChatHistoryConfig>,
49 /// Refuse a model an ACP agent's saved list lacks before the call starts.
50 /// `scv agents check` turns it off so the agent's own list decides.
51 pub precheck_agent_models: bool,
52}
53
54impl Default for ToolsConfig {
55 fn default() -> Self {
56 Self {
57 command_timeout: Duration::from_secs(600),
58 agent_timeout: Duration::from_secs(3600),
59 max_timeout: Duration::from_secs(14400),
60 output_limit_bytes: 64 * 1024,
61 max_read_bytes: 256 * 1024,
62 max_write_bytes: 1024 * 1024,
63 max_delegation_depth: 2,
64 prefer: Vec::new(),
65 conversations: ConversationLimits {
66 max: 8,
67 idle: Duration::from_secs(86400),
68 },
69 delegation: None,
70 max_background: 2,
71 background: None,
72 chat_attach: None,
73 chat_history: None,
74 precheck_agent_models: true,
75 }
76 }
77}
78
79/// What an `agent` call does when the conversation it continues is running
80/// a turn, or has prompts queued (`agent.on_busy`).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum BusyBehavior {
83 /// Queue the prompt as a background job that runs after the turns ahead.
84 #[default]
85 Queue,
86 /// Keep the call waiting until the turns ahead end, then run it.
87 Wait,
88 /// Add the prompt to the running turn when the agent's ACP server can be
89 /// steered, otherwise apply [`BusyConfig::steer_fallback`].
90 Steer,
91 /// Refuse the call as busy.
92 Fail,
93}
94
95impl BusyBehavior {
96 /// The behavior `value` names, or an error saying what may be named.
97 pub(crate) fn parse(value: &str) -> Result<Self, String> {
98 match value {
99 "queue" => Ok(Self::Queue),
100 "wait" => Ok(Self::Wait),
101 "steer" => Ok(Self::Steer),
102 "fail" => Ok(Self::Fail),
103 other => Err(format!(
104 "invalid busy behavior {other:?}; use queue, wait, steer, or fail"
105 )),
106 }
107 }
108}
109
110impl<'de> serde::Deserialize<'de> for BusyBehavior {
111 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
112 Self::parse(&String::deserialize(d)?).map_err(serde::de::Error::custom)
113 }
114}
115
116impl serde::Serialize for BusyBehavior {
117 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
118 s.serialize_str(match self {
119 Self::Queue => "queue",
120 Self::Wait => "wait",
121 Self::Steer => "steer",
122 Self::Fail => "fail",
123 })
124 }
125}
126
127/// How one agent's busy conversations are handled.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct BusyConfig {
130 /// What a call does by default (`on_busy`).
131 pub behavior: BusyBehavior,
132 /// What a steer does when the running turn cannot take the prompt
133 /// (`steer_fallback`): queue, wait, or fail. `Steer` means queue.
134 pub steer_fallback: BusyBehavior,
135 /// Prompts that may wait per conversation (`max_queued_turns`).
136 pub max_queued_turns: usize,
137}
138
139impl BusyConfig {
140 /// What a steer that could not steer does instead.
141 pub(crate) fn fallback(self) -> BusyBehavior {
142 match self.steer_fallback {
143 BusyBehavior::Steer => BusyBehavior::Queue,
144 other => other,
145 }
146 }
147}
148
149impl Default for BusyConfig {
150 fn default() -> Self {
151 Self {
152 behavior: BusyBehavior::Queue,
153 steer_fallback: BusyBehavior::Queue,
154 max_queued_turns: 4,
155 }
156 }
157}
158
159/// The registry and parent session that delegated runs are recorded under.
160#[derive(Debug, Clone)]
161pub struct DelegationContext {
162 pub registry: Arc<DelegationRegistry>,
163 pub session: String,
164 /// Delegation depth the session's client declared (0 for a direct
165 /// client). Runs count from the larger of this and the process's own.
166 pub depth: u32,
167}
168
169impl DelegationContext {
170 /// The depth delegated runs of this session start from.
171 pub(crate) fn owner_depth(&self) -> u32 {
172 self.registry.depth().max(self.depth)
173 }
174}
175
176#[derive(Debug, Clone)]
177pub struct AgentAdapterConfig {
178 pub command: String,
179 pub args: Vec<String>,
180 /// Arguments placed immediately before the prompt, for CLIs that take the
181 /// prompt as a flag value.
182 pub prompt_args: Vec<String>,
183 /// The CLI's own full-autonomy arguments, placed after `args`, when the
184 /// user configured `permissions = "full"`; the approval summary says so.
185 pub full_permission_args: Option<Vec<String>>,
186 /// Arguments appended for a per-call model; `{model}` is substituted.
187 /// Empty means the adapter does not offer model selection.
188 pub model_args: Vec<String>,
189 /// Arguments appended for a per-call effort; `{effort}` is substituted.
190 /// Empty means the adapter does not offer effort selection.
191 pub effort_args: Vec<String>,
192 /// Describes the `model` argument for the calling model.
193 pub model_hint: String,
194 /// Environment for the nested process. SCV supplies an instance-private home.
195 pub environment: Vec<(OsString, OsString)>,
196 /// Per-user install directories searched when `command` is not on `PATH`.
197 pub search_dirs: Vec<PathBuf>,
198 /// What the CLI prints, and so how its reply is read.
199 pub output: OutputFormat,
200 /// How a conversation with the CLI is continued, if it can be.
201 pub resume: Resume,
202 /// SCV's private home for this agent, for files SCV hands the CLI.
203 pub home: Option<PathBuf>,
204 /// How SCV talks to the agent.
205 pub transport: Transport,
206 /// The agent's ACP server, when `[agents.<name>] transport` allows it and
207 /// the adapter table has one.
208 pub acp: Option<AcpAgentLaunch>,
209 /// The user's note on when to choose this agent (`[agents.<name>]
210 /// use_for`), added to its line in the `agent` tool's description.
211 pub use_for: Option<String>,
212 /// The user's model and effort for this agent.
213 pub defaults: AgentDefaults,
214 /// Where SCV keeps the model and effort values this agent's ACP server
215 /// offers (`state/agent-options/<name>.json`); `None` keeps none.
216 pub options_file: Option<PathBuf>,
217 /// How a call to one of this agent's busy conversations is handled:
218 /// `[agents.<name>]` over `[agent]`.
219 pub busy: BusyConfig,
220}
221
222/// The user's model and effort for one agent: `[agents.<name>] model`,
223/// `effort`, and `hard_task_effort`.
224#[derive(Debug, Clone, Default, PartialEq, Eq)]
225pub struct AgentDefaults {
226 /// The model an `agent` call that names none runs on.
227 pub model: Option<String>,
228 /// The effort an `agent` call that names none runs at.
229 pub effort: Option<String>,
230 /// The effort the calling model is told to pass for a hard task. SCV
231 /// never applies it by itself, since only the caller can tell a task is
232 /// hard.
233 pub hard_task_effort: Option<String>,
234}
235
236/// An agent's Agent Client Protocol server, resolved from its adapter-table
237/// entry and `[agents.<name>] transport`.
238#[derive(Debug, Clone)]
239pub struct AcpAgentLaunch {
240 pub command: String,
241 /// Arguments with the `permissions = "full"` switches already applied.
242 pub args: Vec<String>,
243 /// The ACP session mode that grants full permissions, selected in every
244 /// new session when `permissions = "full"`.
245 pub full_mode: Option<String>,
246 /// Extra environment for the ACP server, such as permission settings the
247 /// server reads only from its environment.
248 pub environment: Vec<(OsString, OsString)>,
249 /// `transport = "acp"`: never fall back to one CLI process per turn, so
250 /// the agent is not offered while its ACP server is missing.
251 pub required: bool,
252 /// The server takes `model` and `effort` as session config options even
253 /// where the CLI takes neither as an argument (DeepSeek Harness).
254 pub session_options: bool,
255}
256
257/// Where a skill's text comes from.
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub enum Skill {
260 /// A `SKILL.md` file, read when loaded and only from inside one of the
261 /// configured skill roots.
262 File(PathBuf),
263 /// Built into SCV.
264 Builtin(&'static str),
265}
266
267/// A session's skills by name.
268pub type SkillMap = HashMap<String, Skill>;