diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 0bf00d7d..038efc3b 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -386,6 +386,19 @@ 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. +### Initial AGENTS.md at creation + +A third collapsed section, **instructions**, holds a plain text box for the +text the new agent's `AGENTS.md` starts with. Left empty, or holding only +whitespace, it sends nothing and the agent starts without the file. Otherwise +the text goes as typed in the optional `agents_md` field of `POST /api/agents`. +The agent writes it on its first start, and only when it has no `AGENTS.md` +yet, so creating an agent again under a name that has one never replaces it. +The controller refuses a text over 64 KiB; its reason shows under the box and +no agent is created. After a successful create the box empties. The +[instructions editor](#editing-an-agents-instructions) changes the file later. +The [swarm README](README.md#an-agents-agentsmd) has the wire details. + ### Linking external accounts Each agent on `/agents` opens three dialogs that write a credential for it diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts b/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts new file mode 100644 index 00000000..94c4c04e --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts @@ -0,0 +1,23 @@ +// The create-agent form's initial `AGENTS.md` field, kept out of the +// component so `npm test` can run it. +import type { ProblemDetails } from "@hive/shared/api-error.js"; + +/** + * The `agents_md` to send with a create: nothing while the box is empty or + * holds only whitespace, so the agent starts without the file; otherwise the + * text as typed. + */ +export function agentsMdBody(text: string): { agents_md?: string } { + return text.trim() === "" ? {} : { agents_md: text }; +} + +/** + * The controller's refusal of the text, when `problem` is one: a 400 whose + * detail starts with the field's name. It shows beside the box, so any other + * failure stays with the form's general error. + */ +export function agentsMdRefusal(problem: ProblemDetails): string | null { + return problem.status === 400 && problem.detail?.startsWith("agents_md ") + ? problem.detail + : null; +} diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css index e6e2ac3a..5e131ef1 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.css @@ -69,6 +69,11 @@ .create-agent-result-ok { color: var(--green); } +/* The controller's refusal of the AGENTS.md text, under its box. */ +.create-agent-field-error { + margin: 0; + color: var(--red); +} /* The picked archetype's description, under its select. */ .create-agent-archetype-description { margin: 0; diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx index 3074c765..d27bbd8d 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx @@ -38,6 +38,7 @@ import { SelectField, type SelectOption, } from "../ui/select-field/SelectField.js"; +import { TextAreaField } from "../ui/text-area-field/TextAreaField.js"; import { Button } from "../ui/button/Button.js"; import { capsBody, LimitsFields } from "../agent/detail/AgentLimits.js"; import { @@ -60,6 +61,7 @@ import { requestArchetype, type ArchetypeList, } from "./CreateAgentArchetype.js"; +import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js"; import "./CreateAgentForm.css"; // Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage` @@ -144,6 +146,8 @@ export function CreateAgentForm({ null, ); const [archetype, setArchetype] = useState(""); + // The text the agent's `AGENTS.md` starts with; empty sends none. + const [agentsMd, setAgentsMd] = useState(""); const picked = archetypes?.archetypes.find( (a) => archetypeKey(a) === archetype, ); @@ -211,6 +215,9 @@ export function CreateAgentForm({ label: h.name, })); const hivesReady = hives !== null && hives.length > 0; + // The controller's size refusal of the AGENTS.md text, shown by its box. + const refusal = + result.status === "error" ? agentsMdRefusal(result.problem) : null; async function submit(e: Event) { e.preventDefault(); @@ -232,6 +239,7 @@ export function CreateAgentForm({ ? requestPermissions(selected, catalogue.default) : undefined, archetype: requestArchetype(picked), + ...agentsMdBody(agentsMd), }), }); if (!r.ok) { @@ -241,6 +249,7 @@ export function CreateAgentForm({ const data = (await r.json()) as CreateAgentResponse; setResult({ status: "done", nodeId: data.node_id }); setName(""); + setAgentsMd(""); pickArchetype(""); if (catalogue) setSelected( @@ -258,8 +267,8 @@ export function CreateAgentForm({

Create a new agent's swarm-level identity. This only queues the job — check jobs to watch it settle. Resource - caps and inference are optional; leave them empty to set nothing. A - name set up before keeps the settings stored for it. + caps, inference and instructions are optional; leave them empty to + set nothing. A name set up before keeps the settings stored for it.

{hivesError && ( +
+ instructions +
+ + {refusal !== null && ( +

{refusal}

+ )} +
+