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:
parent
6982b2ec14
commit
d723fac0be
20 changed files with 655 additions and 113 deletions
|
|
@ -16,6 +16,7 @@ use chrono::Utc;
|
|||
use hive_claude::TokenUsage;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use swarm_queue_client::agent_inference::Source;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::harness_state::{
|
||||
|
|
@ -285,6 +286,10 @@ pub struct Bus {
|
|||
/// operator can override at runtime via `POST /api/model`.
|
||||
model: Arc<Mutex<String>>,
|
||||
effort: Arc<Mutex<String>>,
|
||||
/// Where `model` and `effort` come from, as the swarm is told in this
|
||||
/// agent's turn-state header. Set with them.
|
||||
model_source: Arc<Mutex<Source>>,
|
||||
effort_source: Arc<Mutex<Source>>,
|
||||
/// Last-inference token usage from the most recent turn's final
|
||||
/// `assistant` event. Represents the actual context window size at
|
||||
/// turn-end — the number the operator watches to decide whether to
|
||||
|
|
@ -393,8 +398,8 @@ impl Bus {
|
|||
}
|
||||
};
|
||||
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
|
||||
let initial_model = local_model(configured_model());
|
||||
let initial_effort = local_effort(configured_effort());
|
||||
let (initial_model, model_source) = local_model(configured_model());
|
||||
let (initial_effort, effort_source) = local_effort(configured_effort());
|
||||
// Restore rate_limited (and needs_login) from the consolidated
|
||||
// harness state file so the dashboard shows the correct status
|
||||
// on cold load if the harness crashed while parked.
|
||||
|
|
@ -411,6 +416,8 @@ impl Bus {
|
|||
state: Arc::new(Mutex::new((TurnState::Idle, Utc::now().timestamp()))),
|
||||
model: Arc::new(Mutex::new(initial_model)),
|
||||
effort: Arc::new(Mutex::new(initial_effort)),
|
||||
model_source: Arc::new(Mutex::new(model_source)),
|
||||
effort_source: Arc::new(Mutex::new(effort_source)),
|
||||
last_ctx_usage: Arc::new(Mutex::new(None)),
|
||||
last_cost_usage: Arc::new(Mutex::new(None)),
|
||||
rate_limited: Arc::new(AtomicBool::new(was_rate_limited)),
|
||||
|
|
@ -579,18 +586,20 @@ impl Bus {
|
|||
if let Err(e) = persist_model(&value) {
|
||||
tracing::warn!(error = ?e, "model: persist failed");
|
||||
}
|
||||
self.apply_model(value);
|
||||
self.apply_model(value, Source::LocalFile);
|
||||
}
|
||||
|
||||
/// Switch the model for future turns without persisting it, so
|
||||
/// `hyperhive-model` keeps the agent's local value. For a value the
|
||||
/// swarm sets (`crate::swarm_inference`).
|
||||
/// swarm sets (`crate::swarm_inference`). Emits `ModelChanged` even when
|
||||
/// only `source` changed.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the internal lock is poisoned.
|
||||
pub fn apply_model(&self, value: String) {
|
||||
pub fn apply_model(&self, value: String, source: Source) {
|
||||
self.model.lock().unwrap().clone_from(&value);
|
||||
*self.model_source.lock().unwrap() = source;
|
||||
// Mirror the resolved model into hyperhive-harness.json so
|
||||
// hive-c0re can surface it on the dashboard badge without reading
|
||||
// the hyperhive-model override file directly.
|
||||
|
|
@ -624,21 +633,43 @@ impl Bus {
|
|||
if let Err(e) = persist_effort(&value) {
|
||||
tracing::warn!(error = ?e, "effort: persist failed");
|
||||
}
|
||||
self.apply_effort(value);
|
||||
self.apply_effort(value, Source::LocalFile);
|
||||
}
|
||||
|
||||
/// Switch the effort level without persisting it, so `hyperhive-effort`
|
||||
/// keeps the agent's local value. For a value the swarm sets
|
||||
/// (`crate::swarm_inference`).
|
||||
/// (`crate::swarm_inference`). Emits `EffortChanged` even when only
|
||||
/// `source` changed.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the internal lock is poisoned.
|
||||
pub fn apply_effort(&self, value: String) {
|
||||
pub fn apply_effort(&self, value: String, source: Source) {
|
||||
self.effort.lock().unwrap().clone_from(&value);
|
||||
*self.effort_source.lock().unwrap() = source;
|
||||
self.emit(LiveEvent::EffortChanged { effort: value });
|
||||
}
|
||||
|
||||
/// Where [`Self::model`] comes from.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the internal lock is poisoned.
|
||||
#[must_use]
|
||||
pub fn model_source(&self) -> Source {
|
||||
*self.model_source.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Where [`Self::effort`] comes from.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the internal lock is poisoned.
|
||||
#[must_use]
|
||||
pub fn effort_source(&self) -> Source {
|
||||
*self.effort_source.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Seed `last_ctx_usage` + `last_cost_usage` at startup without
|
||||
/// emitting a SSE event. Used by the bin entrypoints to backfill
|
||||
/// from the most recent `turn_stats` row so the per-agent web UI's
|
||||
|
|
|
|||
Loading…
Reference in a new issue