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 (
+ <>
+ {problem}