swarm-ui: per-agent cap on concurrently running subagents
Nothing capped how many subagents an agent runs at once; the operator wants to set that per agent from swarm-ui (#5234, shape (b)). AgentInference gains `max_subagents: Option<u32>`. An absent field decodes as None, so every stored value still parses and means "no cap", which is today's behaviour. swarm-controller accepts 1..=32 on PUT. 0 would refuse every start; 32 is far more concurrent claude sessions than one agent container has memory for, so anything above it is a typo, not a cap. The subagent daemon already read the agent's agent-inference key for subagent_models; it now reads it on every start and continue (not only those naming a model) and refuses a new run once the cap's worth of names is tracked, counted under the same lock as the name reservation. Every tracked slot counts: running, starting, a goal run between turns, and an interrupted one still exiting. A goal re-prompt never goes through the reservation, so it is never refused. A failed read applies no cap, as it falls back for models. swarmctl's `inference set` does GET, merge, PUT; its mirror type now carries the field so a set from the CLI keeps a cap set in swarm-ui instead of dropping it, and `get` prints it.
This commit is contained in:
parent
6d7fc0dd99
commit
9a71d1e094
10 changed files with 393 additions and 94 deletions
|
|
@ -24,6 +24,8 @@ struct Settings {
|
|||
effort: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
subagent_models: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
max_subagents: Option<u32>,
|
||||
}
|
||||
|
||||
/// What `GET` answers: the settings, and what the agent reported running on.
|
||||
|
|
@ -59,6 +61,7 @@ impl SetFlags {
|
|||
model: self.model.or(current.model),
|
||||
effort: self.effort.or(current.effort),
|
||||
subagent_models: self.subagent_models.or(current.subagent_models),
|
||||
max_subagents: current.max_subagents,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -145,12 +148,14 @@ fn render_configured(s: Option<&Settings>) -> String {
|
|||
fn render(s: &Settings) -> String {
|
||||
const UNSET: &str = "(not set; the agent's own)";
|
||||
format!(
|
||||
"model: {}\neffort: {}\nsubagent_models: {}\n",
|
||||
"model: {}\neffort: {}\nsubagent_models: {}\nmax_subagents: {}\n",
|
||||
s.model.as_deref().unwrap_or(UNSET),
|
||||
s.effort.as_deref().unwrap_or(UNSET),
|
||||
s.subagent_models
|
||||
.as_ref()
|
||||
.map_or_else(|| UNSET.to_owned(), |m| m.join(", "))
|
||||
.map_or_else(|| UNSET.to_owned(), |m| m.join(", ")),
|
||||
s.max_subagents
|
||||
.map_or_else(|| "(not set; no cap)".to_owned(), |n| n.to_string())
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -208,6 +213,19 @@ mod tests {
|
|||
assert_eq!(merged.effort.as_deref(), Some("high"));
|
||||
}
|
||||
|
||||
/// `set` has no flag for the cap, and a stored one survives it.
|
||||
#[test]
|
||||
fn set_keeps_the_stored_max_subagents() {
|
||||
let current: Settings = serde_json::from_str(r#"{"max_subagents":3}"#).unwrap();
|
||||
let merged = SetFlags {
|
||||
model: Some("opus".to_owned()),
|
||||
..flags()
|
||||
}
|
||||
.merge(current)
|
||||
.unwrap();
|
||||
assert_eq!(merged.max_subagents, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_with_no_flag_is_refused() {
|
||||
assert!(flags().merge(Settings::default()).is_err());
|
||||
|
|
@ -236,16 +254,18 @@ mod tests {
|
|||
let empty: View = serde_json::from_str(r#"{"configured":{},"effective":null}"#).unwrap();
|
||||
assert_eq!(
|
||||
render_configured(empty.configured.as_ref()).lines().count(),
|
||||
3
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_value_renders_every_field_as_unset() {
|
||||
let row = render(&serde_json::from_str("{}").unwrap());
|
||||
assert_eq!(row.lines().count(), 3);
|
||||
assert_eq!(row.lines().count(), 4);
|
||||
assert!(row.ends_with("max_subagents: (not set; no cap)\n"), "{row}");
|
||||
assert!(
|
||||
row.lines()
|
||||
.take(3)
|
||||
.all(|l| l.ends_with("(not set; the agent's own)")),
|
||||
"{row}"
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue