skills

Know-how an agent can read when it needs it, and extensions a user can drop in.

A skill

A skill is package documentation or a SKILL.md file that an agent reads on demand. Both sources produce a Skill.

Skill stores a body loader, avoiding eager imports and file reads during discovery. Its description is the first paragraph collapsed to one line.

GROUP is the entry-point group installed packages publish under. EXTRA_MODULES is the short list of modules that document themselves without publishing one.

GROUP, EXTRA_MODULES, MAX_SKILL_CHARS
('pyskills', ('exhash.skill',), 20000)
test_eq(GROUP, 'pyskills')
test_eq(EXTRA_MODULES, ('exhash.skill',))
test_eq(MAX_SKILL_CHARS, 20_000)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/usearch/__init__.py:131: UserWarning: Will download `usearch_sqlite` binary from GitHub.
  warnings.warn("Will download `usearch_sqlite` binary from GitHub.", UserWarning)

source

Skill

def Skill(
    name:str, source:str, description:str='', where:str='', _text:object=None
)->None:

One skill: how to name it, when it applies, and how to get the whole text.

_describe('''Search the web and read results into durable memory.

Use it when a question needs pages nobody has read yet.''')
'Search the web and read results into durable memory.'
test_eq(_describe(''), '')
test_eq(_describe('one\ntwo\n\nthree'), 'one two')
test_eq(len(_describe('x ' * 400)), 300)

The body is fetched once, when something asks for it, and clipped. A skill that cannot be read reports itself as one rather than raising into a turn.

s = Skill('nbdev', 'md', 'Develop nbdev projects.', 'nbs/SKILL.md', _text=lambda: 'the whole body')
def explodes(): raise FileNotFoundError('SKILL.md')
broken = Skill('gone', 'md', _text=explodes)
s.text(), broken.text()
('the whole body', 'could not read skill gone: FileNotFoundError: SKILL.md')
test_eq(s.text(), 'the whole body')
assert broken.text().startswith('could not read skill gone'), broken.text()
big = Skill('big', 'md', _text='x' * (MAX_SKILL_CHARS + 500))
test_eq(len(big.text()), MAX_SKILL_CHARS + len('\n…[500 more chars]'))
test_eq(s.dict()['where'], 'nbs/SKILL.md')

Where skills come from

Installed packages publish skills through the pyskills entry-point group. Discovery imports a module only when its description comes from the module docstring.

exhash ships a skill module, and it is a dependency here, so discovery finds a real one.

found = _pyskills()
[(s.name, s.source, s.where) for s in found]
[('editskill', 'pyskill', 'fastcore.editskill'),
 ('rgapi', 'pyskill', 'rgapi.skill'),
 ('ghapi', 'pyskill', 'ghapi.skill'),
 ('kosha', 'pyskill', 'kosha.skill'),
 ('exhash', 'pyskill', 'exhash.skill'),
 ('skill', 'pyskill', 'nbdev.skill'),
 ('skill', 'pyskill', 'pyskills.skill'),
 ('skill', 'pyskill', 'llmsurgery.skill'),
 ('fastcdp', 'pyskill', 'fastcdp.skill'),
 ('dlgskill', 'pyskill', 'aidialog.dlgskill'),
 ('vishalakshi', 'pyskill', 'vishalakshi.skill'),
 ('coding_patterns', 'pyskill', 'ramabana.coding_patterns'),
 ('theory', 'pyskill', 'ramabana.theory'),
 ('write_docs', 'pyskill', 'ramabana.write_docs'),
 ('write_prose', 'pyskill', 'ramabana.write_prose')]
assert any(s.where == 'exhash.skill' for s in found), [s.where for s in found]
ex = next(s for s in found if s.where == 'exhash.skill')
assert ex.description, 'a pyskill with no description should have been dropped'
assert len(ex.text()) > 100, len(ex.text())

Files come from a search path in increasing precedence. Personal habits first, then a project’s, so a repository can override what a person set up for themselves.

The tool factory reads the same installed skill discovered through the pyskills entry point.

from shalya.host import LocalHost
from shalya.skills import discover
from shalya.tools import skill_tools

installed_skills = discover()
installed_host = LocalHost(index=False)
installed_read = {t.__name__: t for t in skill_tools(installed_host, lambda: installed_skills)}['read_skill']
installed_exhash = next(s for s in installed_skills if s.where == 'exhash.skill')
installed_body = installed_read(installed_exhash.name)
assert installed_body.startswith(f'<skill name="{installed_exhash.name}"')
assert len(installed_body) > 100 and 'exhash' in installed_body.lower()

source

discover

def discover(
    roots:tuple=(), cfg:NoneType=None, extra:tuple=()
):

Every skill available to this agent. Pyskills, then skill_dirs, then extra. Later winning.


source

skill_dirs

def skill_dirs(
    roots:tuple=(), cfg:NoneType=None
):

Where SKILL.md files are looked for, in increasing precedence: user first. A project can override.

[str(p) for p in skill_dirs(roots=['/proj'], cfg='/home/k/.config/leela')]
['/home/k/.config/leela/skills',
 '/Users/71293/.agents/skills',
 '/proj/.leela/skills',
 '/proj/.agents/skills']
ds = [str(p) for p in skill_dirs(roots=['/proj'], cfg='/cfg')]
test_eq(ds[0], '/cfg/skills')
test_eq(ds[-1], '/proj/.agents/skills')

Written out, a skill directory follows the Agent Skills layout: <name>/SKILL.md, with frontmatter supplying the name and description when it wants to.

tmp = Path(tempfile.mkdtemp())
d = tmp/'.agents'/'skills'/'notebook-tests'
d.mkdir(parents=True)
(d/'SKILL.md').write_text('''---
name: notebook-tests
description: Write executable test cells in the notebook that owns the code.
---

Put the readable case in the notebook and the bulk assertions in `tests/`.
''')
plain = tmp/'.agents'/'skills'/'plain'
plain.mkdir()
(plain/'SKILL.md').write_text('Say what changed.\n\nAnd nothing else.\n')
[(s.name, s.description) for s in _md_skills(tmp/'.agents'/'skills')]
[('notebook-tests',
  'Write executable test cells in the notebook that owns the code.'),
 ('plain', 'Say what changed.')]
got = {s.name: s for s in _md_skills(tmp/'.agents'/'skills')}
test_eq(sorted(got), ['notebook-tests', 'plain'])
test_eq(got['plain'].description, 'Say what changed.')     # no frontmatter: the first paragraph
assert 'bulk assertions' in got['notebook-tests'].text()
test_eq(got['notebook-tests'].source, 'md')

discover merges every source, with a later one winning a name clash.

skills = discover(roots=[tmp])
[s.name for s in skills]
['coding_patterns',
 'design-taste-frontend',
 'dlgskill',
 'editskill',
 'exhash',
 'fastcdp',
 'find-skills',
 'full-output-enforcement',
 'ghapi',
 'hf-cli',
 'high-end-visual-design',
 'industrial-brutalist-ui',
 'kosha',
 'minimalist-ui',
 'nbdev',
 'notebook-tests',
 'plain',
 'redesign-existing-projects',
 'rgapi',
 'skill',
 'stitch-design-taste',
 'theory',
 'vishalakshi',
 'write_docs',
 'write_prose']
names = [s.name for s in skills]
assert 'notebook-tests' in names and 'exhash' in names, names
test_eq(names, sorted(names))
mine = discover(roots=[tmp], extra=[Skill('plain', 'ext', 'Mine wins.')])
test_eq(next(s for s in mine if s.name == 'plain').description, 'Mine wins.')

The index

What goes in the system prompt is names and clipped descriptions, never bodies. One verbose skill cannot crowd out the rest.


source

find

def find(
    skills, name
):

A skill by exact name, then unique prefix, then unique substring. Ambiguity is None, not a guess.


source

skill_index

def skill_index(
    skills
):

Render skill names and clipped descriptions for the system prompt.

print(skill_index([s for s in skills if s.name == 'notebook-tests']))


## Skills

Know-how available to you. Read one with `read_skill(name)` when its description matches what you are about to do, *before* you do it -- several of these describe tools already installed in this environment, so the code they discuss is also searchable with `search_code`.

- `notebook-tests` -- Write executable test cells in the notebook that owns the code.
ix = skill_index([s for s in skills if s.name == 'notebook-tests'])
assert '`notebook-tests`' in ix and 'read_skill' in ix
assert 'Put the readable case' not in ix, 'the index must not carry a body'
test_eq(skill_index([]), '')
test_eq(len(_clip_desc('word ' * 60)), SKILL_DESC_MAX)

A name resolves exactly, then by unique prefix, then by unique substring. Ambiguity answers None rather than guessing, and the tool that called it then lists what there is.

find(skills, 'notebook-tests').name, find(skills, 'notebook').name, find(skills, 'nope')
('notebook-tests', 'notebook-tests', None)
test_eq(find(skills, 'notebook-tests').name, 'notebook-tests')
test_eq(find(skills, 'notebook').name, 'notebook-tests')     # unique prefix
test_eq(find(skills, 'tests').name, 'notebook-tests')        # unique substring
test_eq(find(skills, 'nope'), None)
two = [Skill('edit', 'md'), Skill('editor', 'md')]
test_eq(find(two, 'edit').name, 'edit')                      # exact beats prefix
test_eq(find(two, 'edi'), None)                              # ambiguous prefix

Extensions

An extension is a Python file a user drops in a directory. Registry is everything it may add, and it is deliberately not a route to anything else: no backend, no engine, no history.

Extensions may hook only the listed lifecycle events. Unknown event names are rejected.

EVENTS
('before_turn',
 'after_turn',
 'before_tool',
 'after_tool',
 'compact',
 'approval')
test_eq(EVENTS, ('before_turn', 'after_turn', 'before_tool', 'after_tool', 'compact', 'approval'))
test_eq(len(set(EVENTS)), len(EVENTS))

source

load

def load(
    reg, roots:tuple=(), cfg:NoneType=None, project:bool=False, paths:tuple=()
):

Load extensions and call each setup(reg).


source

ext_dirs

def ext_dirs(
    roots:tuple=(), cfg:NoneType=None, project:bool=False
):

Return enabled extension directories.


source

Registry

def Registry(
    host:NoneType=None, agent:NoneType=None
):

Extension registration surface.

A real extension, with a real setup(reg), loaded off disk.

extdir = tmp/'extensions'
extdir.mkdir()
(extdir/'wc.py').write_text('''
def setup(reg):
    @reg.tool
    def word_count(path: str) -> str:
        "How many words are in a file."
        return str(len(open(path).read().split()))
    reg.command('wc', lambda agent, arg: 'counted ' + arg, help='count words')
    reg.skill('house-style', 'Dense lines. Short names.', 'How code is written here.')
''')
(extdir/'helpers.py').write_text('SHARED = 1\n')
(extdir/'boom.py').write_text('raise RuntimeError("no")\n')
reg = load(Registry(), cfg=tmp)
reg.notes
['boom.py: failed to load (RuntimeError: no)',
 'helpers.py: loaded, no setup()',
 'wc.py: 1 tool(s), 1 skill(s), 1 command(s)']
test_eq([t.__name__ for t in reg.tools], ['word_count'])
test_eq(list(reg.commands), ['wc'])
test_eq([s.name for s in reg.skills], ['house-style'])
test_eq(reg.skills[0].text(), 'Dense lines. Short names.')
test_eq(reg.tools[0](str(extdir/'helpers.py')), '3')          # the tool really runs
assert any('helpers.py: loaded, no setup()' in n for n in reg.notes), reg.notes
assert any(n.startswith('boom.py: failed to load') for n in reg.notes), reg.notes

Registering an unknown event raises an error.

test_fail(lambda: reg.on('before_lunch', print), contains='unknown event')

A failing hook is recorded without ending the turn.

fired = []
reg.on('before_turn', lambda **kw: 1/0)
reg.on('before_turn', lambda **kw: fired.append(kw))
reg.fire('before_turn', prompt='hello'), fired, reg.notes[-1]
(1,
 [{'prompt': 'hello'}],
 'before_turn hook failed: ZeroDivisionError: division by zero')
test_eq(fired, [{'prompt': 'hello'}])
assert reg.notes[-1].startswith('before_turn hook failed'), reg.notes[-1]

An extension may replace the approval policy. The last replacement wins.

@reg.approval
def approve_reads(tool, **kw): return not tool.startswith('write_')
reg.approve, reg.approve('view_file'), reg.approve('write_file')
(<function __main__.approve_reads(tool, **kw)>, True, False)
test_eq(reg.approve.__name__, 'approve_reads')
test_eq(reg.approve('view_file'), True)
test_eq(reg.approve('write_file'), False)
test_eq(Registry().approve, None)                         # nothing registered approves everything

Project extension directories require explicit opt-in. Cloning a repository never loads its code.

test_eq([str(p) for p in ext_dirs(roots=['/proj'], cfg='/cfg')], ['/cfg/extensions'])
test_eq([str(p) for p in ext_dirs(roots=['/proj'], cfg='/cfg', project=True)][-1],
        '/proj/.leela/extensions')