Le quatrième contrat EPITA du registre (#14058) : le runtime désigne le prochain agent selon une stratégie explicite. La mesure initiale : ADK 2.8 embarque bien un ordonnanceur dynamique, mais aucune surface du dépôt n’exposait de stratégie — l’ordre Planner→Coder→… de vos chaînes était codé à la main, étape par étape, chez l’appelant. Ce lab travaille sur le portage livré par #14685 : un AdkOrchestrator accepte N spécialistes et exécute la chaîne selon un plan déclaratif posé avant le premier appel LLM.
LLM réel (OpenRouter) : le contenu de chaque étape est produit par le modèle ; l’ordre des étapes, lui, n’est jamais décidé par le modèle.
1. Configuration
import syssys.path.insert(0, '..')import warningswarnings.filterwarnings("ignore", message=(r"\[EXPERIMENTAL\] feature "r"FeatureName\.JSON_SCHEMA_FOR_FUNC_DECL is enabled\." ), category=UserWarning, module=r"google\.adk\.models\.llm_request",)from config.providers import get_settings, get_provider_config, get_litellm_modelsettings = get_settings()provider = get_provider_config(settings)print(f"Provider actif : {provider.provider.value}")print(f"Modele : {get_litellm_model(provider)}")print(f"Endpoint externe : {bool(provider.base_url)}")import logging# Runner par etape + session partagee : chaque runner previent (warning)# pour les events de specialistes hors de son arbre -- attendu, bruit seul.# ADK prefixe ses loggers ("google_adk." + __name__) : viser le nom reel.logging.getLogger("google_adk.google.adk.runners").setLevel(logging.ERROR)
Lire la configuration attestée : quel moteur a produit ces sorties
La sortie d’import fixe le paramètre d’expérience du notebook : Provider actif : openrouter, Modele : openrouter/openai/gpt-4.1, Endpoint externe : True. C’est l’attestation que tout ce qui suit — la chaîne de désignation, les réponses des spécialistes, le verdict final — est produit par un vrai moteur LLM externe, pas par un stub ou une réponse codée en dur : la même exécution sur une autre session pourra confronter ses sorties à celles-ci en connaissant le modèle exact. Le reste de la cellule est de la plomberie assumée : l’insertion de .. au sys.path (les utilitaires de l’atelier) et le filtre ciblé d’un avertissement EXPÉRIMENTAL de la bibliothèque ADK — filtré par motif précis, pas en bloquant tous les warnings.
2. La chaîne déclarée : quatre spécialistes, un plan explicite
La désignation C4 se déclare : la flotte d’agents d’un côté (quatre spécialistes construits par build_agent — l’exécuteur porte l’outil réel dataset_profile), le plan de l’autre — un tuple de noms, posé avant toute exécution. L’orchestrateur détient une session ADK unique : chaque spécialiste désigné verra tout ce que ses prédécesseurs ont produit.
from utils.adk_runtime import build_agent, dataset_profilefrom utils.adk_orchestrator import AdkOrchestratorplanner = build_agent( name="planner", description="Planificateur de la chaîne : décompose la demande.", instruction=("Tu es le planificateur d'une chaîne d'analyse de données. ""Décompose la demande en trois étapes courtes et numérotées. ""Ne code rien, ne calcule rien : tu planifies." ),)coder = build_agent( name="coder", description="Codeur de la chaîne : écrit la fonction demandée.", instruction=("Tu es le codeur de la chaîne. Écris une fonction Python courte ""(nommée, commentée) qui réponde à l'étape en cours. Une fonction ""seulement, prête à exécuter." ),)executor = build_agent( name="executor", description="Exécuteur de la chaîne : mesure avec l'outil réel.", instruction=("Tu es l'exécuteur de la chaîne. Quand un profil de dataset est ""en jeu, appelle OBLIGATOIREMENT l'outil dataset_profile avec les ""dimensions (lignes, colonnes) présentes dans la conversation, ""puis restitue ses chiffres." ), tools=(dataset_profile,),)verifier = build_agent( name="verifier", description="Vérificateur de la chaîne : clôt par un verdict.", instruction=("Tu es le vérificateur de la chaîne. Relis ce que tes ""prédécesseurs ont produit et conclus en deux lignes : VERDICT ""(COHÉRENT ou ÉCART), puis l'argument central." ),)plan_complet = ("planner", "coder", "executor", "verifier")chaine = AdkOrchestrator( [planner, coder, executor, verifier], plan=plan_complet,)print(f"Plan declaré AVANT execution : {chaine.plan}")print("La designation est une donnée posée par l'appelant -- l'ordre ""n'est decidé par aucun LLM.")
Plan declaré AVANT execution : ('planner', 'coder', 'executor', 'verifier')
La designation est une donnée posée par l'appelant -- l'ordre n'est decidé par aucun LLM.
Lire le plan imprimé avant l’exécution : la stratégie est une donnée
Le plan est imprimé avant l’exécution : c’est l’observable de la stratégie. Les quatre agents sont de vrais Agent ADK — l’orchestrateur n’assemble que des primitives publiques (un Runner par étape, un InMemorySessionService partagé), jamais une restauration du moteur SK (#14058).
3. La chaîne en action : chaque spécialiste prend le relais
Un seul appel, quatre étapes désignées. La première reçoit le prompt ; chacune des suivantes s’enchaîne sur l’historique de la session partagée, sans nouveau message utilisateur — le spécialiste désigné prend le relais sur ce qui précède. Observez agent_hands (la désignation exécutée) et tool_calls (l’outil réel de l’exécuteur).
import asyncioasyncdef chaine_complete():asyncwith AdkOrchestrator( [planner, coder, executor, verifier], plan=plan_complet ) as orchestrateur:return orchestrateur, await orchestrateur.run_chain("Traite ce dataset de 120 lignes et 8 colonnes : planifie ""l'analyse, écris la fonction de profil, exécute le profil, ""puis vérifie le résultat.", timeout_seconds=240, )orchestrateur, resultat =await chaine_complete()print(f"Désignation exécutée (mains) : {resultat.agent_hands}")print(f"Appels d'outils : {resultat.tool_calls}")print(f"Réponse finale (du dernier désigné, {resultat.final_agent}) :")print(resultat.response_text[:400])
Désignation exécutée (mains) : ('planner', 'coder', 'executor', 'verifier')
Appels d'outils : ('dataset_profile',)
Réponse finale (du dernier désigné, verifier) :
VERDICT : COHÉRENT
L’exécution a correctement vérifié les dimensions du dataset et le profilage de base est conforme à la planification et à la fonction définie.
Lire le verdict final : une vérification affichée, pas réimprimée
Premier compteur : Désignation exécutée (mains) : ('planner', 'coder', 'executor', 'verifier') reproduit exactement le plan imprimé avant l’exécution — la chaîne a suivi la désignation déclarée, sans agent sauté ni ajouté.
La réponse finale imprimée commence par VERDICT : COHÉRENT et statue que « l’exécution a correctement vérifié les dimensions du dataset et le profilage de base est conforme à la planification et à la fonction définie » — le verdict ÉVALUE les dimensions sans les ré-énumérer. Les chiffres 120 lignes × 8 colonnes vivent dans la demande posée à la chaîne (source de la cellule : « Traite ce dataset de 120 lignes et 8 colonnes ») ; ce que la sortie prouve, elle, est double : l’exécuteur a invoqué l’outil de profilage (Appels d'outils : ('dataset_profile',)), et la réponse finale vient du dernier désigné, verifier — la parole finale appartient au plan, pas au hasard des tours. Ce que la sortie ne prouve PAS : d’où viennent exactement les mots du verdict — il ne réimprime ni les dimensions ni le profil ; l’affirmation de conformité est celle du vérificateur, lue telle quelle.
4. Désignation C4 vs handoff C5 : l’ordre posé avant, pas décidé pendant
Le point de distinction avec le Lab 12c : là, le transfert était décidé par l’agent au milieu d’un tour (transfer_to_agent) ; ici, l’ordre est une donnée. La preuve par l’expérience : la même flotte d’agents, un plan réduit — le déroulé change sans qu’aucun agent ne soit modifié.
asyncdef plan_reduit():# MÊME flotte d'agents, AUTRE stratégie : la désignation est une# donnée par chaîne -- on retire coder et executor du plan sans# toucher aux agents eux-mêmes.asyncwith AdkOrchestrator( [planner, coder, executor, verifier], plan=("planner", "verifier"), ) as orchestrateur:return orchestrateur, await orchestrateur.run_chain("Prépare en une étape la vérification directe de ce dataset ""de 120 lignes et 8 colonnes.", timeout_seconds=240, )orchestrateur_reduit, resultat_reduit =await plan_reduit()print(f"Plan réduit : {orchestrateur_reduit.plan}")print(f"Désignation exécutée : {resultat_reduit.agent_hands}")print(f"Appels d'outils : {resultat_reduit.tool_calls or'aucun'}")print("Côté C4, changer l'ordre = changer UNE donnée ; côté C5 ""(Lab 12c), le transfert était décidé par le modèle en cours de tour.")
Plan réduit : ('planner', 'verifier')
Désignation exécutée : ('planner', 'verifier')
Appels d'outils : aucun
Côté C4, changer l'ordre = changer UNE donnée ; côté C5 (Lab 12c), le transfert était décidé par le modèle en cours de tour.
Lire le plan réduit en chiffres : deux mains, zéro outil, quatre agents
La sortie compare trois compteurs : Plan réduit : ('planner', 'verifier'), Désignation exécutée : ('planner', 'verifier') — déclarité et exécution coïncident — et Appels d'outils : aucun. La lecture en creux est la plus instructive : la flotte instanciée compte toujours quatre agents (coder et executor existent, avec leurs instructions et leurs outils), mais seuls deux sont désignés — l’exécution constate leur absence par le compteur d’appels vide et le tuple de deux mains. Changer la stratégie d’une chaîne C4 = changer une donnée (le plan), jamais le code ; changer le cours d’un handoff C5 = réécrire l’instruction d’un agent. C’est la démonstration chiffrée du contrat — l’anti-miroir du handoff C5 du Lab 12c, où la séquence était décidée pendant le tour : les deux contrats cohabitent sans se recouvrir.
5. Mémoire commune intra-chaîne, isolation inter-chaînes
La session partagée donne à la chaîne une mémoire commune : l’historique porte les quatre auteurs. La contre-garde C1 exige la symétrie inverse : deux chaînes sont deux sessions isolées — le message confidentiel de la première ne doit jamais atteindre l’historique de la seconde.
asyncdef memoire_et_isolation():asyncwith AdkOrchestrator( [planner, coder, executor, verifier], plan=plan_complet ) as chaine_a:await chaine_a.run_chain("Analyse confidentielle : 60 lignes, 4 colonnes.", timeout_seconds=240, ) historique_a =await chaine_a.history()asyncwith AdkOrchestrator( [planner, coder, executor, verifier], plan=("planner",) ) as chaine_b:await chaine_b.run_chain("Question banale : 10 lignes, 2 colonnes.", timeout_seconds=240, ) historique_b =await chaine_b.history()return historique_a, historique_bdef textes(historique):return ["".join(p.text or""for p in (event.content.parts or []))for event in historique if event.content ]historique_a, historique_b =await memoire_et_isolation()auteurs_a = [event.author for event in historique_a]print(f"Auteurs dans la chaîne A (mémoire commune) : {auteurs_a}")fuite =any("confidentielle"in t for t in textes(historique_b))print(f"Le message confidentiel de A a-t-il fui dans B ? {fuite}")print("La mémoire est commune À L'INTÉRIEUR d'une chaîne, jamais ENTRE ""chaînes (contre-garde C1).")
Auteurs dans la chaîne A (mémoire commune) : ['user', 'planner', 'coder', 'executor', 'executor', 'executor', 'verifier']
Le message confidentiel de A a-t-il fui dans B ? False
La mémoire est commune À L'INTÉRIEUR d'une chaîne, jamais ENTRE chaînes (contre-garde C1).
Lire l’historique : sept messages, un agent qui triple, aucune fuite
La sortie de la mémoire commune mérite un comptage exact : Auteurs dans la chaîne A : ['user', 'planner', 'coder', 'executor', 'executor', 'executor', 'verifier'] — sept messages, dans l’ordre désigné, chacun ayant lu ses prédécesseurs, et l’exécuteur apparaît trois fois : un agent désigné peut prendre plusieurs tours consécutifs dans une même étape du plan, l’ordre désigné n’est pas un plafond de un tour par main. Second point, l’isolation : fuite = False alors que la chaîne B a tourné juste après — et les demandes des deux chaînes sont distinctes par construction (source de la cellule — A : « Analyse confidentielle : 60 lignes, 4 colonnes. » ; B : « Question banale : 10 lignes, 2 colonnes. ») : même en poussant B juste après A, rien de la conversation de A n’a transité. Le garde mesure précisément la fuite du mot « confidentielle » dans les textes de B : le contre-garde C1 est mesuré, pas seulement affirmé. La mémoire est commune à l’intérieur d’une chaîne, jamais entre chaînes — désignation ou pas.
6. Exercices
Exercice 1 — Vérificateur en tête
Déclare la même flotte avec le plan ("verifier", "planner") et exécute-la. Que produit une vérification qui précède le plan ? Observe agent_hands et la cohérence du verdict final.
# Exercice 1 : a completerprint("Exercice a completer")
Exercice a completer
Exercice 2 — Le plan n’appartient pas aux agents
Construis deux AdkOrchestrator sur la MÊME flotte d’agents avec des plans différents, exécute les deux. Montre que le plan est une donnée par chaîne : aucun des deux déroulés ne modifie l’autre ni les agents.
# Exercice 2 : a completerprint("Exercice a completer")
Exercice a completer
Exercice 3 — Compteur de désignation
Écris nb_etapes(resultat) qui compte les mains distinctes d’un AdkRunResult. Vérifie qu’elle rend 4 pour la chaîne complète (§3) et 2 pour le plan réduit (§4).
# Exercice 3 : a completerprint("Exercice a completer")
Exercice a completer
Lire les trois exercices : l’échelle des contre-contrats
Les cellules d’exercice sont des stubs C.1 suivant le patron canonique du dépôt — chaque invite imprime « Exercice à compléter », aucune erreur volontaire — le notebook s’exécute d’un bout à l’autre, la validation est l’étudiant. L’échelle des énoncés prolonge la leçon : Exercice 1 place un vérificateur en tête de plan — la question devient « que vérifie-t-on quand rien n’a encore été produit ? », l’ordre du plan comme décision d’architecture ; Exercice 2 déplace le plan hors des instructions des agents — le même anti-pattern que le notebook vient de démontrer, à refaire tenir par construction ; Exercice 3 demande un compteur de désignation — instrumenter agent_hands pour mesurer qui a pris la parole, l’observable du contrat C4. Trois gestes : inverser, encadrer, instrumenter.
Dernière observation, sur la forme : chaque stub imprime l’invite canonique « Exercice à compléter » — le patron du dépôt pour une cellule d’exercice, qui la rend à la fois exécutable et informative : l’output atteste que la cellule a tourné, la place de la solution reste à l’étudiant. Partout ailleurs, chaque cellule de démonstration imprime son observable (le plan déclaré, les mains, les appels d’outils, l’historique) ; la zone d’exercice prend le relais exactement là où l’observable attendu devient celui de l’étudiant — et la conclusion ci-dessous referme le contrat C4 en rappelant ce que la désignation garantit que le handoff ne garantit pas.
7. Conclusion
C4 est porté au-dessus d’ADK : AdkOrchestrator exécute une chaîne selon un plan déclaratif — la stratégie est une donnée observable avant l’exécution (orchestrator.plan) et vérifiée après (agent_hands).
La chaîne a une mémoire commune : chaque spécialiste désigné voit tout l’historique ; l’isolement reste scopé par session (contre-garde C1, testée au retrait).
C4 et C5 restent distincts : désignation = ordre posé avant le premier appel LLM ; handoff = décision de l’agent au milieu d’un tour. L’ordonnanceur dynamique natif d’ADK reste la primitive candidate pour une désignation adaptée au contenu — une stratégie enrichie du même contrat.
Registre vivant : #14058 — jamais une restauration SK : ADK reste le runtime.