swarm-ui: make agent creation a five-step dialog with a fixed header and footer
The create dialog was one tall form that scrolled as a whole, so its header and button moved out of view. It is now steps you go through in order: identity, instructions (AGENTS.md in a box of its own), permissions, resources and inference, then a summary of what will be sent and what the agent creation queues, where create is. The header, step tabs and footer stay put and only the step's own fields scroll. A refused AGENTS.md text sends the dialog back to its step. The request is unchanged.
This commit is contained in:
parent
f0a80c9812
commit
339da0aebe
7 changed files with 662 additions and 246 deletions
|
|
@ -527,18 +527,41 @@ restarts the agent after its current turn. The panel reads
|
||||||
the reason and a **retry** button. `swarmctl agent permissions set` sets the
|
the reason and a **retry** button. `swarmctl agent permissions set` sets the
|
||||||
same grant from a shell.
|
same grant from a shell.
|
||||||
|
|
||||||
|
### Creating an agent
|
||||||
|
|
||||||
|
The **+ agent** button opens a dialog of five steps, which you go through in
|
||||||
|
order with **next** and can step back through with **back** or the step tabs:
|
||||||
|
|
||||||
|
1. **identity**: the name, the hive, and optionally an
|
||||||
|
[archetype](archetypes.md) that fills in the settings of the steps after it.
|
||||||
|
**next** waits for a valid name (1-63 lowercase letters, digits and hyphens)
|
||||||
|
and a hive.
|
||||||
|
2. **instructions**: the text the agent's `AGENTS.md` starts with, in a box
|
||||||
|
of its own.
|
||||||
|
3. **permissions**: one checkbox per permission, starting from the default set
|
||||||
|
or the archetype's.
|
||||||
|
4. **resources and inference**: the optional resource caps and inference
|
||||||
|
settings.
|
||||||
|
5. **summary**: every setting the request will carry, and what stays at its
|
||||||
|
default, then **what this creates**. **create** is on this step only.
|
||||||
|
|
||||||
|
The header, the step tabs and the buttons stay in place and only the step's own
|
||||||
|
fields scroll, so a long step never pushes **next** out of view. After
|
||||||
|
**create**, the dialog says it queued the job and links to **jobs**, and offers
|
||||||
|
**create another** and **close**. If the controller refuses the `AGENTS.md` text,
|
||||||
|
the dialog goes back to step 2 and shows the reason there.
|
||||||
|
|
||||||
### Settings at creation
|
### Settings at creation
|
||||||
|
|
||||||
The **+ agent** dialog has two collapsed sections, **resource caps** and
|
Step 4 of the **+ agent** dialog has **resource caps** and **inference**, with
|
||||||
**inference**, with the same fields as the detail panel's editors. Leave the
|
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
|
fields empty and the request is the plain name and hive. Fill them and `POST /api/agents` carries them
|
||||||
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`
|
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
|
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.
|
anything, so a bad value gets a refusal with its reason and creates no agent.
|
||||||
|
|
||||||
The dialog loads the hive list and the permission list when it opens. If either
|
The dialog loads the hive list and the permission list when it opens. If either
|
||||||
load fails, the dialog shows the reason in an error panel above the fields, and
|
load fails, the dialog shows the reason in an error panel above the step's fields, and
|
||||||
the hive picker or the permissions area reads `could not load hives` or
|
the hive picker or the permissions area reads `could not load hives` or
|
||||||
`could not load permissions` in place of `loading…`.
|
`could not load permissions` in place of `loading…`.
|
||||||
|
|
||||||
|
|
@ -550,14 +573,14 @@ stored settings when you leave the fields empty.
|
||||||
|
|
||||||
### Initial AGENTS.md at creation
|
### Initial AGENTS.md at creation
|
||||||
|
|
||||||
A third collapsed section, **instructions**, holds a plain text box for the
|
Step 2, **instructions**, holds a plain text box for the
|
||||||
text the new agent's `AGENTS.md` starts with. Left empty, or holding only
|
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
|
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 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`
|
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.
|
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
|
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
|
box, and the swarm creates no agent. After a successful
|
||||||
create the box empties. The
|
create the box empties. The
|
||||||
[instructions editor](#editing-an-agents-instructions) changes the file later.
|
[instructions editor](#editing-an-agents-instructions) changes the file later.
|
||||||
The [swarm README](README.md#an-agents-agentsmd) has the wire details.
|
The [swarm README](README.md#an-agents-agentsmd) has the wire details.
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,127 @@
|
||||||
/* <CreateAgentForm> — the create-agent form, rendered inside a `Dialog`
|
/* <CreateAgentForm> — the create-agent dialog, a stepper rendered inside a
|
||||||
from `AgentsPage` (see that component's file-top comment for why).
|
`Dialog` from `AgentsPage`. Input/button chrome comes from the shared `ui/`
|
||||||
Input/button chrome comes from the shared `ui/` form kit
|
form kit; this file owns the layout: a header, the step tabs and a footer
|
||||||
(`TextField`/`SelectField`/`Button`); this file only owns this
|
that stay put while only the step's own fields scroll.
|
||||||
component's own layout + copy. Column,
|
|
||||||
not row inside the form itself: with a second field (hive) added, a
|
|
||||||
row layout put fields of different natural widths on one baseline and
|
|
||||||
looked misaligned — mara: "make it a col". Reuses the same base16
|
|
||||||
slots (../theme.css) every other component draws from.
|
|
||||||
|
|
||||||
This layout dropped an explicit page `max-width` after a few rounds:
|
`.create-agent-panel` is a column no taller than the dialog (85vh, the
|
||||||
first added so bare `Panel` (no width opinion of its own) wouldn't
|
dialog's own cap), and its body hands all that height to the step. */
|
||||||
fill `.shell-body`'s full 60em column behind a narrow form (the
|
.create-agent-panel {
|
||||||
original "weird empty space" complaint), then widened for two panels
|
display: flex;
|
||||||
side by side — but once both were equal-width flex children that cap
|
flex-direction: column;
|
||||||
itself became "doesn't fill the space" (mara: "the cards are neither
|
box-sizing: border-box;
|
||||||
centered nor fill the space tho"). Removing it lets the row fill
|
width: min(90vw, 44em);
|
||||||
`.shell-body`'s own width like most pages — nothing sits off-center
|
max-height: 85vh;
|
||||||
once nothing is narrower than the row.
|
}
|
||||||
|
.create-agent-panel > .ui-panel-body {
|
||||||
`.create-agent-form-col`/`.create-agent-info-col` are equal-sized
|
display: flex;
|
||||||
(`flex: 1 1 0` — zero basis so the 1.5em `gap` splits the row evenly,
|
flex-direction: column;
|
||||||
not each column keeping its natural content width) per mara's "both
|
flex: 1 1 auto;
|
||||||
cards should be equal sized". `min-width` is the wrap threshold below
|
min-height: 0;
|
||||||
which the row goes single-column instead (info card moving under the
|
}
|
||||||
form) — no separate media query needed. Inputs fill their whole
|
.create-agent-wizard {
|
||||||
column now too ("inputs should fill full card width") — the kit's
|
display: flex;
|
||||||
`.ui-form-control`/`.ui-form-field` dropped their old 16em cap for
|
flex-direction: column;
|
||||||
exactly this (../ui/form-field/FormField.css). */
|
flex: 1 1 auto;
|
||||||
.create-agent-layout {
|
min-height: 0;
|
||||||
|
gap: 0.75em;
|
||||||
|
}
|
||||||
|
.create-agent-steps {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
gap: 0.35em;
|
||||||
gap: 1.5em;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
.create-agent-form-col,
|
.create-agent-step-tab {
|
||||||
.create-agent-info-col {
|
padding: 0.3em 0.7em;
|
||||||
flex: 1 1 0;
|
border: 1px solid var(--border);
|
||||||
min-width: 16em;
|
border-radius: 999px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.create-agent-step-tab:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.create-agent-step-current {
|
||||||
|
border-color: var(--purple);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
/* The one part that scrolls. A floor keeps the dialog from changing height
|
||||||
|
as the steps change. */
|
||||||
|
.create-agent-step {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 18em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
/* On a short window the floor gives way, so the footer stays on screen. */
|
||||||
|
@media (max-height: 45em) {
|
||||||
|
.create-agent-step {
|
||||||
|
min-height: 8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create-agent-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.75em;
|
||||||
}
|
}
|
||||||
.create-agent-intro {
|
.create-agent-intro {
|
||||||
margin: 0 0 1.5em;
|
margin: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.create-agent-form {
|
.create-agent-heading {
|
||||||
|
margin: 0.5em 0 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.create-agent-footer {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding-top: 0.75em;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
/* Back on the left, then why next waits, then next (or create) on the right. */
|
||||||
|
.create-agent-footer > :last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.create-agent-hint {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 0.25em;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.create-agent-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
gap: 0.35em;
|
||||||
gap: 0.75em;
|
margin: 0;
|
||||||
}
|
}
|
||||||
/* Optional settings, collapsed so the plain name-and-hive form stays short. */
|
.create-agent-summary-row {
|
||||||
.create-agent-section {
|
display: flex;
|
||||||
align-self: stretch;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em 1em;
|
||||||
}
|
}
|
||||||
.create-agent-section > summary {
|
.create-agent-summary-row dt {
|
||||||
cursor: pointer;
|
flex: 0 0 9em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.create-agent-section-body {
|
.create-agent-summary-row dd {
|
||||||
display: flex;
|
flex: 1 1 12em;
|
||||||
flex-direction: column;
|
min-width: 0;
|
||||||
align-items: flex-start;
|
margin: 0;
|
||||||
gap: 0.75em;
|
overflow-wrap: anywhere;
|
||||||
margin-top: 0.75em;
|
|
||||||
}
|
|
||||||
.create-agent-result {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
||||||
.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. */
|
/* The controller's refusal of the AGENTS.md text, under its box. */
|
||||||
.create-agent-field-error {
|
.create-agent-field-error {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,13 @@
|
||||||
// guaranteed to exist yet when this gets a response — so this shows
|
// guaranteed to exist yet when this gets a response — so this shows
|
||||||
// the queued confirmation and links to the Jobs page to watch it
|
// the queued confirmation and links to the Jobs page to watch it
|
||||||
// settle, rather than duplicating JobqGraph's per-node polling here.
|
// settle, rather than duplicating JobqGraph's per-node polling here.
|
||||||
// Scope matches the originating issue exactly: name field only, no
|
|
||||||
// forge/deploy options (those aren't wired server-side yet either).
|
|
||||||
//
|
//
|
||||||
// Rendered inside a `Dialog` from `AgentsPage`'s own "+ agent" button,
|
// Rendered inside a `Dialog` from `AgentsPage`'s own "+ agent" button, not a
|
||||||
// not a standalone route — the roster it feeds is the natural home for
|
// standalone route: the roster it feeds is the natural home for the action
|
||||||
// the action that populates it, and a distinct top-level nav item next
|
// that populates it. It is a stepper (see `CreateAgentSteps`): identity,
|
||||||
// to it was one click of indirection for no benefit. Kept as its own
|
// instructions, permissions, resources and inference, then a summary that
|
||||||
// component (not inlined into AgentsPage) since the two-panel layout
|
// says what will be sent and holds **create**. Every step is gone through, in
|
||||||
// below is unchanged from its former life as a page: same content, same
|
// order; the header, step tabs and footer stay put while the step scrolls.
|
||||||
// review history, just a different mount point.
|
|
||||||
//
|
//
|
||||||
// `hive` field added because agent creation had no way to record which
|
// `hive` field added because agent creation had no way to record which
|
||||||
// hive an agent runs on. `POST /api/agents` now requires it, so the
|
// hive an agent runs on. `POST /api/agents` now requires it, so the
|
||||||
|
|
@ -62,6 +59,16 @@ import {
|
||||||
type ArchetypeList,
|
type ArchetypeList,
|
||||||
} from "./CreateAgentArchetype.js";
|
} from "./CreateAgentArchetype.js";
|
||||||
import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js";
|
import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js";
|
||||||
|
import {
|
||||||
|
NAME_PATTERN,
|
||||||
|
nextStep,
|
||||||
|
previousStep,
|
||||||
|
STEP_TITLES,
|
||||||
|
STEPS,
|
||||||
|
stepProblem,
|
||||||
|
summaryRows,
|
||||||
|
type Step,
|
||||||
|
} from "./CreateAgentSteps.js";
|
||||||
import "./CreateAgentForm.css";
|
import "./CreateAgentForm.css";
|
||||||
|
|
||||||
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
|
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
|
||||||
|
|
@ -89,21 +96,6 @@ type SubmitState =
|
||||||
| { status: "done"; nodeId: number }
|
| { status: "done"; nodeId: number }
|
||||||
| { status: "error"; problem: ProblemDetails };
|
| { status: "error"; problem: ProblemDetails };
|
||||||
|
|
||||||
// Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`) —
|
|
||||||
// a `pattern` hint only, not a substitute for the server's own
|
|
||||||
// validation; a name this rejects would fail server-side anyway, so
|
|
||||||
// catching it before a round-trip is a pure UX win, not a new gate.
|
|
||||||
//
|
|
||||||
// Hyphen escaped (`\-`) rather than left trailing in the class: modern
|
|
||||||
// browsers validate the `pattern` attribute's regex in Unicode-set (`v`)
|
|
||||||
// mode, which is far stricter about hyphen placement than classic mode —
|
|
||||||
// `[a-z0-9-]` throws "Invalid character class" under `v` mode even though
|
|
||||||
// it's valid classic-mode regex (a trailing `-` is unambiguous there).
|
|
||||||
// Reproduced directly: `new RegExp('[a-z0-9-]', 'v')` throws,
|
|
||||||
// `new RegExp('[a-z0-9\\-]', 'v')` doesn't — confirmed via mara's console
|
|
||||||
// exception report on this page.
|
|
||||||
const NAME_PATTERN = "[a-z0-9\\-]{1,63}";
|
|
||||||
|
|
||||||
export function CreateAgentForm({
|
export function CreateAgentForm({
|
||||||
onClose,
|
onClose,
|
||||||
initialName = "",
|
initialName = "",
|
||||||
|
|
@ -118,6 +110,7 @@ export function CreateAgentForm({
|
||||||
* floats its own. */
|
* floats its own. */
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [step, setStep] = useState<Step>("identity");
|
||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [hive, setHive] = useState("");
|
const [hive, setHive] = useState("");
|
||||||
// `null` = still loading, `[]` = loaded but empty (a real, if unusual,
|
// `null` = still loading, `[]` = loaded but empty (a real, if unusual,
|
||||||
|
|
@ -220,8 +213,7 @@ export function CreateAgentForm({
|
||||||
const refusal =
|
const refusal =
|
||||||
result.status === "error" ? agentsMdRefusal(result.problem) : null;
|
result.status === "error" ? agentsMdRefusal(result.problem) : null;
|
||||||
|
|
||||||
async function submit(e: Event) {
|
async function submit() {
|
||||||
e.preventDefault();
|
|
||||||
setResult({ status: "submitting" });
|
setResult({ status: "submitting" });
|
||||||
try {
|
try {
|
||||||
// A section left empty is left out, so the request is the plain
|
// A section left empty is left out, so the request is the plain
|
||||||
|
|
@ -244,7 +236,10 @@ export function CreateAgentForm({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
setResult({ status: "error", problem: await readApiError(r) });
|
const problem = await readApiError(r);
|
||||||
|
setResult({ status: "error", problem });
|
||||||
|
// The text's own refusal shows beside its box, so go back to it.
|
||||||
|
if (agentsMdRefusal(problem) !== null) setStep("instructions");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = (await r.json()) as CreateAgentResponse;
|
const data = (await r.json()) as CreateAgentResponse;
|
||||||
|
|
@ -261,16 +256,52 @@ export function CreateAgentForm({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const problem = stepProblem(step, {
|
||||||
|
name,
|
||||||
|
hive,
|
||||||
|
permissionsReady: catalogue !== null,
|
||||||
|
inferenceProblem,
|
||||||
|
});
|
||||||
|
const done = result.status === "done";
|
||||||
|
const at = STEPS.indexOf(step);
|
||||||
|
|
||||||
|
// Enter goes on to the next step, and creates only from the summary.
|
||||||
|
function onSubmit(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (done) return;
|
||||||
|
if (step === "summary") void submit();
|
||||||
|
else if (problem === null) setStep(nextStep(step));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnother() {
|
||||||
|
setResult({ status: "idle" });
|
||||||
|
setStep("identity");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="create-agent-layout">
|
<Panel
|
||||||
<div class="create-agent-form-col">
|
title="create agent"
|
||||||
<Panel title="create agent" icon="🤖" onClose={onClose}>
|
icon="🤖"
|
||||||
<p class="create-agent-intro">
|
class="create-agent-panel"
|
||||||
Create a new agent's swarm-level identity. This only queues the job
|
onClose={onClose}
|
||||||
— check <Link href="/jobs">jobs</Link> to watch it settle. Resource
|
>
|
||||||
caps, inference and instructions are optional; leave them empty to
|
<form class="create-agent-wizard" onSubmit={onSubmit}>
|
||||||
set nothing. A name set up before keeps the settings stored for it.
|
<ol class="create-agent-steps" aria-label="steps">
|
||||||
</p>
|
{STEPS.map((s, i) => (
|
||||||
|
<li key={s}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`create-agent-step-tab${s === step ? " create-agent-step-current" : ""}`}
|
||||||
|
aria-current={s === step ? "step" : undefined}
|
||||||
|
disabled={i > at || done}
|
||||||
|
onClick={() => setStep(s)}
|
||||||
|
>
|
||||||
|
{i + 1}. {STEP_TITLES[s]}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<div class="create-agent-step">
|
||||||
{hivesError && (
|
{hivesError && (
|
||||||
<ApiErrorPanel
|
<ApiErrorPanel
|
||||||
context="failed to load the hive list"
|
context="failed to load the hive list"
|
||||||
|
|
@ -289,153 +320,232 @@ export function CreateAgentForm({
|
||||||
problem={archetypesError}
|
problem={archetypesError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<form class="create-agent-form" onSubmit={submit}>
|
{done && (
|
||||||
<TextField
|
<div class="create-agent-result create-agent-result-ok">
|
||||||
id="agent-name"
|
<p>
|
||||||
label="agent name"
|
Queued as job node #{result.nodeId} —{" "}
|
||||||
value={name}
|
<Link href="/jobs">watch it in jobs</Link>.
|
||||||
pattern={NAME_PATTERN}
|
</p>
|
||||||
title="1-63 chars: lowercase letters, digits, hyphens"
|
</div>
|
||||||
required
|
)}
|
||||||
onInput={setName}
|
{!done && step === "identity" && (
|
||||||
/>
|
<div class="create-agent-fields">
|
||||||
<SelectField
|
<p class="create-agent-intro">
|
||||||
id="agent-hive"
|
A new agent's swarm-level identity: its name, the hive it runs
|
||||||
label="hive"
|
on, and optionally an archetype that fills in the settings of
|
||||||
value={hive}
|
the later steps. A name set up before keeps the settings stored
|
||||||
onChange={setHive}
|
for it.
|
||||||
options={hiveOptions}
|
</p>
|
||||||
required
|
<TextField
|
||||||
disabled={!hivesReady}
|
id="agent-name"
|
||||||
placeholder={
|
label="agent name"
|
||||||
!hives
|
value={name}
|
||||||
? hivesError
|
pattern={NAME_PATTERN}
|
||||||
? "could not load hives"
|
title="1-63 chars: lowercase letters, digits, hyphens"
|
||||||
: "loading…"
|
required
|
||||||
: hives.length === 0
|
onInput={setName}
|
||||||
? "no hives configured"
|
/>
|
||||||
: "select a hive"
|
<SelectField
|
||||||
}
|
id="agent-hive"
|
||||||
/>
|
label="hive"
|
||||||
{archetypes && archetypes.archetypes.length > 0 && (
|
value={hive}
|
||||||
<>
|
onChange={setHive}
|
||||||
<SelectField
|
options={hiveOptions}
|
||||||
id="agent-archetype"
|
required
|
||||||
label="archetype"
|
disabled={!hivesReady}
|
||||||
value={archetype}
|
placeholder={
|
||||||
onChange={pickArchetype}
|
!hives
|
||||||
disabled={!catalogue}
|
? hivesError
|
||||||
options={[
|
? "could not load hives"
|
||||||
{ value: "", label: "none" },
|
: "loading…"
|
||||||
...archetypes.archetypes.map((a) => ({
|
: hives.length === 0
|
||||||
value: archetypeKey(a),
|
? "no hives configured"
|
||||||
label: a.name,
|
: "select a hive"
|
||||||
})),
|
}
|
||||||
]}
|
/>
|
||||||
/>
|
{archetypes && archetypes.archetypes.length > 0 && (
|
||||||
{picked && (
|
<>
|
||||||
<p class="create-agent-archetype-description">
|
<SelectField
|
||||||
{picked.description} An agent created from an archetype has
|
id="agent-archetype"
|
||||||
no config repo: its hive builds it from the archetype's
|
label="archetype"
|
||||||
module.
|
value={archetype}
|
||||||
</p>
|
onChange={pickArchetype}
|
||||||
)}
|
disabled={!catalogue}
|
||||||
</>
|
options={[
|
||||||
)}
|
{ value: "", label: "none" },
|
||||||
<FormField label="permissions" labelId="agent-permissions-label">
|
...archetypes.archetypes.map((a) => ({
|
||||||
<div role="group" aria-labelledby="agent-permissions-label">
|
value: archetypeKey(a),
|
||||||
{catalogue ? (
|
label: a.name,
|
||||||
<MultiselectFilterOptions
|
})),
|
||||||
options={catalogue.all}
|
]}
|
||||||
filter={{ values: selected, negate: false }}
|
|
||||||
onToggle={(v) => setSelected((s) => togglePermission(s, v))}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
{picked && (
|
||||||
<span>
|
<p class="create-agent-archetype-description">
|
||||||
{catalogueError ? "could not load permissions" : "loading…"}
|
{picked.description} An agent created from an archetype
|
||||||
</span>
|
has no config repo: its hive builds it from the
|
||||||
)}
|
archetype's module.
|
||||||
</div>
|
</p>
|
||||||
</FormField>
|
)}
|
||||||
<details class="create-agent-section">
|
</>
|
||||||
<summary>resource caps</summary>
|
)}
|
||||||
<div class="create-agent-section-body">
|
</div>
|
||||||
<LimitsFields
|
|
||||||
idPrefix="create-limits"
|
|
||||||
cpu={cpu}
|
|
||||||
memory={memory}
|
|
||||||
onCpu={setCpu}
|
|
||||||
onMemory={setMemory}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="create-agent-section">
|
|
||||||
<summary>inference</summary>
|
|
||||||
<div class="create-agent-section-body">
|
|
||||||
<InferenceFields
|
|
||||||
idPrefix="create-inference"
|
|
||||||
draft={inference}
|
|
||||||
onChange={setInference}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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
|
|
||||||
variant="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
result.status === "submitting" ||
|
|
||||||
!hivesReady ||
|
|
||||||
!catalogue ||
|
|
||||||
inferenceProblem !== null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{result.status === "submitting" ? "creating…" : "create"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
{result.status === "done" && (
|
|
||||||
<p class="create-agent-result create-agent-result-ok">
|
|
||||||
queued as job node #{result.nodeId} —{" "}
|
|
||||||
<Link href="/jobs">watch it in jobs</Link>
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{result.status === "error" && refusal === null && (
|
{!done && step === "instructions" && (
|
||||||
<ApiErrorPanel context="failed to queue" problem={result.problem} />
|
<div class="create-agent-fields">
|
||||||
|
<p class="create-agent-intro">
|
||||||
|
The text the agent's AGENTS.md starts with. Optional: leave it
|
||||||
|
empty and the agent starts without the file.
|
||||||
|
</p>
|
||||||
|
<TextAreaField
|
||||||
|
id="agent-agents-md"
|
||||||
|
label="AGENTS.md"
|
||||||
|
value={agentsMd}
|
||||||
|
rows={14}
|
||||||
|
placeholder="what the agent should know and do"
|
||||||
|
onInput={setAgentsMd}
|
||||||
|
/>
|
||||||
|
{refusal !== null && (
|
||||||
|
<p class="create-agent-field-error">{refusal}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
{!done && step === "permissions" && (
|
||||||
</div>
|
<div class="create-agent-fields">
|
||||||
<div class="create-agent-info-col">
|
<p class="create-agent-intro">
|
||||||
{/* Describes the intended end state (a running agent), not today's
|
What the swarm lets the agent do. Leaving the default set ticked
|
||||||
literal behaviour (deploying the container onto the hive isn't
|
keeps the agent on the default if that changes.
|
||||||
wired up server-side yet — see this file's top comment) —
|
</p>
|
||||||
mara's explicit call on this copy: read as finished, not as a
|
<FormField label="permissions" labelId="agent-permissions-label">
|
||||||
running commentary on partial implementation. */}
|
<div role="group" aria-labelledby="agent-permissions-label">
|
||||||
<Panel title="what this creates" icon="🪪">
|
{catalogue ? (
|
||||||
<p>
|
<MultiselectFilterOptions
|
||||||
Submitting this queues everything a new agent needs: a swarm-level
|
options={catalogue.all}
|
||||||
identity, a config repo on the forge with the operator added as a
|
filter={{ values: selected, negate: false }}
|
||||||
collaborator, and a container running the agent on the hive you pick
|
onToggle={(v) =>
|
||||||
above. An agent created from an archetype gets no config repo: its
|
setSelected((s) => togglePermission(s, v))
|
||||||
container runs the archetype's module. Watch it all settle on{" "}
|
}
|
||||||
<Link href="/jobs">jobs</Link>.
|
/>
|
||||||
</p>
|
) : (
|
||||||
</Panel>
|
<span>
|
||||||
</div>
|
{catalogueError
|
||||||
</div>
|
? "could not load permissions"
|
||||||
|
: "loading…"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!done && step === "resources" && (
|
||||||
|
<div class="create-agent-fields">
|
||||||
|
<p class="create-agent-intro">
|
||||||
|
Optional: leave a box empty to set nothing, and the hive's or
|
||||||
|
the agent's own value applies.
|
||||||
|
</p>
|
||||||
|
<h3 class="create-agent-heading">resource caps</h3>
|
||||||
|
<LimitsFields
|
||||||
|
idPrefix="create-limits"
|
||||||
|
cpu={cpu}
|
||||||
|
memory={memory}
|
||||||
|
onCpu={setCpu}
|
||||||
|
onMemory={setMemory}
|
||||||
|
/>
|
||||||
|
<h3 class="create-agent-heading">inference</h3>
|
||||||
|
<InferenceFields
|
||||||
|
idPrefix="create-inference"
|
||||||
|
draft={inference}
|
||||||
|
onChange={setInference}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!done && step === "summary" && (
|
||||||
|
<div class="create-agent-fields">
|
||||||
|
<dl class="create-agent-summary">
|
||||||
|
{summaryRows({
|
||||||
|
name,
|
||||||
|
hive,
|
||||||
|
archetype: picked?.name,
|
||||||
|
permissions: catalogue
|
||||||
|
? requestPermissions(selected, catalogue.default)
|
||||||
|
: undefined,
|
||||||
|
caps: capsBody(cpu, memory),
|
||||||
|
inference: draftBody(inference),
|
||||||
|
agentsMd,
|
||||||
|
}).map((row) => (
|
||||||
|
<div class="create-agent-summary-row" key={row.label}>
|
||||||
|
<dt>{row.label}</dt>
|
||||||
|
<dd>{row.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
{/* Describes the intended end state (a running agent), not
|
||||||
|
today's literal behaviour: mara's call on this copy was to
|
||||||
|
read as finished, not as a running commentary on partial
|
||||||
|
implementation. */}
|
||||||
|
<h3 class="create-agent-heading">what this creates</h3>
|
||||||
|
<p>
|
||||||
|
Creating queues everything a new agent needs: a swarm-level
|
||||||
|
identity, a config repo on the forge with the operator added as
|
||||||
|
a collaborator, and a container running the agent on the hive
|
||||||
|
above. An agent created from an archetype gets no config repo:
|
||||||
|
its container runs the archetype's module. Watch it all settle
|
||||||
|
on <Link href="/jobs">jobs</Link>.
|
||||||
|
</p>
|
||||||
|
{result.status === "error" && refusal === null && (
|
||||||
|
<ApiErrorPanel
|
||||||
|
context="failed to queue"
|
||||||
|
problem={result.problem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="create-agent-footer">
|
||||||
|
{done ? (
|
||||||
|
<>
|
||||||
|
<Button onClick={createAnother}>create another</Button>
|
||||||
|
{onClose && (
|
||||||
|
<Button variant="primary" onClick={onClose}>
|
||||||
|
close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
disabled={at === 0 || result.status === "submitting"}
|
||||||
|
onClick={() => setStep(previousStep(step))}
|
||||||
|
>
|
||||||
|
back
|
||||||
|
</Button>
|
||||||
|
{problem !== null && (step !== "identity" || name !== "") && (
|
||||||
|
<span class="create-agent-hint" role="status">
|
||||||
|
{problem}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{step === "summary" ? (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
result.status === "submitting" || !hivesReady || !catalogue
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{result.status === "submitting" ? "creating…" : "create"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={problem !== null}
|
||||||
|
>
|
||||||
|
next
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
frontend/packages/swarm-ui/src/pages/CreateAgentSteps.ts
Normal file
133
frontend/packages/swarm-ui/src/pages/CreateAgentSteps.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
// The create-agent dialog's steps: which there are, what lets the operator
|
||||||
|
// leave one, and what the last one says will be sent. Kept out of the
|
||||||
|
// component so `npm test` can run it.
|
||||||
|
|
||||||
|
export const STEPS = [
|
||||||
|
"identity",
|
||||||
|
"instructions",
|
||||||
|
"permissions",
|
||||||
|
"resources",
|
||||||
|
"summary",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type Step = (typeof STEPS)[number];
|
||||||
|
|
||||||
|
export const STEP_TITLES: Record<Step, string> = {
|
||||||
|
identity: "identity",
|
||||||
|
instructions: "instructions",
|
||||||
|
permissions: "permissions",
|
||||||
|
resources: "resources and inference",
|
||||||
|
summary: "summary",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`), the
|
||||||
|
// hint the name box shows; the server's own check stays the authority.
|
||||||
|
//
|
||||||
|
// The hyphen is escaped: browsers read a `pattern` attribute in Unicode-set
|
||||||
|
// (`v`) mode, which refuses a bare trailing hyphen in a class.
|
||||||
|
export const NAME_PATTERN = "[a-z0-9\\-]{1,63}";
|
||||||
|
|
||||||
|
export function nameProblem(name: string): string | null {
|
||||||
|
return new RegExp(`^${NAME_PATTERN}$`).test(name)
|
||||||
|
? null
|
||||||
|
: "the name is 1-63 characters: lowercase letters, digits and hyphens";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The step after `step`, or `step` itself on the last. */
|
||||||
|
export function nextStep(step: Step): Step {
|
||||||
|
return STEPS[Math.min(STEPS.indexOf(step) + 1, STEPS.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The step before `step`, or `step` itself on the first. */
|
||||||
|
export function previousStep(step: Step): Step {
|
||||||
|
return STEPS[Math.max(STEPS.indexOf(step) - 1, 0)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the steps need before the one `step` can be left. */
|
||||||
|
export interface StepInputs {
|
||||||
|
name: string;
|
||||||
|
hive: string;
|
||||||
|
// The permission list has loaded, so a pick is made against the real set.
|
||||||
|
permissionsReady: boolean;
|
||||||
|
// `draftProblem` of the inference boxes.
|
||||||
|
inferenceProblem: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why the operator cannot go on from `step`, or null when they can. Every
|
||||||
|
* step must be gone through, so this gates **next**; the last step has
|
||||||
|
* nothing after it.
|
||||||
|
*/
|
||||||
|
export function stepProblem(step: Step, input: StepInputs): string | null {
|
||||||
|
switch (step) {
|
||||||
|
case "identity":
|
||||||
|
return (
|
||||||
|
nameProblem(input.name) ?? (input.hive === "" ? "pick a hive" : null)
|
||||||
|
);
|
||||||
|
case "permissions":
|
||||||
|
return input.permissionsReady ? null : "the permission list is loading";
|
||||||
|
case "resources":
|
||||||
|
return input.inferenceProblem;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One line of the summary: a setting and what will be sent for it. */
|
||||||
|
export interface SummaryRow {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryInput {
|
||||||
|
name: string;
|
||||||
|
hive: string;
|
||||||
|
// The picked archetype's name, or undefined for a custom agent.
|
||||||
|
archetype: string | undefined;
|
||||||
|
// `requestPermissions`: undefined when the default set is kept.
|
||||||
|
permissions: string[] | undefined;
|
||||||
|
// The filled fields only, as `capsBody` and `draftBody` make them.
|
||||||
|
caps: object;
|
||||||
|
inference: object;
|
||||||
|
agentsMd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function settings(values: object): string {
|
||||||
|
const parts = Object.entries(values).map(
|
||||||
|
([key, value]) =>
|
||||||
|
`${key.replace(/_/g, " ")} ${Array.isArray(value) ? value.join(", ") : String(value)}`,
|
||||||
|
);
|
||||||
|
return parts.length === 0 ? "none set" : parts.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the summary lists: every setting the request carries, and for each
|
||||||
|
* one left alone, what then applies.
|
||||||
|
*/
|
||||||
|
export function summaryRows(s: SummaryInput): SummaryRow[] {
|
||||||
|
const words =
|
||||||
|
s.agentsMd.trim() === "" ? 0 : s.agentsMd.trim().split(/\s+/).length;
|
||||||
|
return [
|
||||||
|
{ label: "name", value: s.name },
|
||||||
|
{ label: "hive", value: s.hive },
|
||||||
|
{ label: "archetype", value: s.archetype ?? "none: a custom agent" },
|
||||||
|
{
|
||||||
|
label: "permissions",
|
||||||
|
value:
|
||||||
|
s.permissions === undefined
|
||||||
|
? "the default set"
|
||||||
|
: s.permissions.length === 0
|
||||||
|
? "none"
|
||||||
|
: s.permissions.join(", "),
|
||||||
|
},
|
||||||
|
{ label: "resource caps", value: settings(s.caps) },
|
||||||
|
{ label: "inference", value: settings(s.inference) },
|
||||||
|
{
|
||||||
|
label: "instructions",
|
||||||
|
value:
|
||||||
|
words === 0
|
||||||
|
? "none"
|
||||||
|
: `AGENTS.md, ${words} ${words === 1 ? "word" : "words"}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
/* <LinkMatrixAccountForm> — rendered inside a `Dialog` from `AgentsPage`,
|
/* <LinkMatrixAccountForm> — rendered inside a `Dialog` from `AgentsPage`,
|
||||||
one panel wide (no second info column — this form has no "what this
|
one panel wide. Column layout + `.link-matrix-account-result-ok`
|
||||||
creates" explanation worth a whole sibling card the way
|
mirror `CreateAgentForm.css`'s `.create-agent-fields`/`.create-agent-result-ok`
|
||||||
`CreateAgentForm` does, just the one-line note already in the JSX).
|
— same shape, kept as its own file rather than sharing that
|
||||||
Column layout + `.link-matrix-account-result-ok` mirror
|
|
||||||
`CreateAgentForm.css`'s `.create-agent-form`/`.create-agent-result-ok`
|
|
||||||
exactly — same shape, kept as its own file rather than sharing that
|
|
||||||
one since "form" here means a different component's layout, not a
|
one since "form" here means a different component's layout, not a
|
||||||
generic kit primitive (see that file's own top comment on why
|
generic kit primitive (see that file's own top comment on why
|
||||||
`ui/form-field` stays the only truly shared piece). */
|
`ui/form-field` stays the only truly shared piece). */
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
whichever container the caller gives it — the kit itself has no
|
whichever container the caller gives it — the kit itself has no
|
||||||
opinion on a maximum width; a page that wants one narrower than its
|
opinion on a maximum width; a page that wants one narrower than its
|
||||||
own layout caps it at the layout level (`CreateAgentForm.css`'s
|
own layout caps it at the layout level (`CreateAgentForm.css`'s
|
||||||
`.create-agent-form-col` is the existing example), same reasoning
|
`.create-agent-panel` is the existing example), same reasoning
|
||||||
`Panel` has no width opinion of its own either. `min-height` is a
|
`Panel` has no width opinion of its own either. `min-height` is a
|
||||||
touch-target floor (44px at the default 16px root font — WCAG
|
touch-target floor (44px at the default 16px root font — WCAG
|
||||||
2.5.5's minimum), not a visual choice — it's the same on every
|
2.5.5's minimum), not a visual choice — it's the same on every
|
||||||
|
|
|
||||||
101
frontend/packages/swarm-ui/test/create-agent-steps.test.mjs
Normal file
101
frontend/packages/swarm-ui/test/create-agent-steps.test.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// The create-agent dialog's steps: order, what gates next, and the summary.
|
||||||
|
// Run by `npm test` with Node's type stripping.
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
nameProblem,
|
||||||
|
nextStep,
|
||||||
|
previousStep,
|
||||||
|
STEPS,
|
||||||
|
stepProblem,
|
||||||
|
summaryRows,
|
||||||
|
} from "../src/pages/CreateAgentSteps.ts";
|
||||||
|
|
||||||
|
const ready = {
|
||||||
|
name: "ruth",
|
||||||
|
hive: "h1",
|
||||||
|
permissionsReady: true,
|
||||||
|
inferenceProblem: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
test("the steps run in order and stop at both ends", () => {
|
||||||
|
assert.deepEqual(STEPS, [
|
||||||
|
"identity",
|
||||||
|
"instructions",
|
||||||
|
"permissions",
|
||||||
|
"resources",
|
||||||
|
"summary",
|
||||||
|
]);
|
||||||
|
assert.equal(nextStep("identity"), "instructions");
|
||||||
|
assert.equal(nextStep("resources"), "summary");
|
||||||
|
assert.equal(nextStep("summary"), "summary");
|
||||||
|
assert.equal(previousStep("summary"), "resources");
|
||||||
|
assert.equal(previousStep("identity"), "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name is 1-63 lowercase letters, digits and hyphens", () => {
|
||||||
|
assert.equal(nameProblem("ruth-2"), null);
|
||||||
|
assert.equal(nameProblem("a".repeat(63)), null);
|
||||||
|
assert.notEqual(nameProblem(""), null);
|
||||||
|
assert.notEqual(nameProblem("a".repeat(64)), null);
|
||||||
|
assert.notEqual(nameProblem("Ruth"), null);
|
||||||
|
assert.notEqual(nameProblem("ru th"), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("identity needs a valid name and a hive before next", () => {
|
||||||
|
assert.equal(stepProblem("identity", ready), null);
|
||||||
|
assert.notEqual(stepProblem("identity", { ...ready, name: "" }), null);
|
||||||
|
assert.notEqual(stepProblem("identity", { ...ready, name: "No Way" }), null);
|
||||||
|
assert.equal(stepProblem("identity", { ...ready, hive: "" }), "pick a hive");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("permissions waits for the list, resources for a valid inference", () => {
|
||||||
|
assert.notEqual(
|
||||||
|
stepProblem("permissions", { ...ready, permissionsReady: false }),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.equal(stepProblem("permissions", ready), null);
|
||||||
|
assert.equal(
|
||||||
|
stepProblem("resources", { ...ready, inferenceProblem: "bad model" }),
|
||||||
|
"bad model",
|
||||||
|
);
|
||||||
|
assert.equal(stepProblem("instructions", { ...ready, name: "" }), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the summary lists what is sent and what stays at its default", () => {
|
||||||
|
const rows = (over) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
summaryRows({
|
||||||
|
name: "ruth",
|
||||||
|
hive: "h1",
|
||||||
|
archetype: undefined,
|
||||||
|
permissions: undefined,
|
||||||
|
caps: {},
|
||||||
|
inference: {},
|
||||||
|
agentsMd: "",
|
||||||
|
...over,
|
||||||
|
}).map((r) => [r.label, r.value]),
|
||||||
|
);
|
||||||
|
const plain = rows({});
|
||||||
|
assert.equal(plain.name, "ruth");
|
||||||
|
assert.equal(plain.archetype, "none: a custom agent");
|
||||||
|
assert.equal(plain.permissions, "the default set");
|
||||||
|
assert.equal(plain["resource caps"], "none set");
|
||||||
|
assert.equal(plain.inference, "none set");
|
||||||
|
assert.equal(plain.instructions, "none");
|
||||||
|
|
||||||
|
const set = rows({
|
||||||
|
archetype: "reviewer",
|
||||||
|
permissions: ["meta", "inbox"],
|
||||||
|
caps: { cpu_quota: "200%", memory_max: "2G" },
|
||||||
|
inference: { model: "opus", subagent_models: ["haiku", "sonnet"] },
|
||||||
|
agentsMd: " Be brief.\n",
|
||||||
|
});
|
||||||
|
assert.equal(set.archetype, "reviewer");
|
||||||
|
assert.equal(set.permissions, "meta, inbox");
|
||||||
|
assert.equal(set["resource caps"], "cpu quota 200%; memory max 2G");
|
||||||
|
assert.equal(set.inference, "model opus; subagent models haiku, sonnet");
|
||||||
|
assert.equal(set.instructions, "AGENTS.md, 2 words");
|
||||||
|
assert.equal(rows({ permissions: [] }).permissions, "none");
|
||||||
|
assert.equal(rows({ agentsMd: "x" }).instructions, "AGENTS.md, 1 word");
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue