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,11 +1,13 @@
|
|||
//! Reads the subagent model list the swarm sets for this agent.
|
||||
//! Reads the subagent settings the swarm sets for this agent.
|
||||
//!
|
||||
//! swarm-controller keeps the agent's settings under its key in the
|
||||
//! `agent-inference` KV bucket. Its `subagent_models`, when set, is the list
|
||||
//! a `start` or `continue` naming a `model` is checked against, in place of
|
||||
//! `HIVE_AVAILABLE_MODELS` (`crate::session::check_model`).
|
||||
//! `HIVE_AVAILABLE_MODELS` (`crate::session::check_model`). Its
|
||||
//! `max_subagents`, when set, caps how many subagents run at once
|
||||
//! (`crate::session::State::reserve_under`).
|
||||
//!
|
||||
//! **Read on every such call**, never cached, with the one subject the
|
||||
//! **Read on every `start` and `continue`**, never cached, with the one subject the
|
||||
//! agent's own queue credential is granted in that bucket
|
||||
//! (`swarm_queue_client::agent_inference::read_own`). The connection is made
|
||||
//! the way [`crate::swarm_term`] makes its own: as the agent, never as the
|
||||
|
|
@ -13,14 +15,15 @@
|
|||
//!
|
||||
//! **A subagent start never waits on the queue.** Before the first connect
|
||||
//! succeeds, and when a read fails or outlasts `READ_TIMEOUT`, the read is
|
||||
//! [`Read::Failed`] and the caller checks against `HIVE_AVAILABLE_MODELS`.
|
||||
//! [`Read::Failed`]: the caller checks against `HIVE_AVAILABLE_MODELS` and
|
||||
//! applies no cap.
|
||||
|
||||
#[cfg(test)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use swarm_queue_client::agent_inference::{self, read_own};
|
||||
use swarm_queue_client::agent_inference::{self, AgentInference, read_own};
|
||||
|
||||
use crate::swarm_term::{Target, connect};
|
||||
|
||||
|
|
@ -32,13 +35,12 @@ const RETRY_AFTER: Duration = Duration::from_mins(1);
|
|||
/// misgranted agent's `start` waits.
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
/// What one read of this agent's key said about `subagent_models`.
|
||||
/// What one read of this agent's key found.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Read {
|
||||
/// The swarm sets the list.
|
||||
Set(Vec<String>),
|
||||
/// The key, the field, or the bucket is absent.
|
||||
Unset,
|
||||
/// The settings the swarm holds; an absent key or bucket has every field
|
||||
/// unset.
|
||||
Current(AgentInference),
|
||||
/// Nothing could be read, and why.
|
||||
Failed(String),
|
||||
}
|
||||
|
|
@ -70,7 +72,7 @@ impl Reader {
|
|||
Ok(None) => {
|
||||
tracing::info!(
|
||||
"no swarm queue address or no secret store for this agent; subagent models \
|
||||
are checked against HIVE_AVAILABLE_MODELS only"
|
||||
are checked against HIVE_AVAILABLE_MODELS only, and no subagent cap applies"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
|
@ -78,7 +80,7 @@ impl Reader {
|
|||
tracing::warn!(
|
||||
error = format!("{e:#}"),
|
||||
"this agent's secret store coordinates are incomplete; subagent models are \
|
||||
checked against HIVE_AVAILABLE_MODELS only"
|
||||
checked against HIVE_AVAILABLE_MODELS only, and no subagent cap applies"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
|
@ -103,7 +105,7 @@ impl Reader {
|
|||
(Self { source }, reads)
|
||||
}
|
||||
|
||||
/// This agent's `subagent_models` as the swarm holds it now.
|
||||
/// This agent's settings as the swarm holds them now.
|
||||
pub async fn read(&self) -> Read {
|
||||
match &self.source {
|
||||
Source::Queue { agent, client } => read_queue(agent, client).await,
|
||||
|
|
@ -128,12 +130,10 @@ async fn read_queue(agent: &str, client: &OnceLock<async_nats::Client>) -> Read
|
|||
Err(_) => Read::Failed(format!("no answer within {READ_TIMEOUT:?}")),
|
||||
Ok(Err(e)) => Read::Failed(e.to_string()),
|
||||
Ok(Ok(agent_inference::Read::NoBucket)) => {
|
||||
tracing::debug!("subagent models: the agent-inference bucket does not answer");
|
||||
Read::Unset
|
||||
}
|
||||
Ok(Ok(agent_inference::Read::Current(current))) => {
|
||||
current.subagent_models.map_or(Read::Unset, Read::Set)
|
||||
tracing::debug!("subagent settings: the agent-inference bucket does not answer");
|
||||
Read::Current(AgentInference::default())
|
||||
}
|
||||
Ok(Ok(agent_inference::Read::Current(current))) => Read::Current(current),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ async fn connect_until_up(target: Arc<Target>, client: Arc<OnceLock<async_nats::
|
|||
loop {
|
||||
match connect(&target).await {
|
||||
Ok(connected) => {
|
||||
tracing::info!(url = %target.url, "subagent models: connected as this agent");
|
||||
tracing::info!(url = %target.url, "subagent settings: connected as this agent");
|
||||
let _already_set = client.set(connected);
|
||||
return;
|
||||
}
|
||||
|
|
@ -150,8 +150,8 @@ async fn connect_until_up(target: Arc<Target>, client: Arc<OnceLock<async_nats::
|
|||
tracing::warn!(
|
||||
error = format!("{e:#}"),
|
||||
retry_in = ?RETRY_AFTER,
|
||||
"subagent models: connecting with this agent's own queue credential failed; \
|
||||
HIVE_AVAILABLE_MODELS applies until a connect succeeds"
|
||||
"subagent settings: connecting with this agent's own queue credential failed; \
|
||||
HIVE_AVAILABLE_MODELS and no subagent cap apply until a connect succeeds"
|
||||
);
|
||||
tokio::time::sleep(RETRY_AFTER).await;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue