skill

js/ai/skill.ts

fino:ai/skill — lazy instructions, resources, and tools for agents.

Skills package optional capabilities behind a manifest. An agent configured with a SkillRegistry sees only each skill's name and description in its system prompt, then can call the generated load_skill tool to fetch the full instructions, resources, and bundled tools when they are relevant.

Design

This keeps large or specialized instructions out of the default prompt while preserving discoverability. Skill instructions and resources may be static or loaded lazily. Tool definitions bundled with a skill are registered after the skill is loaded, and a fresh agent over the same messages can re-register those tools from the load result for resume safety.

Remote SkillsMD skills use the same registry path as local skills. A skill declared with repo: 'owner/repo' appears in the manifest immediately using placeholder metadata, then fetches and parses the repository's skill markdown only when load_skill asks for it.

Skills are not a sandbox or trust boundary. Only register skills from code you trust, and treat loaded instructions as part of the prompt surface.

import { agent } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';
import { skill, skillRegistry } from 'fino:ai/skill';

const registry = skillRegistry([
  skill({
    name: 'support',
    description: 'Support-ticket writing guidance.',
    instructions: 'Ask for missing account details before promising refunds.',
    tools: {},
  }),
  skill({ repo: 'acme/support-writing' }),
]);

const bot = agent({ model: openai({ model: 'gpt-4o' }), skills: registry });

Interfaces

