Wszystko, czego potrzebujesz do tworzenia długotrwałych agentów w Google Cloud

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

Co tworzysz

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 .venv i instaluje w nim ADK 2.

logowanie

gcloud auth application-default login – jeśli nie masz jeszcze danych logowania.

project

wskazuje gcloud Twój projekt i ustawia go jako projekt limitu.

Interfejsy API

umożliwia korzystanie z 10 stanowisk, których potrzebuje warsztat. aiplatform, run, cloudbuild, artifactregistry, storage, pubsub, cloudscheduler, cloudtrace, logging i sqladmin.

.env

zapisuje identyfikator projektu, regionu i modelu, dzięki czemu każdy skrypt i adk web może je pobrać.

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.

The Midnight Signal w trasie

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?

$VENUE_URL/panel

Panel sterowania, przyciski. setup.sh wydrukował ten adres URL i umieścił go w .env

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

Miejsce tego kroku

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?

agent.py

agenta – nazwa, model, instrukcja, lista narzędzi;

tools.py

search_events i get_seatmap

venue.py

klient HTTP dla obiektu. Nic ciekawego

config.py

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

localhost:8000 – Twój agent oraz karty Stan / Zdarzenia / Artefakty.

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:

Wykres agenta przedstawiający narzędzia search_events i get_seatmap

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?

monstertix/server.py

czegoś nowego. Serwer WWW z Runnerem za nim

monstertix/clock.py

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: Aktywator i element aktywujący, które są od siebie oddzielone

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.

Co robi Runner w pobliżu agenta

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

POST /wake, po jednej wiadomości naraz

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

tools.py

dodano note_companion. get_seatmap zapisuje teraz mapę miejsc w pliku

memory.py

czegoś nowego. Własny sklep z pamięcią

agent.py

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?

session_service_uri

ADK zapisuje w niej każdą wiadomość, wywołanie narzędzia i zmianę stanu w postaci wierszy (baza danych SQLite).

artifact_service_uri

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

3 narzędzia i miejsca, w których każde z nich zapisuje dane

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

memory://

RAM. Znikną, gdy zatrzymasz proces. Jest to ustawienie domyślne, jeśli nie podasz żadnej flagi.

file://...

folder na dysku. Z czego teraz korzystasz

gs://bucket

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

Pamięć jako interfejs z plikiem za nim

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

InMemoryMemoryService

memory://

Nic Po przerwaniu procesu wszystko zostanie utracone

VertexAiRagMemoryService

rag://

korpus Vertex AI RAG,

VertexAiMemoryBankService

agentengine://

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

--session_service_uri w: adk web

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

artefakt,

--artifact_service_uri w: adk web

file://$WORKSHOP/artifacts

pamięć

w wierszu poleceń

MEMORY_DIR, przeczytaj w: agent/concert/memory.py

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.

Nowa sesja, pierwsza tura

Show me the seat map for the Amsterdam Saturday show.

Nowa sesja, druga tura

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

note_companion

user:prefs, w stanie sesji

2

pełną mapę miejsc (6 KB),

get_seatmap, przez save_artifact

usługa artefaktów,

3

podsumowanie mapy miejsc w 1 wierszu,

get_seatmap do temp:seatmap

nigdy

👉 Sprawdź teraz. Karty Stan i Artefakty znajdują się po lewej stronie ikony adk web.

Karta Stan

Karta Artefakty

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.

Sesja sprzed 2 dni

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

Skrócone wskazówki

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

temp:seatmap

no

nie

nie

party_size – zwykły stan sesji, w two-days-ago

tak

tak

nie

user:prefs

tak

tak

yes

memory/userx.md

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.

5 miejsc, w których może się pojawić fakt, i czas, przez jaki jest on widoczny

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

party_size

z sesją.

do momentu usunięcia sesji.

user:prefs

przeciwko użytkownikowi.

we wszystkich sesjach użytkownika.

app:tour_id

w sprawie aplikacji

w przypadku każdego użytkownika

temp:seatmap

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.

Kompresja działa w sesji. Pamięć to osobny sklep

👉🔴 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.py

agent jest zawarty w App z włączoną kompresją; Drugi agent, budget_split, wykonuje działania arytmetyczne na biletach.

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:

Dwa podsumowania, które pokrywają się w jednym punkcie

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.

Podwykonawca, który widzi tylko zgłoszenie

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.

budget_plan po pierwszym pytaniu dotyczącym budżetu

👉✨ 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ę.

budget_plan po drugim pytaniu dotyczącym budżetu

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.

Kolejność w panelu miejsca

W panelu

Co powinno się wyświetlać

Kupione bilety

1 i prawidłowe – jedna rezerwacja, jedno zamówienie.

