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
|
|
@ -1,5 +1,5 @@
|
|||
//! The per-agent inference-settings KV bucket: one key per agent, holding the
|
||||
//! model, effort and subagent model list the swarm sets for it.
|
||||
//! model, effort, subagent model list and subagent cap the swarm sets for it.
|
||||
//!
|
||||
//! **swarm-controller is the only writer**, and creates the bucket on its
|
||||
//! first write. Every field is optional: an absent field leaves the agent on
|
||||
|
|
@ -46,6 +46,10 @@ pub struct AgentInference {
|
|||
/// The models the agent may start its subagents on.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subagent_models: Option<Vec<String>>,
|
||||
/// How many subagents the agent may have running at once. `None` is no
|
||||
/// cap.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_subagents: Option<u32>,
|
||||
}
|
||||
|
||||
/// A claude `--effort` level, ascending.
|
||||
|
|
@ -196,11 +200,12 @@ mod tests {
|
|||
model: Some("opus".to_owned()),
|
||||
effort: Some(Effort::Xhigh),
|
||||
subagent_models: Some(vec!["haiku".to_owned(), "sonnet".to_owned()]),
|
||||
max_subagents: Some(3),
|
||||
};
|
||||
let encoded = serde_json::to_string(&value).unwrap();
|
||||
assert_eq!(
|
||||
encoded,
|
||||
r#"{"model":"opus","effort":"xhigh","subagent_models":["haiku","sonnet"]}"#
|
||||
r#"{"model":"opus","effort":"xhigh","subagent_models":["haiku","sonnet"],"max_subagents":3}"#
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<AgentInference>(&encoded).unwrap(),
|
||||
|
|
@ -225,6 +230,16 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// A value written before `max_subagents` existed decodes with no cap.
|
||||
#[test]
|
||||
fn a_value_without_max_subagents_has_no_cap() {
|
||||
let decoded: AgentInference =
|
||||
serde_json::from_str(r#"{"model":"opus","effort":"high","subagent_models":["haiku"]}"#)
|
||||
.unwrap();
|
||||
assert_eq!(decoded.max_subagents, None);
|
||||
assert_eq!(decoded.model.as_deref(), Some("opus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_effort_is_stored_as_its_cli_spelling() {
|
||||
for effort in [
|
||||
|
|
|
|||
Loading…
Reference in a new issue