Skills¶
Skills are folder-based capability packs that teach agents new abilities: markdown instructions that get injected into the system prompt, plus optional tools that get merged into the agent's tool list. OrchestratorBot supports them.
Folder Layout¶
skills/
├── facturacion/
│ ├── SKILL.md # instructions injected into the system prompt
│ └── tools.py # optional BaseTool subclasses
└── reportes/
└── SKILL.md
SKILL.md¶
Plain markdown with optional --- frontmatter (name and description).
Without frontmatter, the folder name is used as the skill name:
---
name: facturacion
description: Genera facturas legales colombianas
---
Cuando el usuario pida una factura:
1. Valida NIT y razón social.
2. Usa la herramienta `generar_factura`.
3. Confirma el número de factura generado.
tools.py (optional)¶
Any BaseTool subclass defined in the file is instantiated and merged
into the agent's tool list (classes merely imported are ignored):
from typing import Type
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
class FacturaSchema(BaseModel):
nit: str = Field(..., description="NIT del cliente")
total: float = Field(..., description="Total de la factura")
class GenerarFacturaTool(BaseTool):
name: str = "generar_factura"
description: str = "Genera una factura legal"
args_schema: Type[BaseModel] = FacturaSchema
def _run(self, nit: str, total: float) -> str:
return f"Factura generada para {nit} por ${total}"
Security
tools.py is imported and executed as arbitrary Python code. Only
load skill directories you trust.
Using Skills¶
OrchestratorBot accepts skills (a list of Skill objects) and/or skills_dir
(a directory scanned for skill folders). Both can be combined:
from sonika_ai_toolkit import OrchestratorBot, Skill, load_skills
# Load everything under a directory
bot = OrchestratorBot(
strong_model=model,
fast_model=model,
instructions="Eres un asistente de negocio.",
skills_dir="./skills",
)
# Or load/build skills explicitly
skill = Skill.from_dir("./skills/facturacion")
programmatic = Skill(
name="saludos",
instructions="Saluda siempre con el nombre del usuario.",
tools=[], # optional BaseTool instances
)
bot = OrchestratorBot(
strong_model=model,
fast_model=model,
instructions="Eres un asistente.",
skills=[skill, programmatic],
)
On-demand loading (progressive disclosure) — the default¶
By default, skills load on demand so you don't pay for every skill's full body on every turn. This follows the progressive-disclosure model:
- Level 1 — index (always in the prompt): only a lightweight
## SKILLSblock with one- name — descriptionline per skill is injected. This is what the model reads to decide whether a skill is relevant — a few tokens per skill instead of the whole body. - Level 2 — body on demand: a built-in
load_skilltool is registered automatically. When the model judges a skill relevant, it callsload_skill("<name>")and receives that single skill's full instructions as a tool result, then follows them. Only the skill it actually needs is loaded. - Level 3 — bundled files:
load_skillalso returns the skill's folderpath, so the model can read extra files in the skill (scripts, templates) with the normal file tools when the instructions reference them.
This costs one extra round-trip only when a skill is relevant; unrelated requests never pay for skill bodies at all.
Escape hatch: skills_eager=True¶
Pass skills_eager=True to either bot to restore the legacy behavior — every
skill's full body is injected into the system prompt on every turn and no
load_skill tool is registered. Useful for a tiny set of small skills where
the extra round-trip isn't worth it:
How Skills Interact With the Graph¶
Skills are not a node and don't add any conditional routing — they are resolved once, at bot construction:
- At construction (
__init__),skills=/skills_dir=are loaded once viaresolve_skills(). The prompt block is pre-rendered into one string (self._skills_prompt) — the lightweight index by default, or the full bodies withskills_eager=True. Skill tools are merged into the bot's tool list beforebind_tools()(plus theload_skilltool in on-demand mode) — so they're indistinguishable from any other tool once the graph is built. - Every turn, the
agentnode appendsself._skills_promptto the system prompt — right after the baseinstructionsand the memory context, before mode-specific text like[MODO PLAN]. - Skill tools (and
load_skill) run through the normaltoolsnode like any built-in tool: no interception, no acknowledgment step. They execute for real and show up intools_executed, exactly likeRunBashToolwould.
There's no separate flag to enable skills at runtime (unlike enable_planning)
— passing skills=/skills_dir= at construction is the only switch; the
on-demand vs eager choice is skills_eager.
Semantics¶
- Instructions render as a
## SKILLSblock appended to the system prompt (after the bot's owninstructions): a lightweight name+description index by default, or the full bodies underskills_eager=True. - Tools are merged before the model is bound, deduplicating by tool name.
Tools passed explicitly via
tools=win over a skill tool with the same name. - Broken skills (unreadable SKILL.md, tools.py that fails to import) are logged and skipped — they never abort bot construction.
- Loading is deterministic: skill folders load in sorted order.