# skills


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## A skill

A skill is package documentation or a `SKILL.md` file that an agent
reads on demand. Both sources produce a
[`Skill`](https://vedicreader.github.io/shalya/skills.html#skill).

[`Skill`](https://vedicreader.github.io/shalya/skills.html#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.

``` python
GROUP, EXTRA_MODULES, MAX_SKILL_CHARS
```

    ('pyskills', ('exhash.skill',), 20000)

``` python
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)

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L31"
target="_blank" style="float:right; font-size:smaller">source</a>

### Skill

``` python
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.*

``` python
_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.'

``` python
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.

``` python
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')

``` python
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.

``` python
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')]

``` python
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.

``` python
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()
```

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L109"
target="_blank" style="float:right; font-size:smaller">source</a>

### discover

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

*Every skill available to this agent. Pyskills, then
[`skill_dirs`](https://vedicreader.github.io/shalya/skills.html#skill_dirs),
then `extra`. Later winning.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L82"
target="_blank" style="float:right; font-size:smaller">source</a>

### skill_dirs

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

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

``` python
[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']

``` python
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.

``` python
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.')]

``` python
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`](https://vedicreader.github.io/shalya/skills.html#discover)
merges every source, with a later one winning a name clash.

``` python
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']

``` python
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.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L135"
target="_blank" style="float:right; font-size:smaller">source</a>

### find

``` python
def find(
    skills, name
):
```

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

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L128"
target="_blank" style="float:right; font-size:smaller">source</a>

### skill_index

``` python
def skill_index(
    skills
):
```

*Render skill names and clipped descriptions for the system prompt.*

``` python
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.

``` python
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.

``` python
find(skills, 'notebook-tests').name, find(skills, 'notebook').name, find(skills, 'nope')
```

    ('notebook-tests', 'notebook-tests', None)

``` python
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`](https://vedicreader.github.io/shalya/skills.html#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.

``` python
EVENTS
```

    ('before_turn',
     'after_turn',
     'before_tool',
     'after_tool',
     'compact',
     'approval')

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

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L204"
target="_blank" style="float:right; font-size:smaller">source</a>

### load

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

*Load extensions and call each `setup(reg)`.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L196"
target="_blank" style="float:right; font-size:smaller">source</a>

### ext_dirs

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

*Return enabled extension directories.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/shalya/blob/main/shalya/skills.py#L148"
target="_blank" style="float:right; font-size:smaller">source</a>

### Registry

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

*Extension registration surface.*

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

``` python
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)']

``` python
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.

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

A failing hook is recorded without ending the turn.

``` python
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')

``` python
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.

``` python
@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)

``` python
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.

``` python
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')
```
