Tutto ciò che ti serve per creare agenti a esecuzione prolungata su Google Cloud

1. Prima di iniziare

La maggior parte dei siti di biglietti mette gli acquirenti in coda, sia per evitare arresti anomali del server sia per gestire la forte domanda. Inizi l'acquisto e ti ritrovi in coda con 14.000 persone. La coda si muove per 40 minuti. Quando arriva il tuo turno, hai poco tempo per scegliere i posti e pagare. Se non riesci a partecipare entro il tempo stabilito, perderai il tuo posto in coda.

Molte attività del mondo reale seguono questo schema: un'attività che richiede molto più tempo di una singola conversazione, eseguita su un sistema esterno che non controlli. Alcuni esempi sono l'attesa del rifornimento di un fornitore, l'attesa dell'approvazione di una richiesta o l'attesa in una coda.

Gli agenti AI standard vivono all'interno di un singolo turno di chat, che dura pochi secondi ed è puramente reattivo. Non possono aspettare 40 minuti o agire in autonomia. Per risolvere questo problema, hai bisogno di un agente a esecuzione continua: un sistema che rimane inattivo durante le lunghe attese, mantiene il contesto durante i riavvii del processo e riprende automaticamente quando viene attivato da un trigger esterno.

Questo workshop ti guida nella creazione di un agente di coda per i biglietti dei concerti, illustrando i pattern architetturali di base richiesti per gli agenti di lunga durata su Google Cloud.

Cosa creerai

Cosa stai creando

In questo workshop creerai un agente di prenotazione di biglietti a esecuzione prolungata mentre impari i pattern software agentici chiave:

  • Sospensione e ripresa del processo: sospendi la chiamata di un agente durante i lunghi periodi di attesa e riprendi l'esecuzione tramite trigger esterni.
  • Archiviazione dello stato durevole: mantieni lo stato e gli artefatti della conversazione durante i riavvii del processo utilizzando SQLite/Cloud SQL e Cloud Storage.
  • Protezioni dall'obsolescenza e idempotenza: verifica i dati dinamici (come la disponibilità dei posti) prima di agire e utilizza le chiavi di idempotenza per evitare transazioni duplicate.
  • Grafici del flusso di lavoro ADK: struttura l'esecuzione utilizzando nodi di codice deterministici per regole procedurali e nodi dell'agente per il processo decisionale contestuale.
  • Approvazione human-in-the-loop: metti in pausa l'esecuzione per raccogliere e conservare i limiti di spesa espliciti dell'utente.
  • Deployment cloud: inserisci l'agente in un container ed esegui il deployment in Cloud Run con trigger Cloud Scheduler e Pub/Sub.

Configurazione

Innanzitutto, prepara l'ambiente e assicurati di avere accesso a Google Cloud. Ti consiglio di eseguire questa operazione in Cloud Shell, che dispone già di tutti gli strumenti necessari. Verifica di avere spazio sufficiente e che il tuo account possa creare progetti.

👉💻 Clona il repository ed esegui la configurazione:

cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh

Ti verrà chiesto una volta:

  Which Google Cloud project should this workshop use?
  Find yours at https://console.cloud.google.com  (top-left picker)

  Project id [YOUR_PROJECT_ID]:

Poi esegue le seguenti operazioni, in ordine:

virtualenv

crea .venv e vi installa ADK 2

accesso

gcloud auth application-default login se non hai ancora credenziali

project

punta gcloud al tuo progetto e lo imposta come progetto di quota

API

abilita le dieci API necessarie per l'officina. aiplatform, run, cloudbuild, artifactregistry, storage, pubsub, cloudscheduler, cloudtrace, logging e sqladmin.

.env

scrive l'ID progetto, la regione e l'ID modello, in modo che ogni script e adk web li rilevi

controllo del modello

effettua una chiamata reale a Gemini, quindi un ID progetto o modello errato non va a buon fine

seed

crea una sessione già vecchia di due giorni, che viene aperta al passaggio 3

Cloud SQL

avvia un'istanza Postgres in background

sede

esegue il deployment del tuo venditore di biglietti simulato in Cloud Run

👉💻 Attiva l'ambiente:

cd ~/monstertix
source .venv/bin/activate

Il prompt ora inizia con (.venv).

Panoramica dello scenario

Nello scenario di questo workshop, il tuo agente prenota due biglietti per un concerto ("The Midnight Signal" ad Amsterdam). Le preferenze dell'utente memorizzate nella memoria storica includono:

  • Deve essere uno spettacolo del fine settimana (Sam non può partecipare alle serate feriali).
  • Evita i posti nel settore superiore dello Ziggo Dome (scarsa visibilità).
  • Il prezzo target è di circa 200 $per biglietto, con un limite massimo di 250 $.

The Midnight Signal, in tour

Verifica l'installazione

👉💻 Un comando, otto segni di spunta:

./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.

👉 Apri le schede del secondo browser:

Dove

Descrizione

$VENUE_URL/panel

Il pannello di controllo, i pulsanti. setup.sh ha stampato questo URL e lo ha inserito in .env

2. Prompt per raggiungere l'autonomia?

I prompt non possono avviarsi da soli

Un prompt è semplicemente un testo passato a un modello di AI quando viene effettuata una chiamata. Non può funzionare da solo, riattivarsi o avviare una nuova chiamata.

Se chiedi a un agente nel prompt di "ricontattarti tra 10 minuti", non succede nulla al termine del turno corrente. Il modello non ha orologio interno o loop. Per rendere autonomo un agente, qualcosa al di fuori dell'agente deve attivarlo e chiamarlo.

Dove si trova questo passaggio

Avviare il rig

Inizieremo testando gli strumenti per sviluppatori. adk web avvia l'interfaccia utente web ADK, l'interfaccia di sviluppo fornita con ADK. Ti consente di parlare con l'agente che stai creando senza prima scrivere un frontend e ti mostra ciò che una finestra di chat non può mostrare, ad esempio ogni chiamata di strumento e i relativi argomenti, lo stato della sessione man mano che cambia, gli artefatti salvati dall'agente e gli eventi non elaborati sottostanti. Non lo esporresti in produzione. È la migliore finestra su un agente in esecuzione mentre impari. Nel passaggio 3 crei il frontend che esporrai effettivamente e nel passaggio finale lo implementi.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Terminal 1: carica il codice di questo passaggio e avvia la UI web dell'ADK:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"

use-solution.sh copia il codice completato di un passaggio in agent/, dove lavori. Ogni passaggio inizia con questo comando e stampa i file modificati, in modo che tu sappia cosa leggere.

Cosa devi fare per iniziare:

File

Descrizione

agent.py

l'agente: un nome, un modello, un'istruzione, un elenco di strumenti

tools.py

search_events e get_seatmap

venue.py

un client HTTP per la sede. Niente di interessante

config.py

quale modello utilizzare e una verifica del funzionamento delle tue credenziali

. ./set_env.sh attiva virtualenv, carica il progetto e il modello ed esporta $WORKSHOP, il percorso assoluto di ~/monstertix. Esegui questo comando in ogni terminale che apri. Stampa ciò che ha impostato:

  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

👉 Apri le prime schede del browser:

Dove

Descrizione

UI web dell'ADK

localhost:8000: il tuo agente, più le schede Stato / Eventi / Artefatti

Parla con l'app

👉✨ Nell'interfaccia utente web di ADK, digita:

I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.

Cerca il tour, legge le mappe dei posti e ragiona sul tuo budget e sulla tua azienda. Si tratta di un assistente davvero competente. Tuttavia, quando la prevendita inizia alle 10:00, l'agente non viene eseguito automaticamente perché non è in esecuzione alcun processo o trigger in background.

Leggi cosa è appena successo

La parte centrale dello schermo non è solo un log della chat. Ogni passaggio della svolta è numerato, incluse le chiamate agli strumenti:

  ▸ 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

👉 Fai clic su una riga ⚡. Il riquadro a sinistra mostra gli argomenti scelti dal modello. Fai clic sul ✓ sottostante per il JSON restituito.

Le informazioni sul tour non sono presenti nei dati di addestramento o nel prompt del modello. L'agente ha recuperato questi dati eseguendo lo strumento search_events, che ha eseguito una query sull'API Venue.

Apri il codice che hai appena eseguito

Hai visto che funziona e hai visto la traccia. Ora esamina il codice.

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

👉💻 Apri agent/concert/agent.py. L'intero agente si trova in fondo al file:

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 nome, un modello, una descrizione di come deve comportarsi l'agente e un elenco di funzioni Python. Questo è tutto ciò che fa un Agent. adk web l'ha trovato perché il file si chiama agent.py e la variabile si chiama root_agent.

👉💻 Ora 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)

Una funzione ordinaria. Nessun decoratore, nessuna registrazione, nessun schema da scrivere. ADK legge la firma e la docstring e crea la definizione dello strumento visualizzata dal modello, motivo per cui la docstring è portante e l'eliminazione di un argomento ha modificato il comportamento del modello.

Il modello non esegue mai il tuo codice. Quando hai visto search_events({"city": "Amsterdam"}), il modello chiedeva di effettuare la chiamata. L'ADK ha eseguito la funzione, ha restituito il risultato e il modello ha scritto una frase al riguardo. Il loop è:

