A provider is a data source that produces kits — bundles of %SkillKit.Skill{}
structs and agent definitions. SkillKit.Catalog queries providers on every request
and aggregates their kits in real time.
The Provider Behaviour
Any module that implements SkillKit.Kit.Provider is a valid provider:
@callback list_kits(config :: keyword()) :: {:ok, [SkillKit.Kit.t()]} | {:error, term()}
@callback get_kit(config :: keyword(), name :: String.t()) ::
{:ok, SkillKit.Kit.t()} | {:error, :not_found}
@optional_callbacks [load_kits: 1]list_kits/1 is the primary callback. It receives the config keyword list you supply
when registering the provider and must return {:ok, kits} or {:error, reason}.
get_kit/2 fetches a single kit by name. The default implementation calls
list_kits/1 and searches the result, so you only need to override it for
performance-sensitive cases.
load_kits/1 is a legacy callback retained for backwards compatibility. New
providers should implement list_kits/1 instead.
A %SkillKit.Kit{} wraps:
:name— a string identifier for the bundle (e.g."files"):skills— list of%SkillKit.Skill{}structs:agents— list of agent definitions (optional):metadata— arbitrary map
The "Always Fresh" Model
SkillKit.Catalog calls list_kits/1 on every query — there is no internal
cache. This means the catalog always reflects the live state of its providers.
Dynamic sources like Kit.Memory work naturally as a result: skills added at
runtime appear immediately on the next request without any cache invalidation.
Built-in: Filesystem Provider
SkillKit.Kit.Local loads kits from directories on disk. Each directory
becomes one kit; the kit name is the directory's basename.
Config key: :dir — an absolute directory path.
{SkillKit.Kit.Local, dir: "/app/skills/files"}Directory structure
my_kit/ ← becomes kit "my_kit"
AGENT.md ← root agent (optional)
skills/
read/SKILL.md ← skill "read"
write/SKILL.md ← skill "write"
agents/
summarize.md ← sub-agent definition (optional)Skill file format
Each SKILL.md file uses YAML frontmatter followed by the skill body:
---
name: read
description: Read the contents of a file at the given path.
---
Read the file at $ARGUMENTS and return its full contents.Required frontmatter fields: name, description. The skill is registered
under the fully-qualified name "<kit_name>:<skill_name>" (e.g. "files:read").
Parse failures are logged as warnings and skipped; the rest of the kit still loads.
Built-in: GitHub Provider
SkillKit.Kit.GitHub imports skills from GitHub repositories at runtime. Repos
are downloaded as tarballs, cached locally, and loaded via Kit.Local. The
provider ships built-in skills so agents can import repos mid-conversation.
{SkillKit.Kit.GitHub,
allowed_sources: ["paper-crow/*", "community/tools"],
api_token: {:env, "GITHUB_TOKEN"},
cache_dir: "/tmp/skill_kit/github"
}See the SkillKit.Kit.GitHub moduledoc for reference format, allowed sources
patterns, cache behaviour, and built-in skill details.
Built-in: In-Memory Provider
SkillKit.Kit.Memory is an Agent-backed provider for testing and dynamic skill
injection. Skills can be added or removed at runtime and are visible to the catalog
on the very next query.
{:ok, mem} = SkillKit.Kit.Memory.start_link([])
SkillKit.Kit.Memory.put(mem, %SkillKit.Skill{
name: "greet:hello",
description: "Say hello.",
body: "Say hello to $ARGUMENTS."
})
# Remove a skill by fully-qualified name
SkillKit.Kit.Memory.delete(mem, "greet:hello")Pass the pid (or registered name) as the :provider key in the provider config:
{SkillKit.Kit.Memory, provider: mem}Skills without a namespace separator are grouped under a bare-name kit. Skills
with a "namespace:skill" name are grouped into a kit named by the namespace.
Module-backed Kits
use SkillKit.Kit turns an Elixir module into a provider that reads and parses
all SKILL.md and AGENT.md files at compile time — no runtime filesystem
access is needed. The module implements both SkillKit.Kit.Provider (to supply
skills) and SkillKit.Tool (to execute them).
defmodule MyApp.FilesKit do
use SkillKit.Kit
@impl SkillKit.Tool
def execute(%SkillKit.ToolExecution{} = execution) do
# handle skill execution
end
endDirectory layout
The kit root is the directory containing the module's source file by default.
Skills live in a skills/ subdirectory; an optional AGENT.md at the root
defines the kit's agent identity:
lib/my_app/files_kit/
files_kit.ex ← defmodule MyApp.FilesKit
AGENT.md ← root agent definition (optional)
skills/
read/SKILL.md
write/SKILL.mdOptions
| Option | Default | Description |
|---|---|---|
:path | directory of the source file | Absolute path to the kit root. Skills are loaded from <path>/skills/ and AGENT.md from <path>/AGENT.md. |
:name | last module segment, underscored | Override the inferred kit name. |
use SkillKit.Kit, name: "files", path: "/abs/path/to/kit"Compile-time loading
All SKILL.md and AGENT.md files are read and parsed during compilation.
Parsed structs are stored as module attributes and served from memory at
runtime. Each file is registered as an @external_resource, so the module
recompiles automatically when any skill or agent file changes during
development.
agent_definition/0
Kit modules expose an agent_definition/0 function that returns the parsed
%SkillKit.Agent{} from the kit's AGENT.md, or nil if no
agent file exists. This is useful for passing a kit's agent definition
directly to SkillKit.start_agent/2:
definition = MyApp.FilesKit.agent_definition()
{:ok, agent} = SkillKit.start_agent(definition, caller: self())Generated callbacks
The macro generates default implementations for definition/0, resume/3,
load_kits/1, list_kits/1, get_kit/2, and agent_definition/0. All are
overridable. You must supply execute/1.
Registering Providers as Sources
Pass a :providers list to SkillKit.Catalog.start_link/1 (or embed it in your
supervision tree via SkillKit.start_agent/2). Each entry is a
{provider_module, config} tuple:
children = [
{SkillKit.Catalog,
name: MyApp.Catalog,
providers: [
{SkillKit.Kit.Local, dir: "/app/priv/skills"},
{MyApp.FilesKit, []},
{MyApp.DatabaseProvider, repo: MyApp.Repo}
]}
]
Supervisor.start_link(children, strategy: :one_for_one)Provider failures emit a Logger.warning but do not prevent the catalog from
starting or serving other providers.
Writing a Custom Provider
Implement SkillKit.Kit.Provider and return %SkillKit.Kit{} structs:
defmodule MyApp.DatabaseProvider do
@behaviour SkillKit.Kit.Provider
alias MyApp.Repo
alias MyApp.SkillRecord
alias SkillKit.Kit
alias SkillKit.Skill
@impl true
def list_kits(config) do
repo = Keyword.fetch!(config, :repo)
skills =
repo.all(SkillRecord)
|> Enum.map(&to_skill/1)
kit = %Kit{name: "db", skills: skills}
{:ok, [kit]}
rescue
exception -> {:error, exception}
end
@impl true
def get_kit(config, name) do
case list_kits(config) do
{:ok, kits} -> find_kit(kits, name)
error -> error
end
end
defp find_kit(kits, name) do
case Enum.find(kits, &(&1.name == name)) do
nil -> {:error, :not_found}
kit -> {:ok, kit}
end
end
defp to_skill(%SkillRecord{} = record) do
%Skill{
name: "db:#{record.slug}",
namespace: "db",
description: record.description,
body: record.body,
handler: MyApp.DatabaseHandler
}
end
endThen register it as a source:
{MyApp.DatabaseProvider, repo: MyApp.Repo}Any error returned from list_kits/1 (or raised and rescued) is logged and
the provider is skipped without crashing the catalog.