From e36a15de91ed2b2758928f2016f3749e3a936df0 Mon Sep 17 00:00:00 2001
From: iris
Date: Thu, 15 Oct 2026 11:32:48 +0200
Subject: [PATCH] Set resource caps and inference when creating an agent
POST /api/agents takes optional limits and inference objects, checked by
the same rules as the two PUT routes before anything is queued and stored
ahead of the job graph. The create form gets collapsed sections for both,
reusing the fields of the detail-panel editors.
---
docs/swarm/ui.md | 12 ++
.../src/agent/detail/AgentInference.tsx | 163 +++++++++++-------
.../swarm-ui/src/agent/detail/AgentLimits.tsx | 79 ++++++---
.../swarm-ui/src/pages/CreateAgentForm.css | 15 ++
.../swarm-ui/src/pages/CreateAgentForm.tsx | 53 +++++-
swarm-controller/src/agent_limits.rs | 22 ++-
swarm-controller/src/inference.rs | 22 ++-
swarm-controller/src/main.rs | 123 ++++++++++++-
8 files changed, 389 insertions(+), 100 deletions(-)
diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index da523eab..217fd426 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -296,6 +296,18 @@ 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.
+### Settings at creation
+
+The **+ agent** dialog has two collapsed sections, **resource caps** and
+**inference**, with the same fields as the detail panel's editors. Leave them
+closed or empty and the request is the plain name and hive. Fill them and
+`POST /api/agents` carries them as optional `limits` and `inference` objects,
+the same shapes the two `PUT` routes take. The controller checks them with the
+`PUT` rules before it queues anything, so a bad value gets a refusal with its
+reason and creates no agent. It stores them ahead of the job graph, so they
+are in place before the agent first runs. Creating a name that already exists with no settings
+leaves any stored ones alone.
+
### Linking external accounts
Each agent on `/agents` opens three dialogs that write a credential for it
diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx
index b7ebca60..08e5a7f7 100644
--- a/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx
+++ b/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx
@@ -88,6 +88,100 @@ 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 1 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,
+}: {
+ draft: InferenceDraft;
+ onChange: (d: InferenceDraft) => void;
+}) {
+ 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);
@@ -219,29 +313,17 @@ function InferenceForm({
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 [maxSubagents, setMaxSubagents] = useState(
- configured?.max_subagents?.toString() ?? "",
- );
+ 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 subagentList = parseModels(subagents);
- const formProblem =
- modelNameProblem(model.trim()) ??
- subagentList.map(modelNameProblem).find((p) => p !== null) ??
- (maxSubagentsValue(maxSubagents) === null
- ? `max subagents is a whole number from 1 to ${MAX_SUBAGENTS}, or empty for no cap`
- : null);
- const empty =
- model.trim() === "" &&
- effort === "" &&
- subagentList.length === 0 &&
- maxSubagents.trim() === "";
+ const formProblem = draftProblem(draft);
+ const empty = draftIsEmpty(draft);
async function send(init: RequestInit) {
setBusy(true);
@@ -261,16 +343,10 @@ function InferenceForm({
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;
- const max = maxSubagentsValue(maxSubagents);
- if (typeof max === "number") body.max_subagents = max;
void send({
method: "PUT",
headers: { "content-type": "application/json" },
- body: JSON.stringify(body),
+ body: JSON.stringify(draftBody(draft)),
});
}
@@ -284,38 +360,7 @@ function InferenceForm({
running.