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, ModeReport, 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    /// agent's policy mode is `auto`; the approval summary says so.
213    pub full_permission_args: Option<Vec<String>>,
214    /// The policy mode in effect for this agent (`auto`, `on-risk`,
215    /// `always`, or `read-only`), named in its approval summary and in a
216    /// mode mismatch.
217    pub policy: String,
218    /// How its CLI reports the permission mode it runs in.
219    pub mode_report: ModeReport,
220    /// Whether it has a full-autonomy switch at all. One without (pi,
221    /// OpenCode, Kimi Code, a nested SCV) has no mode to verify.
222    pub mode_switch: bool,
223    /// Arguments appended for a per-call model; `{model}` is substituted.
224    /// Empty means the adapter does not offer model selection.
225    pub model_args: Vec<String>,
226    /// Arguments appended for a per-call effort; `{effort}` is substituted.
227    /// Empty means the adapter does not offer effort selection.
228    pub effort_args: Vec<String>,
229    /// Describes the `model` argument for the calling model.
230    pub model_hint: String,
231    /// Environment for the nested process. SCV supplies an instance-private home.
232    pub environment: Vec<(OsString, OsString)>,
233    /// Per-user install directories searched when `command` is not on `PATH`.
234    pub search_dirs: Vec<PathBuf>,
235    /// What the CLI prints, and so how its reply is read.
236    pub output: OutputFormat,
237    /// How a conversation with the CLI is continued, if it can be.
238    pub resume: Resume,
239    /// SCV's private home for this agent, for files SCV hands the CLI.
240    pub home: Option<PathBuf>,
241    /// Credential identity and pinned settings for Claude Code launches.
242    pub auth: AgentAuthPlan,
243    /// How SCV talks to the agent.
244    pub transport: Transport,
245    /// The agent's ACP server, when `[agents.<name>] transport` allows it and
246    /// the adapter table has one.
247    pub acp: Option<AcpAgentLaunch>,
248    /// The user's note on when to choose this agent (`[agents.<name>]
249    /// use_for`), added to its line in the `agent` tool's description.
250    pub use_for: Option<String>,
251    /// The user's model and effort for this agent.
252    pub defaults: AgentDefaults,
253    /// Where SCV keeps the model and effort values this agent's ACP server
254    /// offers (`state/agent-options/<name>.json`); `None` keeps none.
255    pub options_file: Option<PathBuf>,
256    /// How a call to one of this agent's busy conversations is handled:
257    /// `[agents.<name>]` over `[agent]`.
258    pub busy: BusyConfig,
259}
260
261#[derive(Debug, Clone)]
262pub enum AgentAuthPlan {
263    None,
264    Claude {
265        mode: AgentAuth,
266        settings: serde_json::Value,
267        home: PathBuf,
268    },
269}
270
271/// The user's model and effort for one agent: `[agents.<name>] model`,
272/// `effort`, and `hard_task_effort`.
273#[derive(Debug, Clone, Default, PartialEq, Eq)]
274pub struct AgentDefaults {
275    /// The model an `agent` call that names none runs on.
276    pub model: Option<String>,
277    /// The effort an `agent` call that names none runs at.
278    pub effort: Option<String>,
279    /// The effort the calling model is told to pass for a hard task. SCV
280    /// never applies it by itself, since only the caller can tell a task is
281    /// hard.
282    pub hard_task_effort: Option<String>,
283}
284
285/// An agent's Agent Client Protocol server, resolved from its adapter-table
286/// entry and `[agents.<name>] transport`.
287#[derive(Debug, Clone)]
288pub struct AcpAgentLaunch {
289    pub command: String,
290    /// Arguments with the policy's full-autonomy switches already applied.
291    pub args: Vec<String>,
292    /// The ACP session mode that grants full permissions. Every new session
293    /// selects it under policy mode `auto`, and under any mode the session
294    /// must report the mode the policy expects.
295    pub full_mode: Option<String>,
296    /// Extra environment for the ACP server, such as permission settings the
297    /// server reads only from its environment.
298    pub environment: Vec<(OsString, OsString)>,
299    /// `transport = "acp"`: never fall back to one CLI process per turn, so
300    /// the agent is not offered while its ACP server is missing.
301    pub required: bool,
302    /// The server takes `model` and `effort` as session config options even
303    /// where the CLI takes neither as an argument (DeepSeek Harness).
304    pub session_options: bool,
305}
306
307/// Where a skill's text comes from.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum Skill {
310    /// A `SKILL.md` file, read when loaded and only from inside one of the
311    /// configured skill roots.
312    File(PathBuf),
313    /// Built into SCV.
314    Builtin(&'static str),
315}
316
317/// A session's skills by name.
318pub type SkillMap = HashMap<String, Skill>;