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