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) :
Frontmatterporte 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.instructionsest le niveau 2,Resources(references,assets,scripts) le niveau 3.load_skill_from_dirlit un dossier et refuse un nom différent du dossier.SkillToolsetexpose au modèleload_skill,load_skill_resource,run_skill_script, pluslist_skillsen modeLAZYetsearch_skillssi vous branchez unSkillRegistry.
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) :
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 code | environ 250 (spec, loader, sandbox, boucle) | 15 (exécuteur) + 10 (agent) |
| Validation | stricte en CI, tolérante au chargement | stricte : un skill non conforme lève une erreur |
| Catalogue | système, XML | EAGER (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 session | non | oui |
| Statut | votre code | expérimental selon la documentation, SKILL_TOOLSET stable dans le registre 2.10.0 |
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| Un script de skill appelle une API externe avec votre clé | UnsafeLocalCodeExecutor transmet tout l'environnement et le réseau | exécuteur confiné, conteneur, ou AgentEngineSandboxCodeExecutor |
| L'agent démarre sans un skill attendu | load_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 conversation | discovery_mode laissé à LAZY : le modèle appelle list_skills d'abord | EAGER pour un petit catalogue stable |
| Un script Python tourne cinq minutes | script_timeout ne couvre pas Python | timeout_seconds sur l'exécuteur |
| Comportement modifié après une mise à jour d'ADK | API expérimentale | version é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 ?
Le code de UnsafeLocalCodeExecutor copie os.environ dans le processus enfant : clés et réseau sont accessibles au script. La lenteur n'est pas le sujet ; il exécute du shell via l'enveloppe ADK ; le mode de découverte est indépendant de l'exécuteur.
Vous avez 12 skills stables et constatez que chaque conversation commence par un appel list_skills. Que changez-vous ?
LAZY, le défaut, fait découvrir le catalogue par un outil. Pour un petit catalogue stable, EAGER économise ce tour. Un registre sert aux catalogues distants ou très grands ; filtrer load_skill_resource casserait l'accès aux références.
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.skillsetSkillToolset - Je sais choisir entre
EAGERetLAZY - Je sais remplacer l'exécuteur local par un exécuteur confiné
- Je sais quand garder un chargeur maison