1. Avant de commencer
La plupart des sites de billetterie placent les acheteurs dans une file d'attente, à la fois pour éviter les plantages de serveur et pour gérer la forte demande. Vous commencez votre achat et vous vous retrouvez derrière 14 000 personnes. La file d'attente avance pendant 40 minutes. Lorsque votre tour arrive enfin, vous disposez d'un court délai pour choisir vos sièges et payer. Si vous ne vous connectez pas dans ce délai, votre place dans la file d'attente sera attribuée à quelqu'un d'autre.
De nombreuses tâches réelles suivent ce schéma : une tâche qui prend beaucoup plus de temps qu'une seule conversation, exécutée sur un système externe que vous ne contrôlez pas. Par exemple, vous pouvez attendre qu'un fournisseur réapprovisionne un produit, qu'une réclamation soit approuvée ou qu'une file d'attente se vide.
Les agents IA standards vivent dans un seul tour de discussion, qui dure quelques secondes et est purement réactif. Elles ne peuvent pas attendre 40 minutes ni agir de manière autonome. Pour résoudre ce problème, vous avez besoin d'un agent de longue durée : un système qui reste inactif pendant les longues attentes, conserve le contexte lors des redémarrages de processus et reprend automatiquement lorsqu'il est réactivé par un déclencheur externe.
Cet atelier vous guide dans la création d'un agent de file d'attente pour les billets de concert. Il aborde les principaux modèles architecturaux requis pour les agents de longue durée sur Google Cloud.
Ce que vous allez faire

Dans cet atelier, vous allez créer un agent de réservation de billets de longue durée tout en découvrant les principaux modèles de logiciels agentiques :
- Traitement de la suspension et de la reprise : mettez en veille une invocation d'agent pendant les longues périodes d'attente et reprenez l'exécution à l'aide de déclencheurs externes.
- Stockage d'état durable : conservez l'état et les artefacts de la conversation lors des redémarrages de processus à l'aide de SQLite/Cloud SQL et Cloud Storage.
- Protection contre l'obsolescence et idempotence : vérifiez les données dynamiques (comme la disponibilité des places) avant d'agir et utilisez des clés d'idempotence pour éviter les transactions en double.
- Graphiques de workflow ADK : structurez l'exécution à l'aide de nœuds de code déterministes pour les règles procédurales et de nœuds d'agent pour la prise de décision contextuelle.
- Approbation avec intervention humaine : suspend l'exécution pour collecter et conserver les limites de dépenses explicites de l'utilisateur.
- Déploiement dans le cloud : conteneurisez et déployez l'agent sur Cloud Run avec des déclencheurs Cloud Scheduler et Pub/Sub.
Configuration
Tout d'abord, préparez votre environnement et assurez-vous d'avoir accès à Google Cloud. Je vous recommande d'exécuter cette commande dans Cloud Shell, qui contient déjà tous les outils dont vous avez besoin. Il vous suffit de vérifier que vous disposez de suffisamment d'espace et que votre compte peut créer des projets.
👉💻 Clonez le dépôt et exécutez la configuration :
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
Le message suivant s'affichera une seule fois :
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
Il effectue ensuite les opérations suivantes, dans l'ordre :
virtualenv | crée | |
sign-in |
| |
project | pointe | |
API | active les dix dont l'atelier a besoin. | |
| écrit l'ID de votre projet, de votre région et de votre modèle, afin que chaque script et | |
model check | effectue un véritable appel à Gemini. Un ID de projet ou de modèle incorrect échouera ici. | |
seed | crée une session qui a déjà deux jours, que l'étape 3 ouvre | |
Cloud SQL | démarre une instance Postgres en arrière-plan. | |
venue | déploie votre propre faux vendeur de billets sur Cloud Run. | |
👉💻 Activez l'environnement :
cd ~/monstertix
source .venv/bin/activate
Votre requête commence désormais par (.venv).
Présentation du scénario
Dans ce scénario d'atelier, votre agent réserve deux billets pour un concert ("The Midnight Signal" à Amsterdam). Les préférences de l'utilisateur stockées dans la mémoire historique incluent :
- Il doit s'agir d'un spectacle le week-end (Sam ne peut pas y assister en semaine).
- Évitez les places en gradins supérieurs au Ziggo Dome (visibilité médiocre).
- Le prix cible est d'environ 200 $ par billet, avec une limite maximale de 250 $.

Vérifier votre installation
👉💻 Une commande, huit coches :
./verify.sh
Checking the rig
✓ adk installed adk, version 2.6.2
✓ adk 2 apis App + ResumabilityConfig
✓ workflow graphs Workflow + node
✓ async sqlite drivers sqlalchemy + aiosqlite + greenlet
✓ project your-project-id
✓ vertex ai no API keys anywhere
✓ seeded session 13 events, two days old
✓ venue https://venue-you-xxxx.us-central1.run.app
Ready. Cloud SQL is still building in the background and nothing
before step 10 needs it.
👉 Ouvrez deuxième onglet du navigateur :
Lieu | Description |
| Le panneau de configuration, vos boutons. |
2. Vers l'autonomie grâce aux requêtes ?
Les requêtes ne peuvent pas se lancer d'elles-mêmes
Une requête est simplement un texte transmis à un modèle d'IA lors d'un appel. Il ne peut pas s'exécuter seul, se réveiller ou lancer un nouvel appel.
Si vous demandez à un agent dans son invite de "revenir dans 10 minutes", rien ne se passe une fois le tour actuel terminé. Le modèle ne dispose pas d'horloge ni de boucle internes. Pour qu'un agent soit autonome, un élément extérieur à l'agent doit le réveiller et l'appeler.

