# Skills support Status: design approved, pending user review ## Problem llamachat can already do web search through a single hardcoded tool. It has no way to hand the model a body of reusable instructions, the "skills" a user of this app already keeps for other agents. Those skills live in `~/.agents/skills//SKILL.md` and are plain markdown: a YAML frontmatter with `name` and `description`, then the instructions. This adds a skills subsystem: llamachat reads that directory, lets the model load a skill on demand through a tool call, and lets the user load one by typing `/name` or mentioning the name naturally. A loaded skill's text stays in context for the whole conversation in chat mode. One-shot mode keeps it in memory (never in history) until New or the chip is clicked, so a skill loaded in a scratch window stays active for the rest of it. ## Skills directory and format Skills are read from a single directory, default `~/.agents/skills`. Each skill is a `SKILL.md` file inside its own subdirectory: ``` ~/.agents/skills/ firefly-cli/SKILL.md test-build-slackbuild/SKILL.md ... ``` A file starts with a YAML frontmatter block between `---` lines: ``` --- name: firefly-cli description: Operate a Firefly III instance ... --- # firefly-cli ... instructions ... ``` The `name` is the skill's identity, `description` the one-line summary shown to the model. Only these two keys are read. If the frontmatter is absent or has no `name`, the directory name is used. Parsing is a small line scanner in `skills.py`, no YAML dependency. A skill whose file cannot be read is skipped rather than failing startup. ## Configuration Two keys, both on by default because the directory already exists and is populated: ```toml skills_enabled = true skills_dir = "~/.agents/skills" ``` `skills_dir` is expanded and resolved at load. A flag whose directory does not exist resolves to off, so an upgrade never starts injecting tools or reading files that are not there. This mirrors the web search rule (a flag with no URL stays off). ## New module: skills.py `SkillStore` mirrors `prompts.PromptStore`: - `__init__(dir)` - `names()` — skill names, sorted - `load(name)` — the `Skill` (name, description, text), or `None` - `directory()` — compact `name — description` lines for the tool schema - `match_mentions(text)` — the skill names that appear in `text` as whole words, case-insensitive - `tool_schema()` — the OpenAI tool schema for `load_skill` Descriptions are truncated to one sentence in `directory()` so listing nine skills does not bloat every request. ## Backend: generalizing the tool loop `backend.Client.stream_chat` currently offers only `web_search` and its loop is written around that one tool. It becomes a dispatch over a small set of tools. New signature: ``` stream_chat(model, messages, search_cfg=None, skills_cfg=None) ``` `SkillsConfig` carries `enabled: bool` and a `SkillStore`. The tools offered in a round are `web_search` (when search is on) plus `load_skill` (when skills are on). The `wanted` filter stops matching the one search name and instead accepts every tool call the model made, routing each by name: - `web_search` -> `_run_search` (unchanged) - `load_skill` -> `_run_skill` - anything else -> a tool message saying the tool does not exist `_run_skill` loads the named skill and appends its text as the tool result. When it succeeds it also yields `("skill_loaded", name)` so the UI can record the load. The full text is prefixed with a short header (`[skill: ]`) so the model sees which skill it now holds. The `max_searches` cap still governs the loop, now as "tool rounds per turn", so a model cannot load skills forever. The final round's "this was your last search" note is generalised to a tool-neutral "this was your last tool use" message, phrased per tool. ## UI ### Injection `ui._system_messages()` already assembles the system prompt from the chosen prompt plus the search date note. It gains a third part: the body of every loaded skill, appended under a header, in load order: ``` [skill: firefly-cli] [skill: test-build-slackbuild] <...> ``` A skill whose file has since been deleted or renamed contributes nothing. ### Loaded-skill state - Chat mode: the set of loaded skill names is stored on the session and restored on reopen, exactly like `prompt_name`. - One-shot mode: the set lives in memory only and is cleared when the reply finishes. The UI learns a skill was loaded two ways: the `skill_loaded` stream event from a tool call, or the user path below. Either way the set updates, the chips refresh, and (chat mode) the session is persisted. ### User path Before a message is sent, the typed text is scanned: - `/name` tokens load that skill and are stripped from the message before it is stored and sent. - A skill name appearing as a whole word, case-insensitive, loads it and the word is left in place. Both go through the same load routine, so `/firefly-cli` and a sentence that naturally mentions `firefly-cli` behave identically. ### Chips A small row of removable chips shows the loaded skills, visible only when at least one is loaded. Clicking a chip unloads that skill. Placement is in the top bar area, using the existing widget patterns. ## Database `sessions` gains one column: ```sql skills TEXT NOT NULL DEFAULT '' ``` It holds a JSON array of loaded skill names. Added to the `CREATE TABLE` and to the existing `_migrate` loop, so old databases gain it on open. A `set_skills(session_id, names)` method writes it; reopening reads it back via the existing `get_session`. ## Testing Extend `test_llamachat.py` in its existing assert style: - `skills.py`: frontmatter parsing with and without `name`, directory-name fallback, whole-word matching (including hyphenated names), description truncation. - `backend.py`: a `load_skill` tool round returns the body and yields `skill_loaded`; an unknown tool name returns an error tool message; the final-round withdrawal still forces an answer when only skills are offered. ## Out of scope - Executable skills (shell, file access) — this is instruction text only, not new capabilities the model can act on. - RAG / embedding over skill contents. - Skills outside the one configured directory.