Give bots skills: runbooks they load when a job matches
A skill is a runbook: a name, a unique slug, a one-line description of when it applies, and the markdown body. A run only ever sees the slug and description (one load_skill tool carries them), and reads the whole body only when it calls the tool, so a long runbook costs nothing until needed. Skills are the third extension kind beside MCP servers and custom tools, on the same botExtensionsSql path, so per-bot, per-workspace and all-bots grants work the same way, and attachDefaultExtensions hands an every-bot skill to new bots. load_skill is a reserved tool name, so a custom tool cannot shadow it, and a built-in wins a clash. Managed on Settings > Skills; ExtensionRow links each kind to its own page and a skill never reports a failure, since nothing connects or runs.
This commit is contained in:
@@ -18,13 +18,13 @@ type Props<T> = {
|
||||
/**
|
||||
* Under `/api/admin/`.
|
||||
*/
|
||||
path: "mcps/set-mcp-access" | "tools/set-tool-access";
|
||||
path: "mcps/set-mcp-access" | "tools/set-tool-access" | "skills/set-skill-access";
|
||||
onSaved: (overview: T, msg: string) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Who gets an MCP server or tool: the bots and workspaces picked here.
|
||||
* Who gets an MCP server, tool or skill: the bots and workspaces picked here.
|
||||
* "Every bot" (super admins only) gives it to every bot now and every new
|
||||
* one; each can still drop it. A workspace gives it to every bot in
|
||||
* it, there.
|
||||
|
||||
@@ -11,22 +11,35 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One MCP server or tool a bot or workspace has, and whether it's working.
|
||||
* The settings page each kind is managed on.
|
||||
*/
|
||||
const ExtensionHrefs = {
|
||||
mcp: "/admin/settings/mcps",
|
||||
tool: "/admin/settings/tools",
|
||||
skill: "/admin/settings/skills",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* One MCP server, tool or skill a bot or workspace has, and whether it's
|
||||
* working. A skill has nothing to connect or run, so it never reports a
|
||||
* failure.
|
||||
*/
|
||||
export default function ExtensionRow({ item, busy, onRemove }: Props) {
|
||||
const state = !item.enabled
|
||||
? "Off for every bot"
|
||||
: item.state == "error"
|
||||
? "Not working"
|
||||
: item.state == "untested"
|
||||
? "Not checked yet"
|
||||
: "";
|
||||
: item.kind == "skill"
|
||||
? ""
|
||||
: item.state == "error"
|
||||
? "Not working"
|
||||
: item.state == "untested"
|
||||
? "Not checked yet"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<SettingsListRow
|
||||
avatar={<IconTile Icon={extensionIcon({ kind: item.kind })} />}
|
||||
title={item.name}
|
||||
href={item.kind == "mcp" ? "/admin/settings/mcps" : "/admin/settings/tools"}
|
||||
href={ExtensionHrefs[item.kind]}
|
||||
subtitle={[item.detail, state].filter(Boolean).join(" · ")}
|
||||
>
|
||||
{onRemove ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import { Plug, Wrench } from "lucide-react";
|
||||
import { BookOpen, Plug, Wrench } from "lucide-react";
|
||||
import ExtensionAddSelect from "@/src/components/general/extension-add-select";
|
||||
import useExtensions from "@/src/hooks/use-extensions";
|
||||
import type { BotExtensionsPayload } from "@/src/types";
|
||||
@@ -17,16 +17,16 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The MCP servers and tools a bot or a workspace has, removable, and a
|
||||
* picker to add more. On the bot and workspace settings pages.
|
||||
* The MCP servers, tools and skills a bot or a workspace has, removable,
|
||||
* and a picker to add more. On the bot and workspace settings pages.
|
||||
*/
|
||||
export default function ExtensionsGroup({ owner, owner_name, description, initial }: Props) {
|
||||
const extensions = useExtensions({ owner, owner_name, initial });
|
||||
const id = "bot_id" in owner ? `bot-${owner.bot_id}` : `workspace-${owner.workspace_id}`;
|
||||
|
||||
return (
|
||||
<SettingsGroup id="tools" title="Tools and MCP servers" description={description}>
|
||||
<SettingsList is_empty={!extensions.attached.length} empty="No tools or MCP servers yet.">
|
||||
<SettingsGroup id="tools" title="Tools, MCP servers and skills" description={description}>
|
||||
<SettingsList is_empty={!extensions.attached.length} empty="No tools, MCP servers or skills yet.">
|
||||
{extensions.attached.map((item) => (
|
||||
<ExtensionRow
|
||||
key={`${item.kind}:${item.id}`}
|
||||
@@ -73,6 +73,18 @@ export default function ExtensionsGroup({ owner, owner_name, description, initia
|
||||
>
|
||||
Tools
|
||||
</Button>
|
||||
<Button
|
||||
title="Manage skills"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
href="/admin/settings/skills"
|
||||
beforeIcon={<BookOpen size={14} aria-hidden />}
|
||||
className="gap-1!"
|
||||
>
|
||||
Skills
|
||||
</Button>
|
||||
</Row>
|
||||
<SettingsStatus status={extensions.status} />
|
||||
</SettingsGroup>
|
||||
|
||||
Reference in New Issue
Block a user