1. Zanim zaczniesz
Większość witryn sprzedających bilety umieszcza kupujących w kolejce, aby zapobiec awariom serwera i zarządzać dużym popytem. Rozpoczynasz zakup i okazuje się,że przed Tobą jest 14 tys. osób. Kolejka przesuwa się przez 40 minut. Gdy nadejdzie Twoja kolej, będziesz mieć niewiele czasu na wybranie miejsc i zapłacenie za nie. Jeśli nie zdążysz, stracisz miejsce w kolejce.
Wiele zadań w rzeczywistości przebiega w ten sposób: zadanie, które trwa znacznie dłużej niż jedna rozmowa, jest wykonywane w systemie zewnętrznym, nad którym nie masz kontroli. Może to być na przykład oczekiwanie na uzupełnienie zapasów przez dostawcę, zatwierdzenie roszczenia lub na swoją kolej w kolejce.
Standardowi agenci AI działają w ramach pojedynczej tury czatu, która trwa kilka sekund i ma charakter wyłącznie reaktywny. Nie mogą czekać 40 minut ani działać autonomicznie. Aby rozwiązać ten problem, potrzebujesz długo działającego agenta, czyli systemu, który pozostaje w stanie uśpienia podczas długich okresów oczekiwania, zachowuje kontekst po ponownym uruchomieniu procesu i wznawia działanie automatycznie po wybudzeniu przez zewnętrzny aktywator.
Te warsztaty pokazują, jak utworzyć agenta kolejki biletów na koncert, i omawiają główne wzorce architektury wymagane w przypadku długotrwałych agentów w Google Cloud.
Co utworzysz

Podczas tych warsztatów utworzysz długotrwałego agenta do rezerwacji biletów, poznając przy tym kluczowe wzorce oprogramowania opartego na agentach:
- Wstrzymywanie i wznawianie procesu: wstrzymywanie wywołania agenta w okresach długiego oczekiwania i wznawianie wykonywania za pomocą wyzwalaczy zewnętrznych.
- Trwałe przechowywanie stanu: utrwalanie stanu rozmowy i artefaktów po ponownym uruchomieniu procesu za pomocą SQLite/Cloud SQL i Cloud Storage.
- Ochrona przed nieaktualnymi danymi i idempotentność: przed podjęciem działania zweryfikuj dane dynamiczne (np. dostępność miejsc) i użyj kluczy idempotentności, aby zapobiec duplikowaniu transakcji.
- Wykresy przepływu pracy ADK: strukturyzuj wykonywanie za pomocą deterministycznych węzłów kodu dla reguł proceduralnych i węzłów agenta dla podejmowania decyzji kontekstowych.
- Zatwierdzenie przez człowieka: wstrzymuje wykonywanie, aby uzyskać od użytkownika wyraźne limity wydatków i je zapisać.
- Wdrożenie w chmurze: umieść agenta w kontenerze i wdroż go w Cloud Run za pomocą Cloud Scheduler i aktywatorów Pub/Sub.
Konfiguracja
Najpierw przygotuj środowisko i upewnij się, że masz dostęp do Google Cloud. Zalecam uruchomienie tego polecenia w Cloud Shell, które zawiera wszystkie potrzebne narzędzia. Sprawdź tylko, czy masz wystarczającą ilość miejsca i czy Twoje konto może tworzyć projekty.
👉💻 Sklonuj repozytorium i uruchom konfigurację:
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
Zapyta Cię o to tylko raz:
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
Następnie wykonuje te czynności w podanej kolejności:
virtualenv | tworzy | |
logowanie |
| |
project | wskazuje | |
Interfejsy API | umożliwia korzystanie z 10 stanowisk, których potrzebuje warsztat. | |
| zapisuje identyfikator projektu, regionu i modelu, dzięki czemu każdy skrypt i | |
sprawdzanie modelu | wykonuje 1 prawdziwe wywołanie Gemini, więc nieprawidłowy identyfikator projektu lub modelu powoduje błąd, | |
seed | buduje sesję, która ma już 2 dni, a krok 3 ją otwiera. | |
Cloud SQL | uruchamia instancję Postgres w tle; | |
miejsce | wdraża własnego sprzedawcę biletów testowych w Cloud Run, | |
👉💻 Aktywuj środowisko:
cd ~/monstertix
source .venv/bin/activate
Twój prompt zaczyna się teraz od (.venv).
Omówienie scenariusza
W tym scenariuszu warsztatowym agent rezerwuje 2 bilety na koncert („The Midnight Signal” w Amsterdamie). Preferencje użytkownika przechowywane w pamięci historycznej obejmują:
- Musi to być program weekendowy (Sam nie może uczestniczyć w programach w dni powszednie).
- Unikaj miejsc na górnej trybunie w Ziggo Dome (słaba widoczność).
- Cena docelowa to około 200 USD za bilet, a maksymalny limit to 250 USD.

Weryfikacja instalacji
👉💻 Jedno polecenie, osiem znaczników:
./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.
👉 Otwórz drugą kartę przeglądarki:
Gdzie | Co to jest? |
| Panel sterowania, przyciski. |
2. Czy promptowanie może prowadzić do autonomii?
Prompty nie mogą się same uruchamiać
Prompt to po prostu tekst przekazywany do modelu AI podczas wywołania. Nie może działać samodzielnie, wybudzać się ani rozpoczynać nowego połączenia.
Jeśli w prompcie powiesz agentowi, aby „wrócił za 10 minut”, po zakończeniu bieżącej tury nic się nie stanie. Model nie ma wewnętrznego zegara ani pętli. Aby agent był autonomiczny, coś poza nim musi go wybudzić i wywołać.

Uruchomienie platformy
Zaczniemy od testowania narzędzi dla deweloperów. adk web uruchamia interfejs internetowy ADK, czyli interfejs programistyczny dostarczany z ADK. Umożliwia to rozmowę z tworzonym agentem bez konieczności wcześniejszego pisania interfejsu. Pokazuje też to, czego nie widać w oknie czatu, np. każde wywołanie narzędzia i jego argumenty, stan sesji w miarę jego zmian, artefakty zapisane przez agenta i surowe zdarzenia. Nie udostępniaj go w wersji produkcyjnej. To najlepszy sposób na poznanie działania agenta podczas nauki. W kroku 3 utworzysz interfejs, który będzie dostępny dla użytkowników, a w ostatnim kroku go wdrożysz.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Terminal 1 – wczytaj kod tego kroku i uruchom interfejs internetowy ADK:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
use-solution.sh kopiuje gotowy kod kroku do agent/, czyli miejsca, w którym pracujesz. Każdy krok zaczyna się od tego i wyświetla, które pliki zostały zmienione, aby można było sprawdzić, co należy przeczytać.
Na początek:
Plik | Co to jest? |
| agenta – nazwa, model, instrukcja, lista narzędzi; |
|
|
| klient HTTP dla obiektu. Nic ciekawego |
| którego modelu użyć, oraz sprawdzenie, czy Twoje dane logowania działają. |
. ./set_env.sh aktywuje virtualenv, wczytuje projekt i model oraz eksportuje $WORKSHOP – ścieżkę bezwzględną do ~/monstertix. Uruchamiaj go w każdym otwieranym terminalu. Wyświetla ustawioną wartość:
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
👉 Otwórz pierwsze karty przeglądarki:
Gdzie | Co to jest? |
Interfejs internetowy ADK |
|
Mów do niego
👉✨ W interfejsie internetowym ADK wpisz:
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
Wyszukuje wycieczkę, odczytuje mapy miejsc i analizuje Twój budżet oraz Twoją firmę. To naprawdę kompetentny asystent. Gdy jednak przedsprzedaż rozpocznie się o 10:00, agent nie zostanie uruchomiony automatycznie, ponieważ nie działa żaden proces ani wyzwalacz w tle.
Przeczytaj, co się właśnie wydarzyło
Środek ekranu to nie tylko dziennik czatu. Każdy krok tury jest numerowany, w tym wywołania narzędzi:
▸ 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
👉 Kliknij wiersz ⚡. W panelu po lewej stronie widać argumenty wybrane przez model. Kliknij ✓ pod nim, aby wyświetlić zwrócony kod JSON.
Informacje o wycieczce nie są obecne w danych treningowych modelu ani w prompcie. Agent pobrał te dane, wykonując narzędzie search_events, które wysłało zapytanie do interfejsu API miejsca.
Otwórz kod, który został przed chwilą uruchomiony.
Widzisz, że działa, i widzisz ślad. Teraz przyjrzyj się samemu kodowi.
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
👉💻 Otwórz agent/concert/agent.py. Cały agent znajduje się na dole pliku:
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],
)
Nazwa, model, opis działania agenta i lista funkcji Pythona. To wszystko, co robi Agent. adk web znalazł go, ponieważ plik nazywa się agent.py, a zmienna root_agent.
👉💻 Teraz 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)
zwykłą funkcję, Nie musisz pisać dekoratora, rejestracji ani schematu. ADK odczytuje sygnaturę i ciąg dokumentujący i tworzy definicję narzędzia, którą widzi model. Dlatego ten ciąg dokumentujący jest tak ważny i dlatego usunięcie jednego argumentu zmieniło zachowanie modelu.
Model nigdy nie uruchamia Twojego kodu. Gdy pojawił się komunikat search_events({"city": "Amsterdam"}), model prosił o to połączenie. Pakiet ADK uruchomił funkcję, przekazał wynik z powrotem, a model napisał o tym zdanie. Pętla to:
👉 Ikony wzdłuż lewej krawędzi przełączają zawartość panelu. Drugi to wykres agentów – wypróbuj go, a powinny się pojawić 2 narzędzia:

