swarm UI: an agent that needs a Claude login shows it on its card
The agent's turn-state header ($SWARM.agent-state.<agent>) gains `needs_login`, set by hive-agent from its own LoginState, so the flag reaches the swarm without hive-c0re (mara on #5268: "id prefer the agent container sending the info it needs login without hive"). - swarm-queue-client: `agent_login::Login`, the shared wire type, beside `agent_inference::Effective`. `needs_login: Option<bool>` with `#[serde(default)]`: a header from an older agent decodes as `None` (unknown), never as "needs login" or "logged in". - hive-agent: the header publisher reads the shared LoginState cell. Every writer of that cell emits a status on the bus afterwards, so a login change goes out on the next rebuild, not the 60 s refresh. - swarm-controller: `agent_login::Reported` follows every agent's header and keeps the latest flag; `GET /api/agents/status` merges it into each row as `needs_login` (null when unknown). - swarm-ui: AgentCard shows a warning-tone "login needed" badge when the row's `needs_login` is true. Refs #5268
This commit is contained in:
parent
c335bc0962
commit
afeb0f9794
19 changed files with 365 additions and 15 deletions
53
swarm-queue-client/src/agent_login.rs
Normal file
53
swarm-queue-client/src/agent_login.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! Whether an agent's claude is waiting for a login, as the agent itself
|
||||
//! reports it.
|
||||
//!
|
||||
//! Part of the turn-state header the agent publishes on
|
||||
//! [`crate::agent_state_subject`], beside [`crate::agent_inference::Effective`];
|
||||
//! swarm-controller decodes it out of that header with this same type.
|
||||
|
||||
/// The login fields of an agent's turn-state header.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Login {
|
||||
/// `true` while the agent has no claude session and its turn loop waits
|
||||
/// for one, including while a login is in progress. `None` from an agent
|
||||
/// whose header has no such field, which says nothing about its login.
|
||||
#[serde(default)]
|
||||
pub needs_login: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Login;
|
||||
|
||||
#[test]
|
||||
fn a_header_without_the_field_decodes_as_unknown() {
|
||||
let header = serde_json::json!({
|
||||
"turn_state": "idle",
|
||||
"turn_state_since": "2026-09-13T12:35:03Z",
|
||||
"agent_state": "up",
|
||||
"model": "sonnet",
|
||||
"effort": "high",
|
||||
"model_source": "default",
|
||||
"effort_source": "default",
|
||||
"resolved_model": null,
|
||||
"context_window_tokens": 200_000,
|
||||
"ctx_usage": null,
|
||||
"cost_usage": null,
|
||||
});
|
||||
let login: Login = serde_json::from_value(header).expect("an old header decodes");
|
||||
assert_eq!(login.needs_login, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_flag_round_trips_both_ways() {
|
||||
for needs_login in [true, false] {
|
||||
let sent = Login {
|
||||
needs_login: Some(needs_login),
|
||||
};
|
||||
let value = serde_json::to_value(sent).expect("serialises");
|
||||
assert_eq!(value["needs_login"], serde_json::json!(needs_login));
|
||||
let decoded: Login = serde_json::from_value(value).expect("decodes");
|
||||
assert_eq!(decoded, sent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,6 +221,10 @@ pub mod agent_inference;
|
|||
/// writing needs `kv` and reading needs `agent-limits`.
|
||||
pub mod agent_limits;
|
||||
|
||||
/// The login flag in an agent's turn-state header. No bucket: it travels
|
||||
/// on [`agent_state_subject`] only.
|
||||
pub mod agent_login;
|
||||
|
||||
#[cfg(feature = "kv")]
|
||||
mod kv;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue