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