درس 5 — Choisir et exécuter

Le même chargeur avec Google ADK

7 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Le même chargeur avec Google ADK

Votre équipe plateforme a standardisé ses agents sur Google ADK. Réécrire un chargeur maison pour chaque agent n'a pas de sens si le framework le fait. ADK 2.10 le fait : le code installé contient un module google.adk.skills et un SkillToolset. La page adk.dev/skills qualifie encore la fonctionnalité d'expérimentale ; le registre de fonctionnalités de la version 2.10.0 classe SKILL_TOOLSET en stable (actif par défaut) et SKILL_LIFECYCLE en expérimental (inactif par défaut). Reste à savoir ce qu'il fait exactement, ce qu'il fait de plus que notre chargeur, et le point où sa configuration par défaut n'est pas acceptable en production.

Le mécanisme

Lu dans google/adk/skills/models.py et google/adk/tools/skill_toolset.py (version 2.10.0 installée) :

  • Frontmatter porte le niveau 1 (nom, description, licence, compatibilité, allowed-tools, metadata), avec les mêmes validations que la spécification : 64 caractères, kebab-case, description de 1 à 1 024 caractères.
  • Skill.instructions est le niveau 2, Resources (references, assets, scripts) le niveau 3.
  • load_skill_from_dir lit un dossier et refuse un nom différent du dossier.
  • SkillToolset expose au modèle load_skill, load_skill_resource, run_skill_script, plus list_skills en mode LAZY et search_skills si vous branchez un SkillRegistry.
flowchart LR
  D["skills/*/SKILL.md"] -->|"load_skill_from_dir"| SK["Skill : Frontmatter + instructions + Resources"]
  SK --> TS["SkillToolset(discovery_mode=EAGER)"]
  TS -->|"catalogue XML dans l'instruction système"| AG["Agent ADK"]
  AG -->|"load_skill"| TS
  AG -->|"run_skill_script"| W["Code enveloppe : fichiers du skill matérialisés dans un dossier temporaire"]
  W --> EX["ConfinedCodeExecutor : run_confined, sans réseau"]

Deux modes de découverte : LAZY (défaut) n'injecte rien et laisse le modèle appeler list_skills, ce qui coûte un tour ; EAGER met le catalogue dans l'instruction système, comme notre chargeur. ADK y ajoute un cycle de vie (SkillLifecycleConfig : skill persistant, borné, ou éphémère, libéré à la fin du tour ; derrière le drapeau SKILL_LIFECYCLE, inactif par défaut), une interpolation de l'état de session dans le corps (metadata.adk_inject_state) et des outils supplémentaires activés avec un skill (adk_additional_tools).

La mise en œuvre

L'exécution des scripts passe par un BaseCodeExecutor. ADK fournit notamment UnsafeLocalCodeExecutor, ContainerCodeExecutor, VertexAiCodeExecutor, AgentEngineSandboxCodeExecutor et GkeCodeExecutor. Le premier est le plus simple à brancher, et il hérite de tout os.environ (on le lit dans son code : env = dict(os.environ)) : un script de skill y lit votre GOOGLE_API_KEY et a le réseau. Nous écrivons donc un exécuteur de quinze lignes qui réutilise le bac à sable de la leçon précédente (adk_agent.py) :

files/skillkit/adk_agent.py
class ConfinedCodeExecutor(BaseCodeExecutor):
    """Remplace UnsafeLocalCodeExecutor : même bac à sable que notre harness."""

    def execute_code(self, invocation_context, code_execution_input):
        with tempfile.TemporaryDirectory(prefix="adk-skill-") as td:
            r = run_confined([sys.executable, "-I", "-c", code_execution_input.code], cwd=Path(td),
                             timeout=self.timeout_seconds or 10)
        return CodeExecutionResult(stdout=r.stdout, stderr=r.stderr if r.returncode else "",
                                   exit_code=r.returncode)


def build_agent(skills_dir, model=MODEL) -> Agent:
    skills = [load_skill_from_dir(p) for p in sorted(Path(skills_dir).iterdir()) if (p / "SKILL.md").exists()]
    toolset = SkillToolset(skills=skills, code_executor=ConfinedCodeExecutor(timeout_seconds=10),
                           discovery_mode=SkillDiscoveryMode.EAGER)
    return Agent(name="assistant_interne", model=os.environ.get("MODEL", model),
                 instruction="Vous êtes l'assistant interne d'Atelier Nord. Répondez en français, brièvement.",
                 tools=[toolset])

ADK envoie à l'exécuteur un programme Python qui recrée les fichiers du skill dans un dossier temporaire, puis lance le script avec runpy (Python) ou subprocess (shell). Le paramètre script_timeout du toolset (300 s par défaut) ne s'applique qu'aux scripts shell ; pour un script Python, seul le délai de l'exécuteur protège, d'où les 10 s imposées ici.

