agnets done

This commit is contained in:
2026-08-28 11:02:02 +05:30
parent ab9adde6cd
commit eaf08e061d
58 changed files with 3506 additions and 4308 deletions

View File

@@ -1,13 +1,14 @@
import * as React from 'react';
import {
BookOpen, Boxes, Brain, FileText, Globe, IdCard, Layers, LayoutGrid, MessageSquare, Plus,
SlidersHorizontal, Trash2, X,
SlidersHorizontal, Trash2, Wrench, X,
} from 'lucide-react';
import {
Alert, Button, Field, Input, SegmentedToggle, Select, SelectContent, SelectItem, SelectTrigger,
SelectValue, Switch, Textarea,
} from '@/components/ds';
import { DOMAIN_SURFACES, surfaceFor } from '@/lib/skills/surfaces';
import { getSkillsForPage } from '@/lib/skills/registry';
import { AGENT_ICONS, KNOWLEDGE_KINDS, REASONING_MODES } from '@/lib/agents/vocabulary';
import {
AgentPreview, Collapse, DocField, DocSection, GroupHead, IconPicker, ItemRow, Rail, ScopeChip,
@@ -96,6 +97,16 @@ function useActiveSection(ids) {
/** @param {any} props */
export function AgentConfigure({
fields, agents, customSkills = [], onChange, onOpenSkills, scopeLocked = false,
/* Attaching and detaching a skill goes through the caller's existing write
path, NOT through `set`. Every other control here writes a draft that Save
persists; a skill persists immediately, on purpose — the whole point of the
catalog is that the agent gains the capability there and then. Two write
paths for one field would mean the Configure screen and the catalog
disagreed about when a skill takes effect. */
onToggleSkill = null, pendingSkill = null,
/* The tools this deployment registers, from GET /api/v1/tools. Passed in
rather than fetched here so this component stays a form over `fields`. */
toolCatalogue = [],
}) {
/* Sections are open by default: this is a document, and one that greets its
author with four closed headers hides the thing they came to write. Closing
@@ -107,6 +118,32 @@ export function AgentConfigure({
const [active, setActive] = useActiveSection(SECTION_IDS);
/**
* The skills these pages offer, and the ones not yet attached.
*
* Derived at render from `fields.pages` rather than held in state, for the
* same reason the rail is: a second copy of a list that is already on screen
* is a second thing to keep correct, and the one that drifts is always the
* copy.
*
* Deduplicated by id because an agent covering several pages will be offered
* the same skill by each of them.
*/
const availableSkills = React.useMemo(() => {
const seen = new Map();
for (const page of fields.pages) {
for (const skill of getSkillsForPage(page, { customSources: customSkills })) {
if (!seen.has(skill.id)) seen.set(skill.id, skill);
}
}
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
}, [fields.pages, customSkills]);
const attachableSkills = React.useMemo(
() => availableSkills.filter((sk) => !fields.skills.includes(sk.id)),
[availableSkills, fields.skills]
);
const set = (patch) => onChange({ ...fields, ...patch });
const instructionWords = React.useMemo(
@@ -176,9 +213,10 @@ export function AgentConfigure({
id: 'capabilities',
icon: Boxes,
label: 'Capabilities',
meta: `${fields.pages.length + fields.knowledge.length}`,
meta: `${fields.pages.length + fields.skills.length + fields.knowledge.length}`,
items: [
{ id: 'capabilities-pages', label: 'Pages', meta: fields.pages.length },
{ id: 'capabilities-skills', label: 'Skills', meta: fields.skills.length },
{ id: 'capabilities-knowledge', label: 'Knowledge', meta: fields.knowledge.length },
],
},
@@ -384,6 +422,176 @@ export function AgentConfigure({
</p>
</div>
{/* Skills ── what it can actually do.
The section this screen was missing. Skills were attachable from
the catalog and invisible here, on the one page that lists
everything else an agent carries — so the reading was that this
agent had none.
Availability is bounded by the pages above, which is the rule the
copy under Pages already states: a page decides which skills
exist there, and an agent chooses among them. It can narrow that
list, never widen it. */}
<div className="pt-4">
<GroupHead
id="capabilities-skills"
icon={Boxes}
title="Skills"
count={fields.skills.length}
action={onOpenSkills && (
<Button variant="ghost" size="sm" onClick={onOpenSkills}>
Browse catalog
</Button>
)}
/>
{!fields.pages.length ? (
<p className="mt-2 text-body-sm text-ink-3">
Choose a page first. Skills belong to pages, so there are none to offer until
this agent has somewhere to answer.
</p>
) : (
<div className="mt-2 space-y-3">
{fields.skills.length > 0 && (
<ul className="divide-y divide-border">
{fields.skills.map((id) => {
const skill = availableSkills.find((sk) => sk.id === id);
return (
<ItemRow
key={id}
icon={Boxes}
title={skill?.name || id}
detail={skill?.description}
/* A skill the pages no longer offer is still
attached and still listed — silently dropping it
would edit the definition behind the author's
back. Flagged instead, so removing it is their
decision. */
warning={skill ? null : 'Not available on the pages above.'}
removeLabel={`Remove ${skill?.name || id}`}
onRemove={onToggleSkill ? () => onToggleSkill(id) : undefined}
/>
);
})}
</ul>
)}
{!fields.skills.length && (
<p className="text-body-sm text-ink-3">
No skills attached. This agent answers from the page&apos;s own reader only.
</p>
)}
<Select
value=""
disabled={!onToggleSkill || Boolean(pendingSkill)}
onValueChange={(id) => !fields.skills.includes(id) && onToggleSkill?.(id)}
>
<SelectTrigger className="w-full sm:w-72">
<SelectValue placeholder={
pendingSkill ? 'Saving...' : 'Add a skill...'
} />
</SelectTrigger>
<SelectContent>
{attachableSkills.map((sk) => (
<SelectItem key={sk.id} value={sk.id}>{sk.name}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-caption leading-relaxed text-ink-4">
{attachableSkills.length
? 'Attaching a skill takes effect immediately — it does not wait for Save.'
: 'Every skill these pages offer is already attached.'}
</p>
</div>
)}
</div>
{/* Tools ── what it can actually do.
Skills are guidance the model reads; tools are the calls it may
make. An agent with skills and no tools discusses the work and
looks nothing up, which is what every agent authored here was
before this section existed — the editor had no field for it and
the parser dropped `tools:` on the way in.
The catalogue is served by the backend rather than listed here,
so a tool renamed or withdrawn cannot leave a stale option in
this form and an agent built from one that no longer exists. */}
<div className="pt-4">
<GroupHead
id="capabilities-tools"
icon={Wrench}
title="Tools"
count={fields.tools.length}
/>
<div className="mt-2 space-y-3">
{fields.tools.length > 0 && (
<ul className="divide-y divide-border">
{fields.tools.map((name) => {
const tool = toolCatalogue.find((t) => t.name === name);
return (
<ItemRow
key={name}
icon={Wrench}
title={tool?.name || name}
detail={tool?.description}
/* A tool this deployment no longer registers is kept
and flagged rather than dropped: the backend now
refuses to save a definition naming one, so the
author needs to see which. */
warning={tool
? (tool.effect === 'write'
? 'Proposes changes — a person is asked to approve each one.'
: null)
: 'Not available on this deployment.'}
removeLabel={`Remove ${name}`}
onRemove={() => set({ tools: fields.tools.filter((t) => t !== name) })}
/>
);
})}
</ul>
)}
{!fields.tools.length && (
<p className="text-body-sm text-ink-3">
No tools attached. This agent can discuss its subject but cannot look
anything up.
</p>
)}
<Select
value=""
onValueChange={(name) =>
!fields.tools.includes(name) && set({ tools: [...fields.tools, name] })}
>
<SelectTrigger className="w-full sm:w-72">
<SelectValue placeholder={
toolCatalogue.length ? 'Add a tool...' : 'Loading tools...'
} />
</SelectTrigger>
<SelectContent>
{toolCatalogue
.filter((t) => !fields.tools.includes(t.name))
.map((t) => (
<SelectItem key={t.name} value={t.name}>
{t.name}{t.effect === 'write' ? ' — writes' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-caption leading-relaxed text-ink-4">
A tool marked <em>writes</em> can propose a change. Nothing is written until
somebody approves it, and the agent can only reach what you could reach
yourself.
</p>
</div>
</div>
{/* Knowledge ── what it has been told. */}
<div className="pt-4">
<GroupHead