zamówienie poniżej,

ord_xxxxxxxx — 8× section C · 760

Wycieczka, Amsterdam, sobota

sekcja C spadła o 8

Activity

ord_xxxxxxxx — 8x section C @ 95 = 760

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:

  1. Zapisz stan w bazie danych: zapisz miejsce, w którym agent wstrzymał działanie, aby można było bezpiecznie zakończyć proces.
  2. 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?

tools.py

dodał(a) użytkowników join_queue i check_queue

agent.py

join_queue jest owinięty LongRunningFunctionTool, a App włącza ResumabilityConfig

join_queue od razu wraca z biletem zamiast blokować, a ResumabilityConfig pozwala wstrzymać przejazd i wznowić go później.

Co daje możliwość wznawiania

👉💻 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:

Kolejka, która nadal się przesuwa, mimo że nic Twojego nie jest uruchomione

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.

ten sam bilet po ponownym uruchomieniu;

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.

ten sam zgłoszony problem w ramach procesu, który został zakończony.

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?

join_queue

Bilet do kolejki i pozycja początkowa

Nie. Przetwarzanie jest kontynuowane asynchronicznie w systemie zewnętrznym.

check_queue

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.

które wywołanie jest nadal oczekujące, gdy zwraca wynik.

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.

na początku 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.

Błąd interfejsu API dotyczący miejsca: sekcja A jest wyprzedana

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

mapa miejsc, z której korzysta agent;

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?

fence.py

czegoś nowego. Uruchamia się bezpośrednio przed zakupem i może go zablokować.

agent.py

before_tool_callback=refresh_before_purchase

tools.py

purchase wysyła teraz nagłówek Idempotency-Key

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

Plik danych o miejscach: próba przesłania i odmowa

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:

  1. Agent próbuje wywołać narzędzie do zakupu.
  2. fence.py najpierw przechwytuje żądanie i sprawdza dostępność miejsc na żywo.
  3. Gdy system stwierdzi, że miejsca są wyprzedane, fence.py bezpoś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.

Nadal jedno zamówienie, niezależnie od tego, ile razy spróbujesz

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?

nightly.py

czegoś nowego. Wykres Workflow ADK 2 przedstawiający działanie bez nadzoru

agent.py

root_agent Wykres jest teraz widoczny. Agent, z którym rozmawiasz, nadal jest dostępny, ale ma nową nazwę – buyer_agent. Jest on jednym z węzłów w tym narzędziu.

agent.py

utracił też join_queue i check_queue – wykres jest teraz właścicielem kolejki.

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:

Wykres, który jest w stanie check_front

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

Wykres, który jest w stanie check_front

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.

Wykres, który jest w stanie check_front

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:

Wykres, który jest w stanie check_front

Teraz możesz pisać w 2 miejscach, a każde z nich służy do czegoś innego.

miejsce wpisywania tekstu;

Skutki

pole Enter your response... w przypadku wydarzenia adk_request_input.

odpowiada na przerwanie. Zaparkowane uruchomienie jest kontynuowane od check_front

główne pole czatu u dołu,

rozpoczyna nowe wywołanie. Wykres odtwarza się od START, ponownie wybiera program i pobiera drugi bilet do kolejki.

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

Obudzony raz, ale nadal z jednym biletem

4. Przejdź na początek, a potem zakończ

👉🔴 W panelu miejsca naciśnij POMIŃ OCZEKIWANIE.

Gotowy wykres, wszystkie węzły na zielono

👉✨ Odpowiedz na najnowsze adk_request_input wydarzenie po raz ostatni – ponownie w jego polu, a nie w polu czatu. Gotowy wykres, wszystkie węzły na zielono

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.

Gotowy wykres, wszystkie węzły na zielono

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

Gotowy wykres, wszystkie węzły na zielono

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

Wykres: funkcje dla reguł, pracownicy obsługi klienta dla oceny

# 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

pick_show

agenta. Wybór programu z historii innej osoby jest oceną. Wywołuje funkcję recall, odczytuje wycieczkę i zwraca zweryfikowany obiekt Plan.

queue_up

funkcji. Dołączanie do kolejki jest regułą. Reguła nie powinna być rzutem monetą o 3:00 w nocy

check_front

funkcji. Pytanie „Czy jestem z przodu?” ma jedną prawidłową odpowiedź, a miejsce ma ją.

buy_it

agenta. Jest to buyer_agent – to, czym root_agent było wcześniej, bez zmian. Sekcja A wyprzedała się w trakcie oczekiwania – wybrać B, C czy nic? To jest ocena, której model nauczył się w części 4.

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?

budget.py

Zawiera to, na co dana osoba się zgodziła, czyli wypowiedziane przez nią zdanie. load() odczytuje je na głos, a someone_is_there() odpowiada, czy ktoś jest w pobliżu, aby zadać mu pytanie.

tools.py

dodano set_budget, aby agent mógł wpisać uzgodniony limit do stanu sesji po potwierdzeniu go przez osobę.

nightly.py

nowy pierwszy węzeł agree_budget, który przed uruchomieniem czegokolwiek innego zadaje pytanie i odczytuje odpowiedź; Obejmuje budżet i resztę prośby, zarówno w formie tekstu dowolnego, jak i SEATS jako stałą wartość.

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

Ten sam wykres, ale z parametrem agree_budget na początku

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.

Bieg się zatrzymuje i pojawia się pytanie o to, ile chcesz wydać.

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?

Przed zatwierdzeniem odczytuje numer

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.

budżet na karcie Stan własnymi słowami.

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

Wznowienie działania i brak ponownego pytania

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

Jedno zamówienie, w ramach uzgodnionych warunków

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

rerun_on_resume=True

bez tego węzeł jest traktowany jako ukończony i pomijany.

stabilny interrupt_id

domyślnie jest to nowy identyfikator UUID dla każdego uruchomienia, więc nie ma możliwości wyszukania odpowiedzi.

ctx.resume_inputs[id]

gdzie pojawi się odpowiedź. Nie jest wartością zwracaną przez RequestInput – ADK iteruje węzeł zamiast wysyłać do niego dane.

Wykres

Jest to wykres z kroku 8, ale z dodatkowym węzłem z przodu. Ten sam wykres, ale z parametrem agree_budget na początku

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:

  1. Cloud Compute: uruchom agenta w kontenerze (np. Cloud Run), który pozostaje online.
  2. 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).
  3. 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?

main.py

czegoś nowego. Jeden interfejs FastAPI obsługujący stronę czatu i punkt końcowy wywoływany przez harmonogram

Dockerfile

czegoś nowego. Zwykły python-slim i uvicorn. Nie ma w nim niczego, co dotyczy ADK.

monstertix/

skopiowane, więc strona jest dostarczana w tym samym kontenerze co agent.

concert/

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?

sessions.db

Cloud SQL

zarządzaną bazę danych Postgres. Istnieje niezależnie od tego, czy coś jest uruchomione.

memory/userx.md

Cloud Storage

zasobnik. Pliki jako obiekty dostępne jednocześnie z każdej instancji

artifacts/

Cloud Storage

ten sam zasobnik,

clock.py

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.

tekst alternatywny

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?

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

Cloud SQL

ciąg znaków połączenia,

--artifact_service_uri=file://...

gs://bucket

identyfikator URI,

memory/userx.md

gs://bucket/memory/

ścieżka

adk web

jedna usługa Cloud Run,

Dockerfile i main.py.

monstertix/clock.py

Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

Ty przy klawiaturze

someone_is_there()

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

✗ Cannot access project

Nieprawidłowy identyfikator. rm ~/project_id.txt && ./setup.sh

Model zwraca błąd 404

-latest aliasy są dostępne tylko w AI Studio. Użyj ADK_MODEL=gemini-latest-flash

address already in use

Coś już korzysta z portu 8000 lub 8090. lsof -ti:8000 | xargs kill -9

Agent nie może dotrzeć do obiektu

curl $VENUE_URL/health lub ponownie ./deploy-venue.sh

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 Enter your response... w wydarzeniu adk_request_input. Okno czatu rozpoczyna nowe działanie

Menu jest puste lub aplikacja się nie wczytuje

adk web traktuje każdy folder w agent/ jako aplikację i odrzuca całą listę, jeśli choć jeden z nich nie jest aplikacją. ls agent/ powinno wyświetlać tylko concert/ – uruchamianie elementów z poziomu agent/ może pozostawić niepotrzebny znak memory/. ./use-solution.sh 8 --force porządkuje

[EXPERIMENTAL] ostrzeżenia

Oczekiwane. EventsCompactionConfig i ResumabilityConfig są w wersji przed GA w wersji 2.6.2

Wszystko jest dziwne

Kliknij ZRESETUJ OBIEKT, a potem rm sessions.db.

Plik pamięci pełen zduplikowanych rezerwacji

remember() dołącza do każdego uruchomienia. ./use-solution.sh N --force resetuje go lub cp seed/memory/default.md memory/userx.md

adk: command not found

source .venv/bin/activate – każda nowa karta tego wymaga;

Upłynął limit czasu Cloud Shell

Ponownie uruchom adk web. sessions.db przetrwało – to krok 6, który możesz wykonać bezpłatnie.