Résultat obtenu

Partie ADK de make e2e, gemini-3.5-flash, 2026-10-15 :

OK  ADK SkillToolset : APPEL load_skill {"skill_name": "leave-balance-calculator"}
  | APPEL run_skill_script {"skill_name": "leave-balance-calculator", "args": ["--entree", "2025-11-10", "--pris", …
  | APPEL load_skill_resource {"skill_name": "leave-balance-calculator", "file_path": "references/regles-conges.md"
  | RÉSULTAT {"skill_name": "leave-balance-calculator", "file_path": "scripts/leave_balance.py", "stdout": "{\"dat…
  | RÉPONSE **Solde au 15/10/2026 : 6 jours ouvrés**
- Acquis sur la période (depuis le 01/06/2026) : 9 j (4 mois …

Mêmes six skills, même script, même bac à sable, même solde que notre harness. pytest tests/test_adk.py vérifie sans réseau que les six skills se chargent, que SkillToolset est branché et que l'exécuteur ne voit pas GOOGLE_API_KEY.

Notre chargeur (google-genai)ADK 2.10 SkillToolset
Lignes de codeenviron 250 (spec, loader, sandbox, boucle)15 (exécuteur) + 10 (agent)
Validationstricte en CI, tolérante au chargementstricte : un skill non conforme lève une erreur
Cataloguesystème, XMLEAGER (système) ou LAZY (outil list_skills)
Scripts.py seulement, liste blanche.py, .sh, .bash ; exécuteur au choix
Cycle de vie, registre distant, état de sessionnonoui
Statutvotre codeexpérimental selon la documentation, SKILL_TOOLSET stable dans le registre 2.10.0

Ce qui casse en production

SymptômeCauseParade
Un script de skill appelle une API externe avec votre cléUnsafeLocalCodeExecutor transmet tout l'environnement et le réseauexécuteur confiné, conteneur, ou AgentEngineSandboxCodeExecutor
L'agent démarre sans un skill attenduload_skill_from_dir a levé une erreur (nom différent du dossier) et la boucle de chargement l'a ignorée ou a plantémême validateur strict en CI que pour votre harness
Un tour de plus à chaque conversationdiscovery_mode laissé à LAZY : le modèle appelle list_skills d'abordEAGER pour un petit catalogue stable
Un script Python tourne cinq minutesscript_timeout ne couvre pas Pythontimeout_seconds sur l'exécuteur
Comportement modifié après une mise à jour d'ADKAPI expérimentaleversion épinglée (google-adk==2.10.0) et make e2e avant chaque montée

La règle de décision

Si vos agents sont en ADK, utilisez SkillToolset : il couvre le format, les trois niveaux, et ajoute cycle de vie et registre. Remplacez toujours l'exécuteur local par un exécuteur confiné, et épinglez la version tant que la fonctionnalité est expérimentale. Gardez un chargeur maison si votre harness n'est pas ADK, si vous devez contrôler finement la validation tolérante, ou si vous mesurez les tokens par niveau comme ici.

Pratique

Un collègue branche SkillToolset avec UnsafeLocalCodeExecutor() « parce que c'est en local ». L'agent sera exposé aux équipes internes la semaine prochaine. Quelle est votre objection principale ?

Vous avez 12 skills stables et constatez que chaque conversation commence par un appel list_skills. Que changez-vous ?

Exercice. Activez le cycle de vie ADK pour que csv-data-profiler soit éphémère (libéré à la fin du tour) et vérifiez, sur deux tours successifs, que son corps n'est plus envoyé au second.

Voir la solution
from google.adk.tools.skill_toolset import SkillLifecycleConfig, SkillLifecycleMode
toolset = SkillToolset(skills=skills, code_executor=ConfinedCodeExecutor(timeout_seconds=10),
                       discovery_mode=SkillDiscoveryMode.EAGER,
                       lifecycle_config=SkillLifecycleConfig(
                           skill_overrides={"csv-data-profiler": SkillLifecycleMode.EPHEMERAL}))

Le cycle de vie dépend du drapeau SKILL_LIFECYCLE, inactif par défaut en 2.10.0 : exportez ADK_ENABLE_SKILL_LIFECYCLE=1 avant de lancer l'agent, ou appelez override_feature_enabled(FeatureName.SKILL_LIFECYCLE, True). Sans lui, la configuration est acceptée mais ignorée (toolset._lifecycle_enabled vaut False). Pour l'observer, ajoutez un before_model_callback qui journalise la taille de llm_request.contents ; au second tour, le résultat de load_skill du premier tour ne doit plus porter le corps du skill.

  • Je sais charger des skills avec google.adk.skills et SkillToolset
  • Je sais choisir entre EAGER et LAZY
  • Je sais remplacer l'exécuteur local par un exécuteur confiné
  • Je sais quand garder un chargeur maison
التالي