agnets done
This commit is contained in:
@@ -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'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
|
||||
|
||||
Reference in New Issue
Block a user