diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index da523eab..606819a9 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -296,6 +296,22 @@ 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 the
+fields empty and the request is the plain name and hive; collapsing a section doesn't
+withdraw what you typed in it. 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, before it declares the agent, so they
+are in place before the agent first runs. If the creation then fails, the
+controller puts back what it replaced. Each agent name keys its settings and
+destroying an agent doesn't clear them, so a name set up before keeps its
+stored settings when you leave the fields empty.
+
### 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..4bea9102 100644
--- a/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx
+++ b/frontend/packages/swarm-ui/src/agent/detail/AgentInference.tsx
@@ -88,6 +88,104 @@ 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,
+ 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 (
+ <>
+ {problem}