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:
parent
9a71d1e094
commit
461795b2cf
5 changed files with 48 additions and 14 deletions
|
|
@ -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
|
||||||
|
|
@ -103,11 +103,11 @@ a stored value with no fields. A stored value the controller can't decode,
|
||||||
such as one a newer build wrote, also answers `"configured": null`, with a
|
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
|
||||||
`$JS.API.DIRECT.GET.KV_agent-inference.$KV.agent-inference.<agent>`, the
|
`$JS.API.DIRECT.GET.KV_agent-inference.$KV.agent-inference.<agent>`, the
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue