Watch
0
0
Fork
You've already forked hyperhive
0

swarm: show each agent's effective model and effort, and where they came from

Each agent now reports the model and effort it runs on, and where each
comes from (`swarm`, `agent_nix`, `local_file` or `default`), in its
turn-state header on `$SWARM.agent-state.<agent>`: `effort`,
`model_source` and `effort_source` join `model`. The shared
`swarm_queue_client::agent_inference::{Effective, Source}` types spell
those fields for both ends.

The Bus keeps a source next to the model and effort. `Bus::new` takes it
from the local fallback chain (`harness_state::local_model` /
`local_effort`, which now return the source with the value), a `/model`
or `/effort` pick sets `local_file`, and the swarm poller sets `swarm`
or, on a clear, the local chain's source. A swarm value equal to the one
the agent already runs is now applied once, so the source moves to
`swarm` and the header is republished; before, `next()` skipped it as
unchanged.

swarm-controller subscribes to every agent's header with the reader
grant it already holds for the state relay, and keeps the latest
inference fields per agent in memory (`inference::Reported`).
`GET /api/agents/{name}/inference` now answers
`{configured, effective}`, `effective` being `null` until the agent's
first header with these fields since the controller started.
`swarmctl agent inference get` prints both; `set` reads `configured`.

Refs #5034
This commit is contained in:
atlas 2026-10-10 00:00:25 +02:00 • committed by mara
commit d723fac0be
20 changed files with 655 additions and 113 deletions

View file

@ -6,6 +6,8 @@
use std::path::PathBuf;
use swarm_queue_client::agent_inference::Source;
/// Path to the persisted model file. Overridable via `HYPERHIVE_MODEL_FILE`
/// for dev / tests; otherwise derived from the agent's harness dir.
fn model_file_path() -> PathBuf {
@ -53,25 +55,37 @@ pub(crate) fn load_effort() -> Option<String> {
}
}
/// The model this agent runs when the swarm sets none. Priority:
/// `configured` (`HIVE_DEFAULT_MODEL`, from `services.hyperhive.agent.model`
/// in agent.nix) > the persisted runtime override > [`DEFAULT_MODEL`].
pub(crate) fn local_model(configured: Option<&str>) -> String {
configured.map_or_else(
|| load_model().unwrap_or_else(|| DEFAULT_MODEL.to_owned()),
str::to_owned,
)
/// The model this agent runs when the swarm sets none, and where it comes
/// from. Priority: `configured` (`HIVE_DEFAULT_MODEL`, from
/// `services.hyperhive.agent.model` in agent.nix) > the persisted runtime
/// override > [`DEFAULT_MODEL`].
pub(crate) fn local_model(configured: Option<&str>) -> (String, Source) {
model_from(configured, load_model())
}
/// The effort this agent runs at when the swarm sets none. The inverse of
/// [`local_model`]: a persisted operator pick wins over `configured`
/// (`HIVE_DEFAULT_EFFORT`), so the runtime choice survives a harness restart
/// (it only resets on `--purge`); nix is the default for a never-touched
/// agent.
pub(crate) fn local_effort(configured: Option<&str>) -> String {
load_effort()
.or_else(|| configured.map(str::to_owned))
.unwrap_or_else(|| DEFAULT_EFFORT.to_owned())
fn model_from(configured: Option<&str>, file: Option<String>) -> (String, Source) {
match (configured, file) {
(Some(nix), _) => (nix.to_owned(), Source::AgentNix),
(None, Some(file)) => (file, Source::LocalFile),
(None, None) => (DEFAULT_MODEL.to_owned(), Source::Default),
}
}
/// The effort this agent runs at when the swarm sets none, and where it
/// comes from. The inverse of [`local_model`]: a persisted operator pick
/// wins over `configured` (`HIVE_DEFAULT_EFFORT`), so the runtime choice
/// survives a harness restart (it only resets on `--purge`); nix is the
/// default for a never-touched agent.
pub(crate) fn local_effort(configured: Option<&str>) -> (String, Source) {
effort_from(configured, load_effort())
}
fn effort_from(configured: Option<&str>, file: Option<String>) -> (String, Source) {
match (file, configured) {
(Some(file), _) => (file, Source::LocalFile),
(None, Some(nix)) => (nix.to_owned(), Source::AgentNix),
(None, None) => (DEFAULT_EFFORT.to_owned(), Source::Default),
}
}
pub(crate) fn persist_effort(level: &str) -> std::io::Result<()> {
@ -291,7 +305,10 @@ pub fn context_window_tokens(model: &str) -> u64 {
#[cfg(test)]
mod tests {
use super::{DEFAULT_EFFORT, EFFORT_LEVELS, is_valid_effort};
use super::{
DEFAULT_EFFORT, DEFAULT_MODEL, EFFORT_LEVELS, Source, effort_from, is_valid_effort,
model_from,
};
#[test]
fn effort_validation_accepts_only_known_levels() {
@ -312,4 +329,38 @@ mod tests {
assert!(EFFORT_LEVELS.contains(&DEFAULT_EFFORT));
assert_eq!(DEFAULT_EFFORT, "medium");
}
fn pick(value: &str, source: Source) -> (String, Source) {
(value.to_owned(), source)
}
/// agent.nix wins over the file for the model.
#[test]
fn each_model_source() {
let file = || Some("sonnet".to_owned());
assert_eq!(
model_from(Some("opus"), file()),
pick("opus", Source::AgentNix)
);
assert_eq!(model_from(None, file()), pick("sonnet", Source::LocalFile));
assert_eq!(model_from(None, None), pick(DEFAULT_MODEL, Source::Default));
}
/// The file wins over agent.nix for the effort.
#[test]
fn each_effort_source() {
let file = || Some("low".to_owned());
assert_eq!(
effort_from(Some("high"), file()),
pick("low", Source::LocalFile)
);
assert_eq!(
effort_from(Some("high"), None),
pick("high", Source::AgentNix)
);
assert_eq!(
effort_from(None, None),
pick(DEFAULT_EFFORT, Source::Default)
);
}
}