👉 Le icone sul bordo più a sinistra cambiano il contenuto del pannello. Il secondo è il grafico degli agenti: provalo e dovresti vedere due strumenti.

Grafico dell'agente che mostra gli strumenti search_events e get_seatmap

Quindi, digli di non dormire

Che cosa faresti per risolvere il problema? Prova la cosa che la maggior parte delle persone cerca per prima. Richiama INSTRUCTION dal passaggio 1, il testo fornito al modello a ogni turno, e riscrivilo.

👉💻 Apri agent/concert/agent.py. Sotto INSTRUCTION ce n'è un secondo, già scritto per te. Se vuoi, puoi modificarlo tu:

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],
)

👉💻 In terminale 1, riavvia:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"

👉✨ Spiega il piano esattamente come faresti con una persona:

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.

Accetterà e sembrerà che lo faccia con convinzione.

👉 Ora aspetta. Non digitare altro. Guarda il panel.

Attendi un minuto. NON SUCCEDE NIENTE!

Perché quel prompt non avrebbe mai funzionato

Rileggila con occhi nuovi. Monitora. Nel momento in cui si apre. Continua a controllare. Ognuna di queste descrive qualcosa che accade nel tempo.

Ora ricorda che cos'è instruction=. È una stringa, fornita al modello nell'ambito della richiesta, nel momento in cui qualcuno invia un messaggio. Nessuno lo legge tra un turno e l'altro.

# agent/concert/agent.py
root_agent = Agent(
    instruction=PROACTIVE_INSTRUCTION,   # read on invocation
    ...                                  # ...and there was no invocation
)

Un'istruzione non può richiamare la funzione al cui interno è scritta.

Varianti del prompt non riuscite

Il tentativo di imporre l'esecuzione autonoma tramite le istruzioni del prompt non va a buon fine, indipendentemente dalla formulazione. Tentativi di prompt comuni, ad esempio:

  • "Imposta un timer per le 10:00 di martedì e compra i biglietti a quell'ora".
  • "Controlla ogni cinque minuti se è iniziata la prevendita."
  • "Agisci in autonomia senza attendere l'input dell'utente".

Tutti non riescono per lo stesso motivo: i prompt vengono letti solo quando si verifica una chiamata. Senza un chiamante esterno, non viene eseguito alcun codice tra i turni.

3. Inattività basata su eventi

Gli agenti non rimangono attivi per sempre

Se lasci in esecuzione un processo di polling continuo, sprechi memoria, denaro e si verifica un arresto anomalo se il laptop va in sospensione o si riavvia.

Invece di mantenere un agente in esecuzione ininterrottamente durante l'attesa, un agente a esecuzione prolungata deve rimanere completamente inattivo. Un evento esterno (ad esempio un timer, un webhook o un messaggio) riattiva l'agente solo quando c'è un lavoro effettivo da svolgere.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Carica il codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force

Nota cosa è stato installato: monstertix/, il server, l'orologio e un'altra cosa che non hai ancora visto. Controlla che agent/ sia rimasto invariato, perché in questo passaggio non viene modificato nulla dell'agente. Tienilo a mente, è il punto fondamentale.

File

Descrizione

monstertix/server.py

qualcosa di nuovo. Un server web con un runner dietro

monstertix/clock.py

qualcosa di nuovo. Qualcosa che conosce l'ora e chiama il server

È diviso in due parti, che rimangono in due file fino al cloud: L'attivatore e l'elemento che lo attiva, tenuti separati

diventa, nel passaggio 10

il trigger

un endpoint che può eseguire l'agente

Cloud Run

la persona che ha attivato il trigger

qualcosa con un orologio, che lo chiama

Cloud Scheduler

Il trigger: il tuo server web

Sì, un server web. Qualcosa deve essere in ascolto prima che possa essere chiamato. La UI web di ADK dei passaggi precedenti è un'interfaccia di sviluppo in grado di eseguire un agente.

👉💻 Apri 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 non viene eseguito automaticamente. Si tratta di una descrizione: un nome, un modello, un'istruzione, un elenco di strumenti. Qualcosa deve prendere un messaggio in arrivo, trovare la sessione giusta, passare al modello la conversazione finora, eseguire gli strumenti richiesti, restituire i risultati, ripetere il ciclo finché non smette di chiamare gli strumenti e scrivere ogni passaggio nell'archivio. Questo qualcosa è Runner.

Cosa fa un Runner intorno al tuo agente

L'interfaccia utente web dell'ADK ne ha una predefinita. Qui lo crei tu e questa è l'unica differenza tra la UI web dell'ADK e qualcosa che puoi implementare.

👉💻 Terminale 2: avvialo:

Inizia a correre,

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.

Leggi l'ultima riga. L'attivatore è in esecuzione, l'agente è caricato e non succede nulla. Separa le due idee: essere chiamabile ed essere chiamato.

E una porta d'ingresso, già che ci siamo

Nonostante adk web sia un ottimo strumento di sviluppo, non deve essere qualcosa che viene spedito. Non devi abilitarlo durante il deployment in produzione e non devi indirizzare un cliente a questa pagina. Quindi, lo stesso server gestisce anche una pagina.

👉 Apri http://127.0.0.1:8090 in un browser, lo stesso indirizzo.

Ti presentiamo MonsterTix. Una finestra di chat, un nome nell'angolo e due pulsanti. Pubblica su /wake e visualizza ciò che viene restituito. Funzionerebbe senza modifiche con qualsiasi endpoint che risponde allo stesso modo.

Sulla pagina

Che cosa c'è sotto

la conversazione

POST /wake, un messaggio alla volta

+ Nuova sessione

un nuovo ID sessione: nuova conversazione e, in un secondo momento, un nuovo budget

Reimposta memoria

elimina ogni sessione e il file di memoria. Ti chiede prima e ti mostra esattamente dove si trova lo stato

👉✨ Dì qualcosa: What Amsterdam dates are there? e guarda il terminale 2. Stesso Runner, stesso agente, stesse righe di log. È stato modificato solo il chiamante.

Il trigger, ovvero qualcosa che conosce l'ora

👉💻 Apri monstertix/clock.py. Controlla il codice.

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

È un meccanismo molto semplice: sospensione, POST, forse ripetizione. Ogni pianificatore che hai utilizzato fa lo stesso, oltre a riprovare, gestire i fusi orari e garantire che sopravviva a un riavvio.

👉💻 Terminal 3: un solo sparo, 10 secondi da ora:

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

👉 Ora smetti di digitare. Senza mani. Guarda il terminale 2.

Un minuto dopo, senza nessuno alla tastiera:

