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