interface SkillResource {

Named resource bundled with a skill.

When a skill is loaded, every resource is resolved and appended to the skill's instructions under a ## Resources heading, each as its own ### name section. Because resolution only happens at load time, resources are a good place for large reference material that should not weigh on the agent until the skill is actually needed.

import { skill } from 'fino:ai/skill';

const research = skill({
  name: 'research',
  description: 'Research briefing workflow.',
  instructions: 'Summarize the brief before proposing next steps.',
  resources: [
    { name: 'style.md', content: '# Style\nWrite in plain language.' },
    {
      name: 'brief.md',
      content: async () => (await fetch('https://example.com/brief.md')).text(),
    },
  ],
});

Properties

name: string

Heading the resource content is filed under in the loaded instructions.

content: string | (() => Promise<string>)

Resource text, either inline or produced lazily when the skill loads.

A function form is awaited once per registry cache entry; concurrent loads of the same skill share the result.

interface Skill {

Lazy-loadable agent capability.

Only name and description are surfaced to an agent up front (via the registry manifest); instructions, resources, and tools stay dormant until the skill is loaded. Construct skills with skill() rather than implementing this interface by hand — remote skills carry internal state that only skill() wires up.

import { skill, skillRegistry } from 'fino:ai/skill';
import type { Skill } from 'fino:ai/skill';

const reviewer: Skill = skill({
  name: 'review',
  description: 'Code-review checklist and tone.',
  instructions: async () => 'Point out risky changes before style nits.',
});

const registry = skillRegistry([reviewer]);

Readonly Properties

readonly name: string

Unique identifier the agent passes to load_skill.

For remote skills this starts as the repo name and may be replaced by the fetched frontmatter's name after the first load.

readonly description: string

Short summary shown in the agent's system prompt before the skill loads.

This is the model's only signal for deciding whether to load the skill, so make it concrete about when the skill applies.

readonly instructions: string | (() => Promise<string>)

Full guidance revealed when the skill is loaded.

A function form is awaited lazily at load time, which is how remote skills defer their network fetch.

readonly tools: Record<string, Tool>

Tools the agent registers once the skill is loaded.

Remote skills always have an empty tool map — markdown fetched from a repository never yields executable code.

readonly resources: SkillResource[]

Reference material appended to the instructions at load time.

interface SkillsMdOptions {

Options for SkillsMD-backed remote skills.

All fields are optional; the defaults talk to the public SkillsMD API with the global fetch and a five-minute instruction cache.

import { skill } from 'fino:ai/skill';

const remote = skill({
  repo: 'acme/support-writing',
  skillsmd: {
    apiBaseUrl: 'https://skillsmd.internal/api',
    cacheTtlMs: 60_000,
  },
});

Properties

apiBaseUrl?: string

Base URL for the SkillsMD API.

Defaults to https://skillsmd.dev/api.

fetch?: typeof globalThis.fetch

Fetch implementation used for SkillsMD and GitHub requests.

Tests and embedded runtimes can inject a custom implementation.

cacheTtlMs?: number

Registry cache duration for loaded remote skill instructions.

Defaults to five minutes. Local skills continue to use the registry's normal stable cache.

interface SkillRegistry {

Registry of skills available to an agent.

Pass a registry as skills when constructing an agent: the agent puts manifest() in its system prompt, registers asLoaderTool() so the model can pull skills in on demand, and calls load() to re-register bundled tools for any load_skill calls it finds in resumed message history.

Load results are cached per registry. Local skills are cached for the registry's lifetime; remote skills expire after their configured TTL. Concurrent loads of the same skill share one in-flight promise, and a failed load is evicted so the next attempt retries from scratch.

import { skill, skillRegistry } from 'fino:ai/skill';

const registry = skillRegistry()
  .add(skill({
    name: 'billing',
    description: 'Refund and invoice policies.',
    instructions: 'Never promise a refund without an order id.',
  }))
  .add(skill({ repo: 'acme/support-writing' }));

registry.manifest(); // [{ name: 'billing', ... }, { name: 'support-writing', ... }]
const { instructions, tools } = await registry.load('billing');

Methods

add(...skills: Skill[]): this

Register additional skills, replacing any existing skill with the same name. Returns the registry so calls can be chained.

manifest(): { name: string; description: string; }[]

List the name and description of every registered skill — the only metadata an agent sees before loading.

Remote skills that have not been loaded yet report placeholder metadata derived from their repo identifier; their entries update once a load fetches the real frontmatter.

load(name: string): Promise<{ instructions: string; tools: Record<string, Tool>; }>

Resolve a skill to its full instructions (resources appended) and bundled tools, fetching remote markdown if needed.

Throws if no skill with that name is registered, and rejects if a remote skill's markdown cannot be fetched or parsed.

asLoaderTool(): Tool

Build the load_skill tool the agent exposes to the model.

The tool takes a skill name, delegates to load(), and returns the loaded instructions as its result; the agent runtime takes care of registering the skill's bundled tools for subsequent steps.

interface LocalSkillDefinition {

Local skill definition accepted by skill().

Everything about a local skill is supplied in code: metadata, instructions (inline or lazy), and optionally executable tools and resources.

import { skill } from 'fino:ai/skill';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const deploys = skill({
  name: 'deploys',
  description: 'Rollout and rollback procedures.',
  instructions: 'Check the error budget before any rollout.',
  tools: {
    rollback: tool({
      name: 'rollback',
      description: 'Roll a service back to the previous release.',
      parameters: v.object({ service: v.string() }),
      execute: async ({ service }) => `rolled back ${service}`,
    }),
  },
});

Properties

name: string

Unique skill name; also the argument agents pass to load_skill.

description: string

One-line summary shown in the agent's system prompt.

instructions: string | (() => Promise<string>)

Full guidance, inline or produced lazily when the skill first loads.

tools?: Record<string, Tool>

Tools registered with the agent once the skill loads. Defaults to none.

resources?: SkillResource[]

Reference material appended to the instructions at load time.

interface RemoteSkillDefinition {

SkillsMD-backed remote skill definition accepted by skill().

Only repo is required. The skill appears in the registry manifest immediately with placeholder metadata and fetches the repository's skill markdown on first load. Instructions are the markdown body below the YAML frontmatter; remote skills never carry executable tools or resources.

import { skill } from 'fino:ai/skill';

const remote = skill({
  repo: 'acme/support-writing',
  description: 'House style for support replies.',
});

Properties

repo: string

GitHub owner/repo identifier of the skill repository.

Must be a bare identifier — URLs and extra path segments are rejected.

name?: string

Manifest name override. When omitted, the repo name is used until the fetched frontmatter supplies one; when set, frontmatter never replaces it.

description?: string

Manifest description override. Same precedence as name: an explicit value is never replaced by fetched metadata.

skillsmd?: SkillsMdOptions

Transport and caching options for the SkillsMD fetch.

Functions

function skill(def: LocalSkillDefinition | RemoteSkillDefinition): Skill

Create a skill definition.

A definition with a repo field builds a remote SkillsMD skill; anything else is a local skill. Local skills are returned essentially as given, with tools and resources defaulting to empty. Remote skills defer all network work to load time: loading queries the SkillsMD API, falls back to the repository's skill.md/SKILL.md via the GitHub contents API, then to a README with frontmatter, and rejects if no frontmattered markdown is found anywhere.

Throws immediately if a remote repo is not a bare owner/repo identifier.

import { skill } from 'fino:ai/skill';

const local = skill({
  name: 'triage',
  description: 'Incident triage steps.',
  instructions: async () => 'Page the on-call before mitigating.',
});

const remote = skill({ repo: 'acme/incident-runbooks' });

function skillRegistry(skills?: Skill[]): SkillRegistry

Create a registry of skills that an agent can load on demand.

The optional initial list can be extended later with add(). Local and remote skills mix freely in one registry and load through the same load_skill tool.

import { agent } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';
import { skill, skillRegistry } from 'fino:ai/skill';

const registry = skillRegistry([
  skill({
    name: 'billing',
    description: 'Refund and invoice policies.',
    instructions: 'Never promise a refund without an order id.',
  }),
]);

const bot = agent({ model: openai({ model: 'gpt-4o' }), skills: registry });