[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.

Questa è la risposta al passaggio 2. Qualcosa al di fuori dell'agente lo ha chiamato e questo qualcosa era un sleep e una richiesta HTTP.

👉💻 Ctrl-C sia quando lo hai guardato un paio di volte. Utilizza prima --every 30 se vuoi vederlo ripetuto.

Ora leggi cosa diceva

"Ho acquistato due biglietti per The Midnight Signal ad Amsterdam, sezione A, 420 $".

👉🔴 Controlla il pannello della sede e dovresti vedere che il biglietto è stato acquistato.

Qual è il problema?

Ha acquistato qualcosa. Guarda cosa ha dovuto indovinare. Ha scelto Amsterdam e la sezione A perché sono le prime cose che ha trovato e tu hai detto all'agente di acquistarle direttamente, ma non sa che Sam non può andare alle partite nei giorni feriali. Un agente che agisce in base a ipotesi alle 3 del mattino è peggio di uno che non fa nulla.

Svegliarsi è la parte più facile. Un agente che si attiva senza nulla, senza cronologia, senza preferenze, senza nessuno che ascolti, non vale ancora la pena di attivare.

4. Gestione della durata del contesto

Le informazioni si trovano in posizioni diverse per periodi di tempo diversi

Un agente memorizza i fatti in diversi punti: nella conversazione della chat, nello stato della sessione, nei file (artefatti) o nella memoria a lungo termine dell'utente. Ogni luogo dura un periodo di tempo diverso.

Se inserisci le informazioni nel posto sbagliato:

  • Scompare troppo presto: i dettagli importanti vengono cancellati al termine della chat.
  • Rimane troppo a lungo: i fatti temporanei contaminano la memoria a lungo termine e confondono le conversazioni future.

Devi abbinare ogni informazione al luogo di archiviazione e alla durata corretti.

Carica il codice

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO.

👉💻 Nel terminale 1, esegui:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force

Cosa è cambiato rispetto al passaggio precedente:

File

Che cosa è cambiato

tools.py

aggiunto note_companion. get_seatmap ora salva la mappa dei posti in un file

memory.py

qualcosa di nuovo. Il nostro negozio di ricordi

agent.py

altri tre strumenti nell'elenco e l'istruzione indica all'agente di utilizzarli

👉💻 Riavvia l'agente nel terminale 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"

Due flag che non hai mai utilizzato prima:

Flag

Descrizione

session_service_uri

ADK scrive ogni messaggio, chiamata di strumenti e modifica dello stato come righe (database SQLite).

artifact_service_uri

File in cui l'agente salva i dati.(File)

Senza, ADK mantiene tutto in memoria e lo elimina quando interrompi il processo. Queste due impostazioni sono ciò che ne garantisce la persistenza.

Il database sessions.db contiene una conversazione passata (two-days-ago) con 13 eventi registrati:

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?

Un file di ricordi di tre prenotazioni passate, memory/userx.md, che puoi aprire in un editor:

- 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.

Ecco perché le righe ambra dei giorni feriali sono importanti. Uno spettacolo infrasettimanale costa 10 € in meno a posto, ha ancora posti migliori disponibili e si trova in cima alla lista, ma è la risposta sbagliata per questa persona. L'unico motivo per cui l'agente può saperlo è un file che ha scritto dopo l'ultima prenotazione. Se questo fatto sopravvive è il passaggio 4 e 5.

I tre strumenti che abbiamo aggiunto

I tre strumenti e dove scrive ciascuno

note_companion registra chi sta arrivando e quali limiti ha:

# 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}

Aggiungilo perché "Sam non può uscire nei giorni feriali" deve essere ancora vero la prossima settimana. Scrivilo in user:prefs per farlo accadere e guarda la parte user: del nome fare il lavoro. Ne parleremo più avanti.

recall

e

remember leggere e scrivere un file di testo, memory/userx.md. Mantieni la posizione per un attimo.

👉 Ora guarda i due punti in cui l'agente memorizza il contesto:

località

Cosa contiene

Stato

la memoria di questa sessione, come coppie chiave/valore. Ti trovi qui nel passaggio 4.

Artifacts

i file salvati dall'agente, che non vengono mai inseriti nel prompt

Scopri le novità di

get_seatmap

. In precedenza restituiva l'intera mappa dei posti. Ora salva la mappa dei posti in un file e restituisce un breve riepilogo:

# 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

Ecco perché questa modifica è importante.

Ogni payload di ritorno dello strumento viene aggiunto alla cronologia della conversazione e inviato nuovamente al modello in tutti i turni successivi. La restituzione di oggetti JSON di grandi dimensioni (ad esempio una mappa dei posti di 6 KB) aumenta il consumo di token a ogni turno successivo.

Salvalo in un file e paga un solo nome file. Consenti all'agente di richiedere nuovamente i dettagli con load_artifact, se necessario, e prevedi che la maggior parte delle volte non lo farà.

Dove viene inviato il file: il servizio di artefatti

Guarda di nuovo la riga in get_seatmap, in agent/concert/tools.py:

await tool_context.save_artifact(f"seatmap_{event_id}.json", part)

tool_context proviene da ADK, che lo passa allo strumento come argomento, mentre save_artifact è un metodo. In questa riga lo strumento passa i byte all'ADK.

ADK li scrive utilizzando il servizio di artefatti con cui è stato avviato. Hai scelto quello con il contrassegno:

--artifact_service_uri="file://$WORKSHOP/artifacts"

ADK ha tre posizioni in cui archiviarli:

URI

Dove vanno i byte

memory://

RAM. Scompare quando interrompi la procedura. Questo è il comportamento predefinito se non passi alcun flag

file://...

una cartella sul disco. Cosa stai utilizzando ora

gs://bucket

di archiviazione ideale in Cloud Storage. Cosa utilizza l'agente di cui è stato eseguito il deployment nell'ultimo passaggio

👉💻 Qui scegliamo i file:

ls -R ~/monstertix/artifacts

Per passare dal laptop a Cloud Storage, modifica file:// in gs://. Vedremo questo passaggio nel passaggio 10.

Il nostro negozio di ricordi

Memoria come interfaccia, con un file dietro

L'ADK fornisce un'interfaccia per la memoria a lungo termine chiamata BaseMemoryService. Esistono due metodi:

# 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

ADK include tre implementazioni:

Classe

URI

Cosa serve

InMemoryMemoryService

memory://

zero. Perde tutto quando interrompi la procedura

VertexAiRagMemoryService

rag://

un corpus RAG Vertex AI

VertexAiMemoryBankService

agentengine://

una risorsa Agent Engine

Per questo workshop, ne scriviamo uno noi. Escludi InMemoryMemoryService, che dimentica tutto quando interrompi il processo, ed escludi le altre due, che richiedono risorse cloud la cui creazione richiede minuti. Scegli un file, perché puoi inserire i dati in anticipo e leggerli in un editor. Quindi, considera memory.py come la tua implementazione. Archiviamo tutto in un file 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 e MemoryEntry sono i tipi di ADK e Memory Bank restituisce gli stessi oggetti. Uno ha un servizio gestito dietro. Il nostro ha un file.

Scegli un file per un motivo: puoi aprirlo e leggerlo. L'intera memoria a lungo termine dell'agente è una pagina di testo.

Quindi, dove lo punti?

Per riassumere l'implementazione di questo workshop, due dei tre servizi vengono scelti dalla riga di comando. Per il terzo abbiamo implementato una nostra soluzione per il motivo sopra indicato:

Servizio

Dove lo imposti

Questo workshop

sessione

--session_service_uri su adk web

sqlite+aiosqlite:///$WORKSHOP/sessions.db

artefatto

--artifact_service_uri su adk web

file://$WORKSHOP/artifacts

memory

in nessun punto della riga di comando

MEMORY_DIR, leggi all'interno di agent/concert/memory.py

Quindi memory.py crea il servizio stesso da una variabile di ambiente:

# 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 esporta entrambi. Recupera MEMORY_DIR set da cui l'agente legge la memoria e MEMORY_USER determina il file.

Vai a cercare dove si trovano le cose

Ora hai quattro posizioni in cui inserire un fatto. Guarda tutte e quattro.

Innanzitutto, la tua sessione

👉 Fai clic su Nuova sessione nella barra in alto.

👉✨ Pronuncia tre cose, una dopo l'altra:

Sam can't do weeknights, by the way.

Nuova sessione, primo turno

Show me the seat map for the Amsterdam Saturday show.

Nuova sessione, secondo turno

Actually, hold that thought — what else is on in Amsterdam?

Il terzo messaggio è importante. Termina il turno di lettura della mappa dei posti e la fine di un turno fa scomparire temp:.

Tre elementi sono stati archiviati in tre posizioni diverse:

Cosa è stato memorizzato

Quale strumento ha

Dove è andato

1

Sam non può venire nei giorni feriali

note_companion

user:prefs, nello stato della sessione

2

la mappa completa dei posti, 6 KB

get_seatmap, tramite save_artifact

il servizio di artefatti

3

un riepilogo della mappa dei posti su una riga

get_seatmap, in temp:seatmap

nowhere

👉 Controlla ora. Le schede Stato e Artefatti si trovano a sinistra di adk web.

Scheda Stato

Scheda Artefatti

Nella scheda Stato, nota che temp:seatmap è omesso. Le chiavi di stato con un prefisso temp: esistono solo durante la chiamata attiva e vengono rimosse automaticamente dall'ADK prima di rendere persistente lo stato della sessione nel database.

Poi la conversazione meno recente

👉 Passa a two-days-ago nel menu a discesa della sessione in alto.

Questa sessione contiene la cronologia delle conversazioni precompilata nel database.

La sessione di due giorni fa

👉 Guarda lo stream di eventi (la colonna centrale). Nota che le svolte precedenti sono state sostituite da un blocco di eventi riepilogativo.

Svolte compresse

👉 Guarda la scheda Stato. Tre tipi di chiavi sono seduti insieme:

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

Il prefisso user: (ad es. user:prefs) definisce l'ambito dello stato in tutte le sessioni per un determinato utente. Al contrario, le chiavi senza prefisso (

target_event_id

,

party_size

) rappresentano lo stato della sessione semplice, che è limitato rigorosamente alla sessione attiva e viene eliminato al termine della sessione.

👉 Ora confronta le due schede Stato. Mentre user:prefs e gli artefatti archiviati vengono mantenuti, temp:seatmap è assente in entrambe le sessioni. Le chiavi di stato con un prefisso temp: sono strettamente temporanee e non vengono mai salvate nel database.

Poi il file

👉💻 Apri il file nell'editor:

cloudshell edit ~/monstertix/memory/userx.md

Tre prenotazioni, a partire da gennaio. L'agente lo legge quando chiama recall() e sarà ancora presente anche se abbiamo perso i dati della sessione.

I quattro luoghi

Dove si trova

Sopravvive al messaggio successivo?

Persistono dopo il riavvio?

Sopravvive all'eliminazione della sessione?

temp:seatmap

no

no

no

party_size: stato della sessione semplice, in two-days-ago

sì

sì

no

user:prefs

sì

sì

yes

memory/userx.md

sì

sì

sì e sopravvive all'eliminazione del database

un artefatto

sì

sì

dipende dal servizio

Leggi la tabella come se si riferisse ai contenitori, non al loro contenuto.memory/userx.md sopravvive a tutto e contiene solo ciò che remember() ha scelto di metterci. Non viene scritta la conversazione attuale. Pertanto, la sopravvivenza di un file a una sessione non è la stessa cosa della memorizzazione da parte dell'agente di ciò di cui stavi parlando e il passaggio 5 è il punto in cui questa distinzione si fa sentire.

Cinque posizioni in cui può essere visualizzato un fatto e la durata di ciascuna

In quale stato si trova

Pensa allo stato come a un dizionario che viaggia con la sessione. Leggilo e scrivilo dai tuoi strumenti tramite tool_context.state e lascia che ADK lo salvi insieme alla conversazione.

ADK assegna il significato dei prefissi. Non li definisci da nessuna parte. ADK legge la prima parte del nome della chiave e la utilizza per decidere dove inserire il valore e per quanto tempo:

Una chiave chiamata

ADK lo memorizza

Durata

party_size

con la sessione

fino all'eliminazione della sessione

user:prefs

nei confronti dell'utente

in ogni sessione dell'utente

app:tour_id

contro l'app

per ogni utente

temp:seatmap

nessun luogo

una chiamata

Un'invocazione è un singolo ciclo richiesta-risposta, che include tutto il ragionamento del modello, l'esecuzione dello strumento e la risposta finale.

Al termine di una chiamata, ADK scrive il nuovo stato nel database e rimuove ogni chiave temp: durante il processo.

Una cosa è rimasta inspiegabile

In two-days-ago hai visto un riepilogo al posto dei primi turni di un giocatore. L'ADK l'ha scritto automaticamente una volta che la conversazione è diventata abbastanza lunga.

Ogni fatto ha una longevità

5. Degrado e compattazione del contesto

Riepiloghi: dimentica i dettagli

Quando una conversazione diventa troppo lunga, il sistema riassume i messaggi più vecchi per risparmiare spazio e token.

I riepiloghi sono utili per cogliere il senso generale e per risparmiare l'utilizzo di token.

La compattazione funziona sulla sessione. Memory è un negozio separato

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Passa al codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force

Cosa è cambiato rispetto al passaggio precedente:

File

Che cosa è cambiato

agent.py

l'agente è racchiuso in un App, con la compattazione attivata. Un secondo agente, budget_split, esegue l'aritmetica dei biglietti

tutto il resto

invariata

agent.py non termina più con root_agent. Termina con:

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
    ),
)

Tieni presente che adk web cerca prima una variabile chiamata app e poi torna a root_agent. Racchiudi l'agente in un App ogni volta che hai bisogno di qualcosa che si applichi all'intera applicazione anziché a un solo agente: la compattazione qui e la ripresa in un secondo momento nei passaggi successivi.

compaction_interval=3 significa che l'ADK riassume dopo ogni tre turni. Un turno è costituito da una tua frase e dalla risposta dell'agente, indipendentemente dal numero di strumenti che ha chiamato nel frattempo. Tre è un numero piccolo, quindi viene attivato durante un workshop. Un'app reale utilizzerebbe un numero molto più grande.

overlap_size=1 significa che ogni riepilogo inizia un turno prima del necessario.

ADK riassume le svolte 1, 2 e 3. Tre turni dopo riassume il batch successivo. Con overlap_size=1, il secondo riepilogo inizia di nuovo al turno 3, non al turno 4:

Due riepiloghi che si sovrappongono per un turno

L'avviso del turno 3 viene riepilogato due volte. È intenzionale. Se hai posto una domanda al turno 3 e l'agente ha risposto al turno 4, una divisione pulita inserisce la domanda in un riepilogo e la risposta nell'altro, rendendo entrambi poco chiari. Ripetere un turno è un'assicurazione economica contro l'interruzione di un pensiero.

Imposta entrambi i campi. ADK non ha un valore predefinito per nessuno dei due.

La compattazione e il servizio di memoria non si incontrano mai

È importante fare chiarezza perché le due parole sembrano correlate, ma i sistemi non lo sono.

EventsCompactionConfig si trova al App di agent.py, vicino a root_agent. È una proprietà dell'applicazione, non di un servizio:

# agent/concert/agent.py
app = App(
    name="concert",
    root_agent=root_agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=3,
        overlap_size=1,
    ),
)

Un riepilogo riscrive la conversazione. Non sovrascrive il file.

Questo è il motivo per cui remember() esiste come chiamata intenzionale. Qualsiasi cosa rimanga nella trascrizione viene letta come una parafrasi scritta da qualcun altro; qualsiasi cosa venga scritta nel file viene letta esattamente come è stata memorizzata, il giorno dopo e la settimana successiva. Alla fine di questo passaggio, vedrai la differenza.

Dove viene conservato il riepilogo

In sessions.db, nella tabella events, l'agente scrive un record di compattazione in una riga di eventi, con un timestamp come qualsiasi altro evento di turno.

Un evento ordinario include un campo content e il riepilogo si trova in actions.compaction.

👉💻 Esamina i dati nel tuo database:

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%';"

Ecco l'aspetto della forma dell'evento:

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

Il flusso di eventi estrae ogni evento dai relativi contenuti. Il riepilogo nel database e inviato al modello a ogni turno.

Il secondo agente

budget_split è un agente completamente separato, non uno strumento:

# 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)],
)

Perché un agente e non una semplice funzione? Perché il lavoro richiede un modello. Leggi "Quale di queste sezioni rientra in un budget di 200 $per quattro persone e qual è il totale?" come un problema di aritmetica con un giudizio, con una risposta che una persona deve essere in grado di leggere. Lascia che una funzione Python moltiplichi e non chiedere di decidere quali opzioni vale la pena menzionare.

Allora perché non lasciarlo fare all'agente principale? Guarda la riga successiva include_contents="none" significa che a questo agente non viene assegnata la conversazione. Nessuna chat di gruppo, nessuna preferenza, nessuna data del tour. Riceve la richiesta e nient'altro. Questo flag esiste solo su un Agent, che è il vero motivo per cui si tratta di un agente anziché di una funzione.

Un subagente che vede la richiesta e nient'altro

Preferisci il prompt breve: più economico, più veloce e molto più difficile da far deragliare. Affidati all'aritmetica, che non è influenzata da ciò che è stato detto venti turni fa, perché non lo vede.

Perché

AgentTool

? Aggrega l'agente in modo che l'agente principale possa chiamarlo come qualsiasi altro strumento. Dal punto di vista del modello, budget_split viene letto come identico a search_events.

E

output_key="budget_plan" inserisce la risposta nello stato della sessione con quel nome anziché lasciarla libera nella conversazione. Scopri perché è importante.

Esegui

👉💻 In terminale 1, Ctrl+C e riavvia con gli stessi flag:

cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
  --session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
  --artifact_service_uri="file://$WORKSHOP/artifacts"

👉 Avvia una Nuova sessione e fai una conversazione reale. Cinque o sei giri. Questo funziona:

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?

👉 Apri la scheda Stato.

Lì troverai budget_plan, che contiene la risposta completa dell'agente aritmetico.

budget_plan dopo la prima domanda sul budget

👉✨ Ora modifica i numeri:

what if 10 people come and I can only do $120 each?

👉 Guarda di nuovo budget_plan. È stato sovrascritto, non aggiunto. Perché output_key contiene l'ultima versione.

budget_plan dopo la seconda domanda sul budget

Cosa non puoi vedere: il riepilogo

In una di queste curve, ADK ha riassunto quelle precedenti.

👉💻 In un nuovo terminale, esegui il comando per leggerlo direttamente:

cd ~/monstertix
. ./set_env.sh
./show-compaction.sh

Leggi cosa ha scritto

Ecco un esempio reale, in versione ridotta:

── 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`

Ha mantenuto i prezzi, le sezioni, le città e le preferenze e ha annotato correttamente che non era stato ancora deciso nulla.

Da ciò derivano tre cose:

È una parafrasi. Nessuno ha detto nulla di tutto questo. Si tratta di un resoconto di ciò che è stato detto, prodotto dallo stesso tipo di processo che occasionalmente produce tutto il resto che il modello sbaglia.

Nota: le stringhe di riepilogo vengono archiviate nei record degli eventi di sessione interni e non vengono visualizzate direttamente come schede separate dell'interfaccia utente.

Ogni tre turni, in una conversazione composta in parte da riepiloghi precedenti.

Prova a prenotare senza ripeterti

In una nuova sessione, non avviarne una nuova. Il riepilogo che hai appena letto si trova negli eventi di questa sessione ed è l'unico motivo per cui l'agente sa ancora qualcosa di Amsterdam.

👉✨ Non dare nulla di nuovo:

Book us something.

Guarda l'agente chiedere quale spettacolo, quale sezione e per quante persone. Contrassegna come corretto: il riepilogo stesso afferma che l'utente non ha mai confermato nulla, quindi l'agente ha chiesto. Attribuisci il merito alla compattazione.

Anche quando la compattazione va a buon fine, il riepilogo dei turni precedenti può introdurre ambiguità che richiedono all'agente di riconfermare i vincoli con l'utente.

👉✨ Rispondi e lascia che la prenotazione vada a buon fine:

Amsterdam Saturday, section C, 8 people

Legge l'ordine prima di spendere qualcosa:

"Vuoi prenotare 8 biglietti nella sezione C per The Midnight Signal ad Amsterdam sabato 14 novembre. Il costo totale sarà di $760. Vuoi che proceda con l'acquisto di questi biglietti?"

👉✨ Conferma:

yes

Ora ti dice che l'operazione è andata a buon fine:

"Il tuo acquisto di 8 biglietti nella sezione C per The Midnight Signal ad Amsterdam sabato 14 novembre, per un totale di 760 $, è stato completato. Il tuo ID ordine è ord_227877fe."

Vai a controllare se è stato eseguito.

👉 Passa al pannello della sede. Non fidarti della parola dell'agente.

L'ordine nel pannello della sede

Nel riquadro

Cosa dovresti vedere

Biglietti acquistati

1 e corretto: una prenotazione, un ordine

l'ordine sottostante

ord_xxxxxxxx — 8× section C · 760

The tour, Amsterdam Saturday

La sezione C è diminuita di 8

Attività

ord_xxxxxxxx — 8x section C @ 95 = 760

Controlla il riquadro per scoprire la vendita dei biglietti.

Perché user:prefs è stata la scelta giusta

Ricorda che nel user:prefs ultimo passaggio abbiamo scritto il vincolo di Sam in note_companion e l'obiezione ovvia: perché usare uno strumento quando l'agente potrebbe semplicemente ricordarlo?

Ecco perché. Non includere lo stato nella conversazione e nessun riepilogo lo tocca. Controlla user:prefs e budget_plan rispetto a come sono stati inseriti e scopri che sono rimasti invariati. Poi controlla la trascrizione.

Un riepilogo mantiene la forma generale e perde i dettagli. Conserva tutto ciò che non puoi permetterti di perdere in un'altra posizione rispetto alla conversazione.

6. Ripresa della procedura

Attesa di 40 minuti senza rimanere online

Quando un agente deve attendere in una coda di ticket di 40 minuti, mantenere attivo e in attesa un processo Python è fragile e costoso.

Per gestire correttamente le attese lunghe:

  1. Salva stato in un database: memorizza il punto in cui l'agente ha messo in pausa l'operazione in modo che il processo possa essere arrestato in sicurezza.
  2. Riattivazione su richiesta: al termine dell'attesa, un segnale esterno riavvia l'agente e riprende l'esecuzione dal punto in cui si era interrotta.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Passa al codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force

Esegui

Cosa è cambiato rispetto al passaggio precedente:

File

Che cosa è cambiato

tools.py

ha aggiunto join_queue e check_queue

agent.py

join_queue è avvolto in LongRunningFunctionTool e App si accende ResumabilityConfig

join_queue restituisce immediatamente un biglietto anziché bloccare la corsa, mentre ResumabilityConfig consente di mettere in pausa la corsa e riprenderla in un secondo momento.

Cosa aggiunge la ripresa

👉💻 Terminal 1: l'agente, invariato:

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"

👉✨ Chiedi all'agente di acquistare:

Get us two tickets to the Amsterdam show.

Chiama recall, poi search_events, poi join_queue e risponde con qualcosa di simile a questo:

You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.

Nota che ha scelto Sabato senza chiedertelo. Ci sono due spettacoli ad Amsterdam, il 17 è martedì e il file di memoria dell'ultimo passaggio indica che Sam non lavora nei giorni feriali. Si tratta di un fatto di una prenotazione precedente che decide una domanda in questa.

Tieni presente anche che è entrato in coda prima di guardare un singolo posto o prezzo e non ha detto nulla al riguardo. Questa è la regola della sede: si acquistano i biglietti fino a quando non si raggiunge la parte anteriore, quindi l'unica cosa che vale la pena avere in anticipo è un posto in fila. Quali posti prendere è una domanda che ci porremo tra 40 minuti.

👉 Guarda il pannello di controllo. Il banner recita:

La coda, che continua a muoversi mentre non viene riprodotto nulla di tuo

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.

La coda è effettivamente di 40 minuti e non si sposta mentre lavori.

👉💻 Termina l'agente:

Ctrl-C

👉💻 Avvialo di nuovo nel terminale 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"

👉✨ Nella stessa sessione, chiedi all'agente a che punto è la coda:

Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Lo stesso biglietto, dopo il riavvio

Confronta il ticket e osserva la posizione che continua a scendere mentre non è in esecuzione nulla. Nota che l'agente non sapeva di essere stato terminato. Ha chiamato check_queue con il ticket ottenuto prima del riavvio, perché il ticket era sul disco.

👉💻 Scopri perché:

cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data

Tieni presente che ogni turno di ogni conversazione è una riga e il turno stesso, chi lo ha detto, quale strumento ha chiamato, cosa è tornato e nota che è JSON all'interno di event_data.

👉💻 Trova il tuo ticket della coda qui:

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

Quando join_queue torna, il suo biglietto viene salvato direttamente nel database. Al riavvio, ADK ricarica i turni e i risultati degli strumenti precedenti dal disco, consentendo all'agente di riprendere da dove aveva interrotto.

E nota che non è ripartito da zero. L'agente riprende direttamente anziché riprodurre il flusso di lavoro dall'inizio. Poiché LongRunningFunctionTool e ResumabilityConfig mantengono lo stato di esecuzione sul disco, ADK sa che l'agente è già in attesa in coda, impedendo di cercare nuovamente le date del tour o di richiedere un biglietto duplicato.

Lo stesso ticket, in un processo interrotto

Perché join_queue e non check_queue

Entrambe le funzioni restituiscono il risultato in millisecondi. La distinzione è se il job sottostante è completato quando la funzione restituisce un valore.

Cosa restituisce

il lavoro è stato completato?

join_queue

Numero di coda e posizione iniziale

No.L'elaborazione continua in modo asincrono nel sistema esterno.

check_queue

Stato attuale della coda

Sì. Restituisce una risposta immediata e puntuale.

LongRunningFunctionTool è progettato per le operazioni che restituiscono un handle al lavoro in attesa. Il wrapping join_queue consente all'agente di sospendere l'esecuzione mentre la coda esterna avanza.

Al contrario, check_queue è sincrono e completo. Il wrapping causerebbe la sospensione errata dell'esecuzione ogni volta che l'agente controlla il suo stato, anche quando raggiunge l'inizio della coda e deve completare immediatamente l'acquisto.

Quale chiamata è ancora in sospeso al suo ritorno

Cosa ha funzionato

Tre componenti consentono la sospensione e il ripristino dell'esecuzione:

# 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: contrassegna uno strumento come non bloccante, restituendo un handle attivo in modo che l'agente possa attendere senza bloccare il thread.
  • ResumabilityConfig: indica al runtime di sospendere il ciclo di vita di esecuzione di un'attività incompleta e di riprendere dall'ultimo stato registrato.
  • Session Storage URI (--session_service_uri): indirizza la persistenza dello stato a un archivio esterno (ad es. SQLite, Cloud SQL) anziché alla memoria del processo, impedendo la perdita di dati durante i riavvii.

7. Intervenire sui dati non aggiornati

Le informazioni diventano obsolete mentre aspetti

La cronologia delle conversazioni persistente non è uno stato in tempo reale. Durante i lunghi intervalli di attesa, i dati esterni, come la disponibilità o il prezzo dei biglietti, possono cambiare senza che l'agente lo sappia.

Un agente con informazioni mancanti si ferma per chiedere chiarimenti, mentre un agente con dati obsoleti agisce immediatamente in base a presupposti non aggiornati. Per evitare azioni non valide, i dati sensibili al tempo devono essere verificati nuovamente immediatamente prima dell'esecuzione di operazioni critiche.

Non caricare ancora il nuovo codice. Rimani sull'agente del passaggio precedente (può acquistare biglietti, ma al momento non dispone di salvaguardie per l'obsolescenza o l'idempotenza).

👉🔴 Premi RIPRISTINA LA SEDE, quindi avvia una nuova sessione in adk web: il pulsante + Nuova sessione si trova sopra la chat.

Avvia una nuova sessione per cancellare il ticket della coda precedente e lo stato di acquisto in attesa. Questo passaggio richiede un contesto di esecuzione pulito per dimostrare la corretta gestione dell'obsolescenza.

Lo snapshot si trova

👉✨ Chiedi lo stesso programma di prima:

Get us two tickets to the Amsterdam show.

👉✨ Ora, mentre attende in coda, chiedi informazioni sui posti:

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.

👉🔴 Premi VENDI I POSTI MIGLIORI. La sezione A scende a zero.

👉🔴 Premi SALTA L'ATTESA per inviare la richiesta all'agente in cima alla coda.

All'inizio della coda

👉✨ Chiedi di acquistare:

Buy the two section A seats.

Esegue purchase(section="A") senza verificare nuovamente la disponibilità dei posti, causando la restituzione di una risposta di errore da parte dell'API Venue:

{"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.

Errore dell'API Venue: la sezione A è esaurita

Nota che l'agente ha eseguito una nuova query sulla mappa dei posti dopo aver ricevuto una risposta di errore dell'API. La causa principale è il contesto obsoleto: all'inizio della sessione, l'agente ha registrato "sezione A: 400 disponibili" e questo turno è rimasto nella cronologia della conversazione passata al modello.

Indica chiaramente dove sono memorizzate le vecchie informazioni: rimangono nella cronologia delle conversazioni, che i prefissi di stato come temp: non possono ignorare.

La mappa dei posti su cui l'agente sta ancora lavorando

La correzione

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Ora carica il codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force

👉💻 In terminale 1, Ctrl-C e riavvia:

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"

Che cosa è cambiato:

File

Che cosa è cambiato

fence.py

qualcosa di nuovo. Viene eseguito immediatamente prima di un acquisto e può bloccarlo

agent.py

before_tool_callback=refresh_before_purchase

tools.py

purchase ora invia un'intestazione Idempotency-Key

fence.py è un ADK di callback eseguito nel momento precedente a qualsiasi chiamata di funzione:

root_agent = Agent(
    ...,
    before_tool_callback=refresh_before_purchase,   # re-read inventory, then decide
)

Dimostralo

👉🔴 RIPRISTINA LA SEDE. Metti in coda, premi VENDI I POSTI MIGLIORI a metà attesa, quindi chiedi di acquistare la sezione A.

Questa volta si ferma da solo. Guarda l'agente dire qualcosa del tipo:

"Sembra che la sezione A sia esaurita. Devo rileggere la mappa dei posti."

Poi guardalo chiamare di nuovo get_seatmap, senza che gli venga chiesto, e tornare offrendo invece la sezione C.

👉🔴 Leggi attentamente il feed delle attività della sede, perché mostra entrambe le metà:

error  purchase failed: Section A has 0 seats left, 2 requested. Re-read the
       seat map and pick again.
agent  calling purchase

Il feed del locale: il tentativo e il rifiuto

Nella dashboard vedi l'acquisto di chiamate, ma il contatore dei biglietti rimane a zero. Ciò accade perché fence.py funge da protezione prima dell'esecuzione dello strumento:

  1. L'agente tenta di chiamare lo strumento di acquisto.
  2. fence.py riceve prima la richiesta e controlla la disponibilità dei posti dal vivo.
  3. Poiché i posti sono esauriti, fence.py restituisce direttamente un errore, annullando l'acquisto prima che venga chiamata l'API.

Le istruzioni del prompt non possono garantire che un LLM non tenti di acquistare posti esauriti. Le protezioni del codice (before_tool_callback) garantiscono che l'acquisto errato non raggiunga mai il mondo reale.

👉🔴 Premi RESET THE VENUE (REIMPOSTA IL LUOGO), poi BREAK THE NEXT PURCHASE (INTERROMPI IL PROSSIMO ACQUISTO).

👉✨ Chiedi di acquistare e lascia che riprovi tutte le volte che vuole.

Un solo ordine, indipendentemente dal numero di tentativi

Il conteggio degli ordini rimane a 1 perché il locale ha ricevuto la stessa chiave di idempotenza al nuovo tentativo e ha restituito la conferma originale anziché creare un ordine duplicato.

8. Workflow autonomo

I LLM non sono in grado di seguire regole rigide ogni volta

Gli LLM sono ottimi per prendere decisioni (ad esempio, scegliere un programma in base alle preferenze dell'utente). Tuttavia, affidarsi a un LLM per seguire regole rigorose passo passo (ad esempio "unisciti sempre alla coda prima di controllare i posti") significa che a volte salterà dei passaggi o invertirà l'ordine.

Per risolvere il problema, dividi il flusso di lavoro in due tipi di passaggi:

  • Regole fisse (funzioni di codice): i passaggi che devono essere eseguiti esattamente nello stesso modo ogni volta (ad esempio prendere un ticket di coda, controllare la posizione) vengono scritti come normali nodi di codice.
  • Decisioni (nodi dell'agente): i passaggi che richiedono un giudizio (scelta delle sezioni dei posti, controllo delle preferenze dell'utente) vengono assegnati all'agente AI.

Guarda cosa contiene effettivamente questo flusso. Alcuni non contengono alcun giudizio:

Passaggio

Giudizio?

prendere esattamente un biglietto della coda

No. Sempre uno, sempre prima dell'acquisto

controllare se è il tuo turno

no. Chiedi, leggi la risposta

scrivere il brief per l'acquirente

No. Stessa frase, stessa forma, ogni volta

quale programma, quale sezione

Sì. Prezzo, giorno, chi viene, cosa odia

preferenze e limitazioni dell'utente

Sì.

Le procedure rigorose non devono basarsi su un LLM perché i modelli probabilistici alla fine non riescono a seguire sequenze rigide se non vengono monitorati.

L'Workflow di ADK separa l'esecuzione deterministica dal ragionamento dell'AI:

  • I nodi funzione applicano regole fisse (come l'inserimento in una coda o il controllo della posizione) in modo coerente ogni volta.
  • I nodi agente gestiscono le decisioni contestuali (ad esempio la scelta dei posti in base alle preferenze dell'utente).
  • I nodi di attesa sospendono l'esecuzione senza mantenere attivo un processo in memoria.

L'agente originale non viene sostituito. Diventa il nodo finale nel grafico del flusso di lavoro, mantenendo i suoi strumenti, i controlli di obsolescenza e le salvaguardie di idempotenza. Il grafico controlla l'ordine di esecuzione, mentre l'agente esegue la decisione di acquisto.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO.

👉💻 Passa al codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force

Esegui

👉💻 In terminale 1, Ctrl+C e riavvia. Lo stesso comando di sempre:

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"

Cosa è cambiato rispetto al passaggio precedente:

File

Che cosa è cambiato

nightly.py

qualcosa di nuovo. L'esecuzione automatica come grafico ADK 2 Workflow

agent.py

root_agent ora è il grafico. L'agente con cui hai parlato è ancora lì, è stato rinominato buyer_agent ed è un nodo al suo interno

agent.py

ha anche perso join_queue e check_queue: ora la coda è di proprietà del grafico

Esegui il grafico: tre messaggi, in ordine

Esegui tutte le azioni seguenti in Python notturno (qui si trova il grafico). Invia l'input identico a ogni turno per osservare la transizione del grafico del flusso di lavoro attraverso i suoi stati.

1. Nota a cosa stai parlando ora

👉 Ricarica l'interfaccia utente web di 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

Tieni presente che adk web esegue l'oggetto assegnato a root_agent. Qui, root_agent punta al grafico del workflow nightly, con buyer_agent configurato come nodo finale del grafico.

2. Avvia

👉✨ Digita questo comando e premi Invio:

go

Nota: in un grafico del flusso di lavoro automatizzato, l'esecuzione iniziale inizia senza attendere l'input dell'utente. L'invio di un messaggio in adk web attiva l'esecuzione iniziale.

👉 Ora guarda il browser web. Il riquadro a sinistra disegna il grafico e colora i nodi durante l'esecuzione. Il riquadro a destra è il flusso di eventi, in cui puoi osservare come vengono prese le decisioni:

Il grafico, parcheggiato in check_front

Guarda queue_up scrivere queue_ticket e queue_event_id nello stato della sessione.

Il grafico, parcheggiato in check_front

Dovresti visualizzare una chiamata di funzione denominata adk_request_input, in attesa di una risposta. Poi si ferma e la corsa termina. Il grafico ha raggiunto check_front, ti ha trovato al 14.203° posto e si è fermato.

👉 Controlla il pannello della sede: un biglietto in coda e nessun contenuto in riproduzione.

Il grafico, parcheggiato in check_front

3. Riattivalo e fai attenzione alla casella in cui digiti

Scorri il flusso di eventi fino all'evento adk_request_input e individua la casella di input associata a quell'evento specifico:

Il grafico, parcheggiato in check_front

Ora puoi digitare in due posizioni diverse, che svolgono funzioni diverse.

Dove digiti

Che cosa succede

la casella Enter your response... nell'evento adk_request_input

risponde all'interruzione. L'esecuzione parcheggiata continua dalle ore check_front

la casella della chat principale in basso

avvia una nuova chiamata. Il grafico viene riprodotto da START, sceglie di nuovo uno spettacolo e prende un secondo biglietto per la coda

👉✨ Digita qualsiasi cosa nella casella Enter your response... e premi la freccia.

👉 Terminal 1:

[check_front] #14,197 — not yet. Pausing.

Una riga. Non ha scelto un secondo programma e non ha preso un secondo biglietto per la coda, che ti avrebbe messo in fondo a una coda di 14.203 persone.

Se vedi di nuovo [open] e [pick_show], significa che hai digitato nella casella di chat. Premi AZZERA IL LUOGO e ricomincia la procedura.

👉 Controlla di nuovo il pannello della sede: è ancora presente un biglietto e la posizione è scesa un po' perché l'orologio della sede ha continuato a scorrere mentre il tuo no.

Svegliato una volta, ma ancora un biglietto

4. Arriva in prima posizione e poi finisci

👉🔴 Premi SALTA L'ATTESA nel riquadro del locale.

Il grafico completato, con tutti i nodi verdi

👉✨ Rispondi un'ultima volta all'ultimo evento di adk_request_input, di nuovo nella sua casella, non nella casella di chat. Il grafico completato, con tutti i nodi verdi

Questa volta guarda check_front trovarti davanti e cadere. Guarda i due nodi brief e concert, gli ultimi due, illuminarsi nel riquadro a sinistra.

Il grafico completato, con tutti i nodi verdi

👉 Guarda il browser. Questa è la fine della corsa e ci sono quattro cose che vale la pena leggere.

Il grafico completato, con tutti i nodi verdi

Il grafico è completamente verde e i bordi vengono conteggiati. brief e concert si sono illuminati e i bordi di pick_show, queue_up e check_front ora hanno un badge.

2x

è il numero di volte in cui il grafico ha percorso questo bordo, una volta per ogni riattivazione. I due valori in alto sono il prezzo della ripresa: lo scheduler rientra nel grafico dall'inizio ogni volta. Non significa che il lavoro è stato eseguito tre volte e il pannello della sede ne è la prova: un solo ticket della coda, non due. @node(rerun_on_resume=False) è il motivo.

Il grafico

Il grafico: funzioni per le regole, agenti per il giudizio

# agent/concert/nightly.py
nightly = Workflow(
    name="concert_nightly",
    edges=[(START, open_the_night, pick_show, queue_up,
            check_front, brief, buyer_agent)],
)

Tieni presente che due nodi sono agenti e due sono funzioni e considera quale sia quale come l'intera domanda di progettazione.

Nodo

Perché questa forma

pick_show

agent. Scegliere un programma dalla cronologia di qualcuno è un giudizio. Chiama recall, legge il tour e restituisce un Plan convalidato

queue_up

function. L'inserimento in coda è una regola. Una regola non deve essere un lancio di moneta alle 3 del mattino

check_front

function. "Sono davanti?" ha una sola risposta corretta e il locale la conosce

buy_it

agent. È buyer_agent, ovvero root_agent senza modifiche. La sezione A è esaurita durante l'attesa. Vuoi prendere la sezione B, la sezione C o non prendere nulla? Questo è il giudizio che hai insegnato al modello nel modulo 4.

Guarda l'ultimo nodo e trova l'agente che hai creato. Non è una copia, non è una riscrittura:

# agent/concert/nightly.py
from .agent import buyer_agent

Tutto ciò che ne consegue: ogni strumento, la rilettura della mappa dei posti in fence.py, la chiave di idempotenza in purchase. Ne esiste solo uno.

Perché si è interrotto invece di aspettare

Ecco la versione che nessuno dovrebbe scrivere:

# what NOT to write — this is nobody's file
while not ready:          # never do this
    time.sleep(1)

Mantiene un processo attivo per 40 minuti senza ottenere risultati, termina con il terminale e non può essere ripreso. Ecco cosa fa 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 è un interruzione. Il grafico si arresta, la chiamata va allo spazio di archiviazione della sessione e il processo può essere interrotto.

La corsa non costa nulla durante l'attesa e termina esattamente in un punto: i biglietti acquistati. Potrebbe volerci tutta la notte e diversi risvegli per arrivarci.

9. Lavorare con un operatore

Non spendere soldi senza verificare con una persona

Un agente autonomo in esecuzione durante la notte non dovrebbe prendere decisioni finanziarie costose basate su preferenze ambigue o ipotizzate dalla cronologia della chat.

Prima di eseguire azioni importanti (come spendere soldi per i biglietti), l'agente deve mettere in pausa, chiedere all'utente di confermare il budget ad alta voce e salvare la conferma esplicita prima di effettuare qualsiasi acquisto.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Passa al codice di questo passaggio:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force

Esegui

👉💻 In terminale 1, Ctrl+C e riavvia. Lo stesso comando di prima:

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"

Cosa è cambiato rispetto al passaggio precedente:

File

Che cosa è cambiato

budget.py

Contiene ciò che la persona ha accettato, ovvero la frase che ha detto. load() legge la targa, mentre someone_is_there() risponde se c'è qualcuno nei paraggi a cui chiedere.

tools.py

è stato aggiunto set_budget, in modo che l'agente possa scrivere un limite concordato nello stato della sessione una volta che una persona lo ha confermato

nightly.py

un nuovo primo nodo, agree_budget, che chiede e legge prima di qualsiasi altra operazione. Acquisisce il budget e il resto della richiesta, sia come testo libero, sia SEATS come costante non più presente

Non modellarlo. Memorizza la frase.

Apri agent/concert/budget.py e leggi tutto:

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"

Il nodo verifica due cose: che la persona l'abbia detto e che l'abbia confermato.

Quindi, mantiene la frase nello stato della sessione anziché in user:, di proposito. Viene concordato un budget per una prenotazione. "250 dollari se sono quelli bravi" è stato detto di quella band, in quella notte, e non ha nulla a che fare con la prenotazione dei biglietti.

Lo stesso vale per tutto il resto che ha detto

Non fermarti al budget. Le persone cambiano idea mentre decidono, quindi registra tutto ciò che chiedono qui, anziché fare affidamento su ciò che l'agente ricorda.

"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)

Leggi Solo eventi creati dall'utente, che esamina Solo

text

del function_response, quindi la domanda e la risposta sul budget non vengono ripetute qui. Nessun duplicato.

Nota: agree_budget lo chiama a ogni esecuzione, non solo alla prima, quindi viene inserita anche una correzione digitata mentre l'agente è in coda. Poi open_the_night consegna al selezionatore entrambe le frasi, la richiesta e il budget, e chiedigli di leggerle.

Il grafico, un nodo più lungo

Iniziamo con una conversazione:

# 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)],
)

Lo stesso grafico, con agree_budget in primo piano

Due nodi si fermano e attendono e il meccanismo è lo stesso. check_front attende un luogo e riceve una risposta da un orologio. agree_budget attende una persona e può essere risposto solo da una persona. Questa differenza costituisce l'intero passaggio 10.

Esegui passo passo

1. Avvia

👉 Ricarica la UI web di ADK e scegli l'agente concert.

👉✨ Chiedi quello che vuoi davvero:

Get us two tickets to the Amsterdam show.

L'app si interrompe e ti chiede cosa spenderai

Nel passaggio 8 il grafico non aveva utenti, quindi tutto ciò che hai digitato è stato eliminato e go ha funzionato come qualsiasi altra cosa. Qui agree_budget chiama what_they_asked_for(ctx) nella prima riga, che raccoglie tutto ciò che hai digitato e lo inserisce in state["request"]. Poi open_the_night lo consegna al selezionatore insieme al budget.

Non hai specificato un giorno e ad Amsterdam ci sono due spettacoli. Quindi, guarda il selettore tornare a recall(), trova "Sam bails on weeknights" e prendi il sabato.

Nel browser, l'esecuzione si interrompe in corrispondenza di un evento adk_request_input:

Un grafico ti ha appena fatto una domanda. Stessa interruzione dell'attesa in coda del passaggio 8, ma ora punta a una persona che risponde alla domanda.

2. Rispondi come rispondono le persone

👉✨ Nella casella Enter your response..., non nella casella della chat in basso:

About a 100 for the cheap seats. 250 if they're the good ones.

Guarda l'auto fermarsi e conferma:

[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?

Legge il numero prima di confermarlo

3. Conferma

👉✨ Stessa scatola:

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.

Quattro nodi sono stati eseguiti con una sola parola. 👉 Apri la scheda Stato e trova budget, la tua frase, parola per parola.

budget nella scheda Stato, con parole tue

Nessun prefisso. Questo è lo stato della sessione ed è una decisione che vale la pena prendere di proposito se avvii una nuova sessione e il budget è esaurito, quindi ti verrà chiesto di nuovo.

4. Raggiungi la prima posizione e taglia il traguardo

👉🔴 Premi SALTA L'ATTESA nel riquadro del locale.

👉✨ Rispondi all'ultimo adk_request_input, di nuovo nella sua casella:

go

Woken again, and it does not ask twice

👉 Controlla il pannello della sede: un ordine a un prezzo compreso in quello concordato per quel tipo di posto.

Un ordine, all'interno di quanto concordato

Come un grafico pone una domanda a una persona

Apri nightly.py e leggi agree_budget. Se tre condizioni sono vere contemporaneamente, ci si aspetta lo stesso errore quando una di queste manca: la richiesta viene ripetuta all'infinito.

@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?",
        )
    ...

rerun_on_resume=True

senza di esso il nodo viene considerato completato e ignorato

un supporto stabile interrupt_id

Il valore predefinito è un UUID nuovo per ogni esecuzione, quindi non c'è nulla in base a cui cercare la risposta

ctx.resume_inputs[id]

dove viene visualizzata la risposta. Not il valore restituito di RequestInput: ADK itera un nodo anziché inviarlo

Il grafico

È il grafico del passaggio 8 con un nodo aggiuntivo davanti. Lo stesso grafico, con agree_budget in primo piano

I vincoli confermati dall'utente sono basati sulla sessione e scadono al termine della conversazione. Il mantenimento dei limiti temporanei tra le sessioni rischia di trasformare decisioni una tantum in impostazioni predefinite permanenti non intenzionali.

10. The Cloud Stack

Il laptop va in modalità Sospensione e i file locali scompaiono

Sul laptop, un agente dipende dal computer che rimane attivo, dai database SQLite locali e dai file locali. Se chiudi il coperchio del laptop o esegui il deployment in un container cloud che si riavvia, i file locali e la memoria scompaiono.

Per essere eseguito in modo affidabile in produzione:

  1. Cloud Compute: esegui l'agente in un container (come Cloud Run) che rimane online.
  2. Database cloud durevoli: sostituisci i file locali e SQLite con servizi cloud (Cloud SQL per i dati di sessione, Cloud Storage per i file).
  3. Timer cloud: utilizza scheduler gestiti (come Cloud Scheduler e Pub/Sub) in modo che l'agente si attivi anche quando il laptop è spento.

👉🔴 Nel riquadro del luogo, premi RIPRISTINA IL LUOGO. Ogni passaggio inizia dallo stesso mondo: 8 spettacoli, tutti i posti disponibili, orologio a 1×.

👉💻 Passa al codice finale:

cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force

File

Che cosa è cambiato

main.py

qualcosa di nuovo. Un'istanza di FastAPI che gestisce la pagina della chat e l'endpoint chiamato da uno scheduler

Dockerfile

qualcosa di nuovo. Python-slim e Uvicorn ordinari. Non c'è nulla di specifico per l'ADK

monstertix/

copiata, in modo che la pagina venga spedita nello stesso contenitore dell'agente

concert/

invariato. Byte per byte la cartella con cui hai completato il passaggio 9

La pagina che hai già creato

L'** interfaccia utente web dell'ADK** è un'interfaccia di debug locale per l'ispezione di stati, stream di eventi e grafici del flusso di lavoro durante lo sviluppo. Non è incluso in un pacchetto per il deployment di produzione.

Per la produzione, utilizza il frontend di chat autonomo creato nel passaggio 3. Gestisce i messaggi e le interruzioni dell'interazione degli utenti tramite l'endpoint /wake senza esporre i controlli di debug interni.

Migrazione dello stato locale ai servizi cloud

Arresta il server e guarda cosa ha scritto:

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

Sebbene i file locali funzionino bene per lo sviluppo, non funzionano in produzione perché i container Cloud Run utilizzano uno spazio di archiviazione temporaneo e locale per l'istanza.

Quando viene eseguito in Cloud Run:

  • Scalabilità a zero delle istanze:quando il traffico si interrompe, il container termina e il relativo file system locale viene eliminato.
  • Scalabilità orizzontale indipendente delle istanze:più istanze simultanee non condividono spazio su disco, ognuna viene eseguita con il proprio file system isolato.

Se un agente salva lo stato di una sessione o di una coda in un file SQLite locale (sessions.db), le richieste successive indirizzate a un'istanza container nuova o diversa troveranno un database vuoto. Per mantenere lo stato durevole durante lo scaling e i riavvii, i file locali devono essere sostituiti con servizi esterni gestiti.

Ciò che ti serve è uno stato che sopravviva a qualsiasi contenitore. Tre servizi, uno per ogni file:

Sul laptop

Nel cloud

Descrizione

sessions.db

Cloud SQL

un database Postgres gestito. Esiste indipendentemente dal fatto che qualcosa sia in esecuzione o meno

memory/userx.md

Cloud Storage

un bucket. File come oggetti, raggiungibili da ogni istanza contemporaneamente

artifacts/

Cloud Storage

lo stesso bucket

clock.py

Cloud Scheduler

un cron eseguito da Google. Il terminale non è una sveglia

Creali

Esegui il provisioning di un database PostgreSQL, di un utente del database e di un bucket Cloud Storage. Tieni presente che la creazione dell'istanza Cloud SQL richiede diversi minuti ed è stata avviata in background durante setup.sh.

👉💻 Raccogli le informazioni e crea il resto:

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

Lo script ha creato quattro elementi, uno per riga della tabella sopra riportata, e ha scritto due URI in .env:

CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts

Deployment dell'agente con trigger pianificati gestiti

Con i servizi pronti, dai all'agente la sua sede permanente.

👉💻 Tre o quattro minuti. Inizia e continua a leggere:

cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh

Lo script crea un'immagine container da agent/, ne esegue il deployment su Cloud Run e esegue il provisioning dell'infrastruttura basata su eventi di supporto.

Il deployment completo è costituito da cinque componenti distinti:

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 come origine del trigger

Cloud Scheduler sostituisce i timer di esecuzione locali (clock.py) con un servizio cron gestito che persiste indipendentemente dalle sessioni del terminale locale, gestisce i nuovi tentativi automatici e supporta la pianificazione del fuso orario.

Pub/Sub come livello di disaccoppiamento dei messaggi

L'inserimento di un argomento Pub/Sub tra Cloud Scheduler e Cloud Run offre tre vantaggi architetturali:

  • Durabilità dei messaggi: mette in coda gli eventi trigger in entrata se il servizio Cloud Run è in fase di avvio a freddo, di nuovo deployment o temporaneamente non disponibile.
  • Criteri di ripetizione integrati: riprova automaticamente gli errori di recapito temporanei senza richiedere una logica di ripetizione personalizzata a livello di applicazione.
  • Scalabilità fan-out: consente a più servizi downstream di abbonarsi agli stessi eventi di pianificazione senza riconfigurare lo scheduler.

testo alternativo

La configurazione di trigger_sources=["pubsub"] registra un endpoint push compatibile con i payload envelope Pub/Sub standard, traducendo i messaggi in arrivo direttamente in eventi di chiamata dell'agente.

→ 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 * * *

Parla con l'app

👉 Apri l'URL dell'agente in un browser. 👉✨ Chiedi di prenotare:

Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.

Dovrebbe chiederti quanto vuoi spendere

👉✨ Rispondi come faresti a voce alta, poi conferma:

up to 250 a seat for the good ones

L'esecuzione è attualmente in coda.

👉🔴 Premi SALTA LA CODA nel pannello del locale, in modo che la coda sia pronta quando arriva il messaggio.

Per eseguirlo in base a una pianificazione reale, il job deve essere abilitato. jobs run rifiuta un job in pausa con Job.state must be ENABLED:

gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION

👉 In pochi secondi, l'agente esegue l'acquisto entro i limiti di budget confermati e restituisce automaticamente una conferma dell'ordine.

👉🔴 Controlla il pannello del luogo. Un ordine.

Ecco come puoi farlo eseguire ogni 5 minuti:

gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
  --location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"

Esegui la pulizia

Conta due elementi di cui è stato eseguito il deployment: la sede e l'agente accanto. Entrambi vengono scalati fino a zero, quindi uno inattivo non costa nulla. Eliminali comunque.

./destroy-agent.sh          # service, topic, subscription, scheduler job
./destroy-venue.sh

Nota che questi lasciano intenzionalmente Cloud SQL e il bucket intatti, perché contengono i tuoi dati e entrambi continuano a generare costi di fatturazione. Quando hai la certezza di aver terminato:

./destroy-agent.sh --all

Tutto il resto è locale. Elimina la cartella al termine dell'operazione oppure conservala ed eseguila sul tuo progetto per tutto il tempo che vuoi.

L'agente, i suoi strumenti, i suoi callback e il budget non sono cambiati. Due URI e una variabile di ambiente.

11. Panoramica del sistema e guida alla risoluzione dei problemi

Differenze tra ambienti: architettura locale e cloud

Il tuo laptop

Nel cloud

Che cosa è cambiato

--session_service_uri=sqlite+aiosqlite:///...

Cloud SQL

una stringa di connessione

--artifact_service_uri=file://...

gs://bucket

un URI

memory/userx.md

gs://bucket/memory/

un percorso

adk web

un servizio Cloud Run

Dockerfile e main.py

monstertix/clock.py

Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

tu, alla tastiera

someone_is_there()

un valore predefinito, sostituito per richiesta

L'implementazione dell'agente principale, inclusi root_agent, definizioni degli strumenti, contromisure di callback e applicazione del budget, rimane completamente invariata. Il confronto tra solutions/step10_deploy/concert/ e il passaggio 9 mostra una corrispondenza esatta byte per byte, convalidando il modo in cui l'astrazione del servizio di ADK separa la configurazione dell'infrastruttura dalla logica dell'agente.

Se hai difficoltà

Accedi al codice funzionante completo in qualsiasi passaggio:

./use-solution.sh N --force          # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep   # code only, leave state alone

Quando qualcosa si rompe

Sintomo

Correggi

✗ Cannot access project

ID errato. rm ~/project_id.txt && ./setup.sh

Il modello restituisce l'errore 404

Gli alias -latest sono disponibili solo in AI Studio. Utilizza ADK_MODEL=gemini-latest-flash

address already in use

Qualcosa ha già la porta 8000 o 8090. lsof -ti:8000 | xargs kill -9

L'agente non riesce a raggiungere la sede

curl $VENUE_URL/health o ./deploy-venue.sh di nuovo

La coda non avanza mai

Premi SALTA L'ATTESA. A 1× la coda non raggiungerà l'inizio da sola

Nel pannello viene visualizzato un secondo ticket della coda

Hai risposto nella casella della chat anziché nella casella Enter your response... dell'evento adk_request_input. La casella di chat avvia una nuova esecuzione

Il menu a discesa è vuoto o l'app non si carica

adk web considera ogni cartella in agent/ come un'app e non supera l'intero elenco se una non lo è. ls agent/ deve mostrare concert/ e nient'altro. L'esecuzione di operazioni dall'interno di agent/ può lasciare un memory/ errato. ./use-solution.sh 8 --force lo riordina

[EXPERIMENTAL] avvisi

Previsto. EventsCompactionConfig e ResumabilityConfig sono entrambi pre-GA nella versione 2.6.2

Tutto è strano

Premi RESET THE VENUE (RIPRISTINA LA SEDE), quindi rm sessions.db

File di memoria pieno di prenotazioni duplicate

remember() viene aggiunto a ogni esecuzione. ./use-solution.sh N --force lo reimposta oppure cp seed/memory/default.md memory/userx.md

adk: command not found

source .venv/bin/activate: ogni nuova scheda ne ha bisogno

Timeout di Cloud Shell

Riavvia adk web. sessions.db è sopravvissuto, il che corrisponde al passaggio 6, che ti viene offerto senza costi