Powiedz mu, żeby nie spał
Co zrobisz, żeby to naprawić? Spróbuj tego, po co większość osób sięga w pierwszej kolejności. Przypomnij sobie INSTRUCTION z kroku 1, czyli tekst przekazywany modelowi w każdej turze, i przepisz go.
👉💻 Otwórz agent/concert/agent.py. Pod INSTRUCTION znajduje się drugi, który został już dla Ciebie napisany. Jeśli chcesz, możesz to zmienić:
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],
)
👉💻 W terminalu 1 uruchom ponownie:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ Opisz plan dokładnie tak, jakbyś opowiadał(-a) o nim innej osobie:
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.
Zgodzi się i będzie brzmieć, jakby to było szczere.
👉 Teraz poczekaj. Nie wpisuj niczego innego. Obejrzyj panel.
Poczekaj chwilę. NIC SIĘ NIE DZIEJE!
Dlaczego ten prompt nigdy nie zadziała
Przeczytaj go jeszcze raz, świeżym okiem. Monitorowanie. w momencie otwarcia, Sprawdzaj dalej. Każde z nich opisuje coś, co dzieje się z upływem czasu.
Przypomnij sobie, czym tak naprawdę jest instruction=. To ciąg znaków przekazywany do modelu w ramach żądania w momencie, gdy ktoś wysyła wiadomość. Nic nie odczytuje treści między turami.
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
Instrukcja nie może wywoływać funkcji, w której jest zapisana.
Warianty prompta, które nie działają
Próba wymuszenia autonomicznego wykonywania za pomocą instrukcji w prompcie kończy się niepowodzeniem niezależnie od sformułowania. Typowe próby promptów, takie jak:
- „Ustaw minutnik na wtorek na godzinę 10:00 i wtedy kup bilety”.
- „Sprawdzaj co 5 minut, czy rozpoczęła się przedsprzedaż”.
- „Działać autonomicznie bez czekania na dane wejściowe użytkownika”.
Wszystkie te przypadki kończą się niepowodzeniem z tego samego powodu: prompt jest odczytywany tylko wtedy, gdy nastąpi wywołanie. Bez zewnętrznego wywołującego między turami nie jest wykonywany żaden kod.
3. Brak aktywności wywołany zdarzeniem
Agenci nie są aktywni bez końca
Pozostawienie procesu ciągłego odpytywania powoduje marnowanie pamięci, pieniędzy i awarię, jeśli laptop przejdzie w stan uśpienia lub zostanie ponownie uruchomiony.
Zamiast utrzymywać agenta w stanie ciągłego działania podczas oczekiwania, długo działający agent powinien przejść w stan całkowitego uśpienia. Zdarzenie zewnętrzne (np. timer, webhook lub wiadomość) uaktywnia agenta tylko wtedy, gdy ma on coś do zrobienia.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Wczytaj kod tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
Zwróć uwagę na to, co zostało zainstalowane: monstertix/, serwer, zegar i jeszcze jedna rzecz, której jeszcze nie widziałeś. Sprawdź, czy nie zmienił agent/, ponieważ na tym etapie nic się w nim nie zmienia. Zapamiętaj to, bo to jest najważniejsze.
Plik | Co to jest? |
| czegoś nowego. Serwer WWW z Runnerem za nim |
| czegoś nowego. urządzenie, które zna czas i wywołuje serwer; |
Jest on podzielony na 2 części, które są przechowywane w 2 plikach aż do momentu przesłania do chmury: 
w kroku 10 zmienia się w | |||
aktywator | punkt końcowy, w którym można uruchomić agenta; | Cloud Run | |
osoba, która wywołała zdarzenie | coś z zegarem, co nazywa się | Cloud Scheduler | |
Wywołanie – Twój serwer WWW
Tak, serwer WWW. Aby można było coś wywołać, musi to nasłuchiwać. Interfejs ADK w przeglądarce z poprzednich kroków to interfejs programistyczny, który może uruchamiać agenta.
👉💻 Otwórz 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"])]),
):
...
Agent nie uruchamia się samodzielnie. Jest to opis: nazwa, model, instrukcja, lista narzędzi. Coś musi odbierać przychodzące wiadomości, znajdować odpowiednią sesję, przekazywać modelowi dotychczasową rozmowę, wykonywać narzędzia, o które prosi, przekazywać wyniki z powrotem, powtarzać te czynności, aż przestanie wywoływać narzędzia, i zapisywać każdy krok w pamięci. Tym czymś jest Runner.

Interfejs internetowy ADK ma domyślny adres URL. Tutaj możesz samodzielnie zbudować interfejs, co jest jedyną różnicą między interfejsem internetowym ADK a interfejsem, który możesz wdrożyć.
👉💻 Terminal 2 – uruchom go:
Uruchom Runnera,
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.
Przeczytaj ostatni wiersz. Wywoływacz działa, agent jest załadowany, ale nic się nie dzieje. Rozdziel te 2 pojęcia: możliwość nawiązania połączenia i możliwość odebrania połączenia.
I drzwi wejściowe, skoro już tu jesteśmy.
Mimo że adk web to świetne narzędzie deweloperskie, nie powinno być częścią produktu. Nie należy włączać tej funkcji podczas wdrażania w wersji produkcyjnej. Nie należy też kierować na nią klientów. Ten sam serwer obsługuje też stronę.
👉 Otwórz stronę http://127.0.0.1:8090 w przeglądarce – ten sam adres.
Poznaj MonsterTix. Okno czatu, nazwa w rogu, 2 przyciski. Wysyła żądanie POST do /wake i renderuje otrzymaną odpowiedź. Działa bez zmian w przypadku każdego punktu końcowego, który odpowiada w ten sam sposób.
Na stronie | Co jest pod spodem |
rozmowy |
|
+ Nowa sesja | nowy identyfikator sesji – nowa rozmowa, a później nowy budżet; |
Resetowanie pamięci | usuwa każdą sesję i plik pamięci. Najpierw pyta, a potem pokazuje, gdzie znajduje się dany stan. |
👉✨ Powiedz coś do niego – What Amsterdam dates are there? – i obserwuj terminal 2. Ten sam Runner, ten sam agent, te same wiersze logu. Zmienił się tylko dzwoniący.
Wywołujący – coś, co zna czas
👉💻 Otwórz monstertix/clock.py. Sprawdź kod.
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
To bardzo prosty mechanizm: uśpienie, POST, ewentualnie powtórzenie. Każdy używany przez Ciebie harmonogram robi to samo, a dodatkowo ponawia próby, uwzględnia strefy czasowe i gwarantuje, że przetrwa ponowne uruchomienie.
👉💻 Terminal 3 – uruchom za 10 sekund:
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
👉 Przestań pisać. Bez wątpienia. Obserwuj terminal 2.
Minutę później, gdy nikt nie korzysta z klawiatury:
[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.
To jest odpowiedź na krok 2. Wywołał go podmiot zewnętrzny, który wysłał sleep i żądanie HTTP.
👉💻 Ctrl-C nawet jeśli obejrzysz go kilka razy. Jeśli chcesz zobaczyć powtórkę, najpierw użyj przycisku --every 30.
Przeczytaj, co zostało powiedziane
„Kupiłem 2 bilety na koncert The Midnight Signal w Amsterdamie, sekcja A, 420 zł”.
👉🔴 Sprawdź panel miejsca. Powinien się w nim pojawić zakupiony bilet.
Co jest jeszcze nie tak
Kupił coś. Sprawdź, co musiała odgadnąć. Wybrał Amsterdam i sekcję A, ponieważ były to pierwsze znalezione przez niego opcje. Poinformowałeś agenta, że ma kupić bilety bezpośrednio, a on nie wie, że Sam nie może uczestniczyć w wydarzeniach w dni powszednie. Agent, który o 3:00 w nocy podejmuje działania na podstawie przypuszczeń, jest gorszy od agenta, który nic nie robi.
Budzenie się to łatwiejsza część. Agent, który budzi się bez niczego – bez historii, bez preferencji, bez nikogo, kto by go słuchał – nie jest jeszcze wart obudzenia.
4. Zarządzanie czasem życia kontekstu
Informacje są przechowywane w różnych miejscach przez różny czas
Agent przechowuje fakty w kilku miejscach: w rozmowie na czacie, w stanie sesji, w plikach (artefaktach) lub w pamięci długotrwałej użytkownika. Każde miejsce jest dostępne przez inny czas.
Jeśli umieścisz informacje w niewłaściwym miejscu:
- Zbyt szybko znika: ważne szczegóły są usuwane po zakończeniu czatu.
- Zbyt długo się utrzymuje: tymczasowe fakty zanieczyszczają pamięć długotrwałą i utrudniają przyszłe rozmowy.
Każdy rodzaj informacji musisz dopasować do odpowiedniego miejsca przechowywania i okresu przechowywania.
Wczytywanie kodu
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE.
👉💻 W terminalu 1 uruchom:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
Co się zmieniło od ostatniego kroku:
Plik | Co się zmieniło? |
| dodano |
| czegoś nowego. Własny sklep z pamięcią |
| 3 dodatkowe narzędzia na liście, a instrukcja nakazuje agentowi ich użycie. |
👉💻 Uruchom ponownie agenta w terminalu 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"
2 flagi, których nie używasz:
Flaga | Co to jest? |
| ADK zapisuje w niej każdą wiadomość, wywołanie narzędzia i zmianę stanu w postaci wierszy (baza danych SQLite). |
| Pliki, które agent zapisuje. |
Bez nich ADK przechowuje wszystko w pamięci i usuwa po zakończeniu procesu. To te 2 ustawienia sprawiają, że jest on trwały.
Baza danych sessions.db zawiera wstępnie wypełnioną przeszłą rozmowę (two-days-ago) z 13 zarejestrowanymi zdarzeniami:
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?
Plik pamięci z 3 poprzednich rezerwacji, memory/userx.md, który możesz otworzyć w edytorze:
- 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.
Dlatego tak ważne są bursztynowe wiersze w dni powszednie. Przedstawienie w środku tygodnia jest o 10 zł tańsze, ma lepsze miejsca i znajduje się na szczycie listy, ale nie jest odpowiednią odpowiedzią dla tej osoby. Jedynym powodem, dla którego agent może to wiedzieć, jest plik, który został zapisany po ostatniej rezerwacji. Kroki 4 i 5 pokazują, czy ten fakt przetrwa.
3 dodane narzędzia

note_companion rejestruje, kto przychodzi i co go ogranicza:
# 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}
Dodaj go, ponieważ „Sam nie może w dni powszednie” musi być nadal prawdą w przyszłym tygodniu. Wpisz ją w polu user:prefs, a część user: nazwy wykona za Ciebie całą pracę. Wróć do tego poniżej.
recall
i
remember odczytywać i zapisywać pliki tekstowe, memory/userx.md. Przytrzymaj je przez chwilę.
👉 Teraz przyjrzyj się 2 miejscom, w których agent przechowuje kontekst:
lokalizacja | Co zawiera |
Stan | pamięć tej sesji w postaci par klucz-wartość – w kroku 4 |
Artefakty | pliki zapisane przez agenta, które nigdy nie są uwzględniane w prompcie; |
Sprawdzanie zmian w
get_seatmap
. Wcześniej zwracał całą mapę miejsc. Teraz zapisuje mapę miejsc w pliku i zwraca krótkie podsumowanie:
# 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
Oto dlaczego ta zmiana jest ważna.
Każdy ładunek zwrotny narzędzia jest dołączany do historii rozmowy i ponownie wysyłany do modelu w przypadku wszystkich kolejnych tur. Zwracanie dużych obiektów JSON (np. mapy miejsc o rozmiarze 6 KB) zwiększa zużycie tokenów w każdej kolejnej turze.
Zamiast tego zapisz go w pliku i zapłać za 1 nazwę pliku. Pozwól agentowi poprosić o szczegóły za pomocą load_artifact, jeśli będzie ich potrzebować. Pamiętaj, że w większości przypadków nie będzie to konieczne.
Miejsce docelowe pliku: usługa artefaktów
Spójrz jeszcze raz na wiersz w get_seatmap w agent/concert/tools.py:
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context pochodzi z ADK, który przekazuje go do narzędzia jako argument, a save_artifact to metoda w tym argumencie. W tym miejscu narzędzie przekazuje bajty do ADK.
ADK zapisuje je za pomocą usługi artefaktów, z której został uruchomiony. Wybrany element z oznaczeniem:
--artifact_service_uri="file://$WORKSHOP/artifacts"
ADK ma 3 lokalizacje, w których można je przechowywać:
Identyfikator URI | Gdzie trafiają bajty |
| RAM. Znikną, gdy zatrzymasz proces. Jest to ustawienie domyślne, jeśli nie podasz żadnej flagi. |
| folder na dysku. Z czego teraz korzystasz |
| Cloud Storage. Co wdrożony agent wykorzystuje w ostatnim kroku |
👉💻 Wybieramy pliki:
ls -R ~/monstertix/artifacts
Aby przenieść dane z laptopa do Cloud Storage, zmień file:// na gs://. (Zobaczymy to w kroku 10).
Nasz magazyn pamięci

ADK udostępnia interfejs pamięci długotrwałej o nazwie BaseMemoryService. Możesz to zrobić na 2 sposoby:
# 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 zawiera 3 implementacje tego interfejsu:
Klasa | Identyfikator URI | Wymagania |
|
| Nic Po przerwaniu procesu wszystko zostanie utracone |
|
| korpus Vertex AI RAG, |
|
| zasób Agent Engine, |
Na potrzeby tych warsztatów piszemy własne. Wyklucz InMemoryMemoryService, które po zatrzymaniu procesu zapomina wszystko, oraz pozostałe 2 opcje, które wymagają zasobów w chmurze, których utworzenie zajmuje kilka minut. Wybierz plik, ponieważ możesz go wcześniej zainicjować i odczytać w edytorze. Załóżmy, że memory.py to Twoja implementacja. Wszystko przechowujemy w pliku 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 i MemoryEntry to typy pakietu ADK, a Bank zapamiętanych informacji zwraca te same obiekty. Jedna z nich jest obsługiwana przez usługę zarządzaną. Nasz zawiera plik.
Wybierz plik z jednego powodu: możesz go otworzyć i przeczytać. Cała pamięć długotrwała agenta to strona tekstu.
Gdzie więc ją skierować?
Podsumowując, w tym warsztacie w wierszu poleceń wybieramy 2 z 3 usług. W przypadku trzeciego z powodu opisanego powyżej wdrożyliśmy własne rozwiązanie:
Usługa | Miejsce ustawienia | Te warsztaty |
sesja |
|
|
artefakt, |
|
|
pamięć | w wierszu poleceń |
|
Dlatego memory.py tworzy usługę na podstawie zmiennej środowiskowej:
# 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 eksportuje oba. Pobierz MEMORY_DIR zbiory, z których agent odczytuje pamięć, i MEMORY_USER określ plik.
Znajdź miejsce, w którym coś się znajduje
Masz teraz 4 miejsca, w których możesz umieścić fakt. Przyjrzyj się wszystkim czterem.
Najpierw własna sesja
👉 Na pasku u góry kliknij Nowa sesja.
👉✨ Powiedz 3 rzeczy po kolei:
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?
Ta trzecia wiadomość ma znaczenie. Kończy to turę, w której odczytano mapę miejsc, a zakończenie tury powoduje zniknięcie symbolu temp:.
Trzy rzeczy zostały umieszczone w trzech różnych miejscach:
Co zostało zapisane | Które narzędzie to zrobiło | Gdzie się podziały | |
1 | Sam nie może w dni powszednie |
|
|
2 | pełną mapę miejsc (6 KB), |
| usługa artefaktów, |
3 | podsumowanie mapy miejsc w 1 wierszu, |
| nigdy |
👉 Sprawdź teraz. Karty Stan i Artefakty znajdują się po lewej stronie ikony adk web.


Na karcie State (Stan) zwróć uwagę, że pole temp:seatmap zostało pominięte. Klucze stanu z prefiksem temp: istnieją tylko podczas aktywnego wywołania i są automatycznie usuwane przez ADK przed zapisaniem stanu sesji w bazie danych.
Następnie starszy wątek
👉 W menu sesji u góry przełącz na two-days-ago.
Ta sesja zawiera wstępnie wypełnioną historię rozmów w bazie danych.

👉 Spójrz na strumień zdarzeń (środkowa kolumna). Starsze tury zostały zastąpione blokiem podsumowującym zdarzenia.

👉 Sprawdź kartę Stan. Obok siebie leżą 3 rodzaje kluczy:
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
Prefiks user: (np. user:prefs) określa zakres stanu we wszystkich sesjach danego użytkownika. Natomiast klucze bez prefiksu (
target_event_id
,
party_size
) reprezentują zwykły stan sesji, który jest ściśle ograniczony do aktywnej sesji i usuwany po jej zakończeniu.
👉 Teraz porównaj 2 karty stanu. Chociaż user:prefs i zapisane artefakty są zachowywane, temp:seatmap nie występuje w żadnej z sesji. Klucze stanu z prefiksem temp: są ściśle krótkotrwałe i nigdy nie są zapisywane w bazie danych.
Wtedy plik
👉💻 Otwórz go w edytorze:
cloudshell edit ~/monstertix/memory/userx.md
3 rezerwacje, sięgające stycznia. Agent odczytuje go, gdy wywołuje funkcję recall(), i będzie on nadal dostępny, nawet jeśli utracimy dane sesji.
4 miejsca
Gdzie się znajduje | Czy przetrwa następną wiadomość? | Czy przetrwa ponowne uruchomienie? | Czy przetrwa usunięcie sesji? |
| no | nie | nie |
| tak | tak | nie |
| tak | tak | yes |
| tak | tak | tak, i przetrwa usunięcie bazy danych. |
artefakt, | tak | tak | zależy od usługi, |
Tę tabelę należy traktować jako opis pojemników, a nie ich zawartości.memory/userx.md przetrwa wszystko i zawiera tylko to, co remember()zdecyduje się tam umieścić. Nic nie zapisuje w nim bieżącej rozmowy. Dlatego plik, który przetrwa sesję, nie jest tym samym co agent, który pamięta, o czym rozmawialiście. Różnica ta jest widoczna w kroku 5.

W jakim stanie jest
Stan to słownik, który towarzyszy sesji. Odczytuj i zapisuj go w narzędziach za pomocą tool_context.state, a pakiet ADK zapisze go obok rozmowy.
Znaczenie prefiksów jest przypisywane przez ADK. Nie definiujesz ich w żadnym miejscu. ADK odczytuje pierwszą część nazwy klucza i na jej podstawie decyduje, gdzie umieścić wartość i jak długo ma ona obowiązywać:
Klucz o nazwie | Pakiet ADK przechowuje te informacje. | Trwa |
| z sesją. | do momentu usunięcia sesji. |
| przeciwko użytkownikowi. | we wszystkich sesjach użytkownika. |
| w sprawie aplikacji | w przypadku każdego użytkownika |
| nigdzie | jedno wywołanie |
Wywołanie to pojedynczy cykl żądanie-odpowiedź, obejmujący wszystkie etapy rozumowania modelu, wykonanie narzędzia i odpowiedź końcową.
Po zakończeniu wywołania ADK zapisuje nowy stan w bazie danych i usuwa wszystkie klucze temp:.
Jedna rzecz pozostaje niewyjaśniona
W two-days-ago widzieliśmy podsumowanie w miejscu, w którym wcześniej znajdowało się kilka pierwszych ruchów. ADK napisał go automatycznie, gdy rozmowa się wydłużyła.
Każdy fakt ma swój okres popularności
5. Degradacja i kompresja kontekstu
Podsumowania – zapomnij o szczegółach
Gdy rozmowa staje się zbyt długa, system podsumowuje starsze wiadomości, aby zaoszczędzić miejsce i tokeny.
Podsumowania są przydatne do uzyskania ogólnego zarysu treści i pozwalają zaoszczędzić tokeny.

👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Przejdź do kodu tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
Co się zmieniło od ostatniego kroku:
Plik | Co się zmieniło? |
| agent jest zawarty w |
wszystko inne, | bez zmian |
agent.py nie kończy się już na root_agent. Na koniec mamy:
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
),
)
Pamiętaj, że adk web najpierw szuka zmiennej o nazwie app, a potem wraca do root_agent. Umieść agenta w App, gdy potrzebujesz czegoś, co ma zastosowanie do całej aplikacji, a nie tylko do jednego agenta: kompresji w tym przypadku i możliwości wznowienia w kolejnych krokach.
compaction_interval=3 oznacza, że ADK podsumowuje wyniki po każdych 3 turach. Jedna tura to Twoja wypowiedź i odpowiedź agenta, niezależnie od tego, ile narzędzi zostało w tym czasie wywołanych. Trzy to mała liczba, więc wywołuje zdarzenie podczas warsztatów. Prawdziwa aplikacja używałaby znacznie większej liczby.
overlap_size=1 oznacza, że każde podsumowanie zaczyna się o 1 turę wcześniej, niż jest to konieczne.
ADK podsumowuje tury 1, 2 i 3. Trzy tury później podsumowuje kolejną partię. W przypadku overlap_size=1 drugie podsumowanie zaczyna się od tury 3, a nie od tury 4:

Informacja o zakręcie 3 jest podsumowywana dwukrotnie. To jest celowe. Jeśli w turze 3 zadano pytanie, a w turze 4 pracownik obsługi klienta udzielił odpowiedzi, czysty podział umieści pytanie w jednym podsumowaniu, a odpowiedź w drugim, co będzie mylące. Powtórzenie tury to tanie zabezpieczenie przed przerwaniem myśli w połowie.
Ustaw oba pola. ADK nie ma domyślnej wartości dla żadnego z tych parametrów.
Kompresja i usługa pamięci nigdy się nie spotykają
Warto to wyraźnie podkreślić, ponieważ te dwa słowa brzmią podobnie, ale systemy nie są ze sobą powiązane.
EventsCompactionConfig znajduje się na App w agent.py, obok root_agent. Jest to właściwość aplikacji, a nie żadnej usługi:
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
Podsumowanie to przeredagowana rozmowa. Nie powoduje to ponownego zapisania pliku.
To jest powód, dla którego remember() istnieje jako celowe wywołanie. Wszystko, co pozostanie w transkrypcji, zostanie odczytane jako parafraza tekstu napisanego przez inną osobę. Wszystko, co zostanie zapisane w pliku, zostanie odczytane dokładnie tak, jak zostało zapisane, jutro i w przyszłym tygodniu. Na końcu tego kroku zobaczysz, jak ta różnica się kształtuje.
Gdzie jest przechowywane podsumowanie
W sessions.db w tabeli events agent zapisuje w wierszu zdarzenia rekord kompresji z sygnaturą czasową podobną do innych zdarzeń tury.
Zwykłe zdarzenie zawiera pole content, a podsumowanie znajduje się w polu actions.compaction.
👉💻 Sprawdź to w swojej bazie danych:
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%';"
Tak wygląda kształt zdarzenia:
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
Strumień zdarzeń pobiera każde zdarzenie z jego treści. podsumowanie w bazie danych, które jest wysyłane do modelu przy każdej turze.
Drugi agent
budget_split to osobny agent, a nie narzędzie:
# 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)],
)
Dlaczego agent, a nie zwykła funkcja? Ponieważ praca wymaga modelu. Odczytaj „które z tych sekcji mieszczą się w budżecie 200 zł dla 4 osób i jaka jest łączna kwota” jako działanie arytmetyczne z elementem oceny, a odpowiedź musi być czytelna. Niech funkcja Pythona pomnoży liczby i nie proś jej o określenie, które opcje warto wymienić.
Dlaczego więc nie zlecić tego agentowi głównemu? Wiersz „Look at the next line” include_contents="none" oznacza, że agent nie przejmuje rozmowy. Żadnego czatu grupowego, żadnych preferencji, żadnych 40 dat koncertów. Otrzymuje tylko prośbę. Ta flaga występuje tylko w Agent, co jest prawdziwym powodem, dla którego jest to agent, a nie funkcja.

Preferuj krótkie prompty: są tańsze, szybsze i znacznie trudniej je zepsuć. Polegaj na tym, że działania arytmetyczne są odporne na to, co zostało powiedziane 20 tur temu, ponieważ nigdy tego nie widzą.
Dlaczego
AgentTool
? – umieść agenta w tym tagu, aby główny agent mógł go wywoływać jak każde inne narzędzie. Z punktu widzenia modelu budget_split jest identyczne z search_events.
oraz
output_key="budget_plan" umieszcza odpowiedź w stanie sesji pod tą nazwą, zamiast pozostawiać ją w rozmowie. Zobacz, dlaczego to jest ważne.
Uruchom
👉💻 W terminalu 1 naciśnij Ctrl+C i uruchom ponownie z tymi samymi flagami:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 Rozpocznij nową sesję i prowadź prawdziwą rozmowę. 5–6 obrotów. Działa:
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?
👉 Otwórz kartę Stan.
Znajdź tam element budget_plan, który zawiera pełną odpowiedź agenta arytmetycznego.

👉✨ Teraz zmień liczby:
what if 10 people come and I can only do $120 each?
👉 Spójrz jeszcze raz na budget_plan. Został on zastąpiony, a nie dołączony. Dzieje się tak, ponieważ output_key zawiera najnowszą wersję.

Czego nie widać: podsumowanie
W którymś z tych zwrotów ADK podsumował wcześniejsze.
👉💻 W nowym terminalu uruchom to polecenie, aby odczytać plik bezpośrednio:
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
Przeczytaj, co napisał
Oto prawdziwy przykład w skróconej wersji:
── 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`
Zachował ceny, sekcje, miasta i preferencje, a także prawidłowo zauważył, że nic nie zostało jeszcze ustalone.
Wynikają z tego 3 rzeczy:
To jest parafraza. Nic z tego nie jest tym, co ktoś powiedział. Jest to relacja z tego, co zostało powiedziane, wygenerowana w ramach tego samego procesu, który czasami prowadzi do błędów w przypadku innych treści.
Uwaga: ciągi podsumowujące są przechowywane w wewnętrznych rekordach zdarzeń sesji i nie są bezpośrednio udostępniane jako osobne karty interfejsu.
Co 3 tury w rozmowie, która jest teraz częściowo złożona z poprzednich podsumowań.
Spróbuj dokonać rezerwacji bez powtarzania się
W nowej sesji: nie rozpoczynaj nowej sesji – przeczytane przed chwilą podsumowanie znajduje się w zdarzeniach tej sesji i tylko dlatego agent nadal wie cokolwiek o Amsterdamie.
👉✨ Nie podawaj żadnych nowych informacji:
Book us something.
Zobacz, jak agent pyta o program, sekcję i liczbę osób. Oznacz tę odpowiedź jako prawidłową: w podsumowaniu jest napisane, że użytkownik nigdy niczego nie potwierdził, więc agent zadał pytanie. Zwróć uwagę na kompresję.
Nawet jeśli kompresja się powiedzie, podsumowanie poprzednich tur może wprowadzić niejasności, które wymagają od agenta ponownego potwierdzenia ograniczeń z użytkownikiem.
👉✨ Odpowiedz na pytanie i pozwól na dokonanie rezerwacji:
Amsterdam Saturday, section C, 8 people
Przed wydaniem środków odczytuje zamówienie:
„Chcesz zarezerwować 8 biletów w sekcji C na koncert The Midnight Signal w Amsterdamie w sobotę 14 listopada. Łączny koszt wyniesie 760 zł. Czy mam kupić te bilety?
👉✨ Potwierdź:
yes
Teraz wyświetli się komunikat o tym, że się udało:
„Zakup 8 biletów w sekcji C na koncert The Midnight Signal w Amsterdamie w sobotę 14 listopada za łączną kwotę 760 zł został zrealizowany. Identyfikator Twojego zamówienia to ord_227877fe."
Sprawdź, czy tak się stało.
👉 Przejdź do panelu miejsca. Nie wierz agentowi na słowo.

W panelu | Co powinno się wyświetlać |
Kupione bilety |
|
zamówienie poniżej, |
|
Wycieczka, Amsterdam, sobota | sekcja C spadła o 8 |
Activity |
|
Sprawdź panel, aby dowiedzieć się o sprzedaży biletów.
Dlaczego user:prefs to dobry wybór
Przypomnij sobie note_companion zapisanie ograniczenia Sama w user:prefs ostatnim kroku i oczywisty zarzut: po co używać narzędzia, skoro agent może po prostu zapamiętać to ograniczenie.
Oto dlaczego. Nie uwzględniaj stanu w rozmowie i nie umieszczaj go w podsumowaniu. Sprawdź, czy wartości user:prefs i budget_plan są takie same jak przed zmianą. Następnie sprawdź transkrypcję.
Podsumowanie zachowuje ogólny kształt, ale traci szczegóły. Wszystkie informacje, których nie możesz stracić, zapisuj w innym miejscu niż w rozmowie.
6. Wznowienie procesu
Czekanie 40 minut bez pozostawania online
Gdy pracownik obsługi klienta musi czekać w 40-minutowej kolejce zgłoszeń, utrzymywanie procesu Pythona przy życiu i oczekiwanie jest trudne i kosztowne.
Aby prawidłowo radzić sobie z długim oczekiwaniem:
- Zapisz stan w bazie danych: zapisz miejsce, w którym agent wstrzymał działanie, aby można było bezpiecznie zakończyć proces.
- Wznawianie na żądanie: po zakończeniu oczekiwania sygnał zewnętrzny ponownie uruchamia agenta i wznawia wykonywanie od miejsca, w którym zostało przerwane.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Przejdź do kodu tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
Uruchom
Co się zmieniło od ostatniego kroku:
Plik | Co się zmieniło? |
| dodał(a) użytkowników |
|
|
join_queue od razu wraca z biletem zamiast blokować, a ResumabilityConfig pozwala wstrzymać przejazd i wznowić go później.

👉💻 Terminal 1 – agent bez zmian:
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"
👉✨ Poproś agenta o zakup:
Get us two tickets to the Amsterdam show.
Wywołuje kolejno funkcje recall, search_events i join_queue, a następnie odpowiada w sposób podobny do tego:
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
Zauważ, że bez pytania wybrał sobotę. W Amsterdamie są 2 występy. 17 marca to wtorek, a plik pamięci z ostatniego kroku mówi, że Sam nie występuje w dni powszednie. W tym przypadku fakt z poprzedniej rezerwacji decyduje o odpowiedzi na pytanie w tej rezerwacji.
Zwróć też uwagę, że dołączył do kolejki zanim sprawdził pojedyncze miejsce lub cenę i nie wspomniał o żadnym z tych elementów. Takie są zasady obiektu. Bilety można kupić, dopóki nie dotrze się do przodu kolejki, więc jedyną rzeczą, którą warto mieć wcześniej, jest miejsce w kolejce. To, które miejsca zająć, to pytanie na za 40 minut.
👉 Sprawdź panel sterowania. Na banerze widnieje napis:

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
Kolejka ma 40 minut i nie zniknie, dopóki nie skończysz pracy.
👉💻 Zakończ działanie agenta:
Ctrl-C
👉💻 Uruchom go ponownie w terminalu 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"
👉✨ W tej samej sesji zapytaj agenta, gdzie jest w kolejce:
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Porównaj zgłoszenie i zobacz, jak pozycja spada, gdy nic nie jest uruchomione. Zwróć uwagę, że agent nie wiedział, że został zabity. Wywołał on funkcję check_queue z biletem uzyskanym przed ponownym uruchomieniem, ponieważ ten bilet był zapisany na dysku.
👉💻 Zobacz, dlaczego:
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
Zwróć uwagę, że każdy etap każdej rozmowy to jeden wiersz, a sam etap, osoba, która go wypowiedziała, narzędzie, z którego skorzystała, odpowiedź i uwaga są w formacie JSON w event_data.
👉💻 Znajdź tam swój bilet do kolejki:
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
Gdy join_queue wróci, jego zgłoszenie zostanie zapisane bezpośrednio w bazie danych. Po ponownym uruchomieniu ADK ponownie wczytuje z dysku poprzednie tury i wyniki narzędzi, dzięki czemu agent może kontynuować pracę od miejsca, w którym została przerwana.
Zwróć uwagę na to, że nie zaczęło się od nowa. Agent wznawia działanie bezpośrednio, zamiast odtwarzać proces od początku. Ponieważ LongRunningFunctionTool i ResumabilityConfig zapisują stan wykonania na dysku, ADK wie, że agent już czeka w kolejce, co zapobiega ponownemu wyszukiwaniu dat wycieczek lub żądaniu duplikatu biletu.

Dlaczego join_queue, a nie check_queue
Obie funkcje zwracają wartość w milisekundach. Różnica polega na tym, czy zadanie bazowe jest zakończone, gdy funkcja zwraca wartość.
wyświetlane dane, | czy praca została wykonana? | |
| Bilet do kolejki i pozycja początkowa | Nie. Przetwarzanie jest kontynuowane asynchronicznie w systemie zewnętrznym. |
| Bieżący stan kolejki | Tak. Zwraca natychmiastową odpowiedź w danym momencie. |
LongRunningFunctionTool jest przeznaczona do operacji, które zwracają uchwyt do oczekującej pracy. Zakończenie join_queue umożliwia agentowi wstrzymanie wykonywania działania, podczas gdy kolejka zewnętrzna jest przetwarzana dalej.
Natomiast check_queue jest synchroniczne i kompletne. Zawijanie spowoduje, że agent będzie nieprawidłowo wstrzymywać wykonywanie za każdym razem, gdy sprawdzi swój stan – nawet gdy dotrze na początek kolejki i będzie musiał natychmiast dokonać zakupu.

Co sprawiło, że to zadziałało
Zawieszenie i przywrócenie wykonania umożliwiają 3 komponenty:
# 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: oznacza narzędzie jako nieblokujące, zwracając aktywny uchwyt, dzięki czemu agent może czekać bez zawieszania wątku.ResumabilityConfig: nakazuje środowisku wykonawczemu wstrzymanie cyklu życia wykonywania zadania, które nie zostało ukończone, i wznowienie go od ostatniego zarejestrowanego stanu.Session Storage URI (--session_service_uri): kieruje trwałość stanu do pamięci zewnętrznej (np. SQLite, Cloud SQL) zamiast do pamięci procesu, co zapobiega utracie danych podczas ponownego uruchamiania.
7. Działanie na podstawie nieaktualnych danych
Informacje stają się nieaktualne podczas oczekiwania
Zachowana historia rozmów nie jest stanem w czasie rzeczywistym. Podczas długich okresów oczekiwania dane zewnętrzne, takie jak dostępność biletów lub ceny, mogą się zmienić bez wiedzy agenta.
Agent, któremu brakuje informacji, zatrzymuje się, aby poprosić o wyjaśnienie, ale agent z nieaktualnymi danymi działa natychmiast na podstawie przestarzałych założeń. Aby zapobiec nieprawidłowym działaniom, dane wrażliwe na czas muszą być ponownie weryfikowane bezpośrednio przed wykonaniem krytycznych operacji.
Nie wczytuj jeszcze nowego kodu. Pozostań przy agencie z poprzedniego kroku (może kupować bilety, ale obecnie nie ma zabezpieczeń przed nieaktualnością ani idempotentnością).
👉🔴 Kliknij ZRESETUJ MIEJSCE, a następnie rozpocznij nową sesję w adk web – kliknij przycisk + Nowa sesja nad czatem.
Rozpocznij nową sesję, aby wyczyścić poprzedni bilet do kolejki i stan oczekującego zakupu. Ten krok wymaga czystego kontekstu wykonania, aby prawidłowo zademonstrować obsługę nieaktualności.
Zrzut dysku
👉✨ Poproś o ten sam program co wcześniej:
Get us two tickets to the Amsterdam show.
👉✨ Teraz, gdy czeka w kolejce, zapytaj o miejsca:
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.
👉🔴 Kliknij SPRZEDAJ DOBRE MIEJSCA. Sekcja A spada do zera.
👉🔴 Kliknij POMIŃ OCZEKIWANIE, aby wysłać zgłoszenie do pracownika obsługi klienta na początek kolejki.

👉✨ Poproś o zakup:
Buy the two section A seats.
Wykonuje on purchase(section="A") bez ponownej weryfikacji dostępności miejsc, co powoduje, że interfejs API miejsca zwraca odpowiedź o błędzie:
{"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.

Zwróć uwagę, że agent ponownie wysłał zapytanie o mapę miejsc po otrzymaniu odpowiedzi z błędem interfejsu API. Przyczyną jest nieaktualny kontekst: wcześniej w sesji agent zapisał „sekcja A: 400 dostępnych”, a ta tura pozostała w historii rozmowy przekazanej do modelu.
Wyraźnie wskaż, gdzie są przechowywane stare informacje: są one zachowywane w historii rozmów, której prefiksy stanu, takie jak temp:, nie mogą zastąpić.

Rozwiązanie
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Teraz wczytaj kod tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👉💻 W terminalu 1 naciśnij Ctrl+C i uruchom ponownie:
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"
Co się zmieniło:
Plik | Co się zmieniło? |
| czegoś nowego. Uruchamia się bezpośrednio przed zakupem i może go zablokować. |
|
|
|
|
fence.py to wywołanie zwrotne, które ADK wykonuje tuż przed wywołaniem narzędzia:
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
Pokaż
👉🔴 ZRESETUJ OBIEKT. Zarezerwuj miejsce w kolejce, w trakcie oczekiwania kliknij SPRZEDAJ DOBRE MIEJSCA, a potem wybierz zakup miejsca w sekcji A.
Tym razem zatrzyma się samo. Obserwuj, jak agent mówi coś w tym stylu:
„Wygląda na to, że miejsca w sekcji A zostały wyprzedane. Muszę jeszcze raz przeczytać mapę miejsc”.
Następnie zobacz, jak ponownie wywołuje get_seatmap bez Twojej pomocy i wraca, oferując sekcję C.
👉🔴 Uważnie przeczytaj kanał aktywności miejsca, ponieważ pokazuje on obie połowy:
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

W panelu widzisz zakup połączeń, ale licznik zgłoszeń pozostaje na poziomie zera. Dzieje się tak, ponieważ fence.py działa jako ochrona przed uruchomieniem narzędzia:
- Agent próbuje wywołać narzędzie do zakupu.
fence.pynajpierw przechwytuje żądanie i sprawdza dostępność miejsc na żywo.- Gdy system stwierdzi, że miejsca są wyprzedane,
fence.pybezpośrednio zwraca błąd, anulując zakup, zanim zostanie wywołany interfejs API.
Instrukcje w prompcie nie gwarantują, że LLM nie spróbuje kupić wyprzedanych miejsc. Ochrona kodem (before_tool_callback) gwarantuje, że nieudany zakup nigdy nie trafi do prawdziwego świata.
👉🔴 Kliknij RESET THE VENUE (ZRESETUJ OBIEKT), a potem BREAK THE NEXT PURCHASE (PRZERWIJ KOLEJNY ZAKUP).
👉✨ Poproś o zakup i pozwól mu spróbować ponownie tyle razy, ile zechce.

Liczba zamówień pozostaje na poziomie 1, ponieważ miejsce otrzymało ten sam klucz idempotentności podczas ponownej próby i zwróciło pierwotne potwierdzenie zamiast tworzyć zduplikowane zamówienie.
8. Autonomiczny przepływ pracy
Modele LLM nie zawsze potrafią przestrzegać sztywnych reguł
Modele LLM świetnie sprawdzają się w podejmowaniu decyzji (np. w wybieraniu programu na podstawie preferencji użytkownika). Jednak poleganie na modelu LLM w przypadku ścisłych reguł krok po kroku (np. „zawsze dołączaj do kolejki przed sprawdzeniem miejsc”) oznacza, że czasami będzie on pomijać kroki lub mylić kolejność.
Aby to naprawić, podziel przepływ pracy na 2 rodzaje kroków:
- Stałe reguły (funkcje kodu): czynności, które muszą być wykonywane zawsze w ten sam sposób (np. pobieranie biletu z kolejki, sprawdzanie pozycji), są zapisywane jako zwykłe węzły kodu.
- Decyzje (węzły agenta): czynności wymagające oceny (wybór sekcji miejsc, sprawdzanie preferencji użytkownika) są przekazywane agentowi AI.
Sprawdź, co zawiera ten przepływ. Niektóre z nich nie zawierają żadnej oceny:
Krok | Ocena? |
pobrać dokładnie 1 bilet do kolejki, | nie – zawsze jeden, zawsze przed zakupem. |
sprawdź, czy jest Twoja kolej, | nie.Zadaj pytanie, przeczytaj odpowiedź |
napisz brief dla kupującego, | nie – to samo zdanie, ten sam kształt za każdym razem; |
który program, która sekcja, | Tak. Cena, dzień, kto przyjeżdża, czego nie lubi |
preferencje i ograniczenia użytkownika, | Tak. |
Ścisłe procedury nie powinny opierać się na LLM, ponieważ modele probabilistyczne w końcu przestaną przestrzegać sztywnych sekwencji, gdy nie będą monitorowane.
Workflow ADK oddziela deterministyczne wykonywanie od wnioskowania opartego na AI:
- Węzły funkcji zawsze egzekwują stałe reguły (np. dołączanie do kolejki lub sprawdzanie pozycji).
- Węzły agenta podejmują decyzje kontekstowe (np. wybierają miejsca na podstawie preferencji użytkownika).
- Węzły oczekiwania wstrzymują wykonanie bez utrzymywania procesu w pamięci.
Oryginalny agent nie zostanie zastąpiony. Staje się ostatnim węzłem na wykresie przepływu pracy, zachowując swoje narzędzia, sprawdzanie aktualności i zabezpieczenia przed powtarzalnością. Wykres kontroluje kolejność wykonywania, a agent podejmuje decyzję o zakupie.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE.
👉💻 Przejdź do kodu tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
Uruchom
👉💻 W terminalu 1 naciśnij Ctrl-C i uruchom ponownie. To samo polecenie co zawsze:
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"
Co się zmieniło od ostatniego kroku:
Plik | Co się zmieniło? |
| czegoś nowego. Wykres |
|
|
| utracił też |
Uruchomienie wykresu – 3 wiadomości w odpowiedniej kolejności
Wykonaj wszystkie poniższe działania w nocnej wersji Pythona (w niej znajduje się wykres). Wysyłaj identyczne dane wejściowe w każdej turze, aby obserwować, jak wykres przepływu pracy przechodzi przez poszczególne stany.
1. Zwróć uwagę na to, z czym teraz rozmawiasz
👉 Odśwież internetowy interfejs 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
Pamiętaj, że adk web wykonuje obiekt przypisany do root_agent. W tym przypadku root_agent wskazuje nightly wykres przepływu pracy, a buyer_agent jest skonfigurowany jako węzeł końcowy na wykresie.
2. Uruchom
👉✨ Wpisz to i naciśnij Enter:
go
Uwaga: na wykresie automatycznego przepływu pracy początkowe wykonanie rozpoczyna się bez czekania na dane wejściowe użytkownika. Wysłanie wiadomości w usłudze adk web powoduje pierwsze uruchomienie.
👉 Spójrz teraz na przeglądarkę. W panelu po lewej stronie rysowany jest wykres, a węzły są kolorowane w miarę ich działania. Panel po prawej stronie to strumień zdarzeń, w którym możesz obserwować proces podejmowania decyzji:

Obserwuj, jak queue_up zapisuje wartości queue_ticket i queue_event_id w stanie sesji.

Powinno być widoczne wywołanie funkcji o nazwie adk_request_input, które oczekuje na odpowiedź. Potem następuje cisza i bieg się kończy. Wykres osiągnął wartość check_front, znalazł Cię na 14 203 miejscu i zatrzymał się.
👉 Sprawdź panel miejsca: jeden bilet do kolejki i nic nie jest w trakcie realizacji.

3. Wybudzanie urządzenia i sprawdzanie, w którym polu wpisujesz tekst
Przewiń strumień zdarzeń do zdarzenia adk_request_input i znajdź okno do wprowadzania danych powiązane z tym zdarzeniem:

Teraz możesz pisać w 2 miejscach, a każde z nich służy do czegoś innego.
miejsce wpisywania tekstu; | Skutki |
pole | odpowiada na przerwanie. Zaparkowane uruchomienie jest kontynuowane od |
główne pole czatu u dołu, | rozpoczyna nowe wywołanie. Wykres odtwarza się od |
👉✨ Wpisz dowolny tekst w polu Enter your response... i naciśnij strzałkę.
👉 Terminal 1:
[check_front] #14,197 — not yet. Pausing.
Jeden wiersz. Nie wybrał drugiego programu i nie pobrał drugiego biletu do kolejki, co spowodowałoby,że znalazłbyś się na końcu kolejki liczącej 14 203 osoby.
Jeśli zobaczysz ponownie symbole [open] i [pick_show], oznacza to, że wpisujesz tekst w polu czatu. Naciśnij RESET THE VENUE (ZRESETUJ OBIEKT) i zacznij od nowa.
👉 Sprawdź ponownie panel miejsca: nadal jest jeden bilet, a pozycja nieco się obniżyła, ponieważ zegar miejsca działał dalej, a Twój nie.

4. Przejdź na początek, a potem zakończ
👉🔴 W panelu miejsca naciśnij POMIŃ OCZEKIWANIE.

👉✨ Odpowiedz na najnowsze adk_request_input wydarzenie po raz ostatni – ponownie w jego polu, a nie w polu czatu. 
Tym razem zegarek check_front znajdzie Cię z przodu i spadnie. Obserwuj, jak zapalają się diody brief i concert na panelu po lewej stronie, czyli 2 ostatnie węzły.

👉 Sprawdź przeglądarkę. To koniec serii. Warto przeczytać 4 rzeczy.

Wykres jest w całości zielony, a krawędzie są uwzględniane. brief i concert zostały podświetlone, a krawędzie w pick_show, queue_up i check_front mają teraz odznakę.
2x
to liczba przejść po tej krawędzi wykresu (raz na każde wybudzenie). Dwie wartości u góry to cena wznowienia: harmonogram za każdym razem ponownie wchodzi do wykresu od początku. Nie oznacza to, że praca została wykonana 3 razy, a dowodem na to jest panel miejsca: 1 bilet do kolejki, a nie 2. @node(rerun_on_resume=False).
Wykres

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
Zwróć uwagę, że 2 węzły to agenci, a 2 to funkcje. To, który węzeł jest którym, jest głównym pytaniem projektowym.
Węzeł | Dlaczego ten kształt |
| agenta. Wybór programu z historii innej osoby jest oceną. Wywołuje funkcję |
| funkcji. Dołączanie do kolejki jest regułą. Reguła nie powinna być rzutem monetą o 3:00 w nocy |
| funkcji. Pytanie „Czy jestem z przodu?” ma jedną prawidłową odpowiedź, a miejsce ma ją. |
| agenta. Jest to |
Sprawdź ostatni węzeł i znajdź utworzonego agenta. Nie jest to kopia ani przeredagowanie:
# agent/concert/nightly.py
from .agent import buyer_agent
Wszystko jest przekazywane: wszystkie narzędzia, ponownie odczytana mapa miejsc w fence.py i klucz idempotentności w purchase. Jest tylko jeden taki element.
Dlaczego zatrzymał się zamiast czekać
Oto wersja, której nikt nie powinien pisać:
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
Utrzymuje proces przy życiu przez 40 minut, nie osiągając niczego, kończy się wraz z terminalem i nie można go wznowić. Zamiast tego check_front wykonuje te działania:
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput to przerwanie. Wykres zatrzymuje się w miejscu, w którym się znajduje, wywołanie trafia do magazynu sesji, a proces może się zakończyć.
Działanie nie kosztuje nic, gdy czeka, i zawsze kończy się w jednym miejscu: zakupem biletów. Może to potrwać całą noc i wymagać kilku pobudek.
9. Współpraca z człowiekiem
Nie wydawaj pieniędzy bez sprawdzenia tego z inną osobą
Autonomiczny agent działający w nocy nie powinien podejmować kosztownych decyzji finansowych na podstawie domysłów lub niejasnych preferencji z historii czatu.
Przed wykonaniem działań o dużym znaczeniu (np. wydaniem pieniędzy na bilety) agent musi się zatrzymać, poprosić użytkownika o potwierdzenie budżetu na głos i zapisać to wyraźne potwierdzenie przed dokonaniem zakupu.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Przejdź do kodu tego kroku:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
Uruchom
👉💻 W terminalu 1 naciśnij Ctrl-C i uruchom ponownie. To samo polecenie co wcześniej:
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"
Co się zmieniło od ostatniego kroku:
Plik | Co się zmieniło? |
| Zawiera to, na co dana osoba się zgodziła, czyli wypowiedziane przez nią zdanie. |
| dodano |
| nowy pierwszy węzeł |
Nie modeluj go. Zapisz zdanie.
Otwórz agent/concert/budget.py i przeczytaj całość:
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"
Węzeł sprawdza 2 rzeczy: czy dana osoba to powiedziała i czy to potwierdziła.
Następnie celowo zachowuje zdanie w stanie sesji, a nie w sekcji user:. W przypadku jednej rezerwacji ustalany jest budżet. „250 USD, jeśli są dobrzy” – to zdanie zostało wypowiedziane o tym zespole tego wieczoru i nie ma nic wspólnego z rezerwacją biletów.
To samo dotyczy wszystkiego innego, co powiedzieli.
Nie ograniczaj się do budżetu. Podczas podejmowania decyzji użytkownicy mogą zmienić zdanie, dlatego zapisuj wszystko, o co proszą, zamiast polegać na tym, co zapamięta konsultant.
"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)
Odczytuje Tylko zdarzenia utworzone przez użytkownika, czyli tylko
text
części z function_response, więc pytanie i odpowiedź dotyczące budżetu nie są tu powtarzane. Brak duplikatów
Uwaga: agree_budget wywołuje tę funkcję przy każdym uruchomieniu, a nie tylko przy pierwszym, więc poprawka wpisana podczas oczekiwania agenta w kolejce też zostanie uwzględniona. Następnie open_the_night wręcz osobie wybierającej obie sentencje, prośbę i budżet, a ona je przeczyta.
Wykres o 1 węzeł dłuższy
Zacznijmy od rozmowy:
# 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)],
)

Dwa węzły zatrzymują się i czekają, a mechanizm jest taki sam. check_front czeka na miejsce wydarzenia i otrzymuje odpowiedź od zegara. agree_budget czeka na osobę i może na nie odpowiedzieć tylko człowiek. Ta różnica to cały krok 10.
Uruchamianie krok po kroku
1. Uruchom
👉 Załaduj ponownie interfejs ADK i wybierz agenta concert.
👉✨ Poproś o to, czego naprawdę chcesz:
Get us two tickets to the Amsterdam show.

W kroku 8 na wykresie nie było użytkownika, więc wszystko, co wpisano, zostało odrzucone, a go poradził sobie tak samo jak każdy inny użytkownik. W tym przypadku agree_budget wywołuje what_they_asked_for(ctx) w pierwszym wierszu, co powoduje zebranie wszystkiego, co zostało wpisane, i umieszczenie tego w state["request"]. Następnie open_the_night przekazuje go osobie kompletującej zamówienie wraz z budżetem.
Nie podano dnia, a w Amsterdamie odbywają się 2 przedstawienia. Wybierz recall()„Sam odwołuje spotkania w dni powszednie” i wybierz sobotę.
W przeglądarce działanie zatrzymuje się podczas zdarzenia adk_request_input:
Wykres zadał Ci pytanie. To samo przerwanie co w przypadku oczekiwania w kolejce w kroku 8, ale teraz wskazywało na osobę, która miała odpowiedzieć na pytanie.
2. Odpowiadaj tak, jak robią to ludzie
👉✨ W polu Enter your response..., a nie w polu czatu u dołu:
About a 100 for the cheap seats. 250 if they're the good ones.
Obserwuj, jak się zatrzymuje, i potwierdź:
[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. Potwierdź
👉✨ To samo pole:
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.
Cztery węzły zostały uruchomione na jednym słowie. 👉 Otwórz kartę State i znajdź budget – Twoje zdanie w dosłownym brzmieniu.

Brak prefiksu. To jest stan sesji. Jeśli rozpoczniesz nową sesję, a budżet się wyczerpie, warto podjąć świadomą decyzję, ponieważ system ponownie poprosi Cię o jego uzupełnienie.
4. Przejdź na początek i zakończ
👉🔴 W panelu miejsca naciśnij POMIŃ OCZEKIWANIE.
👉✨ Odpowiedz na najnowsze adk_request_input – ponownie w jego polu:
go

👉 Sprawdź panel miejsca: jedno zamówienie w cenie zgodnej z ustaleniami dotyczącymi tego rodzaju miejsca.

Jak wykres zadaje pytanie
Otwórz nightly.py i przeczytaj agree_budget. Spraw, aby 3 rzeczy były prawdziwe jednocześnie, i oczekuj tej samej porażki, gdy brakuje którejkolwiek z nich: będzie pytać w nieskończoność.
@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?",
)
...
| bez tego węzeł jest traktowany jako ukończony i pomijany. | |
stabilny | domyślnie jest to nowy identyfikator UUID dla każdego uruchomienia, więc nie ma możliwości wyszukania odpowiedzi. | |
| gdzie pojawi się odpowiedź. Nie jest wartością zwracaną przez | |
Wykres
Jest to wykres z kroku 8, ale z dodatkowym węzłem z przodu. 
Ograniczenia potwierdzone przez użytkownika są ograniczone do sesji i wygasają po zakończeniu rozmowy. Utrzymywanie tymczasowych limitów w różnych sesjach może spowodować, że jednorazowe decyzje staną się niezamierzonymi domyślnymi ustawieniami na stałe.
10. Stos chmury
Laptopy przechodzą w tryb uśpienia, a pliki lokalne znikają
Na laptopie agent zależy od tego, czy komputer jest włączony, lokalnych baz danych SQLite i plików lokalnych. Jeśli zamkniesz pokrywę laptopa lub wdrożysz kontener w chmurze, który się ponownie uruchomi, pliki lokalne i pamięć znikną.
Aby działać niezawodnie w środowisku produkcyjnym:
- Cloud Compute: uruchom agenta w kontenerze (np. Cloud Run), który pozostaje online.
- Trwałe bazy danych w chmurze: zastąp pliki lokalne i SQLite usługami w chmurze (Cloud SQL w przypadku danych sesji, Cloud Storage w przypadku plików).
- Cloud Timers: używaj zarządzanych harmonogramów (takich jak Cloud Scheduler i Pub/Sub), aby agent budził się nawet wtedy, gdy laptop jest wyłączony.
👉🔴 W panelu miejsca kliknij ZRESETUJ MIEJSCE. Każdy krok zaczyna się w tym samym świecie: 8 programów, wszystkie miejsca dostępne, zegar ustawiony na 1×.
👉💻 Przejdź do kodu końcowego:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
Plik | Co się zmieniło? |
| czegoś nowego. Jeden interfejs FastAPI obsługujący stronę czatu i punkt końcowy wywoływany przez harmonogram |
| czegoś nowego. Zwykły python-slim i uvicorn. Nie ma w nim niczego, co dotyczy ADK. |
| skopiowane, więc strona jest dostarczana w tym samym kontenerze co agent. |
| niezmienione. bajt po bajcie folder, w którym zakończono krok 9. |
strona, którą już masz,
** Interfejs internetowy ADK** to wyłącznie lokalny interfejs debugowania, który służy do sprawdzania stanu, strumieni zdarzeń i wykresów przepływu pracy podczas programowania. Nie jest on spakowany do wdrożenia w środowisku produkcyjnym.
Na potrzeby produkcji użyj samodzielnego interfejsu czatu utworzonego w kroku 3. Obsługuje wiadomości użytkowników i przerwy w interakcjach za pomocą punktu końcowego /wake bez udostępniania wewnętrznych elementów sterujących debugowaniem.
Migracja stanu lokalnego do usług w chmurze
Zatrzymaj serwer i sprawdź, co zostało zapisane:
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
Pliki lokalne sprawdzają się w przypadku programowania, ale nie działają w środowisku produkcyjnym, ponieważ kontenery Cloud Run korzystają z efemerycznej pamięci lokalnej instancji.
Podczas działania w Cloud Run:
- Skalowanie instancji do zera: gdy ruch ustaje, kontener jest zamykany, a jego lokalny system plików jest usuwany.
- Niezależne skalowanie instancji: wiele współbieżnych instancji nie współdzieli miejsca na dysku, każda działa z własnym odizolowanym systemem plików.
Jeśli agent zapisze bilet do kolejki lub stan sesji w lokalnym pliku SQLite (sessions.db), kolejne żądania kierowane do nowej lub innej instancji kontenera znajdą pustą bazę danych. Aby zachować trwały stan podczas skalowania i ponownego uruchamiania, pliki lokalne muszą zostać zastąpione zarządzanymi usługami zewnętrznymi.
Zamiast tego potrzebujesz stanu, który przetrwa dłużej niż jakikolwiek kontener. 3 usługi, po jednej dla każdego pliku:
Na laptopie | W chmurze | Co to jest? |
| Cloud SQL | zarządzaną bazę danych Postgres. Istnieje niezależnie od tego, czy coś jest uruchomione. |
| Cloud Storage | zasobnik. Pliki jako obiekty dostępne jednocześnie z każdej instancji |
| Cloud Storage | ten sam zasobnik, |
| Cloud Scheduler | cron, który uruchamia Google. Urządzenie nie jest budzikiem |
Utwórz je
utworzyć bazę danych PostgreSQL, użytkownika bazy danych i zasobnik Cloud Storage; Pamiętaj, że tworzenie instancji Cloud SQL trwa kilka minut i zostało zainicjowane w tle podczas kroku setup.sh.
👉💻 Zbierz je i wykonaj resztę:
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
Ten skrypt utworzył 4 elementy (po jednym na każdy wiersz w tabeli powyżej) i zapisał w .env 2 adresy URI:
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
Wdrażanie agenta za pomocą zarządzanych zaplanowanych reguł
Gdy usługi będą gotowe, przypisz agentowi stałe miejsce.
👉💻 3–4 minuty. Zacznij i czytaj dalej:
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
Skrypt tworzy obraz kontenera z agent/, wdraża go w Cloud Run i udostępnia pomocniczą infrastrukturę opartą na zdarzeniach.
Pełne wdrożenie składa się z 5 odrębnych komponentów:
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 jako źródło wyzwalacza
Cloud Scheduler zastępuje lokalne timery wykonywania (clock.py) zarządzaną usługą cron, która działa niezależnie od lokalnych sesji terminala, obsługuje automatyczne ponawianie i harmonogramowanie stref czasowych.
Pub/Sub jako warstwa odsprzęgania wiadomości
Umieszczenie tematu Pub/Sub między Cloud Scheduler a Cloud Run daje 3 zalety architektoniczne:
- Trwałość wiadomości: kolejkuje przychodzące zdarzenia wyzwalające, jeśli usługa Cloud Run uruchamia się „na zimno”, jest wdrażana ponownie lub jest tymczasowo niedostępna.
- Wbudowane zasady ponawiania: automatyczne ponawianie przejściowych błędów dostarczania bez konieczności stosowania niestandardowej logiki ponawiania na poziomie aplikacji.
- Skalowalność rozpowszechniania: umożliwia wielu usługom podrzędnym subskrybowanie tych samych zdarzeń harmonogramu bez ponownej konfiguracji harmonogramu.

Konfigurowanie trigger_sources=["pubsub"] rejestruje punkt końcowy push zgodny ze standardowymi ładunkami kopert Pub/Sub, tłumacząc przychodzące wiadomości bezpośrednio na zdarzenia wywołania agenta.
→ 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 * * *
Mów do niego
👉 Otwórz adres URL agenta w przeglądarce. 👉✨ Poproś o rezerwację:
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
Powinno się w nim pojawić pytanie o to, ile chcesz wydać
👉✨ Odpowiedz tak, jakbyś mówił(-a) na głos, a następnie potwierdź:
up to 250 a seat for the good ones
Uruchomienie jest obecnie w kolejce.
👉🔴 W panelu miejsca naciśnij SKIP THE WAIT (Pomiń oczekiwanie), aby kolejka była gotowa, gdy nadejdzie wiadomość.
Aby uruchomić go zgodnie z rzeczywistym harmonogramem, zadanie musi być włączone – w przypadku wstrzymanego zadania usługa jobs run odmawia wykonania, zwracając błąd Job.state must be ENABLED:
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 W ciągu kilku sekund agent dokonuje zakupu w ramach potwierdzonych limitów budżetu i automatycznie zwraca potwierdzenie zamówienia.
👉🔴 Sprawdź panel miejsca. Jedno zamówienie.
Aby uruchamiać go co 5 minut:
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
Czyszczenie danych
Zlicz 2 elementy: miejsce i agenta obok niego. Oba skalują się do zera, więc nieużywany kosztuje 0. Usuń je mimo to.
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
Zwróć uwagę, że celowo nie uwzględniliśmy Cloud SQL i zasobnika, ponieważ zawierają one Twoje dane i oba nadal generują opłaty. Gdy będziesz mieć pewność, że wszystko jest gotowe:
./destroy-agent.sh --all
Wszystko inne jest lokalne. Po zakończeniu usuń folder lub zachowaj go i uruchamiaj w swoim projekcie tak długo, jak chcesz.
Agent, jego narzędzia, wywołania zwrotne i budżet nie uległy zmianie. Dwa identyfikatory URI i jedna zmienna środowiskowa.
11. Omówienie systemu i przewodnik rozwiązywania problemów
Różnice w środowisku: architektura lokalna a chmurowa
Twój laptop | W chmurze | Co się zmieniło? |
| Cloud SQL | ciąg znaków połączenia, |
|
| identyfikator URI, |
|
| ścieżka |
| jedna usługa Cloud Run, |
|
| Cloud Scheduler → Pub/Sub |
|
Ty przy klawiaturze |
| wartość domyślna zastępowana w przypadku każdego żądania, |
Podstawowa implementacja agenta, w tym root_agent, definicje narzędzi, zabezpieczenia wywołań zwrotnych i egzekwowanie budżetu, pozostaje bez zmian. Porównanie solutions/step10_deploy/concert/ z krokiem 9 pokazuje dokładne dopasowanie bajt po bajcie, co potwierdza, że abstrakcja usługi ADK oddziela konfigurację infrastruktury od logiki agenta.
Jeśli utkniesz
W dowolnym momencie możesz uzyskać dostęp do pełnego kodu:
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
Gdy coś się zepsuje
Krótki opis problemu | Napraw |
| Nieprawidłowy identyfikator. |
Model zwraca błąd 404 |
|
| Coś już korzysta z portu 8000 lub 8090. |
Agent nie może dotrzeć do obiektu |
|
Kolejka nigdy się nie przesuwa | Kliknij SKIP THE WAIT (POMIŃ OCZEKIWANIE). Przy mnożniku 1 kolejka nie dotrze na początek sama z siebie. |
W panelu pojawi się drugi bilet do kolejki. | Odpowiedź została wpisana w oknie czatu, a nie w polu |
Menu jest puste lub aplikacja się nie wczytuje |
|
| Oczekiwane. |
Wszystko jest dziwne | Kliknij ZRESETUJ OBIEKT, a potem |
Plik pamięci pełen zduplikowanych rezerwacji |
|
|
|
Upłynął limit czasu Cloud Shell | Ponownie uruchom |