Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: fix the stray doc text, name the agent in the cap-read warning

docs/swarm/inference.md had a table-row suffix pasted before the H1 by a
bad substitution, and the PUT row had lost it; the text is back in the
row.

A failed read of the agent-inference key already applied no cap (an
unreachable swarm must not block subagent work) and warned, but the
warning did not say which agent. It now names the agent and the error,
a test pins that, and subagent.md says a failed read starts anyway.

The max-subagents field stays a text field with a digit pattern: the
review-settings page's numeric field (concurrent reviews) is a TextField
with pattern="[0-9]+" and validation, and no field in swarm-ui uses
type="number". The new field gets the same pattern.
This commit is contained in:
atlas 2026-10-13 19:46:59 +02:00 • committed by mara
commit 461795b2cf
5 changed files with 48 additions and 14 deletions

View file

@ -1,4 +1,4 @@
or a model name with whitespace or a leading `-`, or a `max_subagents` outside 1 to 32 |# Per-agent inference settings # Per-agent inference settings
The swarm can set the model and reasoning effort each agent's turns run on, The swarm can set the model and reasoning effort each agent's turns run on,
plus the models it may start subagents on and how many subagents it may run plus the models it may start subagents on and how many subagents it may run
@ -104,9 +104,9 @@ such as one a newer build wrote, also answers `"configured": null`, with a
warning in its log, until the next `PUT` replaces it. warning in its log, until the next `PUT` replaces it.
| method | path | body | | method | path | body |
| -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/api/agents/<agent>/inference` | —; answers `{"configured": <the stored value, or null>, "effective": <what the agent reported, or null>}` | | `GET` | `/api/agents/<agent>/inference` | —; answers `{"configured": <the stored value, or null>, "effective": <what the agent reported, or null>}` |
| `PUT` | `/api/agents/<agent>/inference` | the whole value, replacing the stored one; the controller refuses unknown fields, an unknown effort, or a model name with whitespace or a leading `-` | | `PUT` | `/api/agents/<agent>/inference` | the whole value, replacing the stored one; the controller refuses unknown fields, an unknown effort, or a model name with whitespace or a leading `-`, or a `max_subagents` outside 1 to 32 |
| `DELETE` | `/api/agents/<agent>/inference` | —; answers 204 | | `DELETE` | `/api/agents/<agent>/inference` | —; answers 204 |
An agent's own queue credential grants it An agent's own queue credential grants it

View file

@ -355,7 +355,7 @@ cap never stops it. Lowering the cap stops nothing already running.
The daemon reads the cap with the model list, on every `start` and The daemon reads the cap with the model list, on every `start` and
`continue`, so a change applies to the next call. Without a swarm value, or `continue`, so a change applies to the next call. Without a swarm value, or
when the read fails, no cap applies. when the read fails, no cap applies. A failed read logs a warning naming the agent and the error, and the start proceeds: an unreachable swarm must not block subagent work.
## Runtime ## Runtime

View file

@ -310,6 +310,7 @@ function InferenceForm({
label="max subagents" label="max subagents"
value={maxSubagents} value={maxSubagents}
placeholder="empty for no cap" placeholder="empty for no cap"
pattern="[0-9]*"
onInput={setMaxSubagents} onInput={setMaxSubagents}
/> />
{formProblem ? ( {formProblem ? (

View file

@ -560,7 +560,11 @@ impl State {
Some(reader) => Some(reader.read().await), Some(reader) => Some(reader.read().await),
None => None, None => None,
}; };
let cap = subagent_cap(read.as_ref()); let agent = self
.models
.as_ref()
.map_or("", crate::swarm_models::Reader::agent);
let cap = subagent_cap(read.as_ref(), agent);
if model.is_some() { if model.is_some() {
check_model(model, &model_list(read, (self.local_models)()))?; check_model(model, &model_list(read, (self.local_models)()))?;
} }
@ -1089,13 +1093,14 @@ fn model_list(read: Option<crate::swarm_models::Read>, local: Option<String>) ->
/// The swarm's `max_subagents` for this agent, or `None`, no cap: when it /// The swarm's `max_subagents` for this agent, or `None`, no cap: when it
/// sets none, when `read` is `None` because this daemon has no swarm queue, /// sets none, when `read` is `None` because this daemon has no swarm queue,
/// or when the read failed, which is logged. /// or when the read failed, which is logged as a warning naming the agent and the error.
fn subagent_cap(read: Option<&crate::swarm_models::Read>) -> Option<u32> { fn subagent_cap(read: Option<&crate::swarm_models::Read>, agent: &str) -> Option<u32> {
use crate::swarm_models::Read; use crate::swarm_models::Read;
match read? { match read? {
Read::Current(settings) => settings.max_subagents, Read::Current(settings) => settings.max_subagents,
Read::Failed(error) => { Read::Failed(error) => {
tracing::warn!( tracing::warn!(
agent,
%error, %error,
"reading this agent's subagent settings from the swarm failed; applying no \ "reading this agent's subagent settings from the swarm failed; applying no \
subagent cap" subagent cap"
@ -4697,11 +4702,29 @@ mod tests {
#[test] #[test]
fn the_cap_is_the_swarm_value_and_none_otherwise() { fn the_cap_is_the_swarm_value_and_none_otherwise() {
assert_eq!(subagent_cap(Some(&capped(3))), Some(3)); assert_eq!(subagent_cap(Some(&capped(3)), "a"), Some(3));
assert_eq!(subagent_cap(Some(&unset())), None); assert_eq!(subagent_cap(Some(&unset()), "a"), None);
assert_eq!(subagent_cap(None), None); assert_eq!(subagent_cap(None, "a"), None);
let failed = crate::swarm_models::Read::Failed("no answer".to_owned()); let failed = crate::swarm_models::Read::Failed("no answer".to_owned());
assert_eq!(subagent_cap(Some(&failed)), None); assert_eq!(subagent_cap(Some(&failed), "a"), None);
}
#[test]
fn a_failed_cap_read_warns_naming_the_agent_and_the_error() {
let logs = CapturedLogs::default();
let subscriber = tracing_subscriber::fmt()
.with_writer(logs.clone())
.with_ansi(false)
.finish();
let failed = crate::swarm_models::Read::Failed("no answer".to_owned());
let cap =
tracing::subscriber::with_default(subscriber, || subagent_cap(Some(&failed), "iris"));
assert_eq!(cap, None);
let logged = logs.text();
assert!(
logged.contains("WARN") && logged.contains("iris") && logged.contains("no answer"),
"{logged}"
);
} }
/// `start` and `continue` read the cap live: the swarm's cap of 1 refuses /// `start` and `continue` read the cap live: the swarm's cap of 1 refuses

View file

@ -105,6 +105,16 @@ impl Reader {
(Self { source }, reads) (Self { source }, reads)
} }
/// The agent whose key is read; `"test"` for a fixed reader.
#[must_use]
pub fn agent(&self) -> &str {
match &self.source {
Source::Queue { agent, .. } => agent,
#[cfg(test)]
Source::Fixed { .. } => "test",
}
}
/// This agent's settings as the swarm holds them now. /// This agent's settings as the swarm holds them now.
pub async fn read(&self) -> Read { pub async fn read(&self) -> Read {
match &self.source { match &self.source {