Compare commits
4 changed files with 13 additions and 287 deletions
|
|
@ -7,17 +7,17 @@ domain, covers host-level detail for one hive.
|
||||||
|
|
||||||
## What it shows
|
## What it shows
|
||||||
|
|
||||||
| route | what |
|
| route | what |
|
||||||
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `/` | the hive directory, each hive with its last reported status |
|
| `/` | the hive directory, each hive with its last reported status |
|
||||||
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents, link forge, matrix and GitHub accounts |
|
| `/agents` | every agent: status, config PR, wanted state, inference settings; create agents, link forge, matrix and GitHub accounts |
|
||||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
||||||
| `/issues` | a cross-repo issue report |
|
| `/issues` | a cross-repo issue report |
|
||||||
| `/review-runs` | the review classifier's recent runs: which PRs, what it answered, which failed |
|
| `/review-runs` | the review classifier's recent runs: which PRs, what it answered, which failed |
|
||||||
| `/review-tryout` | try a review classification against a real PR |
|
| `/review-tryout` | try a review classification against a real PR |
|
||||||
|
|
||||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||||
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
An agent created here or with `swarmctl agent create` starts `paused`; set it
|
||||||
|
|
@ -48,7 +48,7 @@ with no icon keeps the hyperhive mark.
|
||||||
|
|
||||||
The tab also has an agent badge, with the agent's icon and name, ahead of the
|
The tab also has an agent badge, with the agent's icon and name, ahead of the
|
||||||
connection badge. It opens the agent's details in a popover under the badge:
|
connection badge. It opens the agent's details in a popover under the badge:
|
||||||
the same status, wanted state, inference settings, resource caps and linked accounts as the
|
the same status, wanted state, inference settings and linked accounts as the
|
||||||
panel on `/agents`, without the terminal preview. The popover doesn't dim the
|
panel on `/agents`, without the terminal preview. The popover doesn't dim the
|
||||||
page. Selecting outside it, or Escape, closes it. You can stop or pause the
|
page. Selecting outside it, or Escape, closes it. You can stop or pause the
|
||||||
agent, or link an account, without leaving the terminal. A subagent's tab opens
|
agent, or link an account, without leaving the terminal. A subagent's tab opens
|
||||||
|
|
@ -216,30 +216,6 @@ The panel reads `GET /api/agents/{agent}/inference`; before the agent's first
|
||||||
report reaches the controller, it shows the swarm's stored values only.
|
report reaches the controller, it shows the swarm's stored values only.
|
||||||
[inference](inference.md) describes the settings themselves.
|
[inference](inference.md) describes the settings themselves.
|
||||||
|
|
||||||
### Resource caps
|
|
||||||
|
|
||||||
The agent's detail panel shows its CPU quota and memory max under **resource
|
|
||||||
caps**. A cap is the swarm's, so it follows the agent to whichever hive runs
|
|
||||||
it, and it replaces what the hive says for that field. A field the swarm
|
|
||||||
leaves out shows as the hive deciding it. **edit** opens a dialog with the two
|
|
||||||
fields, filled with what the swarm stores. A cpu quota is a percentage of one
|
|
||||||
core (`400%` is four cores). A memory max is a size such as `512M` or `8G`, a
|
|
||||||
percentage of the machine's memory, or `infinity`.
|
|
||||||
|
|
||||||
Saving does a `PUT /api/agents/{agent}/limits` of the filled fields. A `PUT`
|
|
||||||
replaces the stored value whole, so an empty box clears that cap and the hive
|
|
||||||
decides that field again. **clear the swarm's caps** sends the `DELETE`. The
|
|
||||||
controller checks each value against systemd's own syntax, because systemd
|
|
||||||
ignores a value it can't parse and the agent would run uncapped, and it
|
|
||||||
refuses, for example, a percentage with a leading zero. The dialog shows the
|
|
||||||
refusal's reason under the fields and leaves them as you typed them.
|
|
||||||
|
|
||||||
A saved change isn't instant. The hive reads the caps about every 30 seconds,
|
|
||||||
and a running agent takes a changed cap at its next spawn or rebuild, so the
|
|
||||||
row shows the new value before the agent runs under it. The panel reads
|
|
||||||
`GET /api/agents/{agent}/limits`. [hivectl](../tools/hivectl.md#per-agent-resource-limits)
|
|
||||||
sets the hive's own limits.
|
|
||||||
|
|
||||||
### Linking external accounts
|
### Linking external accounts
|
||||||
|
|
||||||
Each agent on `/agents` opens three dialogs that write a credential for it
|
Each agent on `/agents` opens three dialogs that write a credential for it
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// <AgentDetail> — one agent's details: its status, wanted state, hive, config
|
// <AgentDetail> — one agent's details: its status, wanted state, hive, config
|
||||||
// PR, inference settings, resource caps and linked accounts, plus the actions on them. The
|
// PR, inference settings and linked accounts, plus the actions on them. The
|
||||||
// body of the detail panel on `/agents` (where `withTerminal` adds the live
|
// body of the detail panel on `/agents` (where `withTerminal` adds the live
|
||||||
// terminal and subagents below it) and of the detail dialog on a terminal
|
// terminal and subagents below it) and of the detail dialog on a terminal
|
||||||
// page (which is already beside the terminal, so omits it).
|
// page (which is already beside the terminal, so omits it).
|
||||||
|
|
@ -11,7 +11,6 @@ import { Badge } from "@hive/shared/badge.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import type { AgentActions } from "../roster/AgentActions.js";
|
import type { AgentActions } from "../roster/AgentActions.js";
|
||||||
import { AgentInference } from "./AgentInference.js";
|
import { AgentInference } from "./AgentInference.js";
|
||||||
import { AgentLimits } from "./AgentLimits.js";
|
|
||||||
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
|
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
|
||||||
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
|
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
|
||||||
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
||||||
|
|
@ -103,10 +102,6 @@ export function AgentDetail({
|
||||||
<dd>
|
<dd>
|
||||||
<AgentInference key={row.name} agent={row.name} />
|
<AgentInference key={row.name} agent={row.name} />
|
||||||
</dd>
|
</dd>
|
||||||
<dt>resource caps</dt>
|
|
||||||
<dd>
|
|
||||||
<AgentLimits key={row.name} agent={row.name} />
|
|
||||||
</dd>
|
|
||||||
<dt>accounts</dt>
|
<dt>accounts</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{row.hive ? (
|
{row.hive ? (
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/* <AgentLimits> — one cap per line, then the edit action. */
|
|
||||||
.ui-agent-limits {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.3em;
|
|
||||||
}
|
|
||||||
.ui-agent-limits-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.3em;
|
|
||||||
}
|
|
||||||
.ui-agent-limits-muted {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.ui-agent-limits-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
.ui-agent-limits-hint {
|
|
||||||
margin: 0 0 0.5em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.ui-agent-limits-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
// <AgentLimits> — one agent's resource caps on the detail panel: the CPU and
|
|
||||||
// memory caps the swarm sets for it, and an editor for them.
|
|
||||||
//
|
|
||||||
// Reads `GET /api/agents/{name}/limits`, which answers the stored caps as
|
|
||||||
// `{ cpu_quota?, memory_max? }` (`{}` when the swarm sets none). A cap
|
|
||||||
// replaces what the hive says for that field, and a field the swarm leaves
|
|
||||||
// out stays with the hive, which is why an empty box means "the hive decides".
|
|
||||||
// The editor PUTs the filled fields, a PUT replacing the stored value whole, so
|
|
||||||
// a field left out is cleared; the clear action DELETEs. The route checks each
|
|
||||||
// value against systemd's own syntax, and its refusal shows as it arrives, next
|
|
||||||
// to the fields.
|
|
||||||
//
|
|
||||||
// Its state belongs to one agent: callers key it by agent name, so switching
|
|
||||||
// agents remounts it instead of showing the previous agent's caps.
|
|
||||||
import { useEffect, useState } from "preact/hooks";
|
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
||||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
|
||||||
import { Button } from "../../ui/button/Button.js";
|
|
||||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
|
||||||
import { TextField } from "../../ui/text-field/TextField.js";
|
|
||||||
import "./AgentLimits.css";
|
|
||||||
|
|
||||||
// The caps as the route carries them; both fields are optional.
|
|
||||||
interface Caps {
|
|
||||||
cpu_quota?: string;
|
|
||||||
memory_max?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HIVE_DECIDES = "the hive decides";
|
|
||||||
|
|
||||||
function limitsUrl(agent: string): string {
|
|
||||||
return `/api/agents/${encodeURIComponent(agent)}/limits`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgentLimits({ agent }: { agent: string }) {
|
|
||||||
const [caps, setCaps] = useState<Caps | null>(null);
|
|
||||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
|
||||||
// Bumped after a save or a clear, so the caps are fetched again.
|
|
||||||
const [reload, setReload] = useState(0);
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
(async () => {
|
|
||||||
const r = await fetch(limitsUrl(agent));
|
|
||||||
if (!r.ok) {
|
|
||||||
if (!cancelled) setError(await readApiError(r));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const body = (await r.json()) as Caps;
|
|
||||||
if (cancelled) return;
|
|
||||||
setCaps(body);
|
|
||||||
setError(null);
|
|
||||||
})().catch((e: unknown) => {
|
|
||||||
if (!cancelled) setError({ detail: String(e) });
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [agent, reload]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
tone="negative"
|
|
||||||
value="resource caps unavailable"
|
|
||||||
title={error.detail ?? "reading the resource caps failed"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (caps === null) return <span class="ui-agent-limits-muted">…</span>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="ui-agent-limits">
|
|
||||||
<ul class="ui-agent-limits-list">
|
|
||||||
<li>
|
|
||||||
<Badge label="cpu quota" value={caps.cpu_quota ?? HIVE_DECIDES} />
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Badge label="memory max" value={caps.memory_max ?? HIVE_DECIDES} />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Badge
|
|
||||||
variant="quiet"
|
|
||||||
value="edit"
|
|
||||||
title={`set the swarm's CPU and memory caps for ${agent}`}
|
|
||||||
onClick={() => setEditing(true)}
|
|
||||||
/>
|
|
||||||
<Dialog
|
|
||||||
open={editing}
|
|
||||||
onClose={() => setEditing(false)}
|
|
||||||
label="edit resource caps"
|
|
||||||
plain
|
|
||||||
>
|
|
||||||
{/* Remounted per open, so a cancelled edit leaves nothing behind. */}
|
|
||||||
{editing ? (
|
|
||||||
<LimitsForm
|
|
||||||
agent={agent}
|
|
||||||
caps={caps}
|
|
||||||
onClose={() => setEditing(false)}
|
|
||||||
onSaved={() => {
|
|
||||||
setEditing(false);
|
|
||||||
setReload((n) => n + 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LimitsForm({
|
|
||||||
agent,
|
|
||||||
caps,
|
|
||||||
onClose,
|
|
||||||
onSaved,
|
|
||||||
}: {
|
|
||||||
agent: string;
|
|
||||||
caps: Caps;
|
|
||||||
onClose: () => void;
|
|
||||||
onSaved: () => void;
|
|
||||||
}) {
|
|
||||||
const [cpu, setCpu] = useState(caps.cpu_quota ?? "");
|
|
||||||
const [memory, setMemory] = useState(caps.memory_max ?? "");
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const [problem, setProblem] = useState<ProblemDetails | null>(null);
|
|
||||||
|
|
||||||
const empty = cpu.trim() === "" && memory.trim() === "";
|
|
||||||
const capped = caps.cpu_quota !== undefined || caps.memory_max !== undefined;
|
|
||||||
|
|
||||||
async function send(init: RequestInit) {
|
|
||||||
setBusy(true);
|
|
||||||
setProblem(null);
|
|
||||||
try {
|
|
||||||
const r = await fetch(limitsUrl(agent), init);
|
|
||||||
if (!r.ok) {
|
|
||||||
// The form stays as typed, so the operator can fix it and save again.
|
|
||||||
setProblem(await readApiError(r));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved();
|
|
||||||
} catch (e: unknown) {
|
|
||||||
setProblem({ detail: String(e) });
|
|
||||||
} finally {
|
|
||||||
setBusy(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(e: Event) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Only the filled fields: a PUT replaces the stored value whole, so an
|
|
||||||
// empty box is cleared and the hive decides that field again.
|
|
||||||
const body: Caps = {};
|
|
||||||
if (cpu.trim() !== "") body.cpu_quota = cpu.trim();
|
|
||||||
if (memory.trim() !== "") body.memory_max = memory.trim();
|
|
||||||
void send({
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel title={`resource caps — ${agent}`} icon="📏" onClose={onClose}>
|
|
||||||
<p>
|
|
||||||
Caps what {agent} may use, wherever it runs. A cap here replaces the
|
|
||||||
hive's own answer for that field; an empty box leaves the field to the
|
|
||||||
hive. The hive reads a change within about 30 seconds, and a running
|
|
||||||
agent takes it at its next spawn or rebuild.
|
|
||||||
</p>
|
|
||||||
<form class="ui-agent-limits-form" onSubmit={save}>
|
|
||||||
<TextField
|
|
||||||
id="limits-cpu-quota"
|
|
||||||
label="cpu quota"
|
|
||||||
value={cpu}
|
|
||||||
placeholder="e.g. 400%"
|
|
||||||
onInput={setCpu}
|
|
||||||
/>
|
|
||||||
<p class="ui-agent-limits-hint">
|
|
||||||
A percentage of one core: 100% is one core, 400% is four.
|
|
||||||
</p>
|
|
||||||
<TextField
|
|
||||||
id="limits-memory-max"
|
|
||||||
label="memory max"
|
|
||||||
value={memory}
|
|
||||||
placeholder="e.g. 8G"
|
|
||||||
onInput={setMemory}
|
|
||||||
/>
|
|
||||||
<p class="ui-agent-limits-hint">
|
|
||||||
A size such as 512M or 8G, a percentage of the machine's memory such
|
|
||||||
as 50%, or infinity.
|
|
||||||
</p>
|
|
||||||
{problem ? (
|
|
||||||
<ApiErrorPanel context="the caps were not saved" problem={problem} />
|
|
||||||
) : null}
|
|
||||||
<div class="ui-agent-limits-actions">
|
|
||||||
<Button variant="primary" type="submit" disabled={busy || empty}>
|
|
||||||
{busy ? "saving…" : "save"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={busy || !capped}
|
|
||||||
onClick={() => void send({ method: "DELETE" })}
|
|
||||||
>
|
|
||||||
clear the swarm's caps
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue