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