refactor(#2464): rename hive-ag3nt crate to hive-agent, collapse lib into main

This commit is contained in:
damocles 2026-07-15 01:18:22 +02:00 committed by mara
commit 3f1643c594
57 changed files with 101 additions and 130 deletions

View file

@ -0,0 +1,132 @@
//! Stats + loose-ends + bash-tasks read endpoints.
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use super::{AppState, error_response};
#[derive(Deserialize)]
pub(super) struct StatsQuery {
window: Option<String>,
}
pub(super) async fn api_stats(
State(state): State<AppState>,
axum::extract::Query(q): axum::extract::Query<StatsQuery>,
) -> axum::Json<crate::stats::Snapshot> {
let window = crate::stats::Window::parse(q.window.as_deref().unwrap_or("24h"));
let mut snapshot = crate::stats::snapshot_default(window);
// Pass the window span to the reminder-stats RPC so the broker
// filters its counts to the same time range as the chart data.
let window_secs = window.span_secs();
let window_secs_u = u64::try_from(window_secs).unwrap_or(0);
snapshot.reminder_stats = fetch_reminder_stats(&state.socket, window_secs_u).await;
axum::Json(snapshot)
}
/// Fetch reminder activity stats from the broker via the per-agent / manager
/// socket. Returns None on any transport / decode failure — the stats are
/// decorative, not authoritative.
async fn fetch_reminder_stats(
socket: &std::path::Path,
window_secs: u64,
) -> Option<hive_sh4re::ReminderStats> {
match super::broker_request(
socket,
&hive_sh4re::Request::ReminderRollup {
since_secs: window_secs,
agent: None,
},
)
.await
{
Ok(hive_sh4re::Response::ReminderRollup(stats)) => Some(stats),
_ => None,
}
}
/// Proxy this agent's loose-ends list via the per-agent socket. The
/// web UI surfaces the result as a collapsible section in the page
/// so the operator can see at a glance what's pending against the
/// agent (questions asked by it, peer questions targeting it,
/// reminders it scheduled, approvals for the manager). Same data
/// the `mcp__hyperhive__get_loose_ends` tool sees from inside the
/// container.
pub(super) async fn api_loose_ends(State(state): State<AppState>) -> Response {
match super::broker_request(
&state.socket,
&hive_sh4re::Request::GetLooseEnds { agent: None },
)
.await
{
Ok(hive_sh4re::Response::LooseEnds { loose_ends }) => {
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
}
Ok(hive_sh4re::Response::Err { message }) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("get_loose_ends: {message}"),
),
Ok(other) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("get_loose_ends: unexpected response: {other:?}"),
),
Err(e) => super::broker_error_response(&e, "get_loose_ends"),
}
}
/// `GET /api/bash-tasks` — snapshot of this agent's in-flight bash tasks.
///
/// The `hive-bash-mcp` daemon runs in this same container and writes one
/// `<id>.json` ([`hive_sh4re::TaskFile`]) per task under the harness
/// `bash-tasks/` dir. This reads that dir and returns the tasks still
/// `Pending` or `Running`, so the agent page can show what's running without
/// going through the broker. Snapshot only — the page polls/refreshes it like
/// `/api/loose-ends`; there's no live SSE push for task state yet. Unreadable
/// or malformed files (incl. the daemon's `.json.tmp` scratch writes, which
/// don't match the `.json` extension) are skipped so one stray file can't
/// fail the whole list.
pub(super) async fn api_bash_tasks() -> Response {
let dir = crate::paths::harness_dir().join("bash-tasks");
// The dir scan + per-file reads are blocking fs I/O; run them off the
// async executor so a slow or large tasks dir can't stall other requests.
let tasks = tokio::task::spawn_blocking(move || {
let mut tasks: Vec<hive_sh4re::TaskFile> = Vec::new();
let Ok(rd) = std::fs::read_dir(&dir) else {
return tasks;
};
for entry in rd.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(task) = serde_json::from_str::<hive_sh4re::TaskFile>(&text) else {
continue;
};
if matches!(
task.status,
hive_sh4re::TaskStatus::Pending | hive_sh4re::TaskStatus::Running
) {
tasks.push(task);
}
}
// Running before Pending, then oldest-first so a long-runner sits on top.
tasks.sort_by(|a, b| {
let rank = |s: &hive_sh4re::TaskStatus| match s {
hive_sh4re::TaskStatus::Running => 0,
_ => 1,
};
rank(&a.status)
.cmp(&rank(&b.status))
.then(a.created_at.cmp(&b.created_at))
});
tasks
})
.await
.unwrap_or_default();
axum::Json(serde_json::json!({ "tasks": tasks })).into_response()
}