Skip to main content

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>;