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