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:
2026-10-22 12:31:42 +01:00
parent 379ef35e08
commit 08d02e6033
62 changed files with 1438 additions and 79 deletions
@@ -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>