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
|
|
@ -6,8 +6,8 @@
|
|||
//! seconds, and its next turn runs on it. A field the swarm does not set
|
||||
//! leaves the agent on its own value.
|
||||
//!
|
||||
//! The shape below mirrors the controller's `InferenceSettings`, for the
|
||||
//! reason [`crate::agent`]'s module doc gives.
|
||||
//! The shapes below mirror the controller's `InferenceSettings` and
|
||||
//! `AgentInferenceView`, for the reason [`crate::agent`]'s module doc gives.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -27,6 +27,22 @@ struct Settings {
|
|||
subagent_models: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// What `GET` answers: the settings, and what the agent reported running on.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct View {
|
||||
configured: Settings,
|
||||
effective: Option<Effective>,
|
||||
}
|
||||
|
||||
/// The model and effort an agent runs on, and where each comes from.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Effective {
|
||||
model: String,
|
||||
effort: String,
|
||||
model_source: String,
|
||||
effort_source: String,
|
||||
}
|
||||
|
||||
/// What `agent inference set` was asked to change.
|
||||
pub(crate) struct SetFlags {
|
||||
pub model: Option<String>,
|
||||
|
|
@ -65,14 +81,18 @@ fn member(agent: &str) -> Result<String> {
|
|||
/// Run `swarmctl agent inference get`.
|
||||
pub(crate) fn get(socket: &Path, agent: &str) -> Result<()> {
|
||||
let uri = member(agent)?;
|
||||
let current: Settings = runtime()?.block_on(send(
|
||||
let view: View = runtime()?.block_on(send(
|
||||
socket,
|
||||
hyper::Method::GET,
|
||||
&uri,
|
||||
None::<&()>,
|
||||
"inference-get",
|
||||
))?;
|
||||
print!("{}", render(¤t));
|
||||
print!(
|
||||
"{}{}",
|
||||
render(&view.configured),
|
||||
render_effective(view.effective.as_ref())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -81,14 +101,14 @@ pub(crate) fn get(socket: &Path, agent: &str) -> Result<()> {
|
|||
pub(crate) fn set(socket: &Path, agent: &str, flags: SetFlags) -> Result<()> {
|
||||
let uri = member(agent)?;
|
||||
let rt = runtime()?;
|
||||
let current: Settings = rt.block_on(send(
|
||||
let current: View = rt.block_on(send(
|
||||
socket,
|
||||
hyper::Method::GET,
|
||||
&uri,
|
||||
None::<&()>,
|
||||
"inference-get",
|
||||
))?;
|
||||
let wanted = flags.merge(current)?;
|
||||
let wanted = flags.merge(current.configured)?;
|
||||
let stored: Settings = rt.block_on(send(
|
||||
socket,
|
||||
hyper::Method::PUT,
|
||||
|
|
@ -127,6 +147,23 @@ fn render(s: &Settings) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// What the agent runs on, each value with where it comes from.
|
||||
fn render_effective(e: Option<&Effective>) -> String {
|
||||
e.map_or_else(
|
||||
|| {
|
||||
"running: not reported since swarm-controller started; the agent reports on its \
|
||||
next change\n"
|
||||
.to_owned()
|
||||
},
|
||||
|e| {
|
||||
format!(
|
||||
"running model: {} ({})\nrunning effort: {} ({})\n",
|
||||
e.model, e.model_source, e.effort, e.effort_source
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -169,6 +206,22 @@ mod tests {
|
|||
assert!(flags().merge(Settings::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_shows_the_running_values_with_their_sources() {
|
||||
let view: View = serde_json::from_str(
|
||||
r#"{"configured":{"model":"opus"},"effective":{"model":"opus","effort":"medium",
|
||||
"model_source":"swarm","effort_source":"local_file"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(view.configured.model.as_deref(), Some("opus"));
|
||||
assert_eq!(
|
||||
render_effective(view.effective.as_ref()),
|
||||
"running model: opus (swarm)\nrunning effort: medium (local_file)\n"
|
||||
);
|
||||
let unknown: View = serde_json::from_str(r#"{"configured":{},"effective":null}"#).unwrap();
|
||||
assert!(render_effective(unknown.effective.as_ref()).starts_with("running: not reported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_value_renders_every_field_as_unset() {
|
||||
let row = render(&serde_json::from_str("{}").unwrap());
|
||||
|
|
|
|||
|
|
@ -227,7 +227,8 @@ enum AgentVerb {
|
|||
#[derive(Subcommand)]
|
||||
enum InferenceVerb {
|
||||
/// Show what the swarm sets for an agent, naming the fields it doesn't
|
||||
/// set.
|
||||
/// set, and the model and effort the agent runs on, each with where it
|
||||
/// comes from.
|
||||
Get(InferenceAgentArgs),
|
||||
/// Set fields for an agent. Only the flags given change; the others keep
|
||||
/// what's stored.
|
||||
|
|
|
|||
Loading…
Reference in a new issue