feat: add set_status MCP tool and status field to whoami/dashboard (closes #325)
This commit is contained in:
parent
6f3b56ad84
commit
fe2933b213
8 changed files with 170 additions and 16 deletions
|
|
@ -727,6 +727,14 @@
|
|||
}
|
||||
body.append(head);
|
||||
|
||||
// ── agent status text ─────────────────────────────────────────
|
||||
if (c.status_text) {
|
||||
body.append(el('div', { class: 'agent-status', title: 'agent self-reported status' },
|
||||
el('span', { class: 'status-icon' }, '◈ '),
|
||||
c.status_text,
|
||||
));
|
||||
}
|
||||
|
||||
// ── action buttons ───────────────────────────────────────────
|
||||
const actions = el('div', { class: 'actions' });
|
||||
if (c.running) {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,16 @@ a:hover {
|
|||
color: var(--red); border-color: var(--red);
|
||||
text-shadow: 0 0 6px rgba(243, 139, 168, 0.5);
|
||||
}
|
||||
.agent-status {
|
||||
font-size: 0.82em;
|
||||
color: var(--subtext0, #a6adc8);
|
||||
padding: 0.1em 0.3em 0.25em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.agent-status .status-icon { opacity: 0.65; }
|
||||
|
||||
.container-row.tombstone {
|
||||
border-style: dashed;
|
||||
background: rgba(24, 24, 37, 0.35);
|
||||
|
|
|
|||
|
|
@ -220,11 +220,39 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
},
|
||||
}
|
||||
}
|
||||
AgentRequest::Whoami => AgentResponse::Whoami {
|
||||
name: agent.to_owned(),
|
||||
role: "agent".to_owned(),
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
},
|
||||
AgentRequest::Whoami => {
|
||||
let status_text = crate::container_view::read_agent_status_text(agent);
|
||||
AgentResponse::Whoami {
|
||||
name: agent.to_owned(),
|
||||
role: "agent".to_owned(),
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
status_text,
|
||||
}
|
||||
}
|
||||
AgentRequest::SetStatus { text } => {
|
||||
let path = crate::coordinator::Coordinator::agent_notes_dir(agent)
|
||||
.join("hyperhive-status");
|
||||
let result = if text.trim().is_empty() {
|
||||
// Empty = clear: remove the file (ignore missing).
|
||||
std::fs::remove_file(&path)
|
||||
.or_else(|e| if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
})
|
||||
} else {
|
||||
std::fs::write(&path, format!("{}\n", text.trim()))
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
// Kick a container rescan so the dashboard updates live.
|
||||
let coord2 = Arc::clone(coord);
|
||||
tokio::spawn(async move { coord2.rescan_containers_and_emit().await });
|
||||
AgentResponse::Ok
|
||||
}
|
||||
Err(e) => AgentResponse::Err { message: format!("set_status write failed: {e}") },
|
||||
}
|
||||
}
|
||||
AgentRequest::CancelLooseEnd { kind, id } => crate::questions::handle_cancel_loose_end(
|
||||
coord, agent, *kind, *id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ pub struct ContainerView {
|
|||
/// the file is absent or the agent declares no links.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub extra_links: Vec<DashboardLink>,
|
||||
/// Free-text status set by the agent via `mcp__hyperhive__set_status`.
|
||||
/// Persisted to `{state_dir}/hyperhive-status`. `None` when the file
|
||||
/// is absent or empty — the agent hasn't set one yet.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status_text: Option<String>,
|
||||
}
|
||||
|
||||
/// Build the full container list. Wraps `lifecycle::list()` and
|
||||
|
|
@ -119,6 +124,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
.and_then(|(_, model)| resolve_ctx_window(model, &coord.context_window_tokens));
|
||||
let rate_limited = is_rate_limited(&logical);
|
||||
let extra_links = read_dashboard_links(&logical);
|
||||
let status_text = read_status_text(&logical);
|
||||
out.push(ContainerView {
|
||||
port: lifecycle::agent_web_port(&logical),
|
||||
running: lifecycle::is_running(&logical).await,
|
||||
|
|
@ -133,6 +139,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
context_window_tokens,
|
||||
rate_limited,
|
||||
extra_links,
|
||||
status_text,
|
||||
});
|
||||
}
|
||||
out
|
||||
|
|
@ -172,6 +179,20 @@ fn is_rate_limited(name: &str) -> bool {
|
|||
.exists()
|
||||
}
|
||||
|
||||
/// Read the agent's free-text status set via `mcp__hyperhive__set_status`.
|
||||
/// Returns `None` when the file is absent or empty — best-effort, never panics.
|
||||
/// `pub` so `agent_server` and `manager_server` can include it in `Whoami` responses.
|
||||
pub fn read_agent_status_text(name: &str) -> Option<String> {
|
||||
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
|
||||
let s = std::fs::read_to_string(path).ok()?;
|
||||
let trimmed = s.trim();
|
||||
if trimmed.is_empty() { None } else { Some(trimmed.to_owned()) }
|
||||
}
|
||||
|
||||
fn read_status_text(name: &str) -> Option<String> {
|
||||
read_agent_status_text(name)
|
||||
}
|
||||
|
||||
/// Read the agent's most recent completed turn from its turn-stats
|
||||
/// `SQLite`: the context-window size (prompt tokens) and the model name.
|
||||
/// Returns `None` when the file is absent or has no rows. Best-effort
|
||||
|
|
|
|||
|
|
@ -480,11 +480,36 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
},
|
||||
}
|
||||
}
|
||||
ManagerRequest::Whoami => ManagerResponse::Whoami {
|
||||
name: MANAGER_AGENT.to_owned(),
|
||||
role: "manager".to_owned(),
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
},
|
||||
ManagerRequest::Whoami => {
|
||||
let status_text = crate::container_view::read_agent_status_text(MANAGER_AGENT);
|
||||
ManagerResponse::Whoami {
|
||||
name: MANAGER_AGENT.to_owned(),
|
||||
role: "manager".to_owned(),
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
status_text,
|
||||
}
|
||||
}
|
||||
ManagerRequest::SetStatus { text } => {
|
||||
let path = Coordinator::agent_notes_dir(MANAGER_AGENT).join("hyperhive-status");
|
||||
let result = if text.trim().is_empty() {
|
||||
std::fs::remove_file(&path)
|
||||
.or_else(|e| if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
})
|
||||
} else {
|
||||
std::fs::write(&path, format!("{}\n", text.trim()))
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let coord2 = Arc::clone(coord);
|
||||
tokio::spawn(async move { coord2.rescan_containers_and_emit().await });
|
||||
ManagerResponse::Ok
|
||||
}
|
||||
Err(e) => ManagerResponse::Err { message: format!("set_status write failed: {e}") },
|
||||
}
|
||||
}
|
||||
ManagerRequest::CancelLooseEnd { kind, id } => crate::questions::handle_cancel_loose_end(
|
||||
coord,
|
||||
MANAGER_AGENT,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue