There’s a quiet cost to giving an agent lots of tools or workflows: each one consumes context. With ten skills at ~400 tokens each, you’ve burned 4 K tokens before the model has done anything. With thirty, you’re at 12 K. On a phone, where you’re running a 1.7B–8B on-device model with a 4 K–8 K context, this is a problem.

Skills solve it with progressive disclosure.

What the model sees

At the start of a run, the orchestrator builds a system prompt that includes a skills index — just the name and description for each skill. That’s typically 20–30 tokens per skill. With 30 skills, that’s ~600 tokens.

# Available skills

- `summarise-email` — Summarise an email into 3 bullet points.
- `triage-notifications` — Score notifications by urgency.
- `compose-reply` — Draft a reply in the user's voice.
- `extract-receipt` — Parse a photo of a receipt into structured fields.
- ... (26 more)

That’s it. The bodies — the actual step-by-step instructions, the tool usage hints, the example outputs — are nowhere in context. They sit on disk at /workspace/.skills/<name>/SKILL.md.

What happens on invocation

When the model decides it wants a skill, it emits a tool call:

Sh("skill invoke summarise-email")
// or directly
{ "tool": "skill.invoke", "args": { "name": "summarise-email" } }

The skill.invoke tool reads the bundle, injects its body as a new system-message turn, and the model continues with the skill’s instructions front-and-center. After the skill finishes (often a single turn or a few), the body is evicted from context — only the skill’s output is retained.

So a 30-skill catalog uses 600 tokens at the index level, plus 400 tokens for whichever single skill is active. 1 K tokens total instead of 12 K.

Writing a skill

A skill is a directory with at least a SKILL.md:

---
name: summarise-email
description: Summarise an email into 3 bullet points.
---

# Steps

1. Read the email body via `/bin/gmail.show --id $1`.
2. Extract the subject, sender, and the 3 most important sentences.
3. Emit JSON: `{ subject, from, bullets: [...] }`.

# Tools

- `/bin/gmail.show`
- `/bin/jq`

# Example output

\`\`\`json
{
  "subject": "Q3 board prep",
  "from": "ceo@example.com",
  "bullets": [
    "Q3 numbers come in Tuesday — board deck due Wednesday morning.",
    "Marketing's launch slipped — replace section 4 with a roadmap slide.",
    "Investor list needs updating — add the two new participants from June."
  ]
}
\`\`\`

That’s all that’s needed. The orchestrator picks it up next time it scans /workspace/.skills/.

Versioning and discovery

Skills are just files. Ship them in the app bundle, sync them from a server, let the user write their own. agentlib doesn’t care; it just scans the directory.

For app-shipped skills, copy them into the workspace on first launch:

final dir = Directory('${docsDir.path}/workspace/.skills/summarise-email');
await dir.create(recursive: true);
final asset = await rootBundle.loadString('assets/skills/summarise-email.md');
await File('${dir.path}/SKILL.md').writeAsString(asset);

For user-written or remotely-synced skills, just write the files; the next agent run sees them.

Skill composition

A skill’s body can call out to other skills:

# Steps

1. If the email is from the user's manager, invoke `triage-urgent` first.
2. Otherwise summarise per the standard pattern.
3. If the summary is longer than 3 bullets, invoke `compress-bullets`.

The orchestrator handles the dispatch. This lets you build a small tree — one “front-door” skill that knows when to delegate.

When skills are wrong

Skills are great for procedures the agent does often. They’re not the right shape for:

  • Data (use the VFS — /workspace/<file>).
  • Tools (register a CLI in /bin/).
  • System prompt material that should always be in context (just put it in the orchestrator’s instructions).
  • Per-user memory (use /memory — the SQLite-backed mount).

If you find yourself loading the same skill on every turn, it’s probably not a skill — it’s an instruction.

Reference

  • The default skills directory is configured on RunConfig. Override with RunConfig(skillsDir: ...).
  • SkillRegistry is the in-memory cache. Call registry.reload() if you wrote a new skill mid-run.
  • Skill invocation goes through the same hook pipeline as any tool call — PreToolUse and PostToolUse fire on skill.invoke.