Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: show how recent each subagent is in the details panel

The subagent list endpoint now returns {name, last_row_unix} per
subagent, the publish time of its newest terminal row, read from the
queue controller-side so agents publish nothing new. The panel sorts
newest first, shows a ticking age beside each name, and dims one quiet
for 15 minutes.

Closes #4928
This commit is contained in:
iris 2026-10-04 16:21:12 +02:00 • committed by mara
commit 84c89de71c
4 changed files with 124 additions and 30 deletions

View file

@ -8,6 +8,8 @@
//! `swarm_queue_client::subagent_term` spells. Subagents are spawned on demand
//! under caller-chosen names, so the list is the set of subjects that stream
//! holds rows for, read with `STREAM.INFO`. A missing stream lists nothing.
//! Each subagent carries the stream's own publish time of its newest row, so
//! a reader can tell a subagent that is running from one that has gone quiet.
//!
//! **No hive lookup.** Unlike [`crate::term_stream`], there is no
//! hive-scoped subject to follow: only the agent's own credential is granted
@ -28,7 +30,9 @@ use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, KeepAlive, Sse};
use futures_util::{Stream, StreamExt as _, TryStreamExt as _};
use serde::Serialize;
use swarm_queue_client::subagent_term;
use utoipa::ToSchema;
use crate::AppState;
use crate::wanted::WantedWriter;
@ -84,14 +88,25 @@ async fn live_agents(wanted: &WantedWriter, hives: &[String]) -> BTreeSet<String
crate::agent_renewal::live_agents(&declarations)
}
/// One subagent of an agent, as the list endpoint reports it.
#[derive(Debug, PartialEq, Eq, Serialize, ToSchema)]
pub(crate) struct SubagentEntry {
/// The subagent's name, as it appears in its stream subject.
pub name: String,
/// When its newest row in the stream was published, unix seconds, by the
/// stream's own clock. `None` when that row could not be read.
pub last_row_unix: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/agents/{name}/subagents",
params(("name" = String, Path, description = "agent whose subagents to list")),
responses(
(status = 200, description = "names of the agent's subagents with terminal rows \
in the swarm queue, sorted; empty when the agent has published none",
body = Vec<String>),
(status = 200, description = "the agent's subagents with terminal rows in the \
swarm queue, sorted by name, each with the publish time of its newest row; \
empty when the agent has published none",
body = Vec<SubagentEntry>),
(status = 400, description = "the agent name is not shaped like an identifier \
(problem+json)", body = String),
(status = 503, description = "no swarm queue is configured on this host, or the \
@ -104,7 +119,7 @@ async fn live_agents(wanted: &WantedWriter, hives: &[String]) -> BTreeSet<String
pub(crate) async fn list_subagents(
State(state): State<AppState>,
Path(agent): Path<String>,
) -> Result<Json<Vec<String>>, problem_details::ProblemDetails> {
) -> Result<Json<Vec<SubagentEntry>>, problem_details::ProblemDetails> {
let agent = ident(&agent).map_err(bad_request)?;
let client = connected_client(&state).map_err(|e| unavailable(&e))?;
let js = async_nats::jetstream::new(client);
@ -121,10 +136,41 @@ pub(crate) async fn list_subagents(
.try_collect()
.await
.map_err(|e| read_failed(&agent, &e))?;
Ok(Json(subagent_term::subagent_names(
&agent,
subjects.iter().map(|(s, _)| s.as_str()),
)))
let names = subagent_term::subagent_names(&agent, subjects.iter().map(|(s, _)| s.as_str()));
let mut entries = Vec::with_capacity(names.len());
for name in names {
let last_row_unix = last_row_unix(&stream, &agent, &name).await;
entries.push(SubagentEntry {
name,
last_row_unix,
});
}
Ok(Json(entries))
}
/// The publish time of a subagent's newest row. A row that cannot be read,
/// aged out between the listing and this lookup say, is a missing time and
/// not a failed list.
async fn last_row_unix(
stream: &async_nats::jetstream::stream::Stream,
agent: &str,
subagent: &str,
) -> Option<i64> {
match stream
.get_last_raw_message_by_subject(&subagent_term::subject(agent, subagent))
.await
{
Ok(row) => Some(row.time.unix_timestamp()),
Err(e) => {
tracing::debug!(
%agent,
%subagent,
error = %swarm_queue_client::chain(&e),
"listing subagents: reading the newest row failed"
);
None
}
}
}
#[utoipa::path(