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