Creating Skills
Two paths:
- Built-in skill — adding a Python file to the source tree, requires rebuild.
- Custom Python skill — runtime registration via
skill_manager, no rebuild needed.
Path 1: Built-in skill
Step 1 — Implement SkillBase
Create src/skills/builtin/your_skill.py:
from src.skills.base import SkillBase, SkillResult
class YourSkill(SkillBase):
name = "your_skill"
description = "What the skill does, briefly."
parameters_schema = {
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to fetch"},
"max_chars": {"type": "integer", "default": 8000},
},
"required": ["url"],
}
async def execute(self, **params) -> SkillResult:
url = params.get("url", "")
max_chars = params.get("max_chars", 8000)
if not url:
return SkillResult(skill_name=self.name, success=False,
output="", error="url is required")
try:
# ... your logic ...
return SkillResult(skill_name=self.name, success=True, output="...")
except Exception as e:
return SkillResult(skill_name=self.name, success=False,
output="", error=str(e)[:300])
Step 2 — Register the skill
In src/skills/builtin/__init__.py:
from .your_skill import YourSkill
# Add to register_builtin_skills() or the relevant aggregation
registry.register(YourSkill())
# Add to the capability map
_CAPABILITY_MAP["your_skill"] = CapabilityLevel.CONTROLLED
Step 3 — Side-effect skills: add intent regex
If your skill has side-effects (sends data, writes external state), add an intent regex to src/policy/intent_gate.py:
SIDE_EFFECT_SKILLS = {"gmail", "agent_manager", "task_manager", "your_skill"}
INTENT_GATE_PATTERNS["your_skill"] = re.compile(
r"(?:verb-pattern-here)",
re.IGNORECASE,
)
Step 4 — Add a regression case
In src/policy/regression_checks.py, add a case asserting expected behavior:
("your-skill-needs-explicit-intent",
"do something", # user input
check_no_inferred_side_effect,
True,
"your_skill must not run on inferred intent"),
Step 5 — Build and verify
docker compose build agent-core
docker compose up -d agent-core
The build runs the regression suite. If your check fails, fix the implementation. After the build succeeds, verify in the dashboard at /skills.
Path 2: Custom Python skill (runtime)
For skills you want to add without rebuilding:
skill_manager(
action="create",
name="my-slug",
description="What the skill does",
params="param1,param2",
code="""
from src.skills.base import SkillBase, SkillResult
class MySkill(SkillBase):
name = "my-slug"
description = "..."
async def execute(self, **params) -> SkillResult:
return SkillResult(skill_name=self.name, success=True, output="...")
""")
The skill is saved at /data/skills/my-slug/skill.py. load_all_python_skills() at startup scans /data/skills/ and registers each one.
Custom skills:
- Run in-process and inherit the agent's permissions.
- Cannot extend
SIDE_EFFECT_SKILLS(that requires a code-level change). - Default to CONTROLLED capability level.
- Show up in
skill_manager(action="list")with typepython-custom.
Review the code carefully before enabling. Any bug or security flaw runs with the agent's full permissions.
SkillResult
@dataclass
class SkillResult:
skill_name: str
success: bool
output: str = "" # rendered for LLM consumption (and via redact() for audit)
error: str | None = None
metadata: dict | None = None # arbitrary structured data
Always set skill_name. Use success=False for any non-success path; populate error with the cause (≤ 300 chars).
output should be human-readable for the LLM — avoid raw JSON unless that's the explicit contract.
Anticipatory simulation
For RESTRICTED and PRIVILEGED skills, the executor runs a pre-execution LLM simulation. The result is appended to output for the next round of self-reflection. This is automatic; no skill code changes needed.
Audit logging
CONTROLLED, RESTRICTED, and PRIVILEGED skills are auto-audited. Audit entries pass through redact() to strip secrets. Shell skill commands also pass through _redact_command().
Testing
# Test a skill in isolation
docker exec agent-core python -c "
import asyncio
from src.skills.builtin.your_skill import YourSkill
async def t():
r = await YourSkill().execute(url='https://example.com')
print(r)
asyncio.run(t())
"
Or trigger via Telegram / dashboard chat with a precise instruction:
call your_skill with url=https://example.com
See also
- Skills — built-in catalog
- Skill Safety — capability levels and intent gating
- Project Structure
- Extending — beyond skills