// — one agent's inference settings on the detail panel: the // model and effort it runs on now, with where each came from, and an editor // for the value the swarm sets. // // Reads `GET /api/agents/{name}/inference`, which answers // `{ configured, effective }`: `configured` is the swarm's stored value (null // when none), `effective` what the agent reported running on (null until its // first report reaches the controller). The editor PUTs the whole value and // the clear action DELETEs it; a cleared field puts the agent back on its own // value, which is why an empty field is left out of the body rather than sent. // // Its state belongs to one agent: callers key it by agent name, so switching // agents remounts it instead of showing the previous agent's settings. 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 { Dialog } from "../../ui/dialog/Dialog.js"; import { Panel } from "../../ui/panel/Panel.js"; import { SelectField } from "../../ui/select-field/SelectField.js"; import { TextField } from "../../ui/text-field/TextField.js"; import { Button } from "../../ui/button/Button.js"; import "./AgentInference.css"; type Source = "swarm" | "agent_nix" | "local_file" | "default"; // Mirrors the swarm's `InferenceSettings`; every field is optional. interface Settings { model?: string; effort?: string; subagent_models?: string[]; } // Mirrors `EffectiveInference`. interface Effective { model: string; effort: string; model_source: Source; effort_source: Source; } interface View { configured: Settings | null; effective: Effective | null; } // The values the swarm accepts for `effort`. const EFFORTS = ["low", "medium", "high", "xhigh", "max"]; const EFFORT_OPTIONS = [ { value: "", label: "the agent's own" }, ...EFFORTS.map((e) => ({ value: e, label: e })), ]; const SOURCE_LABEL: Record = { swarm: "swarm", agent_nix: "agent.nix", local_file: "its own pick", default: "default", }; // The same checks the swarm makes on a model name, so an obviously bad one // is refused before the round trip; the swarm stays the real gate. function modelNameProblem(name: string): string | null { if (/\s/.test(name)) return "a model name has no whitespace"; if (name.startsWith("-")) return "a model name does not start with -"; return null; } // Subagent models are typed as one list, split on commas and whitespace. function parseModels(text: string): string[] { return text.split(/[\s,]+/).filter((m) => m !== ""); } function inferenceUrl(agent: string): string { return `/api/agents/${encodeURIComponent(agent)}/inference`; } export function AgentInference({ agent }: { agent: string }) { const [view, setView] = useState(null); const [error, setError] = useState(null); // Bumped after a save or a clear, so the settings are fetched again. const [reload, setReload] = useState(0); const [editing, setEditing] = useState(false); useEffect(() => { let cancelled = false; (async () => { const r = await fetch(inferenceUrl(agent)); if (!r.ok) { if (!cancelled) setError(await readApiError(r)); return; } const body = (await r.json()) as Partial; if (cancelled) return; setView({ configured: body.configured ?? null, effective: body.effective ?? null, }); setError(null); })().catch((e: unknown) => { if (!cancelled) setError({ detail: String(e) }); }); return () => { cancelled = true; }; }, [agent, reload]); if (error) { return ( ); } if (view === null) return …; const { configured, effective } = view; const swarmModels = configured?.subagent_models ?? []; return (
  • {effective ? ( <> {SOURCE_LABEL[effective.model_source]} ) : ( )}
  • {effective ? ( <> {SOURCE_LABEL[effective.effort_source]} ) : ( )}
  • 0 ? swarmModels.join(", ") : "its own"} />
setEditing(true)} /> setEditing(false)} label="edit inference settings" plain > {/* Remounted per open, so a cancelled edit leaves nothing behind. */} {editing ? ( setEditing(false)} onSaved={() => { setEditing(false); setReload((n) => n + 1); }} /> ) : null}
); } function InferenceForm({ agent, configured, onClose, onSaved, }: { agent: string; configured: Settings | null; onClose: () => void; onSaved: () => void; }) { const [model, setModel] = useState(configured?.model ?? ""); const [effort, setEffort] = useState(configured?.effort ?? ""); const [subagents, setSubagents] = useState( (configured?.subagent_models ?? []).join(", "), ); const [busy, setBusy] = useState(false); const [problem, setProblem] = useState(null); const subagentList = parseModels(subagents); const modelProblem = modelNameProblem(model.trim()) ?? subagentList.map(modelNameProblem).find((p) => p !== null) ?? null; const empty = model.trim() === "" && effort === "" && subagentList.length === 0; async function send(init: RequestInit) { setBusy(true); try { const r = await fetch(inferenceUrl(agent), init); if (!r.ok) { setProblem(await readApiError(r)); return; } onSaved(); } catch (e: unknown) { setProblem({ detail: String(e) }); } finally { setBusy(false); } } function save(e: Event) { e.preventDefault(); const body: Settings = {}; if (model.trim() !== "") body.model = model.trim(); if (effort !== "") body.effort = effort; if (subagentList.length > 0) body.subagent_models = subagentList; void send({ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } return (

Sets what the swarm makes {agent} run on. A field left empty stays the agent's own value. The agent picks a change up within seconds; a turn already running finishes on what it started with.

{modelProblem ? (

{modelProblem}

) : null} {problem ? ( ) : null}
); }