+ Stops this run and every sub-agent it spawned. + The kill flag expires on its own after 1 hour; + re-killing refreshes it. +
+ ) : ( ++ Clears the kill flag on this run and, with it, on the sub-agents + it spawned. +
+ ); + } + return action === "kill" ? ( ++ Stops every run whose leaf agent is this name, + across the whole swarm — not just the ones you can see. Sub-agents + running under other names are not affected. The kill flag expires + on its own after 24 hours; re-killing refreshes it. +
+ ) : ( ++ Clears the swarm-wide kill flag for this agent. Every run of this + agent may resume. +
+ ); +} diff --git a/gateway/internal/adminapi/ui/src/components/StatusBadge.tsx b/gateway/internal/adminapi/ui/src/components/StatusBadge.tsx new file mode 100644 index 000000000..d76e2828b --- /dev/null +++ b/gateway/internal/adminapi/ui/src/components/StatusBadge.tsx @@ -0,0 +1,92 @@ +// StatusBadge — the run / agent kill-state pill, plus the two tiny +// derivations that decide which one to show. +// +// The derivation is deliberately shallow and lives here so every +// page agrees on what "running" means. Inputs are things the SPA +// already has (the /state snapshot and the newest call timestamp +// from the call log) — no new backend. +// +// killed state.killed. The kill key is set; the run's (or the +// agent's runs') next LLM call is rejected when the swarm +// has enforce_macaroons=true, logged otherwise. +// exceeded agents only: current_spend_usd >= configured_cap_usd. +// Runs carry their caps inside the macaroon, which /state +// doesn't surface — a run that hit its cap simply stops +// making calls and reads as "done". +// running a call landed within RUN_ACTIVE_WINDOW_MS (either the +// newest call-log row or the /state step counter moving +// between polls). This is a heuristic: a run idling in a +// long tool call reads as "done" until its next LLM call. +// done none of the above. + +import type { AgentStateResponse } from "../api/types"; + +export type Status = "running" | "killed" | "exceeded" | "done"; + +/** How recent the last LLM call must be for a run to count as + * in-flight. Five minutes covers the long tool calls we see in + * practice without keeping a finished run "running" all afternoon. */ +export const RUN_ACTIVE_WINDOW_MS = 5 * 60_000; + +export function deriveRunStatus(args: { + killed: boolean; + /** Epoch ms of the most recent evidence of activity, if any. */ + lastActivityMs?: number; + now?: number; +}): Status { + const { killed, lastActivityMs, now = Date.now() } = args; + if (killed) return "killed"; + if ( + lastActivityMs !== undefined && + now - lastActivityMs < RUN_ACTIVE_WINDOW_MS + ) { + return "running"; + } + return "done"; +} + +/** Agents have no "running"/"done" — an agent is a name, not a + * process — so only the two blocking states get a badge. `null` + * means "nothing to flag". */ +export function deriveAgentStatus( + state: AgentStateResponse, +): Extract