Skip to content

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,
) -> SkillSource

This 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.

  • 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 — filter narrows 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 shared skills/ folder and a few skills generated at runtime.

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.

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 asyncio
from 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 asyncio
from 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 asyncio
import os
from 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 prompt
assert "- **refund**:" in prompt
assert "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())
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.

Same SkillSource as load_skillsskills, tool, prompt().

  • load_skills — Directory discovery, the <skill_content> envelope, and the base SkillDef/filter/sample_limit example.
  • list_skills — Same inputs, no tool built, plus typed skip reasons for what was rejected and why.
  • create_toolkit — Takes skills_dir, skills, skills_filter, skill_sample_limit, and skill_provider for a lazy source.