Démarrer le simulateur
Nous allons commencer par tester les outils pour les développeurs. adk web démarre l'UI Web ADK, l'interface de développement fournie avec ADK. Il vous permet de discuter avec l'agent que vous créez sans avoir à écrire de frontend au préalable. Il vous montre ce qu'une fenêtre de chat ne peut pas afficher, comme chaque appel d'outil et ses arguments, l'état de la session à mesure qu'il change, les artefacts enregistrés par l'agent et les événements bruts sous-jacents. Vous ne l'exposeriez pas en production. C'est la meilleure fenêtre sur un agent en cours d'exécution pendant que vous apprenez. À l'étape 3, vous créez l'interface que vous exposerez réellement, puis vous la déployez à la dernière étape.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Terminal 1 : chargez le code de cette étape et démarrez l'UI Web de l'ADK :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
use-solution.sh copie le code terminé d'une étape dans agent/, qui est l'endroit où vous travaillez. Chaque étape commence par cette commande et affiche les fichiers modifiés pour que vous sachiez ce qu'il faut lire.
Ce dont vous avez besoin pour commencer :
Fichier | Description |
| l'agent : un nom, un modèle, une instruction, une liste d'outils |
|
|
| un client HTTP pour le lieu. Rien d'intéressant |
| le modèle à utiliser et vérifie que vos identifiants fonctionnent. |
. ./set_env.sh active le virtualenv, charge votre projet et votre modèle, et exporte $WORKSHOP, le chemin d'accès absolu à ~/monstertix. Exécutez-le dans chaque terminal que vous ouvrez. Il affiche ce qu'il a défini :
folder /home/you/monstertix
project your project id
model gemini-2.5-flash (We'll avoid using 3.x for now to avoid high usage demands)
venue https://venue-yourname-xxxx.run.app
👉 Ouvrez les premiers onglets du navigateur :
Lieu | Description |
UI Web ADK |
|
Lui parler
👉✨ Dans l'UI Web d'ADK, saisissez :
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
Il recherche des visites, lit les plans de salle et raisonne en fonction de votre budget et de votre entreprise. Il s'agit d'un assistant vraiment performant. Toutefois, lorsque la prévente commence à 10h, l'agent ne s'exécute pas automatiquement, car aucun processus ni déclencheur en arrière-plan n'est en cours d'exécution.
Lire ce qui vient de se passer
Le milieu de l'écran n'est pas qu'un journal de chat. Chaque étape du tour est numérotée, y compris les appels d'outils :
▸ I want to see The Midnight Signal. I'm in Amsterdam... ← user input
⚡ search_events({"city": "Amsterdam"}) ← tool execution
✓ search_events ← venue API response
Two Amsterdam shows: Tuesday 10 Nov and Saturday 14 ... ← model response
👉 Cliquez sur une ligne ⚡. Le panneau de gauche affiche les arguments choisis par le modèle. Cliquez sur le ✓ en dessous pour afficher le code JSON renvoyé.
Les informations sur la visite ne figurent pas dans les données d'entraînement ni dans l'invite du modèle. L'agent a récupéré ces données en exécutant l'outil search_events, qui a interrogé l'API du lieu.
Ouvrez le code que vous venez d'exécuter.
Vous avez vu le fonctionnement et la trace. Examinons maintenant le code lui-même.
agent/concert/
├── agent.py the agent — about twenty lines
├── tools.py what it can do
├── venue.py an httpx wrapper, nothing interesting
└── config.py which model, and a credentials check
👉💻 Ouvrez agent/concert/agent.py. L'intégralité de l'agent se trouve en bas du fichier :
INSTRUCTION = """
You help someone plan a trip to see a band on tour.
You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""
root_agent = Agent(
name="concert",
model=MODEL,
instruction=INSTRUCTION,
tools=[search_events, get_seatmap],
)
Un nom, un modèle, une description du comportement de l'agent et une liste de fonctions Python. C'est tout ce qu'est un Agent. adk web l'a trouvé parce que le fichier s'appelle agent.py et que la variable s'appelle root_agent.
👉💻 Maintenant agent/concert/tools.py :
def search_events(city: str = "", weekday: str = "") -> dict:
"""List tour dates. Call with no arguments to see the whole tour.
This venue sells one artist and one tour, so there is nothing to search by
name — never ask the user which artist they mean.
Args:
city: Optional. Filter to one city, e.g. "Amsterdam".
weekday: Optional. Filter to one weekday, e.g. "Saturday".
"""
return venue.get("/events", city=city, weekday=weekday)
Fonction ordinaire. Pas de décorateur, pas d'enregistrement, pas de schéma à écrire. L'ADK lit la signature et la docstring, puis crée la définition de l'outil que le modèle voit. C'est pourquoi cette docstring est porteuse et que la suppression d'un argument a modifié le comportement du modèle.
Le modèle n'exécute jamais votre code. Lorsque vous avez vu search_events({"city": "Amsterdam"}), le modèle demandait cet appel. L'ADK a exécuté la fonction, renvoyé le résultat et le modèle a écrit une phrase à ce sujet. La boucle est la suivante :
👉 Les icônes situées tout à gauche permettent de modifier le contenu du panneau. Le second est le graphique de l'agent. Essayez-le, vous devriez voir deux outils :

Alors dis-lui de ne pas dormir
Que pouvez-vous faire pour résoudre le problème ? Essayez la solution que la plupart des utilisateurs choisissent en premier. Rappelez-vous de INSTRUCTION de l'étape 1, la prose transmise au modèle à chaque tour, et réécrivez-la.
👉💻 Ouvrez agent/concert/agent.py. Vous trouverez ci-dessous INSTRUCTION, ainsi qu'un deuxième exemple déjà rédigé. Si vous le souhaitez, vous pouvez essayer de la modifier vous-même :
PROACTIVE_INSTRUCTION = """
You are a proactive ticket-buying assistant.
Monitor the presale for The Midnight Signal. The moment it opens at 10:00 on
Tuesday, buy two tickets for the Amsterdam Saturday show — do not wait to be
asked, and do not wait for me to say anything. Act on your own.
Keep checking until the tickets are bought.
"""
# agent/concert/agent.py
root_agent = Agent(
name="concert",
model=MODEL,
instruction=PROACTIVE_INSTRUCTION, # ← was INSTRUCTION
tools=[search_events, get_seatmap],
)
👉💻 Dans le terminal 1, redémarrez :
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ Expliquez-lui le plan, exactement comme vous le feriez avec une personne :
I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.
Il sera d'accord et aura l'air de le penser.
👉 Attendez maintenant. Ne saisissez rien d'autre. Regardez le débat.
Patientez une minute. RIEN NE SE PASSE !
Pourquoi cette requête n'a jamais fonctionné
Relisez-le d'un œil neuf. Surveillez. Dès qu'il s'ouvre. N'hésitez pas à revenir consulter cette page. Chacune de ces expressions décrit quelque chose qui se produit au fil du temps.
Rappelons ce qu'est instruction=. Il s'agit d'une chaîne de caractères transmise au modèle dans la requête au moment où quelqu'un envoie un message. Rien ne le lit entre les tours.
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
Une instruction ne peut pas appeler la fonction dans laquelle elle est écrite.
Variantes de requêtes qui échouent
Toute tentative d'appliquer une exécution autonome via des instructions dans l'invite échoue, quelle que soit la formulation. Tentatives de requête courantes, par exemple :
- "Règle un minuteur sur 10h mardi et achète les billets à ce moment-là."
- "Vérifie toutes les cinq minutes si la prévente a commencé."
- "Agis de manière autonome sans attendre l'intervention de l'utilisateur."
Toutes échouent pour la même raison : les invites sont en lecture seule lorsqu'une invocation se produit. Sans appelant externe, aucun code n'est exécuté entre les tours.
3. Inactivité basée sur les événements
Les agents ne restent pas éveillés éternellement
Laisser un processus d'interrogation continue en cours d'exécution gaspille de la mémoire, de l'argent et provoque des plantages si votre ordinateur portable se met en veille ou redémarre.
Au lieu de laisser un agent s'exécuter en continu pendant qu'il attend, un agent de longue durée doit devenir complètement inactif. Un événement externe (tel qu'un minuteur, un webhook ou un message) réveille l'agent uniquement lorsqu'il y a du travail à faire.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Chargez le code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
Notez ce qui a été installé : monstertix/, qui contient le serveur, l'horloge et une autre chose que vous n'avez pas encore vue. Vérifiez qu'il a laissé agent/ intact, car rien ne change dans l'agent à cette étape. Retenez bien cela, c'est important.
Fichier | Description |
| de nouveau. Serveur Web avec un Runner derrière |
| de nouveau. Un élément qui connaît l'heure et appelle le serveur |
Il est divisé en deux parties, qui restent dans deux fichiers jusqu'au cloud : 
devient, à l'étape 10 | |||
le déclencheur | un point de terminaison pouvant exécuter l'agent. | Cloud Run | |
le déclencheur | quelque chose avec une horloge, qui l'appelle | Cloud Scheduler | |
Le déclencheur : votre propre serveur Web
Oui, un serveur Web. Quelque chose doit être à l'écoute avant que quoi que ce soit puisse l'appeler. L'UI Web de l'ADK des étapes précédentes est une interface de développement qui peut exécuter un agent.
👉💻 Ouvrez monstertix/server.py :
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent
session_service = InMemorySessionService()
runner = Runner(
agent=root_agent,
app_name="concert",
session_service=session_service,
)
@app.post("/wake")
async def wake(payload: dict = Body(default={})):
session_id = f"wake-{uuid.uuid4().hex[:8]}" # brand new, every time
await session_service.create_session(...)
async for event in runner.run_async(
user_id=USER_ID, session_id=session_id,
new_message=types.Content(role="user",
parts=[types.Part(text=payload["message"])]),
):
...
Un Agent ne s'exécute pas tout seul. Il s'agit d'une description : un nom, un modèle, une instruction, une liste d'outils. Un élément doit prendre un message entrant, trouver la bonne session, transmettre la conversation au modèle jusqu'à présent, exécuter les outils demandés, renvoyer les résultats, boucler jusqu'à ce qu'il cesse d'appeler des outils et écrire chaque étape dans le stockage. Ce quelque chose, c'est le Runner.

L'UI Web de l'ADK en possède une par défaut. Ici, vous le créez vous-même. C'est la seule différence entre l'UI Web de l'ADK et un élément que vous pouvez déployer.
👉💻 Terminal 2 : démarrez-le :
Démarrez le Runner.
cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.
Lis la dernière ligne. Le déclencheur est en cours d'exécution et l'agent est chargé, mais rien ne se passe. Séparez les deux idées : être appelable et être appelé.
Et une porte d'entrée, tant qu'on y est
Bien que adk web soit un excellent outil de développement, il ne doit pas être inclus dans votre produit. Vous ne devez pas l'activer lorsque vous déployez en production, et vous ne devez pas y orienter un client si vous l'avez fait. Le même serveur sert également une page.
👉 Ouvrez http://127.0.0.1:8090 dans un navigateur (il s'agit de la même adresse).
Découvrez MonsterTix. Une fenêtre de chat, un nom dans l'angle, deux boutons. Il publie sur /wake et affiche ce qui est renvoyé. Il fonctionnerait sans modification sur n'importe quel point de terminaison qui répond de la même manière.
Sur la page | Ce qui se trouve en dessous |
la conversation |
|
+ Nouvelle session | un nouvel ID de session (nouvelle conversation, puis nouveau budget) ; |
Réinitialiser la mémoire | supprime chaque session et le fichier de mémoire. Il vous demande d'abord et vous montre exactement où se trouve cet état. |
👉✨ Dites quelque chose à What Amsterdam dates are there? et regardez le terminal 2. Même Runner, même agent, mêmes lignes de journaux. Seul l'appelant a changé.
Le déclencheur : un élément qui connaît l'heure
👉💻 Ouvrez monstertix/clock.py. Examinez le code.
time.sleep(args.delay) # wait
while True:
client.post(f"{TRIGGER_URL}/wake", # knock
json={"message": args.message})
if not args.every:
return # once, unless told otherwise
time.sleep(args.every) # or again, forever
Le mécanisme est très simple : mise en veille, POST, et peut-être répétition. Chaque planificateur que vous avez utilisé fait de même, en plus des nouvelles tentatives, des fuseaux horaires et de la garantie qu'il survivra à un redémarrage.
👉💻 Terminal 3 : déclenchez une fois, dans 10 secondes :
cd ~/monstertix
. ./set_env.sh
python -m monstertix.clock --in 10
[clock] target http://127.0.0.1:8090/wake
[clock] first fire in 60s, once only
[clock] go and watch the other terminal — do not touch the keyboard
👉 Arrêtez de taper. c'est facile ! Regardez le terminal 2.
Une minute plus tard, sans personne au clavier :
[clock] firing → POST /wake
[server] woken · session wake-23930983
[server] called search_events
[server] called purchase
[server] said I've purchased 2 tickets for The Midnight Signal in
Amsterdam on Saturday, section A. Your total is 420.
[clock] done. the agent ran and you were not involved.
Il s'agit de la réponse à l'étape 2. Quelque chose d'extérieur à l'agent l'a appelé, et ce quelque chose était un sleep et une requête HTTP.
👉💻 Ctrl-C lorsque vous l'avez regardée plusieurs fois. Utilisez d'abord --every 30 si vous souhaitez voir la vidéo se répéter.
Maintenant, lisez ce qui a été dit
"J'ai acheté deux billets pour The Midnight Signal à Amsterdam, section A, 420 $."
👉🔴 Consultez le panneau de l'établissement. Vous devriez voir que le billet a été acheté.
Qu'est-ce qui ne va toujours pas ?
Il a acheté quelque chose. Regardez ce qu'il a dû deviner. Il a choisi Amsterdam et la section A, car ce sont les premières choses qu'il a trouvées. Vous avez demandé à l'agent de les acheter directement, mais il ne sait pas que Sam ne peut pas y aller en semaine. Un agent qui agit sur la base de suppositions à 3 h du matin est pire qu'un agent qui ne fait rien.
Se réveiller est la partie facile. Un agent qui se réveille sans rien (pas d'historique, pas de préférences, personne à l'écoute) ne vaut pas encore la peine d'être réveillé.
4. Gérer la durée de vie du contexte
Les informations sont disponibles à différents endroits et à différents moments
Un agent stocke des faits à plusieurs endroits : dans la conversation par chat, dans l'état de la session, dans les fichiers (artefacts) ou dans la mémoire utilisateur à long terme. Chaque lieu dure un certain temps.
Si vous avez saisi des informations au mauvais endroit :
- Il disparaît trop vite : les informations importantes sont effacées à la fin du chat.
- Il reste trop longtemps : les faits temporaires contaminent la mémoire à long terme et perturbent les conversations futures.
Vous devez associer chaque information à l'emplacement de stockage et à la durée de vie appropriés.
Charger le code
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT.
👉💻 Dans le terminal 1, exécutez la commande suivante :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
Ce qui a changé depuis la dernière étape :
Fichier | Qu'est-ce qui a changé ? |
|
|
| de nouveau. Notre propre stockage de souvenirs |
| trois autres outils dans la liste, et l'instruction indique à l'agent de les utiliser. |
👉💻 Redémarrez l'agent dans le terminal 1 :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
Deux indicateurs que vous n'avez jamais utilisés :
Option | Description |
| L'ADK y écrit chaque message, appel d'outil et changement d'état sous forme de lignes (base de données SQLite). |
| Fichiers que l'agent enregistre.(Fichiers) |
Sans eux, ADK conserve tout en mémoire et le supprime lorsque vous arrêtez le processus. Ces deux paramètres permettent à l'état de persister.
La base de données sessions.db contient une conversation passée prédéfinie (two-days-ago) avec 13 événements enregistrés :
you We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you Sam can't do weeknights though, bailed on every single one.
you Priya's out, she's away that weekend. Just me and Sam.
agent Two it is. Still Saturday the 14th, still Amsterdam.
you You said the upper bowl at Ziggo was bad last time?
Fichier de mémoire de trois réservations passées, memory/userx.md, que vous pouvez ouvrir dans un éditeur :
- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.
C'est pourquoi les rangées ambre en semaine sont importantes. Un spectacle en semaine est 10 $ moins cher par place, il reste de meilleures places et il se trouve en haut de la liste. Il s'agit de la mauvaise réponse pour cette personne. La seule raison pour laquelle l'agent peut le savoir est qu'il s'agit d'un fichier qu'il a écrit après la dernière réservation. Les étapes 4 et 5 permettent de déterminer si ce fait est conservé.
Les trois outils que nous avons ajoutés

note_companion enregistre qui arrive et ce qui le limite :
# agent/concert/tools.py
def note_companion(name: str, constraint: str, tool_context: ToolContext) -> dict:
prefs = dict(tool_context.state.get("user:prefs", {}))
prefs[name] = constraint
tool_context.state["user:prefs"] = prefs
return {"companions": prefs}
Ajoute-le, car "Sam ne peut pas les soirs de semaine" doit rester vrai la semaine prochaine. Pour ce faire, écrivez-le dans user:prefs et regardez la partie user: du nom faire le travail. Nous y reviendrons plus bas.
recall
et
remember : lire et écrire un fichier texte, memory/userx.md. Maintenez-les un instant.
👉 Regardez maintenant les deux endroits où l'agent stocke son contexte :
position | Contenu |
État | la mémoire de cette session, sous forme de paires clé/valeur (vous êtes ici à l'étape 4) |
Artefacts | les fichiers enregistrés par l'agent, qui n'entrent jamais dans le prompt ; |
Découvrez les modifications apportées à
get_seatmap
. Auparavant, elle renvoyait le plan de cabine complet. Il enregistre maintenant le plan de la salle dans un fichier et renvoie un bref récapitulatif :
# agent/concert/tools.py
async def get_seatmap(event_id: str, tool_context: ToolContext) -> dict:
seatmap = venue.get(f"/events/{event_id}/seatmap") # about 6 KB of JSON
part = types.Part(inline_data=types.Blob(
mime_type="application/json",
data=json.dumps(seatmap, indent=2).encode()))
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
summary = {"event_id": ..., "sections": [...], "captured_at": ...}
tool_context.state["temp:seatmap"] = summary
return summary
Voici pourquoi ce changement est important.
Chaque charge utile de retour d'outil est ajoutée à l'historique des conversations et renvoyée au modèle à chaque tour suivant. Le renvoi d'objets JSON volumineux (comme un plan de salle de 6 Ko) augmente la consommation de jetons à chaque tour suivant.
Enregistrez-le plutôt dans un fichier et payez un nom de fichier. Laissez l'agent redemander le détail avec load_artifact s'il en a besoin, mais attendez-vous à ce que la plupart du temps, il ne le fasse pas.
Où le fichier est-il envoyé ? Au service d'artefacts
Regardez à nouveau la ligne dans get_seatmap, dans agent/concert/tools.py :
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context provient de l'ADK, qui le transmet à votre outil en tant qu'argument, et save_artifact est une méthode sur celui-ci. Cette ligne correspond à l'endroit où votre outil transmet les octets à ADK.
L'ADK les écrit ensuite, en utilisant le service d'artefacts avec lequel il a été démarré. Vous avez choisi l'option suivante :
--artifact_service_uri="file://$WORKSHOP/artifacts"
L'ADK dispose de trois emplacements pour les stocker :
URI | Où vont les octets ? |
| RAM. Disparaît lorsque vous arrêtez le processus. Il s'agit de la valeur par défaut si vous ne transmettez aucune option. |
| un dossier sur le disque. Ce que vous utilisez actuellement |
| sous forme de fichier de modèle dans Google Cloud Storage. Ce que l'agent déployé utilise lors de la dernière étape |
👉 💻 Ici, nous choisissons les fichiers :
ls -R ~/monstertix/artifacts
Pour passer de votre ordinateur portable à Cloud Storage, remplacez file:// par gs://. (Nous y reviendrons à l'étape 10.)
Notre mémoire

ADK vous fournit une interface pour la mémoire à long terme appelée BaseMemoryService. Il existe deux méthodes :
# google.adk.memory.BaseMemoryService — ADK's interface, not your file
async def add_session_to_memory(session) # take a conversation in
async def search_memory(*, app_name, user_id, query) # hand memories back
L'ADK propose trois implémentations :
Classe | URI | Ce dont il a besoin |
|
| un pipeline à partir de zéro. Perte de toutes les données lorsque vous arrêtez le processus |
|
| un corpus RAG Vertex AI |
|
| une ressource Agent Engine |
Nous allons écrire les nôtres pour cet atelier. Éliminez InMemoryMemoryService, qui oublie tout lorsque vous arrêtez le processus, ainsi que les deux autres, qui ont besoin de ressources cloud qui prennent des minutes à créer. Choisissez plutôt un fichier, car vous pouvez l'amorcer à l'avance et le lire dans un éditeur. Considérez memory.py comme votre propre implémentation. Nous stockons tout dans un fichier Markdown :
class MarkdownMemoryService(BaseMemoryService):
async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
path = self._path(user_id) # memory/userx.md
return SearchMemoryResponse(memories=[
MemoryEntry(content=types.Content(
role="user", parts=[types.Part(text=path.read_text())]))])
SearchMemoryResponse et MemoryEntry sont des types d'ADK, et Memory Bank renvoie les mêmes objets. L'un d'eux est associé à un service géré. Le nôtre contient un fichier.
Vous pouvez choisir un fichier pour une seule raison : vous pouvez l'ouvrir et le lire. La mémoire à long terme de votre agent se limite à une page de texte.
Alors, où le pointez-vous ?
Pour récapituler l'implémentation de cet atelier, deux des trois services sont choisis sur la ligne de commande. Pour la troisième, nous avons effectué notre propre implémentation pour la raison ci-dessus :
Service | Où vous l'avez configuré | Cet atelier |
session |
|
|
artefact |
|
|
Mémoire | nulle part sur la ligne de commande |
|
memory.py construit le service lui-même à partir d'une variable d'environnement :
# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)
. ./set_env.sh exporte les deux. MEMORY_DIR définit l'emplacement à partir duquel l'agent lit sa mémoire, et MEMORY_USER détermine le fichier.
Aller chercher des choses
Vous disposez désormais de quatre emplacements pour stocker un fait. Regardez les quatre.
Votre propre session
👉 Cliquez sur New Session (Nouvelle session) dans la barre supérieure.
👉✨ Dites trois choses, l'une après l'autre :
Sam can't do weeknights, by the way.

Show me the seat map for the Amsterdam Saturday show.

Actually, hold that thought — what else is on in Amsterdam?
Ce troisième message est important. Cela met fin au tour de lecture du plan de salle, et c'est la fin du tour qui fait disparaître temp:.
Trois éléments ont été placés dans trois emplacements différents :
Éléments stockés | Quel outil a | Où sont passés les fonds ? | |
1 | Sam ne peut pas venir en semaine |
|
|
2 | le plan de cabine complet, 6 Ko |
| le service d'artefacts |
3 | un résumé du plan de cabine sur une ligne ; |
| nowhere |
👉 Vérifiez maintenant. Les onglets État et Artefacts se trouvent à gauche de adk web.


Dans l'onglet État, notez que temp:seatmap est omis. Les clés d'état avec un préfixe temp: n'existent que pendant l'invocation active et sont automatiquement supprimées par ADK avant de persister l'état de la session dans la base de données.
Ensuite, la conversation plus ancienne
👉 Passez à two-days-ago dans le menu déroulant des sessions en haut de l'écran.
Cette session contient un historique de conversation prérempli dans la base de données.

👉 Consultez le flux d'événements (colonne du milieu). Notez que les tours plus anciens ont été remplacés par un bloc d'événements récapitulatif.

👉 Consultez l'onglet État. Trois types de clés sont disponibles :
target_event_id ms-ams-01 ← plain: this booking only
party_size 2 ← plain: this booking only
user:prefs {"excluded_weekdays": [...]} ← the person, every session
Le préfixe user: (par exemple, user:prefs) définit l'état pour toutes les sessions d'un utilisateur donné. En revanche, les clés sans préfixe (
target_event_id
,
party_size
) représentent l'état de session simple, qui est limité à la session active et supprimé à la fin de la session.
👉 Comparez maintenant les deux onglets "État". Alors que user:prefs et les artefacts stockés persistent, temp:seatmap est absent des deux sessions. Les clés d'état avec un préfixe temp: sont strictement éphémères et ne sont jamais conservées dans la base de données.
Ensuite, le fichier
👉💻 Ouvrez-le dans l'éditeur :
cloudshell edit ~/monstertix/memory/userx.md
Trois réservations, remontant à janvier. L'agent le lit lorsqu'il appelle recall(), et il sera toujours là même si nous avons perdu les données de session.
Les quatre emplacements
Où se trouve-t-il ? | Survit au prochain message ? | Survit à un redémarrage ? | La suppression de la session est-elle conservée ? |
| no | non | non |
| oui | oui | non |
| oui | oui | oui |
| oui | oui | Oui, et il survit à la suppression de la base de données |
un artefact | oui | oui | dépend du service. |
Lisez ce tableau comme s'il concernait les conteneurs, et non leur contenu.memory/userx.md survit à tout et ne contient que ce que remember() a choisi d'y mettre. Rien n'écrit votre conversation actuelle dans le fichier. Le fait qu'un fichier survive à une session n'est pas la même chose que l'agent se souvienne de ce dont vous venez de parler. C'est à l'étape 5 que cette distinction devient importante.

Quel est l'état de
Considérez l'état comme un dictionnaire qui accompagne la session. Lisez-le et écrivez-le depuis vos outils via tool_context.state, et laissez ADK l'enregistrer à côté de la conversation.
ADK attribue la signification des préfixes. Vous ne les définissez nulle part. L'ADK lit la première partie du nom de la clé et l'utilise pour déterminer où la valeur doit être placée et combien de temps elle doit durer :
Une clé appelée | ADK le stocke | Dure |
| avec la session | jusqu'à ce que la session soit supprimée. |
| à l'encontre de l'utilisateur | dans chaque session de cet utilisateur. |
| contre l'application | pour chaque utilisateur |
| nulle part | un appel |
Une invocation est un cycle requête-réponse unique, qui inclut tout le raisonnement du modèle, l'exécution de l'outil et la réponse finale.
Lorsqu'une invocation se termine, ADK écrit le nouvel état dans la base de données et supprime chaque clé temp: en cours de route.
Une chose reste inexpliquée
Dans two-days-ago, vous avez vu un résumé à l'endroit où se trouvaient les premiers tours de jeu d'une personne. L'ADK l'a écrit automatiquement une fois la conversation suffisamment longue.
Chaque fait a une durée de vie
5. Dégradation et compaction du contexte
Résumés : oubliez les détails
Lorsqu'une conversation devient trop longue, le système résume les messages les plus anciens pour économiser de l'espace et des jetons.
Les résumés sont utiles pour saisir l'idée générale et économiser des jetons.

👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Accédez au code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
Ce qui a changé depuis la dernière étape :
Fichier | Qu'est-ce qui a changé ? |
| l'agent est encapsulé dans un |
tout le reste | aucune modification |
agent.py ne se termine plus par root_agent. Elle se termine par :
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3, # summarise after every 3 turns
overlap_size=1, # re-read 1 turn either side when summarising
),
)
Sachez que adk web recherche d'abord une variable appelée app, puis revient à root_agent. Encapsulez l'agent dans un App chaque fois que vous avez besoin de quelque chose qui s'applique à l'ensemble de l'application plutôt qu'à un seul agent : la compaction ici et la reprise plus tard dans les prochaines étapes.
compaction_interval=3 signifie que l'ADK résume après chaque série de trois tours. Un tour de conversation correspond à une intervention de votre part et à la réponse de l'agent, quel que soit le nombre d'outils qu'il a appelés au cours de la conversation. Trois est une petite valeur, donc l'événement se déclenche lors d'un atelier. Une application réelle utiliserait un nombre beaucoup plus élevé.
overlap_size=1 signifie que chaque résumé commence un tour plus tôt que nécessaire.
ADK résume les tours 1, 2 et 3. Trois tours plus tard, il résume le prochain lot. Avec overlap_size=1, ce deuxième résumé recommence au tour 3, et non au tour 4 :

Notez que le tour 3 est résumé deux fois. C'est intentionnel. Si vous avez posé une question au tour 3 et que l'agent y a répondu au tour 4, une séparation nette place la question dans un résumé et la réponse dans l'autre, ce qui est déroutant. Répéter un tour est une assurance bon marché contre l'interruption d'une pensée.
Définissez les deux champs. L'ADK ne comporte aucune valeur par défaut pour ces deux éléments.
La compaction et votre service de mémoire ne se rencontrent jamais
Il est important de le préciser, car les deux mots semblent liés, mais les systèmes ne le sont pas.
EventsCompactionConfig est défini sur App dans agent.py, à côté de root_agent. Il s'agit d'une propriété de l'application, et non d'un service :
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
Un résumé réécrit la conversation. Il ne réécrit pas le fichier.
C'est la raison pour laquelle remember() existe en tant qu'appel délibéré. Tout ce qui reste dans la transcription est finalement lu comme une paraphrase écrite par quelqu'un d'autre. Tout ce qui est écrit dans le fichier est lu exactement tel qu'il a été stocké, le lendemain et la semaine suivante. À la fin de cette étape, vous verrez la différence se concrétiser.
Où est conservé le résumé ?
Dans sessions.db, dans le tableau events, un enregistrement de compaction est écrit dans une ligne d'événement par l'agent, avec un code temporel comme tout autre événement de tour.
Un événement ordinaire comporte un champ content et le récapitulatif se trouve sous actions.compaction.
👉 💻 Consultez-le dans votre propre base de données :
cd ~/monstertix
. ./set_env.sh
sqlite3 sessions.db \
"select json_extract(event_data,'\$.actions.compaction.compacted_content.parts[0].text')
from events where event_data like '%compaction%';"
Voici à quoi ressemble la forme de l'événement :
events row
└── event_data (JSON)
├── id, author, timestamp, invocation_id ← like every other event
├── content ← ABSENT on this one
└── actions
└── compaction
├── start_timestamp ← the window it replaced
├── end_timestamp
└── compacted_content.parts[0].text ← the summary itself
Le flux d'événements extrait chaque événement de son contenu. Le résumé dans la base de données et envoyé au modèle à chaque tour.
Le deuxième agent
budget_split est un agent distinct, et non un outil :
# agent/concert/agent.py
budget_split = Agent(
name="budget_split",
model=MODEL,
include_contents="none", # sees none of the conversation
instruction="You do ticket arithmetic and nothing else...", #just the budget
output_key="budget_plan", # its answer goes into state
)
root_agent = Agent(
...,
tools=[..., AgentTool(agent=budget_split)],
)
Pourquoi un agent et non une simple fonction ? Parce que le travail nécessite un modèle. Interprétez la question "Parmi ces sections, lesquelles correspondent à un budget de 200 $ pour quatre personnes ? Quel est le total ?" comme un problème d'arithmétique avec un jugement, dont la réponse doit être lisible par une personne. Laissez une fonction Python effectuer la multiplication et ne lui demandez pas de décider quelles options valent la peine d'être mentionnées.
Alors pourquoi ne pas laisser l'agent principal s'en charger ? La ligne suivante include_contents="none" signifie que cet agent ne reçoit aucune partie de la conversation. Pas de chat de groupe, pas de préférences, pas de quarante dates de tournée. Il reçoit la requête et rien d'autre. Cet indicateur n'existe que sur un Agent, ce qui explique pourquoi il s'agit d'un agent plutôt que d'une fonction.

Privilégiez les requêtes courtes : elles sont moins chères, plus rapides et beaucoup plus difficiles à dérailler. Comptez sur l'arithmétique pour être immunisée contre ce qui a été dit il y a 20 tours, car elle ne le voit jamais.
Pourquoi ?
AgentTool
? Encapsulez l'agent avec cet outil pour que l'agent principal puisse l'appeler comme n'importe quel autre outil. Du côté du modèle, budget_split est considéré comme identique à search_events.
et
output_key="budget_plan" place la réponse dans l'état de session sous ce nom au lieu de la laisser libre dans la conversation. Découvrez pourquoi c'est important.
Exécuter l'application
👉💻 Dans le terminal 1, appuyez sur Ctrl+C et redémarrez avec les mêmes indicateurs :
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 Lancez une nouvelle session et engagez une véritable conversation. Cinq ou six tours. La proposition ci-dessous fonctionne :
hello
what's on in Amsterdam?
Sam is coming with us, and he can't do weeknights
there are 4 of us and I can spend about $200 a head. What fits?
👉 Ouvrez l'onglet État.
Vous y trouverez budget_plan, qui contient la réponse complète de l'agent arithmétique.

👉✨ Modifiez maintenant les chiffres :
what if 10 people come and I can only do $120 each?
👉 Relisez budget_plan. Il a été remplacé, et non ajouté. En effet, output_key contient la dernière valeur.

Ce que vous ne pouvez pas voir : le résumé
À un moment donné, ADK a résumé les précédentes.
👉💻 Dans un nouveau terminal, exécutez la commande suivante pour le lire directement :
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
Lire ce qu'il a écrit
Voici un exemple réel, abrégé :
── summary 1 ──────────────────────────────────────────────────────
Conversation Language: English
**User Request:**
The user initially wanted to know what concerts are playing in Amsterdam.
Subsequently, the user provided a constraint regarding a companion, Sam.
**Context Summary:**
The user is planning a concert trip. The agent identified two events in
Amsterdam: "The Midnight Signal" at the Ziggo Dome on Tuesday, November
10 and Saturday, November 14, 2026. The user then informed the agent that a
companion named "Sam" will be joining, but "he can't do weeknights." The
agent has successfully noted this constraint for Sam.
**Key Decisions/Information Obtained:**
* **City of interest:** Amsterdam
* **Events found:** "The Midnight Signal" at the Ziggo Dome, Tuesday
10 November and Saturday 14 November 2026.
* **Companion:** Sam
* **Companion constraint:** Sam cannot attend concerts on weeknights.
**Unresolved Questions/Tasks:**
* The agent has not explicitly confirmed if the found event (which is
on a Saturday) is suitable given Sam's constraint, nor has it asked the
user for the next steps regarding this event or further event searches.
**Tools Used:**
* `recall`
* `search_events`
* `note_companion`
Il a conservé les prix, les sections, les villes et les préférences, et a correctement noté qu'aucune décision n'avait encore été prise.
Trois éléments découlent de cette affirmation :
Il s'agit d'une paraphrase. Rien de ce qui est dit ici n'est l'opinion de qui que ce soit. Il s'agit d'un compte rendu de ce qui a été dit, produit par le même type de processus qui produit tout ce que le modèle se trompe parfois.
Remarque : Les chaînes récapitulatives sont stockées dans les enregistrements d'événements de session internes et ne sont pas directement exposées sous forme d'onglets d'interface utilisateur distincts.
Toutes les trois interactions, dans une conversation qui est désormais en partie constituée de résumés précédents.
Essayer de réserver sans vous répéter
Dans une nouvelle session : ne démarrez pas de nouvelle session. Le résumé que vous venez de lire se trouve dans les événements de cette session. C'est la seule raison pour laquelle l'agent connaît encore des informations sur Amsterdam.
👉✨ Ne lui donnez rien de nouveau :
Book us something.
Regardez l'agent demander le spectacle, la section et le nombre de personnes. La réponse est correcte : le résumé indique que l'utilisateur n'a jamais rien confirmé, c'est pourquoi l'agent a posé la question. Faites confiance à la compaction.
Même lorsque la compaction réussit, la synthèse des tours précédents peut introduire une ambiguïté qui oblige l'agent à reconfirmer les contraintes avec l'utilisateur.
👉✨ Répondez à la question et laissez la réservation se faire :
Amsterdam Saturday, section C, 8 people
Il relit la commande avant de dépenser quoi que ce soit :
"Vous souhaitez réserver huit billets dans la section C pour The Midnight Signal à Amsterdam le samedi 14 novembre. Le coût total sera de 760 $. Voulez-vous que j'achète ces billets ?"
👉 ✨ Confirmez :
yes
Maintenant, il vous indique que l'opération a fonctionné :
Votre achat de huit billets en section C pour The Midnight Signal à Amsterdam le samedi 14 novembre, pour un montant total de 760 $, est terminé. Votre numéro de commande est ord_227877fe."
Vérifiez si c'est le cas.
👉 Passez au panneau de l'établissement. Ne vous fiez pas à la parole de l'agent.

Sur le panneau | Ce que vous devriez voir |
Billets achetés |
|
la commande en dessous |
|
The tour, Amsterdam Saturday | La section C est en baisse de 8 |
Activité |
|
Consultez le panneau pour connaître la vente de billets.
Pourquoi user:prefs était le bon choix
Rappelez-vous que nous avons écrit la contrainte de Sam note_companion dans la dernière étape de user:prefs, et l'objection évidente : pourquoi s'embêter avec un outil alors que l'agent pourrait simplement s'en souvenir ?
Voici pourquoi. Ne mentionnez pas l'état dans la conversation et ne l'incluez pas dans le résumé. Vérifiez que les valeurs user:prefs et budget_plan sont identiques à celles que vous avez saisies. Vérifiez ensuite la transcription.
Un résumé conserve la forme générale et perd les détails. Stockez tout ce que vous ne pouvez pas vous permettre de perdre ailleurs que dans la conversation.
6. Reprise du processus
Attendre 40 minutes sans rester en ligne
Lorsqu'un agent doit attendre dans une file d'attente de tickets de 40 minutes, le maintien d'un processus Python en vie et en attente est fragile et coûteux.
Pour gérer correctement les longues attentes :
- Enregistrer l'état dans une base de données : stockez l'endroit où l'agent a été mis en pause afin que le processus puisse s'arrêter en toute sécurité.
- Réveil à la demande : une fois l'attente terminée, un signal externe redémarre l'agent et reprend l'exécution là où elle s'était arrêtée.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Accédez au code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
Exécuter l'application
Ce qui a changé depuis la dernière étape :
Fichier | Qu'est-ce qui a changé ? |
| Ajout de |
|
|
join_queue renvoie immédiatement un ticket au lieu de bloquer, et ResumabilityConfig permet de mettre en pause l'exécution et de la reprendre ensuite.

👉💻 Terminal 1 : l'agent, inchangé :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉✨ Demandez à l'agent d'acheter :
Get us two tickets to the Amsterdam show.
Il appelle recall, puis search_events, puis join_queue, et répond avec quelque chose de proche de ceci :
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
Notez qu'il a sélectionné Saturday (Samedi) sans vous le demander. Il y a deux spectacles à Amsterdam, le 17 est un mardi, et le fichier de mémoire de la dernière étape indique que Sam ne joue pas en semaine. Il s'agit d'un fait issu d'une réservation précédente qui détermine une question dans celle-ci.
Notez également qu'il a rejoint la file d'attente avant de regarder un seul siège ou un seul prix, et qu'il n'a rien dit à ce sujet. C'est la règle de la salle : vous achetez les billets jusqu'à ce que vous arriviez devant. La seule chose qui vaille la peine d'avoir tôt, c'est une place dans la file d'attente. La question de savoir où s'asseoir se posera dans 40 minutes.
👉 Examinez le panneau de configuration. La bannière indique :

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
La file d'attente dure vraiment 40 minutes et ne disparaîtra pas pendant que vous travaillez.
👉💻 Arrêtez l'agent :
Ctrl-C
👉💻 Redémarrez-le dans le terminal 1 :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉✨ Dans la même session, demandez à l'agent où se trouve la personne dans la file d'attente :
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Comparez le ticket et observez la position continuer de baisser alors que rien n'était en cours d'exécution. Notez que l'agent ne savait pas qu'il avait été tué. Il a appelé check_queue avec le ticket qu'il avait obtenu avant le redémarrage, car ce ticket se trouvait sur le disque.
👉💻 Découvrez pourquoi :
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
Notez que chaque tour de chaque conversation correspond à une ligne. Le tour lui-même, l'auteur de la réponse, l'outil appelé, le résultat obtenu et la note sont au format JSON dans event_data.
👉 💻 Recherchez votre ticket de file d'attente :
sqlite3 sessions.db "select event_data from events;" \
| grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203
Lorsque join_queue est renvoyé, son ticket est enregistré directement dans la base de données. Au redémarrage, l'ADK recharge les tours et les résultats d'outils précédents à partir du disque, ce qui permet à l'agent de reprendre là où il s'était arrêté.
Notez qu'il n'a pas recommencé. L'agent reprend directement le workflow au lieu de le relire depuis le début. Comme LongRunningFunctionTool et ResumabilityConfig conservent l'état d'exécution sur le disque, l'ADK sait que l'agent est déjà en attente, ce qui l'empêche de rechercher à nouveau des dates de visite ou de demander un billet en double.

Pourquoi join_queue et non check_queue ?
Les deux fonctions renvoient des valeurs en millisecondes. La distinction réside dans le fait de savoir si le job sous-jacent est terminé lorsque la fonction renvoie un résultat.
Valeur renvoyée | Les travaux sont-ils terminés ? | |
| File d'attente et position initiale | Non.Le traitement se poursuit de manière asynchrone dans le système externe. |
| État actuel de la file d'attente | Oui. Renvoie une réponse immédiate et ponctuelle. |
LongRunningFunctionTool est conçu pour les opérations qui renvoient un handle vers le travail en attente. L'encapsulation join_queue permet à l'agent de suspendre l'exécution pendant que la file d'attente externe progresse.
En revanche, check_queue est synchrone et complet. Si vous l'encapsulez, l'agent suspend l'exécution de manière incorrecte chaque fois qu'il vérifie son état, même lorsqu'il arrive en tête de file et doit finaliser l'achat immédiatement.

Ce qui a fonctionné
Trois composants permettent la suspension et la reprise de l'exécution :
# agent/concert/agent.py
LongRunningFunctionTool(func=join_queue) # the tool returns without finishing
ResumabilityConfig(is_resumable=True) # the run may pause and be picked up
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db"
LongRunningFunctionTool: marque un outil comme non bloquant, en renvoyant un handle actif pour que l'agent puisse attendre sans bloquer le thread.ResumabilityConfig: indique au runtime de suspendre le cycle de vie d'exécution sur une tâche incomplète et de reprendre à partir du dernier état enregistré.Session Storage URI (--session_service_uri): dirige la persistance de l'état vers un stockage externe (par exemple, SQLite ou Cloud SQL) plutôt que vers la mémoire du processus, ce qui évite la perte de données lors des redémarrages.
7. Agir sur les données obsolètes
Les informations deviennent obsolètes pendant que vous attendez
L'historique des conversations conservé n'est pas un état en temps réel. Pendant les longs intervalles d'attente, les données externes (comme la disponibilité des billets ou les tarifs) peuvent changer sans que l'agent ne le sache.
Un agent dont il manque des informations s'arrête pour demander des précisions, mais un agent dont les données sont obsolètes agit immédiatement sur la base d'hypothèses dépassées. Pour éviter les actions non valides, les données sensibles au facteur temps doivent être revérifiées immédiatement avant l'exécution d'opérations critiques.
Ne chargez pas encore de nouveau code. Restez sur l'agent de l'étape précédente (il peut acheter des billets, mais n'a actuellement aucune protection contre l'obsolescence ou l'idempotence).
👉🔴 Appuyez sur RÉINITIALISER LE LIEU, puis démarrez une nouvelle session dans adk web en cliquant sur le bouton + Nouvelle session au-dessus du chat.
Démarrez une nouvelle session pour effacer le ticket de file d'attente précédent et l'état d'achat en attente. Cette étape nécessite un contexte d'exécution propre pour démontrer correctement la gestion de la fraîcheur.
L'instantané se trouve
👉✨ Demandez la même série que précédemment :
Get us two tickets to the Amsterdam show.
👉✨ Maintenant, pendant qu'il fait la queue, posez des questions sur les sièges :
How much are the seats, and what's still available?
Section A: $210 per ticket, with 400 seats available.
Section B: $145 per ticket, with 900 seats available.
Section C: $95 per ticket, with 1200 seats available.
👉🔴 Appuyez sur VENDRE LES BONNES PLACES. La section A passe à zéro.
👉🔴 Appuyez sur PASSER L'ATTENTE pour envoyer l'agent en tête de file d'attente.

👉 ✨ Demandez-lui d'acheter :
Buy the two section A seats.
Il exécute purchase(section="A") sans revérifier la disponibilité des places, ce qui entraîne une réponse d'erreur de l'API du lieu :
{"error": "sold_out", "available": 0,
"message": "those seats are gone — re-fetch the seatmap before buying"}
Oh no! It looks like the Section A tickets just sold out. Let me get an
updated seat map to see what's still available.

Notez que l'agent n'a relancé la requête pour le plan de la salle qu'après avoir reçu une réponse d'erreur de l'API. La cause première est un contexte obsolète : plus tôt dans la session, l'agent a indiqué "section A : 400 places disponibles", et ce tour est resté dans l'historique de la conversation transmis au modèle.
Indiquez clairement où sont stockées les anciennes informations : elles sont conservées dans l'historique des conversations, que les préfixes d'état tels que temp: ne peuvent pas remplacer.

Solution
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : huit spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Maintenant, chargez le code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👉💻 Dans le terminal 1, appuyez sur Ctrl+C et redémarrez :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
Ce qui a changé :
Fichier | Qu'est-ce qui a changé ? |
| de nouveau. S'exécute immédiatement avant un achat et peut le bloquer |
|
|
|
|
fence.py est un rappel que l'ADK exécute juste avant tout appel d'outil :
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
Regardez donc !
👉🔴 RÉINITIALISER LE LIEU Mettez-vous en file d'attente, appuyez sur VENDS LES BONNES PLACES pendant l'attente, puis demandez-lui d'acheter des places dans la section A.
Cette fois, il s'arrête tout seul. Regardez l'agent dire quelque chose comme :
"Il semble que la section A soit épuisée. Je dois relire le plan de la salle."
Observez ensuite l'appel de get_seatmap à nouveau, sans y être invité, et le retour avec la section C.
👉🔴 Lisez attentivement le flux d'activité du lieu, car il affiche les deux mi-temps :
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

L'achat d'appels s'affiche dans le tableau de bord, mais le compteur de tickets reste à zéro. Cela se produit parce que fence.py fait office de garde-fou avant l'exécution de l'outil :
- L'agent tente d'appeler l'outil d'achat.
fence.pyintercepte d'abord la demande et vérifie la disponibilité des places pour le direct.- Étant donné que les places sont épuisées,
fence.pyrenvoie directement une erreur, ce qui annule l'achat avant même que l'API ne soit appelée.
Les instructions de requête ne peuvent pas garantir qu'un LLM n'essaiera pas d'acheter des places épuisées. Les gardes-fous de code (before_tool_callback) garantissent que l'achat incorrect n'est jamais effectué.
👉🔴 Appuyez sur RÉINITIALISER LE LIEU, puis sur ANNULER LE PROCHAIN ACHAT.
👉✨ Demandez-lui d'acheter et laissez-le réessayer autant de fois qu'il le souhaite.

Le nombre de commandes reste à 1, car le lieu a reçu la même clé d'idempotence lors de la nouvelle tentative et a renvoyé la confirmation d'origine au lieu de créer une commande en double.
8. Workflow autonome
Les LLM ne sont pas capables de suivre des règles strictes à chaque fois
Les LLM sont très efficaces pour prendre des décisions (par exemple, choisir une série en fonction des préférences de l'utilisateur). Toutefois, si vous demandez à un LLM de suivre des règles strictes en plusieurs étapes (par exemple, "rejoignez toujours la file d'attente avant de vérifier les places disponibles"), il arrivera qu'il saute des étapes ou qu'il se trompe dans l'ordre.
Pour résoudre ce problème, divisez le workflow en deux types d'étapes :
- Règles fixes (fonctions de code) : les étapes qui doivent se dérouler exactement de la même manière à chaque fois (prendre un ticket de file d'attente, vérifier la position) sont écrites sous forme de nœuds de code ordinaires.
- Décisions (nœuds d'agent) : les étapes nécessitant un jugement (choix des sections de sièges, vérification des préférences de l'utilisateur) sont confiées à l'agent d'IA.
Regardez ce qui se trouve réellement dans ce flux. Certains n'émettent aucun jugement :
Étape | Jugement ? |
prendre exactement un ticket de file d'attente ; | Non. Toujours un, toujours avant d'acheter |
vérifier si c'est à votre tour de jouer ; | non.Posez la question, lisez la réponse |
rédiger le brief pour l'acheteur ; | Non. Même phrase, même forme, à chaque fois |
quelles séries et quelles sections | Oui. Prix, jour, qui vient, ce qu'il déteste |
Préférences et limites de l'utilisateur | Oui. |
Les procédures strictes ne doivent pas s'appuyer sur un LLM, car les modèles probabilistes finiront par ne pas suivre des séquences rigides lorsqu'ils ne sont pas surveillés.
Workflow d'ADK sépare l'exécution déterministe du raisonnement de l'IA :
- Les nœuds de fonction appliquent des règles fixes (comme rejoindre une file d'attente ou vérifier une position) de manière cohérente à chaque fois.
- Les nœuds d'agent gèrent les décisions contextuelles (comme le choix des sièges en fonction des préférences de l'utilisateur).
- Les nœuds d'attente suspendent l'exécution sans maintenir un processus actif en mémoire.
L'agent d'origine n'est pas remplacé. Il devient le nœud final du graphique de workflow, en conservant ses outils, ses vérifications de fraîcheur et ses protections d'idempotence. Le graphique contrôle l'ordre d'exécution, tandis que l'agent exécute la décision d'achat.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT.
👉💻 Accédez au code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
Exécuter l'application
👉💻 Dans le terminal 1, appuyez sur Ctrl+C et redémarrez. La même commande que d'habitude :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
Ce qui a changé depuis la dernière étape :
Fichier | Qu'est-ce qui a changé ? |
| de nouveau. Graphe d'exécution sans assistance en tant qu'ADK 2 |
|
|
| il a également perdu |
Exécuter le graphique : trois messages, dans l'ordre
Exécutez toutes les actions suivantes dans le fichier Python nightly (c'est là que se trouve le graphique). Envoyez la même entrée à chaque tour pour observer la transition du graphique de workflow à travers ses états.
1. Remarquez à qui vous parlez maintenant
👉 Actualisez l'UI Web ADK.
# concert/agent.py, at the bottom
buyer_agent = Agent(name="concert", ...) # ← everything you built, renamed
from .nightly import nightly
root_agent = nightly # ← what adk web actually runs
Notez que adk web exécute l'objet attribué à root_agent. Ici, root_agent pointe vers le graphique de workflow nightly, avec buyer_agent configuré comme nœud final du graphique.
2. Lance-le
👉✨ Saisissez la commande suivante et appuyez sur Entrée :
go
Remarque : Dans un graphique de workflow automatisé, l'exécution initiale commence sans attendre l'entrée utilisateur. L'envoi d'un message dans adk web déclenche la première exécution.
👉 Regardez maintenant le navigateur Web. Le panneau de gauche affiche le graphique et colore les nœuds à mesure qu'ils s'exécutent. Le panneau de droite correspond au flux d'événements. Il vous permet de voir comment les décisions sont prises :

Écrivez queue_ticket et queue_event_id dans l'état de la session queue_up.

Un appel de fonction nommé adk_request_input doit s'afficher, en attente d'une réponse. Ensuite, le silence se fait et l'exécution se termine. Le graphique a atteint check_front, vous a trouvé à la 14 203e position et s'est arrêté.
👉 Consultez le panneau de l'établissement : un ticket de file d'attente et rien en cours d'exécution.

3. Réveillez-le, mais faites attention à la zone de saisie
Faites défiler le flux d'événements jusqu'à l'événement adk_request_input et localisez la zone de saisie associée à cet événement spécifique :

Vous pouvez désormais saisir du texte à deux endroits différents, et ils ont des fonctions différentes.
L'endroit où vous saisissez du texte | Que se passe-t-il ? |
la case | répond à l'interruption. L'exécution mise en veille se poursuit à partir de |
la zone de chat principale en bas ; | démarre une nouvelle invocation. Le graphique est rejoué à partir de |
👉✨ Saisissez ce que vous voulez dans la zone Enter your response..., puis appuyez sur la flèche.
👉 Terminal 1 :
[check_front] #14,197 — not yet. Pausing.
Une ligne. Il n'a pas sélectionné de deuxième émission ni pris de deuxième billet de file d'attente, ce qui vous aurait placé à la fin d'une file d'attente de 14 203 personnes.
Si [open] et [pick_show] s'affichent à nouveau, cela signifie que vous avez saisi du texte dans la zone de chat. Appuyez sur RÉINITIALISER LE LIEU et recommencez l'étape.
👉 Vérifiez à nouveau le panneau de l'établissement : il reste un billet et la position a légèrement baissé, car l'horloge de l'établissement a continué de tourner alors que la vôtre était à l'arrêt.

4. Arriver en tête, puis terminer
👉🔴 Appuyez sur PASSER L'ATTENTE dans le panneau du lieu.

👉✨ Réponds une dernière fois à l'événement adk_request_input le plus récent, toujours dans sa boîte, et non dans la boîte de discussion. 
Cette fois, la montre check_front vous trouvera à l'avant et vous laissera tomber. Regardez les deux derniers nœuds s'allumer dans le panneau de gauche : brief et concert.

👉 Regardez le navigateur. Il s'agit de la fin de l'exécution. Quatre éléments méritent d'être lus.

Le graphique est entièrement vert et les arêtes sont comptabilisées. brief et concert sont désormais mis en évidence, et les bords de pick_show, queue_up et check_front sont désormais associés à un badge.
2x
correspond au nombre de fois où le graphique a parcouru cette arête, une fois par réveil. Les deux 2 en haut correspondent au prix de la reprise : le planificateur réintègre le graphique depuis le début à chaque fois. Cela ne signifie pas que le travail a été effectué trois fois. Le panneau "Lieu" en est la preuve : un billet pour la file d'attente, et non deux. @node(rerun_on_resume=False) est la raison.
Le graphique

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
Notez que deux nœuds sont des agents et deux sont des fonctions. La question de conception consiste à déterminer lesquels sont lesquels.
Nœud | Pourquoi cette forme ? |
| agent. Choisir une série dans l'historique d'une personne est un jugement. Il appelle |
| . Rejoindre une file d'attente est une règle. Une règle ne doit pas être une décision prise à pile ou face à 3h du matin |
| . La question "Suis-je au premier rang ?" a une seule bonne réponse, qui est fournie par le lieu. |
| agent. Il s'agit de |
Examinez le dernier nœud et recherchez l'agent que vous avez créé. Pas une copie, pas une réécriture :
# agent/concert/nightly.py
from .agent import buyer_agent
Tout est inclus : tous les outils dont il dispose, la relecture du plan de salle dans fence.py, la clé d'idempotence dans purchase. Il n'y en a qu'un.
Pourquoi la lecture s'est-elle arrêtée au lieu d'être mise en pause ?
Voici la version à ne pas écrire :
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
Il maintient un processus en vie pendant quarante minutes pour ne rien faire, meurt avec le terminal et ne peut pas être repris. Voici ce que fait check_front :
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput est une interruption. Le graphique s'arrête là où il en est, l'invocation est envoyée au magasin de sessions et le processus peut se terminer.
L'exécution ne coûte rien pendant l'attente et se termine à un seul endroit : les billets achetés. Il faudra peut-être toute la nuit et plusieurs réveils pour y parvenir.
9. Travailler avec un humain
Ne dépensez pas d'argent sans vérifier auprès d'une personne
Un agent autonome qui s'exécute la nuit ne doit pas faire de choix financiers coûteux en se basant sur des préférences devinées ou ambiguës de l'historique des discussions.
Avant d'effectuer des actions importantes (comme dépenser de l'argent pour des billets), l'agent doit faire une pause, demander à l'utilisateur de confirmer son budget à voix haute et enregistrer cette confirmation explicite avant d'effectuer tout achat.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉💻 Accédez au code de cette étape :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
Exécuter l'application
👉💻 Dans le terminal 1, appuyez sur Ctrl+C et redémarrez. Exécutez la même commande que précédemment :
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
Ce qui a changé depuis la dernière étape :
Fichier | Qu'est-ce qui a changé ? |
| Contient ce que la personne a accepté, sous la forme de la phrase qu'elle a prononcée. |
| ajouté |
| un nouveau premier nœud, |
Ne le modélisez pas. Stockez la phrase.
Ouvrez agent/concert/budget.py et lisez l'intégralité du contenu :
def load(state) -> str:
"""What they agreed, verbatim. Empty string means nobody has said."""
return str(state.get("budget") or "")
state["budget"] = "up to $100 for the upper bowl or general admission, up to
$250 for the lower bowl, and up to $300 for a Saturday show"
Le nœud vérifie deux choses : que la personne l'a dit et qu'elle l'a confirmé.
Ensuite, il conserve la phrase dans l'état de session plutôt que sous user:, à dessein. Un budget est convenu pour une réservation. "250 $, si ce sont les bons" a été dit à propos de ce groupe, ce soir-là, et cela n'a rien à voir avec la réservation de billets.
Il en va de même pour tout le reste de ce qu'ils ont dit.
Ne vous arrêtez pas au budget. Les utilisateurs peuvent changer d'avis pendant qu'ils prennent leur décision. Vous devez donc noter tout ce qu'ils demandent ici, plutôt que de vous fier à ce dont l'agent se souvient.
"I live in NYC, I can only do weekends, I have 10 people coming with me"
# agent/concert/nightly.py
def what_they_asked_for(ctx) -> str:
"""Everything this person typed, joined, in their own words."""
said = []
for event in (ctx.session.events or []):
if event.author != "user":
continue
for part in (event.content.parts if event.content else []) or []:
text = (getattr(part, "text", None) or "").strip()
if text and text not in said:
said.append(text)
return "\n".join(said)
L'option Événements créés par l'utilisateur uniquement ne prend en compte que
text
parties de function_response. Par conséquent, la question et la réponse concernant le budget ne sont pas reprises ici. Pas de doublons.
Notez que agree_budget l'appelle à chaque exécution, et pas seulement la première. Une correction saisie pendant que l'agent est en file d'attente est donc également prise en compte. Ensuite, open_the_night remet au sélecteur les deux phrases, la demande et le budget, et le sélecteur les lit.
Le graphique, un nœud plus long
Commençons par une conversation :
# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, agree_budget, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)

Deux nœuds s'arrêtent et attendent, et il s'agit du même mécanisme. check_front attend un lieu et reçoit une horloge en réponse. agree_budget attend une personne et ne peut être répondu que par une personne. Cette différence correspond à l'étape 10 dans son intégralité.
Exécuter le code, étape par étape
1. Lance-le
👉 Actualisez l'UI Web d'ADK et sélectionnez l'agent concert.
👉✨ Demandez ce que vous voulez vraiment :
Get us two tickets to the Amsterdam show.

À l'étape 8, le graphique ne comportait aucun utilisateur. Par conséquent, tout ce que vous avez saisi a été ignoré et go a fait aussi bien que n'importe quoi. Ici, agree_budget appelle what_they_asked_for(ctx) sur sa toute première ligne, ce qui collecte tout ce que vous avez saisi et le place dans state["request"]. open_the_night remet ensuite le budget et la liste au préparateur.
Vous n'avez pas indiqué de jour, et Amsterdam propose deux spectacles. Regardez donc le sélecteur revenir à recall() pour cela, trouvez "Sam bails on weeknights" et prenez le samedi.
Dans le navigateur, l'exécution s'arrête sur un événement adk_request_input :
Un graphique vient de vous poser une question. Même interruption que l'attente dans la file d'attente à l'étape 8, mais elle est maintenant dirigée vers une personne pour répondre à la question.
2. Répondez comme les utilisateurs répondent réellement
👉✨ Dans la zone Enter your response... (et non dans la zone de chat en bas de l'écran) :
About a 100 for the cheap seats. 250 if they're the good ones.
Regardez-le s'arrêter et confirmez :
[agree_budget] reading back: About a hundred for the cheap seats. Two-fifty
if they're the good ones.
⚡ adk_request_input
So: About a hundred for the cheap seats. Two-fifty if they're the good
ones. Have I got that right?

3. Confirmer
👉✨ Même boîte :
Yes, that's right.
[agree_budget] agreed: About a hundred for the cheap seats. Two-fifty if
they're the good ones.
[open] Budget: About a hundred for the cheap seats. Two-fifty if
they're the good ones.
[pick_show] → {"section": "A", "reason": "..."}
[queue_up] ms-ams-01 section A — ticket q_5924edc2 at #14,203
[check_front] #14,203 — not yet. Pausing.
Quatre nœuds ont été exécutés à partir d'un mot. 👉 Ouvrez l'onglet État et recherchez budget, qui correspond à votre phrase, mot pour mot.

Aucun préfixe Il s'agit de l'état de la session. Si vous démarrez une nouvelle session, le budget disparaît et vous êtes donc à nouveau invité à le définir.
4. Prenez la tête et terminez la course
👉🔴 Appuyez sur PASSER L'ATTENTE dans le panneau du lieu.
👉✨ Répondez à la dernière adk_request_input, encore une fois dans sa boîte :
go

👉 Consultez le panneau de l'établissement : une commande, à un prix correspondant à celui que vous avez accepté pour ce type de siège.

Comment un graphique pose-t-il une question à une personne ?
Ouvrez nightly.py et lisez agree_budget. Faites en sorte que trois choses soient vraies en même temps et attendez-vous au même échec si l'une d'elles manque : la requête est infinie.
@node(rerun_on_resume=True) # 1. run again when woken
def agree_budget(node_input, ctx):
answers = ctx.resume_inputs or {} # 3. where the answer arrives
said = answers.get(ASK_BUDGET)
if not said:
return RequestInput(
interrupt_id=ASK_BUDGET, # 2. STABLE id, not the default
message="What are you willing to spend?",
)
...
| Sans cela, le nœud est considéré comme terminé et ignoré. | |
| La valeur par défaut est un nouvel UUID à chaque exécution. Il n'y a donc rien à rechercher pour trouver la réponse. | |
| où la réponse atterrit. Pas la valeur renvoyée de | |
Le graphique
Il s'agit du graphique de l'étape 8 avec un nœud supplémentaire devant. 
Les contraintes confirmées par l'utilisateur sont de portée session et expirent à la fin de la conversation. Si vous conservez les limites temporaires d'une session à l'autre, vous risquez de transformer des décisions ponctuelles en paramètres par défaut permanents non souhaités.
10. La pile Cloud
Les ordinateurs portables se mettent en veille et les fichiers locaux disparaissent
Sur votre ordinateur portable, un agent dépend de la mise en veille de votre ordinateur, des bases de données SQLite locales et des fichiers locaux. Si vous fermez le couvercle de votre ordinateur portable ou si vous déployez un conteneur cloud qui redémarre, les fichiers et la mémoire locaux disparaissent.
Pour s'exécuter de manière fiable en production :
- Cloud Compute : exécutez l'agent dans un conteneur (comme Cloud Run) qui reste en ligne.
- Bases de données cloud durables : remplacez les fichiers locaux et SQLite par des services cloud (Cloud SQL pour les données de session, Cloud Storage pour les fichiers).
- Cloud Timers : utilisez des planificateurs gérés (comme Cloud Scheduler et Pub/Sub) pour que l'agent se réveille même lorsque votre ordinateur portable est éteint.
👉🔴 Dans le panneau de l'établissement, appuyez sur RÉINITIALISER L'ÉTABLISSEMENT. Chaque étape commence dans le même monde : 8 spectacles, toutes les places disponibles, horloge à 1x.
👉 💻 Accédez au code final :
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
Fichier | Qu'est-ce qui a changé ? |
| de nouveau. Un FastAPI servant la page de chat et le point de terminaison qu'un planificateur appelle |
| de nouveau. Python-slim et uvicorn ordinaires. Il n'y a rien de spécifique à ADK. |
| copiée, de sorte que la page est fournie dans le même conteneur que l'agent. |
| inchangée. Octet par octet, le dossier avec lequel vous avez terminé l'étape 9 |
La page que vous avez déjà créée
L'** interface utilisateur Web de l'ADK** est strictement une interface de débogage locale permettant d'inspecter l'état, les flux d'événements et les graphiques de workflow pendant le développement. Il n'est pas empaqueté pour le déploiement en production.
Pour la production, utilisez l'interface utilisateur de chat autonome créée à l'étape 3. Il gère les interruptions de messagerie et d'interaction des utilisateurs via le point de terminaison /wake sans exposer les commandes de débogage internes.
Migrer l'état local vers les services cloud
Arrêtez le serveur et examinez ce qu'il a écrit :
sessions.db a SQLite file next to the code
memory/userx.md a markdown file you can open in an editor
artifacts/ seat maps written to a folder
Bien que les fichiers locaux fonctionnent bien pour le développement, ils échouent en production, car les conteneurs Cloud Run utilisent un stockage éphémère et local à l'instance.
Lorsque vous exécutez le code dans Cloud Run :
- Mise à l'échelle des instances à zéro : lorsque le trafic s'arrête, le conteneur se termine et son système de fichiers local est supprimé.
- Les instances effectuent un scaling horizontal indépendamment : plusieurs instances simultanées ne partagent pas d'espace disque. Chacune s'exécute avec son propre système de fichiers isolé.
Si un agent enregistre un état de session ou un ticket de file d'attente dans un fichier SQLite local (sessions.db), les demandes ultérieures acheminées vers une instance de conteneur nouvelle ou différente trouveront une base de données vide. Pour maintenir un état durable lors de la mise à l'échelle et des redémarrages, les fichiers locaux doivent être remplacés par des services externes gérés.
Vous avez plutôt besoin d'un état qui survit à n'importe quel conteneur. Trois services, un pour chaque fichier :
Sur votre ordinateur portable | Dans le cloud | Description |
| Cloud SQL | une base de données Postgres gérée. Il existe, qu'une application soit en cours d'exécution ou non. |
| Cloud Storage | un bucket. Fichiers en tant qu'objets, accessibles depuis chaque instance à la fois |
| Cloud Storage | le même bucket. |
| Cloud Scheduler | un cron exécuté par Google. Votre terminal n'est pas un réveil |
Créez-les
Provisionnez une base de données PostgreSQL, un utilisateur de base de données et un bucket Cloud Storage. Notez que la création de l'instance Cloud SQL prend plusieurs minutes et a été lancée en arrière-plan lors de l'étape setup.sh.
👉 💻 Récupérez-le et faites le reste :
cd ~/monstertix
./setup-cloud-state.sh
→ instance RUNNABLE
→ database adk created
→ user adk created
→ bucket created
→ memory memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env
Ce script a créé quatre éléments, un par ligne du tableau ci-dessus, et a écrit deux URI dans .env :
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
Déployer l'agent avec des déclencheurs programmés gérés
Une fois les services prêts, donnez à l'agent son emplacement permanent.
👉💻 Trois ou quatre minutes. Lancez-le et poursuivez la lecture :
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
Le script crée une image de conteneur à partir de agent/, la déploie sur Cloud Run et provisionne l'infrastructure événementielle associée.
Le déploiement complet se compose de cinq éléments distincts :
1. gcloud run deploy the container: your agent, the graph, and the page
2. a Pub/Sub topic a queue for messages
3. a service account an identity, so the message is allowed in
4. a push subscription topic ──► /trigger/wake
5. a Cloud Scheduler job the alarm clock, on a cron
Cloud Scheduler comme source de déclencheur
Cloud Scheduler remplace les minuteurs d'exécution locaux (clock.py) par un service Cron géré qui persiste indépendamment des sessions de terminal locales, gère les nouvelles tentatives automatiques et prend en charge la planification des fuseaux horaires.
Pub/Sub comme couche de découplage des messages
Placer un sujet Pub/Sub entre Cloud Scheduler et Cloud Run présente trois avantages architecturaux :
- Durabilité des messages : les files d'attente reçoivent les événements de déclenchement entrants si le service Cloud Run est en démarrage à froid, en cours de redéploiement ou temporairement indisponible.
- Règles de réessai intégrées : les échecs de remise temporaires sont automatiquement réessayés sans nécessiter de logique de réessai personnalisée au niveau de l'application.
- Scalabilité de la diffusion : permet à plusieurs services en aval de s'abonner aux mêmes événements de planification sans reconfigurer le planificateur.

La configuration de trigger_sources=["pubsub"] enregistre un point de terminaison push compatible avec les charges utiles d'enveloppe Pub/Sub standards, en traduisant les messages entrants directement en événements d'appel d'agent.
→ agent https://concert-you-xxxx.run.app
→ topic presale-you created
→ iam concert-trigger@your-project... may invoke concert-you
→ push → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *
Lui parler
👉 Ouvrez l'URL de l'agent dans un navigateur. 👉 ✨ Demandez-lui de réserver :
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
Il devrait vous demander combien vous êtes prêt à dépenser.
👉✨ Répondez à voix haute, puis confirmez :
up to 250 a seat for the good ones
L'exécution est actuellement en attente dans la file d'attente.
👉🔴 Appuyez sur PASSER L'ATTENTE dans le panneau de l'établissement pour que la file d'attente soit prête lorsque le message arrive.
Pour l'exécuter selon une programmation réelle, le job doit être activé. jobs run refuse un job mis en veille avec Job.state must be ENABLED :
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 En quelques secondes, l'agent effectue l'achat dans les limites du budget confirmé et renvoie automatiquement une confirmation de commande.
👉🔴 Consultez le panneau de l'établissement. Une seule commande.
Voici comment l'exécuter toutes les cinq minutes :
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
Effectuer un nettoyage
Comptez deux éléments déployés : le lieu et l'agent à côté. Les deux sont mis à l'échelle zéro, donc un service inactif ne coûte rien. Supprimez-les quand même.
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
Notez que nous laissons délibérément Cloud SQL et le bucket de côté, car ils contiennent vos données et continuent tous les deux à générer des frais. Lorsque vous êtes sûr d'avoir terminé :
./destroy-agent.sh --all
Tout le reste est local. Supprimez le dossier une fois que vous avez terminé, ou conservez-le et exécutez-le sur votre propre projet aussi longtemps que vous le souhaitez.
L'agent, ses outils, ses rappels et le budget n'ont pas changé. Deux URI et une variable d'environnement.
11. Présentation du système et guide de dépannage
Différences d'environnement : architecture locale et cloud
Votre ordinateur portable | Dans le cloud | Qu'est-ce qui a changé ? |
| Cloud SQL | une chaîne de connexion. |
|
| un URI |
|
| un chemin |
| un service Cloud Run. |
|
| Cloud Scheduler → Pub/Sub |
|
vous, au clavier |
| une valeur par défaut, remplacée pour chaque requête ; |
L'implémentation de l'agent principal, y compris root_agent, les définitions d'outils, les protections de rappel et l'application du budget, restent complètement inchangées. La comparaison de solutions/step10_deploy/concert/ avec l'étape 9 montre une correspondance exacte octet par octet, ce qui confirme comment l'abstraction de service de l'ADK sépare la configuration de l'infrastructure de la logique de l'agent.
Si vous êtes bloqué
Accédez au code fonctionnel complet à n'importe quelle étape :
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
En cas de problème
Problème constaté | Solution |
| ID incorrect. |
Le modèle renvoie une erreur 404 | Les alias |
| Le port 8000 ou 8090 est déjà utilisé. |
L'agent ne peut pas accéder au lieu |
|
La file d'attente n'avance jamais | Appuyez sur PASSER L'ATTENTE. À la vitesse 1x, la file d'attente n'atteindra pas le début d'elle-même. |
Un deuxième ticket de file d'attente s'affiche dans le panneau. | Vous avez répondu dans la zone de chat au lieu de la zone |
Le menu déroulant est vide ou l'application ne se charge pas |
|
| C'est normal. |
Tout est étrange | Appuyez sur RÉINITIALISER LE LIEU, puis sur |
Fichier de mémoire plein de réservations en double |
|
|
|
Délai Cloud Shell expiré | Redémarrez |