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