// — 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"; import { MAX_SUBAGENTS, maxSubagentsValue } from "./max-subagents.js"; 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[]; max_subagents?: number; } // 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`; } // The four inference boxes as typed. Shared by the editor here and the // create-agent form, which send what `draftBody` makes of them. export interface InferenceDraft { model: string; effort: string; subagents: string; maxSubagents: string; } export const EMPTY_INFERENCE_DRAFT: InferenceDraft = { model: "", effort: "", subagents: "", maxSubagents: "", }; // Why the draft would be refused, or null when it is fine to send. export function draftProblem(d: InferenceDraft): string | null { return ( modelNameProblem(d.model.trim()) ?? parseModels(d.subagents) .map(modelNameProblem) .find((p) => p !== null) ?? (maxSubagentsValue(d.maxSubagents) === null ? `max subagents is a whole number from 0 (no subagents) to ${MAX_SUBAGENTS}, or empty for no cap` : null) ); } export function draftIsEmpty(d: InferenceDraft): boolean { return ( d.model.trim() === "" && d.effort === "" && parseModels(d.subagents).length === 0 && d.maxSubagents.trim() === "" ); } // Only the filled fields: an empty one leaves the agent on its own value. export function draftBody(d: InferenceDraft): Settings { const body: Settings = {}; if (d.model.trim() !== "") body.model = d.model.trim(); if (d.effort !== "") body.effort = d.effort; const models = parseModels(d.subagents); if (models.length > 0) body.subagent_models = models; const max = maxSubagentsValue(d.maxSubagents); if (typeof max === "number") body.max_subagents = max; return body; } export function InferenceFields({ draft, onChange, idPrefix = "inference", }: { draft: InferenceDraft; onChange: (d: InferenceDraft) => void; // Ids must be unique on the page, and the agents page can hold the editor // and the create dialog at once. idPrefix?: string; }) { const problem = draftProblem(draft); return ( <> onChange({ ...draft, model })} /> onChange({ ...draft, effort })} /> onChange({ ...draft, subagents })} /> onChange({ ...draft, maxSubagents })} /> {problem ?

{problem}

: null} ); } 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]); // Shown as text, not a hover title. Settings already loaded stay under a // failed refetch, so a save that worked is not hidden by a read that did not. const errorPanel = error ? ( ) : null; if (view === null) { return errorPanel ?? …; } const { configured, effective } = view; const swarmModels = configured?.subagent_models ?? []; return (
{errorPanel}
  • {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 [draft, setDraft] = useState({ model: configured?.model ?? "", effort: configured?.effort ?? "", subagents: (configured?.subagent_models ?? []).join(", "), maxSubagents: configured?.max_subagents?.toString() ?? "", }); const [busy, setBusy] = useState(false); const [problem, setProblem] = useState(null); const formProblem = draftProblem(draft); const empty = draftIsEmpty(draft); 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(); void send({ method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(draftBody(draft)), }); } return (

Sets what the swarm makes {agent} run on. A field left empty stays the agent's own value; an empty max subagents is no cap. The agent picks a change up within seconds; a turn already running finishes on what it started with, and subagents already running past a lowered cap keep running.

{problem ? ( ) : null}
); }