subagents: add effort param, default to medium, document in skill

This commit is contained in:
damocles 2026-09-13 17:45:42 +02:00
commit 77c3c656b2
3 changed files with 104 additions and 12 deletions

View file

@ -289,7 +289,14 @@ fn subagent_otel_attrs(name: &str) -> String {
}
}
/// Build the `Config` one subagent turn runs against. `prompt_file`, when
/// Build the `Config` one subagent turn runs against. `model` maps straight
/// onto `Config::model` — `--model`, omitted when `None` so claude falls
/// back to its own default. `effort` does not: an omitted `effort` defaults
/// to `"medium"` here rather than falling through to claude's own default
/// (`high` on most models) — a deliberate hive policy for subagent work
/// specifically, not a reflection of Anthropic's own recommendation, on the
/// same "cheaper than you" cost-consciousness the `base:claude-subagents`
/// skill already asks of `model`. `prompt_file`, when
/// given, becomes `--append-system-prompt-file` — the subagent's task
/// instructions. `dir`, when given, becomes `Config::cwd` (e.g. a worktree
/// the caller already prepared); `None` inherits this daemon's own working
@ -305,6 +312,7 @@ fn subagent_otel_attrs(name: &str) -> String {
fn build_config(
name: &str,
model: Option<String>,
effort: Option<String>,
prompt_file: Option<&str>,
dir: Option<&str>,
) -> Config {
@ -315,6 +323,7 @@ fn build_config(
}
Config {
model,
effort: Some(effort.unwrap_or_else(|| "medium".to_owned())),
cwd: dir.map(PathBuf::from),
mcp_config: crate::mcp_config::build(),
strict_mcp_config: true,
@ -354,6 +363,7 @@ pub fn start(
state: &Arc<State>,
name: &str,
model: Option<String>,
effort: Option<String>,
prompt_file: &str,
trigger: String,
dir: Option<&str>,
@ -365,7 +375,15 @@ pub fn start(
// Only commit the remembered `dir` now that `reserve` has actually
// claimed `name` — see `resolve_dir`'s doc for why the order matters.
let dir = state.resolve_dir(name, dir);
let result = start_reserved(state, name, model, prompt_file, trigger, dir.as_deref());
let result = start_reserved(
state,
name,
model,
effort,
prompt_file,
trigger,
dir.as_deref(),
);
if result.is_err() {
state.release_reservation(name);
}
@ -380,11 +398,12 @@ fn start_reserved(
state: &Arc<State>,
name: &str,
model: Option<String>,
effort: Option<String>,
prompt_file: &str,
trigger: String,
dir: Option<&str>,
) -> anyhow::Result<String> {
let config = build_config(name, model, Some(prompt_file), dir);
let config = build_config(name, model, effort, Some(prompt_file), dir);
let store = build_store(&config)?;
if store.find_by_title(name).is_some() {
tracing::info!(
@ -424,6 +443,7 @@ pub fn continue_(
name: &str,
prompt: String,
model: Option<String>,
effort: Option<String>,
dir: Option<&str>,
) -> anyhow::Result<String> {
validate_name(name)?;
@ -438,7 +458,7 @@ pub fn continue_(
// Only commit the remembered `dir` now that `reserve` has actually
// claimed `name` — see `resolve_dir`'s doc for why the order matters.
let dir = state.resolve_dir(name, dir);
let result = continue_reserved(state, name, prompt, model, dir.as_deref());
let result = continue_reserved(state, name, prompt, model, effort, dir.as_deref());
if result.is_err() {
state.release_reservation(name);
}
@ -467,9 +487,10 @@ fn continue_reserved(
name: &str,
prompt: String,
model: Option<String>,
effort: Option<String>,
dir: Option<&str>,
) -> anyhow::Result<String> {
let config = build_config(name, model, None, dir);
let config = build_config(name, model, effort, None, dir);
let store = build_store(&config)?;
if store.find_by_title(name).is_none() {
anyhow::bail!(
@ -561,7 +582,7 @@ pub fn status(state: &State, name: &str, dir: Option<&str>) -> anyhow::Result<St
// those states answer on their own, and the store read is the only
// expensive part of this call.
let session_exists = if occupancy.is_none() && killed.is_none() {
let config = build_config(name, None, None, dir.as_deref());
let config = build_config(name, None, None, None, dir.as_deref());
build_store(&config)?.find_by_title(name).is_some()
} else {
false
@ -775,14 +796,14 @@ mod tests {
#[test]
fn build_config_only_appends_system_prompt_when_given() {
let with = build_config("n", None, Some("/tmp/p.md"), None);
let with = build_config("n", None, None, Some("/tmp/p.md"), None);
assert!(
with.extra_args
.contains(&"--append-system-prompt-file".to_owned())
);
assert!(with.extra_args.contains(&"/tmp/p.md".to_owned()));
let without = build_config("n", None, None, None);
let without = build_config("n", None, None, None, None);
assert!(
!without
.extra_args
@ -865,6 +886,7 @@ mod tests {
&state,
"dup",
None,
None,
"/tmp/prompt.md",
"trigger".to_owned(),
Some("/tmp/rejected"),
@ -1054,10 +1076,33 @@ mod tests {
#[test]
fn build_config_sets_cwd_only_when_a_dir_is_given() {
let with = build_config("n", None, None, Some("/tmp/some-worktree"));
let with = build_config("n", None, None, None, Some("/tmp/some-worktree"));
assert_eq!(with.cwd, Some(PathBuf::from("/tmp/some-worktree")));
let without = build_config("n", None, None, None);
let without = build_config("n", None, None, None, None);
assert_eq!(without.cwd, None);
}
#[test]
fn build_config_sets_effort_alongside_model() {
let config = build_config(
"n",
Some("opus".to_owned()),
Some("high".to_owned()),
None,
None,
);
assert_eq!(config.model, Some("opus".to_owned()));
assert_eq!(config.effort, Some("high".to_owned()));
}
#[test]
fn build_config_defaults_effort_to_medium_when_omitted() {
// Unlike `model`, an omitted `effort` does not fall through to
// claude's own default (`high` on most models) — this daemon picks
// `medium` itself, a deliberate cost-conscious choice for subagent
// work.
let config = build_config("n", None, None, None, None);
assert_eq!(config.effort, Some("medium".to_owned()));
}
}