Le décorateur @skill
Un skill est une capacité atomique qu’un agent expose à l’extérieur. Il est appelable par la CLI, l’API REST, l’app Desktop, ou un autre agent via A2A. @skill marque une méthode async d’une classe @agent pour la déclarer comme telle.
La signature de la méthode est le schéma I/O. Pas de manifeste à écrire à la main : le SDK introspecte les annotations et produit le JSON Schema (cf. Partie IV).
Exemple minimal
from apollia import agent, skill
from apollia.types import Ctx
@agent(name="echo", version="0.1.0", description="Echo agent.")
class Echo:
@skill("echo.say", description="Echo a value back to the caller.")
async def say(self, value: str, ctx: Ctx) -> dict:
return {"echoed": value}
Trois exigences :
- la méthode est
async def; - elle prend
ctx: Ctxen argument ; - elle retourne un
dictJSON-sérialisable, ou lève une exception typée (cf. Partie V).
Signature
def skill(
skill_id: str,
*,
description: str = "",
requires_approval: bool = False,
dangerous: bool = False,
examples: list[dict] | None = None,
) -> Callable[[F], F]:
skill_id
Identifiant unique de la skill, en dot.snake_case minuscule. La regex appliquée est ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$. Quelques exemples valides :
"greet""pdf.read_text""web.search_and_extract""file.list_directory"
Et quelques invalides qui lèvent AgentConfigError au load :
"Pdf.ReadText"(majuscules)"pdf-read"(tirets)"pdf..read"(segment vide)""(chaîne vide)
Le dot.namespace permet de regrouper plusieurs skills d’un même domaine. Convention recommandée : préfixer par un nom court qui rappelle l’agent ou la famille (pdf.*, web.*, chart.*).
description
Phrase courte affichée à l’invocant (autre agent, LLM, opérateur humain). Si elle est absente, le manifeste retombe sur la première ligne du docstring de la méthode. Toujours fournir l’une des deux : c’est ce que voit le LLM quand il décide d’appeler la skill.
examples
Liste optionnelle de payloads d’exemple. Chaque entrée est un dict JSON-sérialisable représentant un appel réaliste. Le tool descriptor LLM les exposera tel quel (cf. chapitre 20).
@skill(
"pdf.read_text",
description="Extract text from a PDF, optionally limited to a page range.",
examples=[
{"path": "/tmp/report.pdf"},
{"path": "/tmp/report.pdf", "page_range": "1-10"},
],
)
async def read_text(
self,
path: str,
page_range: str | None = None,
ctx: Ctx = None,
) -> dict: ...
Le SDK ne valide pas les exemples contre le schéma inféré : cela créerait une dépendance circulaire au moment de la construction du manifeste. L’auteur est responsable de la cohérence.
requires_approval et dangerous
requires_approval=True: déclenche une pause HITL avant l’invocation. Le runtime suspend, demande à l’opérateur de valider, puis reprend (cf. chapitre 23).dangerous=True: marque la skill comme potentiellement destructive ; affiche un bandeau d’avertissement dans l’UI Desktop et la CLI interactive.
Les deux flags sont indépendants. Un pdf.delete_pages typique est à la fois dangerous=True et requires_approval=True.
Plusieurs skills sur le même agent
Une classe @agent peut exposer autant de skills que de méthodes décorées. C’est le pattern standard d’un worker (cf. chapitre 3).
@agent(name="pdf-worker", version="1.0.0", description="PDF utilities.", agent_type="worker")
class PdfWorker:
@skill("pdf.read_text", description="Extract text from a PDF.")
async def read_text(self, path: str, ctx: Ctx) -> dict: ...
@skill("pdf.count_pages", description="Count the pages of a PDF.")
async def count_pages(self, path: str, ctx: Ctx) -> dict: ...
@skill("pdf.extract_metadata", description="Read PDF metadata (author, title).")
async def metadata(self, path: str, ctx: Ctx) -> dict: ...
apollia inspect listera les trois skills avec leur schéma inféré et leurs exemples.
Contraintes validées au load
@skill lève AgentConfigError à l’import si :
skill_idn’est pas une chaîne non vide ;skill_idne respecte pas la regexdot.snake_case;descriptionn’est pas une chaîne ;examplesn’est pas unelist[dict];- la méthode décorée n’est pas
async def; @skillest appliqué deux fois sur la même méthode.
Le @agent parent enforce en plus :
- pas de doublon de
skill_idà travers les méthodes (deux@skill("pdf.read_text")⇒ erreur) ; - mutuelle exclusion avec
@orchestrated(cf. chapitre 9).
Anti-patterns
Ne pas rendre la méthode non-async :
# CASSE au load
@skill("foo.bar")
def bar(self, x: int, ctx): ... # ⇒ AgentConfigError "must be 'async def'"
Ne pas retourner un AIPResult à la main :
# OK : retournez juste un dict
async def bar(self, x: int, ctx) -> dict:
return {"value": x * 2}
# NON : AIPResult est interne au SDK
async def bar(self, x: int, ctx):
return AIPResult.completed({"value": x * 2}).to_dict() # ⇒ erreur runtime
Le boundary du dispatcher emballe automatiquement votre dict en AIPResult.completed et trappe vos exceptions en AIPResult.failed (cf. chapitre 22). Vous n’avez jamais à manipuler AIPResult directement.
Ne pas réutiliser le même skill_id pour plusieurs méthodes : AgentConfigError au load.
ADRs
ADR-098: Decorator-firstADR-099: Signature inference comme schéma I/OADR-109:AIPResultinterne au SDK
(ADRs disponibles prochainement, cf. l’encadré “ADRs et wiki” en introduction.)