createSkillsMiddleware<
TContext = unknown
>(
optionsCreate backend-agnostic middleware for loading and exposing agent skills.
This middleware loads skills from configurable backend sources and injects skill metadata into the system prompt. It implements the progressive disclosure pattern: skill names and descriptions are shown in the prompt, but the agent reads full SKILL.md content only when needed.
Skills are loaded once per thread and stored in state. To pick up skills
added, edited, or deleted since then, set skillsMetadata to null; the
next model call reloads every source:
await agent.updateState(config, { skillsMetadata: null });
// or as part of the next run's input
await agent.invoke({ messages, skillsMetadata: null }, config);
Loading happens in beforeModel, so the reload is served by the next model
call rather than the next run. A middleware of your own can therefore
invalidate from any hook ā including mid-run, from afterModel ā and see
the fresh list on the following call. See skillsMetadataValue.
To guarantee the model gets a skill's instructions, for example when the
user names it, pass its name in pinnedSkills. Before the next model call,
each named skill's SKILL.md, without its frontmatter, is appended to the
conversation as its own HumanMessage, with additional_kwargs.lc_source
set to "pinned_skill" and the skill's name, path and description
under additional_kwargs.skill:
await agent.invoke(
{
messages: [new HumanMessage("/write-tests /house-style for auth.py")],
pinnedSkills: ["write-tests", "house-style"],
},
config,
);
Pins resolve against the skills loaded for the thread, so reset
skillsMetadata in the same input to pin a skill added since. See
pinnedSkillsValue.
A skill can list the tools its instructions use, separated by spaces, under
metadata.include_tools in its SKILL.md frontmatter:
metadata:
include_tools: create_customer_request list_customer_requests
Pass those tools as tools, either as an array or as a
SkillToolResolver that looks them up by name when a skill is read
or pinned, so one name can stand for a family of tools whose real names are
only known at runtime. The model sees a skill tool only after it reads, with
read_file, a skill that lists it, or the skill is pinned, and only while
that read or pin stays in the conversation it is sent; compaction that drops
the read or pin withdraws the tool.
Until then, calling the tool fails with the standard invalid-tool error and
the tool doesn't run. This controls what is in the model's context; it is
not a security boundary, since the model can read any SKILL.md at any
time.
include_tools can also name a tool passed to the agent rather than to this
middleware. That tool wins over a skill tool of the same name. If it is
deferred (extras: { defer_loading: true }), reading or pinning the skill
discloses it early, and it stays deferred and searchable; if it is bound,
nothing changes.
| Name | Type | Description |
|---|---|---|
options* | SkillsMiddlewareOptions<TContext> |
Configuration options
const middleware = createSkillsMiddleware({
backend: new FilesystemBackend({ rootDir: "/" }),
sources: ["/skills/user/", "/skills/project/"],
});