load_skills
Python · package toolnexus · SPEC §3 · python/src/toolnexus/skill.py
def load_skills( dirs: str | list[str] | None = None, *, skills: list[SkillDef] | None = None, filter: dict[str, bool] | None = None, sample_limit: int = 0,) -> SkillSourceThis is the same load_skills function — Python has one entry point,
not two — scoped here to the part of its surface that is not “walk a directory”: supplying skills
as data (skills=[SkillDef(...)]) with no filesystem involved at all, and narrowing whatever
was discovered — from disk, from data, or both — with a per-agent filter allowlist/drop-list.
When to use it
Section titled “When to use it”- Skills come from somewhere that is not a directory of Markdown files: generated at startup, read from a database row, assembled from a config object.
- One process serves several agents and each should see a different slice of the same skill
catalog —
filternarrows without re-discovering anything. - You want to combine on-disk and in-memory skills in one
SkillSource, because an agent’s capabilities come from both a sharedskills/folder and a few skills generated at runtime.
Why this and not the alternative
Section titled “Why this and not the alternative”For plain on-disk discovery with no data skills and no filter, load_skills (directories)
already covers that shape end-to-end, including a SkillDef + filter + sample_limit example —
this page goes further into the allowlist semantics and data-only sourcing.
Examples
Section titled “Examples”1. Skills as data only — no directory at all
Section titled “1. Skills as data only — no directory at all”dirs=None is valid: the whole catalog comes from skills, each getting a logical
skill://<name>/ base and touching disk never.
import asynciofrom toolnexus import load_skills, SkillDef
source = load_skills( skills=[ SkillDef( name="lookup-order", description="Look up an order by id.", content="# Lookup Order\n\nCall the orders API with the given id.", ), SkillDef( name="cancel-order", description="Cancel an order that has not shipped.", content="# Cancel Order\n\nOnly cancel if status is 'pending'.", ), ],)
assert sorted(source.skills) == ["cancel-order", "lookup-order"]assert all(s.origin == "logical" for s in source.skills.values())
async def main(): res = await source.tool.execute({"name": "lookup-order"}) assert res.is_error is False assert "Base directory for this skill: skill://lookup-order/" in res.output print("ok:", sorted(source.skills))
asyncio.run(main())2. filter as a drop-list — only-False keeps everything else on
Section titled “2. filter as a drop-list — only-False keeps everything else on”The allowlist form ({"x": True}) narrows to just x; this is the other shape — a filter with
only False values drops those names and leaves everything else enabled. Same parameter,
opposite polarity, chosen by what values are present.
import asynciofrom toolnexus import load_skills, SkillDef
source = load_skills( skills=[ SkillDef(name="alpha", description="Alpha skill.", content="a"), SkillDef(name="beta", description="Beta skill.", content="b"), SkillDef(name="internal-debug", description="Internal only, never expose.", content="d"), ], # No True anywhere ⇒ drop-list: only "internal-debug" is removed. filter={"internal-debug": False},)
assert sorted(source.skills) == ["alpha", "beta"]
# An unknown filter name is ignored (and warned on stderr) rather than raising.noisy = load_skills( skills=[SkillDef(name="alpha", description="Alpha skill.", content="a")], filter={"does-not-exist": True, "alpha": True},)assert sorted(noisy.skills) == ["alpha"]
print("ok:", sorted(source.skills))3. Directory skills plus data skills plus a real allowlist, together
Section titled “3. Directory skills plus data skills plus a real allowlist, together”Combine dirs and skills in one call — both feed the same name→SkillInfo map, duplicate
names resolve first-wins (directory candidates are merged before data candidates) — then apply an
allowlist across the combined set.
import asyncioimport osfrom toolnexus import load_skills, SkillDef
source = load_skills( os.path.abspath("examples/skills"), # contributes "hello-world" skills=[ SkillDef(name="escalate", description="Escalate to a human.", content="# Escalate"), SkillDef(name="refund", description="Process a refund.", content="# Refund"), ], # >= 1 True present ⇒ allowlist: only these two names survive, from either source. filter={"hello-world": True, "refund": True},)
assert sorted(source.skills) == ["hello-world", "refund"]assert source.skills["hello-world"].origin == "fs"assert source.skills["refund"].origin == "logical"
# The catalog only lists what filter let through.prompt = source.prompt()assert "- **hello-world**:" in promptassert "- **refund**:" in promptassert "escalate" not in prompt
async def main(): denied = await source.tool.execute({"name": "escalate"}) assert denied.is_error is True print("ok:", sorted(source.skills))
asyncio.run(main())Options
Section titled “Options”| Option | Type | What it does |
|---|---|---|
dirs |
str | list[str] | None |
Directory root(s) to walk for **/SKILL.md, same as load_skills (directories). None means data-only. |
skills |
list[SkillDef] | None |
Skills supplied as data. SkillDef(name, content, description=None, resources=None, base=None). Logical base defaults to skill://<name>/. |
filter |
dict[str, bool] | None |
Per-agent narrowing over the merged dirs+data catalog. None/empty ⇒ all. Any True present ⇒ allowlist (only True names survive). Only False values ⇒ drop-list (those names removed, rest stay on). Unknown names are ignored and warned once on stderr. |
sample_limit |
int |
0 ⇒ default 10 sampled files. n > 0 ⇒ cap at n. -1 ⇒ omit <skill_files> entirely. Applies to both fs and data skills — for data skills the cap is over the SkillDef.resources list rather than a directory walk. |
Returns
Section titled “Returns”Same SkillSource as load_skills — skills, tool, prompt().
See also
Section titled “See also”load_skills— Directory discovery, the<skill_content>envelope, and the baseSkillDef/filter/sample_limitexample.list_skills— Same inputs, no tool built, plus typed skip reasons for what was rejected and why.create_toolkit— Takesskills_dir,skills,skills_filter,skill_sample_limit, andskill_providerfor a lazy source.