Skip to content

feat(adk,#14058): measure first 3 EPITA behavioural contracts against real ADK runtime - #14383

Merged
jsboige merged 1 commit into
mainfrom
feature/14058-adk-contracts-tranche1
Sep 2, 2026
Merged

jsboige merged 1 commit into
mainfrom
feature/14058-adk-contracts-tranche1

Conversation

@jsboige

@jsboige jsboige commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Grain: MED/test — lane myia-po-2024:CoursIA — prev: MED/tooling #14378

See #14058 (tranche 1, steer ai-01 du 2026-09-02) — l'issue reste ouverte : tranches suivantes (contrats restants : désignation, handoff, spécialistes) + liste complète attestée.

Ce que fait cette PR

Tranche 1 de l'oracle de propriétés #14058 : 6 tests mesurant les 3 premiers contrats comportementaux EPITA contre le VRAI runtime ADK 2.8 — Runner, InMemorySessionService, auto-function-calling et exécution de l'outil réel dataset_profile. Seul le LLM est scripté (un BaseLlm de test qui rejoue des réponses) : le contrat teste la mécanique du runtime, pas les réponses d'un modèle — c'est la définition même de l'oracle.

Nouveau fichier unique : utils/test_adk_runtime_contracts.py, à côté des 13 tests existants du runtime (même conftest, même régime d'invocation locale — la série n'est câblée dans aucun workflow CI, les tests Track2 s'exécutent par invocation explicite documentée).

Les 3 contrats, mesurés

Contrat EPITA Verdict Preuve de mesure
C1 — état partagé scopé par session (l'état l'est au sein d'une session, jamais entre sessions) PORTÉ (isolement) — NON PORTÉ (persistance durable entre tours) Isolement : un message en session A n'apparaît jamais dans l'historique de B. Persistance : deux run_agent_turn avec le même session_id ne partagent rien — le wrapper instancie un service frais à chaque appel ; l'historique du tour 1 n'atteint jamais le llm_request du tour 2 (observable capturé). Le portage de la persistance est un grain au-dessus d'ADK (jamais une restauration SK) — il inversera test_state_does_not_persist_across_calls, nommé comme tel.
C2 — round-trip d'outil observable PORTÉ function_call ET function_response exposés par AdkRunResult ; et la jambe réponse porte la valeur calculée par l'outil réel (dataset_profile(120, 8) → 960 cellules, 15.0 lignes/colonne) — pas un écho du modèle.
C3 — budget de tour PORTÉ (timeout wrapper) + mécanique ADK réelle (plafond d'appels LLM) asyncio.wait_for(timeout_seconds) lève AdkRuntimeUnavailable verdict RECOVERABLE-LOCAL à expiration. Et RunConfig(max_llm_calls=2) coupe réellement une boucle de 3 appels (LlmCallsLimitExceededError) — le budget d'appels est une mécanique ADK joignable, pas une abstraction.

Critère 4 de l'acceptance — vérifié AU RETRAIT (mutations locales)

Un contrat n'est réputé porté que si son test échoue quand on le retire. Chaque mutation a été appliquée au runtime, le test ciblé relancé, puis la mutation revertée :

Mutation Test ciblé Résultat sous mutation
C2 retiré : le runtime cesse de relayer les tool_responses (collecte remplacée par pass) test_c2_tool_round_trip_exposes_both_legs FAILED ✓
C3 retiré : asyncio.wait_for(consume_events(), timeout=...) neutralisé en await consume_events() test_c3_turn_budget_raises_recoverable_local_on_timeout FAILED ✓ (le tour finit normalement après la pause → DID NOT RAISE)
C1 falsifié : sessions fusionnées (session-b → même id que session-a) test_c1_sessions_isolate_state FAILED ✓ (la fuite est détectée)

Note d'instrument (vérifiée en cours de route) : la première version du test C3 restait verte sous mutation — le script « sleep » ne yieldait rien, et l'AdkRuntimeUnavailable observé venait du catch-all du runtime sur réponse vide, pour la mauvaise raison. Le fake yield désormais une réponse finale après la pause, ce qui rend la mutation discriminante. Un garde de ce type se valide par ses faux négatifs (anti-regression).

Validation

  • python -m pytest utils/test_adk_runtime_contracts.py (depuis Track2-GoogleADK/) → 6/6 OK (~7 s)
  • python -m pytest utils/test_adk_runtime.py → 13/13 OK (tests existants intacts)
  • Exécution locale, Python 3.13, google-adk==2.8.0 installé ce jour (règle F ; la chaîne litellm 1.83.14 / openai 2.24.0 / pydantic 2.12.5 était déjà en place)

Hors scope / résiduel

  • La liste complète des contrats avec attestation fichier EPITA (acceptance point 1) va en commentaire sur Agentique : EPITA devient oracle de proprietes, ADK/Aspire reste le runtime — ne pas restaurer le runtime SK #14058, pas dans l'arbre. Question posée à ai-01 (créateur de l'issue) sur la localisation du code EPITA source — les mesures du dépôt/GDrive/dépôts Epita n'ont pas de runtime SK candidat ; l'attestation précisera à réponse.
  • Contrats restants (désignation, handoff, budgets de jetons, spécialistes) = tranches suivantes.
  • Aucun notebook modifié ; aucun changement du runtime mesuré (adk_runtime.py byte-identique à main).

…inst the real ADK runtime

Tranche 1 of the property-oracle decision: EPITA says WHAT must hold,
the ADK runtime says whether it is carried. Six tests drive the REAL
ADK 2.8 (Runner, InMemorySessionService, auto-function-calling, real
dataset_profile tool); only the LLM is scripted - the contract tests
the runtime mechanics, not model output.

Contracts measured:
- C1 session isolation: carried (state never leaks between sessions);
  falsified under simulated fusion. The DURABLE cross-turn shared state
  is NOT carried (run_agent_turn builds a fresh service per call) -
  measured, and the porting is a grain above ADK, never an SK restore.
- C2 tool round-trip: carried, and the response leg carries the value
  computed by the REAL tool (960 cells), not a model echo.
- C3 turn budget: carried via asyncio.wait_for + RECOVERABLE-LOCAL
  verdict; ADK's own max_llm_calls hard cap is measured reachable
  through RunConfig.

Acceptance criterion 4 verified at removal: mutations C2 (response
relay removed), C3 (wait_for neutralised) and C1 (fused sessions) each
make their test FAIL - journal in the PR body. Existing runtime tests
stay green (13/13).

Co-Authored-By: Claude-Code <noreply@anthropic.com>
@github-actions github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2024:CoursIA a deja consomme son budget LIGHT du jour (une LIGHT anterieure de cette lane).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-po-2024:CoursIA` voit ces signaux actifs sur les mergees du jour (UTC 2026-09-02) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=3 genre=7 cap=4)
  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=3 genre=7 cap=4)

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@jsboige
jsboige merged commit 8cebd67 into main Sep 2, 2026
11 checks passed

@jsboige jsboige left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] — CoursIA #14383 contrats comportementaux ADK 2.8 (#14058 tranche 1). Lecture complète du fichier (+281, nouveau test-only, runtime byte-identique).

Vérifié :

  • L'oracle est bien un oracle : seul le LLM est scripté, Runner/InMemorySessionService/auto-function-calling/outil réel dataset_profile exécutés. C2 vérifié au niveau événements ADK — la jambe réponse porte la valeur calculée (dataset_profile(120, 8) → 960 cellules, 15.0), pas un écho.
  • Mutation journal au retrait (critère 4) : 3/3 FAILED sous mutation, et surtout la note d'instrument sur le test C3 v1 qui restait vert (réponse vide → catch-all → AdkRuntimeUnavailable pour la mauvaise raison) corrigée par un fake qui répond après la pause — c'est exactement la discipline anti-faux-négatifs attendue.
  • test_state_does_not_persist_across_calls nommé comme mesure du non-portage, avec instruction d'inversion — bon pattern de contrat documenté négativement.

Concerns (non bloquants, tranches suivantes / attestation) :

  1. test_c3_llm_call_budget_is_a_real_adk_mechanic : pytest.raises(Exception) + assert "limit" in str(...) est large — une LlmCallsLimitExceededError mais aussi un échec de config/session ADK non lié au plafond satisferait l'assert. Serrer sur google.adk.runners.LlmCallsLimitExceededError (import direct) ou au moins matcher le nom d'exception en plus du message.
  2. La mesure du non-portage de persistance (C1) se fait via run_agent_turn (wrapper qui instancie un service frais par appel) — le contrat EPITA « état durable entre tours » n'est donc pas mesuré comme non-porté par ADK lui-même (un Runner + service partagé entre appels porterait l'historique), mais comme non-porté par le wrapper. Nuance à formuler telle quelle dans l'attestation #14058, sinon elle survendendrait le non-portage au runtime ADK.

Security scan : 0 match. 6/6 + 13/13 revendiqués, non rejoués ici (oracle d'exécution = tranches suivantes ; les guards CI ne couvrent pas Track2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant