Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7fc1c9af | ||
|
|
f068d0b2fc | ||
|
|
30ba34f411 |
5 changed files with 114 additions and 3 deletions
|
|
@ -386,6 +386,20 @@ 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
|
destroying an agent doesn't clear them, so a name set up before keeps its
|
||||||
stored settings when you leave the fields empty.
|
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
|
||||||
|
section, open or not, and the swarm creates no agent. 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
|
### Linking external accounts
|
||||||
|
|
||||||
Each agent on `/agents` opens three dialogs that write a credential for it
|
Each agent on `/agents` opens three dialogs that write a credential for it
|
||||||
|
|
|
||||||
23
frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts
Normal file
23
frontend/packages/swarm-ui/src/pages/CreateAgentAgentsMd.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,11 @@
|
||||||
.create-agent-result-ok {
|
.create-agent-result-ok {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
/* The controller's refusal of the AGENTS.md text, under its section. */
|
||||||
|
.create-agent-field-error {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
/* The picked archetype's description, under its select. */
|
/* The picked archetype's description, under its select. */
|
||||||
.create-agent-archetype-description {
|
.create-agent-archetype-description {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
SelectField,
|
SelectField,
|
||||||
type SelectOption,
|
type SelectOption,
|
||||||
} from "../ui/select-field/SelectField.js";
|
} from "../ui/select-field/SelectField.js";
|
||||||
|
import { TextAreaField } from "../ui/text-area-field/TextAreaField.js";
|
||||||
import { Button } from "../ui/button/Button.js";
|
import { Button } from "../ui/button/Button.js";
|
||||||
import { capsBody, LimitsFields } from "../agent/detail/AgentLimits.js";
|
import { capsBody, LimitsFields } from "../agent/detail/AgentLimits.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -60,6 +61,7 @@ import {
|
||||||
requestArchetype,
|
requestArchetype,
|
||||||
type ArchetypeList,
|
type ArchetypeList,
|
||||||
} from "./CreateAgentArchetype.js";
|
} from "./CreateAgentArchetype.js";
|
||||||
|
import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js";
|
||||||
import "./CreateAgentForm.css";
|
import "./CreateAgentForm.css";
|
||||||
|
|
||||||
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
|
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
|
||||||
|
|
@ -144,6 +146,8 @@ export function CreateAgentForm({
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [archetype, setArchetype] = useState("");
|
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(
|
const picked = archetypes?.archetypes.find(
|
||||||
(a) => archetypeKey(a) === archetype,
|
(a) => archetypeKey(a) === archetype,
|
||||||
);
|
);
|
||||||
|
|
@ -211,6 +215,10 @@ export function CreateAgentForm({
|
||||||
label: h.name,
|
label: h.name,
|
||||||
}));
|
}));
|
||||||
const hivesReady = hives !== null && hives.length > 0;
|
const hivesReady = hives !== null && hives.length > 0;
|
||||||
|
// The controller's size refusal of the AGENTS.md text, shown under its
|
||||||
|
// section so a collapsed section still shows it.
|
||||||
|
const refusal =
|
||||||
|
result.status === "error" ? agentsMdRefusal(result.problem) : null;
|
||||||
|
|
||||||
async function submit(e: Event) {
|
async function submit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -232,6 +240,7 @@ export function CreateAgentForm({
|
||||||
? requestPermissions(selected, catalogue.default)
|
? requestPermissions(selected, catalogue.default)
|
||||||
: undefined,
|
: undefined,
|
||||||
archetype: requestArchetype(picked),
|
archetype: requestArchetype(picked),
|
||||||
|
...agentsMdBody(agentsMd),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|
@ -241,6 +250,7 @@ export function CreateAgentForm({
|
||||||
const data = (await r.json()) as CreateAgentResponse;
|
const data = (await r.json()) as CreateAgentResponse;
|
||||||
setResult({ status: "done", nodeId: data.node_id });
|
setResult({ status: "done", nodeId: data.node_id });
|
||||||
setName("");
|
setName("");
|
||||||
|
setAgentsMd("");
|
||||||
pickArchetype("");
|
pickArchetype("");
|
||||||
if (catalogue)
|
if (catalogue)
|
||||||
setSelected(
|
setSelected(
|
||||||
|
|
@ -258,8 +268,8 @@ export function CreateAgentForm({
|
||||||
<p class="create-agent-intro">
|
<p class="create-agent-intro">
|
||||||
Create a new agent's swarm-level identity. This only queues the job
|
Create a new agent's swarm-level identity. This only queues the job
|
||||||
— check <Link href="/jobs">jobs</Link> to watch it settle. Resource
|
— check <Link href="/jobs">jobs</Link> to watch it settle. Resource
|
||||||
caps and inference are optional; leave them empty to set nothing. A
|
caps, inference and instructions are optional; leave them empty to
|
||||||
name set up before keeps the settings stored for it.
|
set nothing. A name set up before keeps the settings stored for it.
|
||||||
</p>
|
</p>
|
||||||
{hivesError && (
|
{hivesError && (
|
||||||
<ApiErrorPanel
|
<ApiErrorPanel
|
||||||
|
|
@ -365,6 +375,22 @@ export function CreateAgentForm({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<details class="create-agent-section">
|
||||||
|
<summary>instructions</summary>
|
||||||
|
<div class="create-agent-section-body">
|
||||||
|
<TextAreaField
|
||||||
|
id="agent-agents-md"
|
||||||
|
label="AGENTS.md"
|
||||||
|
value={agentsMd}
|
||||||
|
rows={8}
|
||||||
|
placeholder="the text the agent's AGENTS.md starts with; leave empty for none"
|
||||||
|
onInput={setAgentsMd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{refusal !== null && (
|
||||||
|
<p class="create-agent-field-error">{refusal}</p>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -384,7 +410,7 @@ export function CreateAgentForm({
|
||||||
<Link href="/jobs">watch it in jobs</Link>
|
<Link href="/jobs">watch it in jobs</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{result.status === "error" && (
|
{result.status === "error" && refusal === null && (
|
||||||
<ApiErrorPanel context="failed to queue" problem={result.problem} />
|
<ApiErrorPanel context="failed to queue" problem={result.problem} />
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
43
frontend/packages/swarm-ui/test/create-agents-md.test.mjs
Normal file
43
frontend/packages/swarm-ui/test/create-agents-md.test.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// What the create-agent form sends for the initial AGENTS.md, and which error
|
||||||
|
// it shows beside the box. Run by `npm test`, which Node runs with its
|
||||||
|
// built-in test runner and type stripping, so the `.ts` module is imported as
|
||||||
|
// it is.
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
agentsMdBody,
|
||||||
|
agentsMdRefusal,
|
||||||
|
} from "../src/pages/CreateAgentAgentsMd.ts";
|
||||||
|
|
||||||
|
test("an empty or whitespace-only box sends no agents_md", () => {
|
||||||
|
assert.deepEqual(agentsMdBody(""), {});
|
||||||
|
assert.deepEqual(agentsMdBody(" \n\t \n"), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text is sent as typed, whitespace and all", () => {
|
||||||
|
assert.deepEqual(agentsMdBody("# me\n\nDo the thing.\n"), {
|
||||||
|
agents_md: "# me\n\nDo the thing.\n",
|
||||||
|
});
|
||||||
|
assert.deepEqual(agentsMdBody(" indented"), { agents_md: " indented" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the size refusal shows beside the box", () => {
|
||||||
|
const detail = "agents_md is 70000 bytes; the limit is 65536";
|
||||||
|
assert.equal(agentsMdRefusal({ status: 400, detail }), detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("other failures stay with the general error", () => {
|
||||||
|
assert.equal(
|
||||||
|
agentsMdRefusal({ status: 400, detail: "name is not a valid identifier" }),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
agentsMdRefusal({
|
||||||
|
status: 500,
|
||||||
|
detail: "agents_md is 70000 bytes; the limit is 65536",
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.equal(agentsMdRefusal({ status: 400 }), null);
|
||||||
|
assert.equal(agentsMdRefusal({ detail: "agents_md is too long" }), null);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue