Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: GET inference answers configured: null without a key

`GET /api/agents/{name}/inference` answered `"configured": {}` both for
an agent with no key in `agent-inference` and for a stored value with no
fields, so a reader could not tell "the swarm sets nothing" from "a
setting with every field unset". It now reads the key as
`Option<AgentInference>`: an absent or deleted key is `null`, a stored
`{}` stays `{}`. The shared `schedules::read` helper is unchanged.

`swarmctl agent inference get` prints "no swarm setting; the agent runs
on its own model and effort" for `null`; `set` merges into an empty
value in that case, as before.

The agent itself still treats no key and `{}` the same.

Refs #5034
This commit is contained in:
atlas 2026-10-10 00:32:36 +02:00 • committed by mara
commit 7364ccdc98
3 changed files with 58 additions and 14 deletions

View file

@ -106,8 +106,9 @@ impl InferenceSettings {
/// What `GET` answers for one agent.
#[derive(Debug, Serialize, ToSchema)]
pub struct AgentInferenceView {
/// The settings the swarm sets; `{}` when none is set.
configured: InferenceSettings,
/// The settings the swarm sets; `null` when it has none for the agent (no
/// key, or a cleared one), `{}` for a stored setting with no fields.
configured: Option<InferenceSettings>,
/// What the agent reported running on in its latest turn-state header;
/// `null` when this daemon has received none with these fields since it
/// started.
@ -241,11 +242,13 @@ pub(crate) async fn get_agent_inference(
let agent = agent_name(&name).map_err(refused)?;
let client = queue(&state).map_err(refused)?;
let store = open(&client).await?;
let (current, _) = read::<AgentInference>(&store, &agent)
// `Option`'s default is the `None` an absent or deleted key reads as; a
// stored `{}` decodes to `Some`.
let (current, _) = read::<Option<AgentInference>>(&store, &agent)
.await
.map_err(|e| queue_problem(&e))?;
Ok(Json(AgentInferenceView {
configured: current.into(),
configured: current.map(Into::into),
effective: state
.reported_inference
.as_ref()
@ -444,7 +447,7 @@ mod tests {
fn the_view_answers_both_halves() {
let view = |effective: Option<Effective>| {
serde_json::to_value(AgentInferenceView {
configured: InferenceSettings::default(),
configured: Some(InferenceSettings::default()),
effective: effective.map(Into::into),
})
.unwrap()
@ -453,6 +456,14 @@ mod tests {
view(None),
serde_json::json!({"configured": {}, "effective": null})
);
let unset = AgentInferenceView {
configured: None,
effective: None,
};
assert_eq!(
serde_json::to_value(unset).unwrap(),
serde_json::json!({"configured": null, "effective": null})
);
let reported = Reported::default();
reported.record(&iris(), NEW_HEADER);
assert_eq!(
@ -484,4 +495,15 @@ mod tests {
let json = r#"{"model":"claude-opus-4-8","subagent_models":["claude-haiku-4-5"]}"#;
assert!(settings(json).unwrap().into_stored().is_ok());
}
/// What `get_agent_inference` reads with: an absent or deleted key is
/// `Option`'s default, `None`; a stored `{}` is a setting with no fields.
#[test]
fn no_key_and_an_empty_setting_read_apart() {
assert_eq!(Option::<AgentInference>::default(), None);
assert_eq!(
serde_json::from_slice::<Option<AgentInference>>(b"{}").unwrap(),
Some(AgentInference::default())
);
}
}