1. Hinweis
Auf den meisten Ticket-Websites werden Käufer in eine Warteschlange gestellt, um Serverabstürze zu vermeiden und die hohe Nachfrage zu bewältigen. Sie beginnen mit dem Kauf und stellen fest,dass Sie an 14.000. Stelle in der Warteschlange stehen. Die Warteschlange bewegt sich 40 Minuten lang. Wenn Sie an der Reihe sind, haben Sie nur wenig Zeit, um Plätze auszuwählen und zu bezahlen. Wenn du diese Zeit verpasst, wird dein Platz in der Warteschlange an jemand anderen vergeben.
Viele Aufgaben in der Praxis folgen diesem Muster: Eine Aufgabe, die viel länger als eine einzelne Unterhaltung dauert und in einem externen System ausgeführt wird, das Sie nicht kontrollieren. Beispiele hierfür sind das Warten auf die Wiederauffüllung durch einen Lieferanten, das Warten auf die Genehmigung eines Anspruchs oder das Warten in einer Warteschlange.
Standard-KI-Agents sind auf einen einzelnen Chat-Turn beschränkt, der nur wenige Sekunden dauert und rein reaktiv ist. Sie können nicht 40 Minuten warten oder autonom handeln. Um dieses Problem zu lösen, benötigen Sie einen Langzeit-Agent: ein System, das während langer Wartezeiten inaktiv bleibt, den Kontext über Prozessneustarts hinweg beibehält und automatisch fortgesetzt wird, wenn es durch einen externen Trigger aktiviert wird.
In diesem Workshop erfahren Sie, wie Sie einen Agenten für die Warteschlange für Konzerttickets erstellen. Dabei werden die wichtigsten Architekturmuster für Agents mit langer Laufzeit in Google Cloud behandelt.
Aufgaben

In diesem Workshop erstellen Sie einen Agenten für die Buchung von Tickets, der lange ausgeführt wird, und lernen dabei wichtige Softwaremuster für Agenten kennen:
- Unterbrechung und Fortsetzung der Verarbeitung: Parken Sie den Aufruf eines Agenten während langer Wartezeiten und setzen Sie die Ausführung über externe Trigger fort.
- Durable State Storage: Der Konversationsstatus und Artefakte werden mithilfe von SQLite/Cloud SQL und Cloud Storage prozessübergreifend beibehalten.
- Staleness Guards und Idempotenz: Überprüfen Sie dynamische Daten (z. B. die Verfügbarkeit von Sitzplätzen), bevor Sie Maßnahmen ergreifen, und verwenden Sie Idempotenzschlüssel, um doppelte Transaktionen zu verhindern.
- ADK-Workflow-Diagramme: Strukturieren Sie die Ausführung mithilfe von deterministischen Codeknoten für prozedurale Regeln und Agentenknoten für kontextbezogene Entscheidungsfindung.
- Genehmigung durch den Nutzer: Die Ausführung wird angehalten, um explizite Ausgabenlimits vom Nutzer zu erfassen und zu speichern.
- Cloud-Bereitstellung: Containerisieren und Bereitstellen des Agents in Cloud Run mit Cloud Scheduler- und Pub/Sub-Triggern.
Einrichten
Bereiten Sie zuerst Ihre Umgebung vor und sorgen Sie dafür, dass Sie Zugriff auf Google Cloud haben. Ich empfehle, diesen Befehl in Cloud Shell auszuführen, da dort bereits alle erforderlichen Tools vorhanden sind. Prüfen Sie einfach, ob Sie genügend Speicherplatz haben und ob mit Ihrem Konto Projekte erstellt werden können.
👉💻 Klonen Sie das Repository und führen Sie die Einrichtung aus:
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
Sie werden einmal gefragt:
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
Anschließend werden die folgenden Schritte in der angegebenen Reihenfolge ausgeführt:
virtualenv | Erstellt | |
Anmeldung |
| |
project | verweist | |
APIs | ermöglicht die zehn erforderlichen Schritte. | |
| schreibt Ihre Projekt-, Regions- und Modell-ID, sodass jedes Skript und | |
Modellprüfung | führt einen echten Aufruf an Gemini aus. Eine falsche Projekt- oder Modell-ID führt hier zu einem Fehler. | |
Ausgangsprompt | Erstellt eine Sitzung, die bereits zwei Tage alt ist. Schritt 3 wird geöffnet. | |
Cloud SQL | startet eine Postgres-Instanz im Hintergrund | |
Veranstaltungsort | stellt Ihren eigenen Mock-Ticketverkäufer in Cloud Run bereit | |
👉💻 Aktivieren Sie die Umgebung:
cd ~/monstertix
source .venv/bin/activate
Ihr Prompt beginnt jetzt mit (.venv).
Szenarioübersicht
In diesem Workshop-Szenario bucht Ihr Agent zwei Tickets für ein Konzert („The Midnight Signal“ in Amsterdam). Die im Verlaufsspeicher gespeicherten Einstellungen des Nutzers umfassen:
- Es muss eine Wochenendshow sein (Sam kann an Wochentagen nicht teilnehmen).
- Vermeiden Sie Sitzplätze im oberen Rang des Ziggo Dome (schlechte Sicht).
- Der Zielpreis liegt bei etwa 200 $ pro Ticket, mit einem Höchstlimit von 250 $.

Installation prüfen
👉💻 Ein Befehl, acht Ticks:
./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.
👉 Zweiten Browsertab öffnen:
Dabei gilt: | Beschreibung |
| Das Bedienfeld, Ihre Tasten. |
2. Mit Prompts zur Autonomie?
Prompts können nicht von selbst gestartet werden
Ein Prompt ist einfach Text, der bei einem Aufruf an ein KI-Modell übergeben wird. Es kann nicht von selbst ausgeführt werden, sich selbst aktivieren oder einen neuen Anruf starten.
Wenn Sie einem Agenten in seinem Prompt sagen, er solle sich in 10 Minuten wieder melden, passiert nach dem Ende der aktuellen Runde nichts. Das Modell hat keine interne Uhr oder Schleife. Damit ein Agent autonom agieren kann, muss er von etwas außerhalb des Agents geweckt und aufgerufen werden.

Rig starten
Wir beginnen mit dem Testen der Entwicklertools. Mit adk web wird die ADK-Web-UI gestartet, die Entwicklungsoberfläche, die mit dem ADK ausgeliefert wird. So können Sie mit dem Agenten, den Sie erstellen, sprechen, ohne zuerst ein Frontend zu schreiben. Außerdem sehen Sie Dinge, die in einem Chatfenster nicht angezeigt werden, z. B. jeden Tool-Aufruf und seine Argumente, den Sitzungsstatus, wenn er sich ändert, die Artefakte, die der Agent gespeichert hat, und die zugrunde liegenden Rohereignisse. In der Produktionsumgebung würden Sie sie nicht verfügbar machen. Es ist das beste Fenster in einen laufenden Agenten, während Sie lernen. In Schritt 3 erstellen Sie das Frontend, das Sie tatsächlich bereitstellen würden, und im letzten Schritt stellen Sie es bereit.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Terminal 1: Laden Sie den Code für diesen Schritt und starten Sie die ADK-Web-UI:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
Mit use-solution.sh wird der fertige Code eines Schritts in agent/ kopiert, wo Sie arbeiten. Jeder Schritt beginnt damit und es wird ausgegeben, welche Dateien geändert wurden, damit Sie wissen, was Sie lesen müssen.
Das ist Ihre Ausgangssituation:
Datei | Beschreibung |
| Der KI-Agent – ein Name, ein Modell, eine Anleitung, eine Liste von Tools |
|
|
| ein HTTP-Client für den Veranstaltungsort. Nichts Interessantes |
| welches Modell verwendet werden soll, und eine Prüfung, ob Ihre Anmeldedaten funktionieren. |
. ./set_env.sh aktiviert die virtuelle Umgebung, lädt Ihr Projekt und Modell und exportiert $WORKSHOP – den absoluten Pfad zu ~/monstertix. Führen Sie den Befehl in jedem Terminal aus, das Sie öffnen. Es wird ausgegeben, was festgelegt wurde:
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
👉 Erste Browsertabs öffnen:
Dabei gilt: | Beschreibung |
ADK-Web-UI |
|
Mit ihr sprechen
👉✨ Geben Sie in der ADK-Webbenutzeroberfläche Folgendes ein:
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
Das Tool durchsucht die Tour, liest Sitzpläne und berücksichtigt Ihr Budget und Ihr Unternehmen. Das ist ein wirklich leistungsfähiger Assistent. Wenn der Vorverkauf um 10:00 Uhr beginnt, wird der Agent jedoch nicht automatisch ausgeführt, da kein Prozess oder Hintergrundtrigger aktiv ist.
Lesen, was gerade passiert ist
Die Mitte des Bildschirms ist nicht nur ein Chatprotokoll. Jeder Schritt des Zuges ist nummeriert, einschließlich der Tool-Aufrufe:
▸ 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
👉 Klicken Sie auf eine Zeile mit ⚡. Im linken Bereich sehen Sie die Argumente, die das Modell ausgewählt hat. Klicken Sie darunter auf das ✓, um den zurückgegebenen JSON-Code zu sehen.
Die Informationen zur Tour sind nicht in den Trainingsdaten oder im Prompt des Modells enthalten. Der Agent hat diese Daten durch Ausführen des Tools search_events abgerufen, mit dem die Venue API abgefragt wurde.
Gerade ausgeführten Code öffnen
Sie haben gesehen, dass es funktioniert, und den Trace gesehen. Sehen Sie sich nun den Code an.
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
👉💻 Öffne agent/concert/agent.py. Der gesamte KI-Agent befindet sich am Ende der Datei:
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],
)
Einen Namen, ein Modell, eine Beschreibung des Verhaltens des KI-Agents und eine Liste von Python-Funktionen. Das ist alles, was ein Agent ist. adk web hat es gefunden, weil die Datei agent.py heißt und die Variable root_agent.
👉💻 Jetzt 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)
Eine gewöhnliche Funktion. Kein Decorator, keine Registrierung, kein Schema zum Schreiben. Das ADK liest die Signatur und den Docstring und erstellt die Tool-Definition, die das Modell sieht. Deshalb ist der Docstring so wichtig und deshalb hat sich das Verhalten des Modells geändert, als ein Argument gelöscht wurde.
Das Modell führt Ihren Code nie aus. Als Sie search_events({"city": "Amsterdam"}) gesehen haben, hat das Modell nach diesem Aufruf gefragt. Das ADK hat die Funktion ausgeführt, das Ergebnis zurückgegeben und das Modell hat einen Satz dazu geschrieben. Die Schleife lautet:
👉 Mit den Symbolen ganz links können Sie ändern, was im Bereich angezeigt wird. Das zweite ist das Agent-Diagramm. Wenn Sie es ausprobieren, sollten Sie zwei Tools sehen:

Sag ihm also, dass es nicht schlafen soll.
Was würden Sie tun, um das Problem zu beheben? Greife zuerst nach dem, was die meisten Leute als Erstes nehmen. Rufen Sie INSTRUCTION aus Schritt 1 auf, den Text, der dem Modell bei jedem Zug übergeben wird, und schreiben Sie ihn neu.
👉💻 Öffne agent/concert/agent.py. Unter INSTRUCTION finden Sie ein zweites, das bereits für Sie geschrieben wurde. Sie können die Antwort auch selbst ändern:
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],
)
👉💻 Starte in Terminal 1 neu:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ Erkläre den Plan so, wie du ihn einer Person erklären würdest:
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.
Es wird zustimmen und es wird so klingen, als ob es das auch so meint.
👉 Warten Sie nun. Geben Sie nichts anderes ein. Podiumsdiskussion ansehen
Das kann eine Weile dauern. ES PASSIERT NICHTS!
Warum dieser Prompt nie funktionieren konnte
Lies es noch einmal mit frischen Augen. Monitor. Sobald es geöffnet wird. Sehen Sie weiterhin regelmäßig nach. Jede dieser Situationen beschreibt etwas, das im Laufe der Zeit passiert.
instruction= ist ein String, der dem Modell als Teil der Anfrage übergeben wird, wenn jemand eine Nachricht sendet. Zwischen den Zügen wird nichts gelesen.
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
Eine Anweisung kann die Funktion, in der sie geschrieben ist, nicht aufrufen.
Fehlerhafte Prompt-Variationen
Der Versuch, die autonome Ausführung über Prompt-Anweisungen zu erzwingen, schlägt unabhängig von der Formulierung fehl. Häufige Versuche mit Prompts, z. B.:
- „Stell einen Timer für Dienstag um 10:00 Uhr und kauf dann die Tickets.“
- „Prüfe alle fünf Minuten, ob der Vorverkauf begonnen hat.“
- „Autonom handeln, ohne auf Nutzereingaben zu warten.“
Alle schlagen aus demselben Grund fehl: Prompts haben Lesezugriff, wenn ein Aufruf erfolgt. Ohne externen Aufrufer wird zwischen den Zügen kein Code ausgeführt.
3. Ereignisgesteuerte Inaktivität
Agents bleiben nicht für immer aktiv
Wenn ein Prozess kontinuierlich ausgeführt wird, wird unnötig Arbeitsspeicher verbraucht, es entstehen Kosten und der Prozess stürzt ab, wenn Ihr Laptop in den Ruhemodus wechselt oder neu gestartet wird.
Anstatt einen Agenten während des Wartens ununterbrochen auszuführen, sollte ein Agent mit langer Laufzeit vollständig inaktiv werden. Ein externes Ereignis (z. B. ein Timer, ein Webhook oder eine Nachricht) aktiviert den Agent nur, wenn tatsächlich etwas zu tun ist.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Code für diesen Schritt laden:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
Sehen Sie sich an, was installiert wurde: monstertix/, der Server, die Uhr und noch etwas, das Sie noch nicht gesehen haben. Prüfen Sie, ob agent/ unverändert geblieben ist, da sich in diesem Schritt nichts am Agenten ändert. Das ist der Punkt.
Datei | Beschreibung |
| etwas Neues. Ein Webserver mit einem Runner dahinter |
| etwas Neues. Etwas, das die Zeit kennt und den Server aufruft |
Die beiden Hälften werden in zwei Dateien gespeichert, die in der Cloud verbleiben: 
wird in Schritt 10 zu | |||
Trigger | einen Endpunkt, über den der Agent ausgeführt werden kann | Cloud Run | |
der Auslöser | etwas mit einer Uhr, die das aufruft | Cloud Scheduler | |
Der Trigger: Ihr eigener Webserver
Ja, ein Webserver. Etwas muss auf Anfragen warten, bevor es aufgerufen werden kann. Die ADK-Web-UI aus den vorherigen Schritten ist eine Entwicklungsschnittstelle, die zufällig einen Agenten ausführen kann.
👉💻 monstertix/server.py öffnen:
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"])]),
):
...
Eine Agent wird nicht automatisch ausgeführt. Es handelt sich um eine Beschreibung: einen Namen, ein Modell, eine Anleitung und eine Liste von Tools. Es muss etwas geben, das eine eingehende Nachricht entgegennimmt, die richtige Sitzung findet, dem Modell die bisherige Unterhaltung übergibt, alle Tools ausführt, die es anfordert, die Ergebnisse zurückgibt, so lange wiederholt, bis keine Tools mehr aufgerufen werden, und jeden Schritt in den Speicher schreibt. Das ist die Runner.

Die ADK-Web-UI hat eine Standardeinstellung für Sie. Hier erstellen Sie es selbst. Das ist der einzige Unterschied zwischen der ADK-Web-UI und einer Bereitstellung.
👉💻 Terminal 2 – starten Sie es:
Starten Sie den Runner.
cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.
Lies die letzte Zeile vor. Der Trigger wird ausgeführt und der Agent wird geladen, aber es passiert nichts. Trennen Sie die beiden Ideen: erreichbar sein und angerufen werden.
Und eine Haustür, wenn wir schon dabei sind
adk web ist zwar ein großartiges Entwicklungstool, sollte aber nicht in der ausgelieferten Version enthalten sein. Sie sollten sie nicht aktivieren, wenn Sie die App in der Produktion bereitstellen, und Sie würden einen Kunden nicht darauf verweisen, wenn Sie es doch tun. Derselbe Server stellt also auch eine Seite bereit.
👉 Öffnen Sie http://127.0.0.1:8090 in einem Browser – dieselbe Adresse.
Das ist MonsterTix. Ein Chatfenster, ein Name in der Ecke, zwei Schaltflächen. Es wird ein POST-Vorgang an /wake gesendet und die Antwort gerendert. Der Code würde unverändert für jeden Endpunkt funktionieren, der auf dieselbe Weise antwortet.
Auf der Seite | Was darunter liegt |
die Unterhaltung |
|
+ Neue Sitzung | eine neue Sitzungs-ID – neue Unterhaltung und später ein neues Budget |
Speicher zurücksetzen | löscht jede Sitzung und die Datei mit den gemerkten Informationen. Sie werden zuerst gefragt und es wird Ihnen genau angezeigt, wo sich dieser Status befindet. |
👉✨ Sagen Sie etwas zu What Amsterdam dates are there? und sehen Sie sich Terminal 2 an. Dasselbe Runner, derselbe Agent, dieselben Logzeilen. Nur der Anrufer hat sich geändert.
Der Triggerer – etwas, das die Zeit kennt
👉💻 Öffne monstertix/clock.py. Sehen Sie sich den Code an.
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
Das ist ein ganz einfacher Mechanismus: schlafen, POST, vielleicht wiederholen. Jeder von Ihnen verwendete Scheduler macht dasselbe, plus Wiederholungen, Zeitzonen und eine Garantie, dass er einen Neustart übersteht.
👉💻 Terminal 3 – einmal auslösen, in 10 Sekunden:
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
👉 Hören Sie jetzt mit dem Tippen auf. mühelos. Sehen Sie sich das Terminal 2 an.
Eine Minute später, ohne dass jemand an der Tastatur sitzt:
[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.
Das ist die Antwort auf Schritt 2. Etwas außerhalb des Agent hat ihn aufgerufen. Dieses Etwas war ein sleep und eine HTTP-Anfrage.
👉💻 Ctrl-C, wenn du es dir mehrmals angesehen hast. Verwenden Sie zuerst --every 30, wenn Sie die Wiederholung sehen möchten.
Lies jetzt, was dort steht.
„Ich habe zwei Tickets für ‚The Midnight Signal‘ in Amsterdam, Bereich A, für 420 $ gekauft.“
👉🔴 Prüfe das Panel für den Veranstaltungsort. Dort sollte das gekaufte Ticket angezeigt werden.
Was ist also noch falsch?
Es hat etwas gekauft. Sehen Sie sich an, was das Modell erraten musste. Es hat Amsterdam und Abschnitt A ausgewählt, weil das die ersten Ergebnisse waren, die es gefunden hat. Außerdem haben Sie dem KI-Agenten gesagt, dass er die Tickets direkt kaufen soll. Der KI-Agent weiß nicht, dass Sam abends nicht kann. Ein Agent, der um 3 Uhr morgens auf Vermutungen reagiert, ist schlimmer als ein Agent, der nichts tut.
Das Aufwachen ist der einfache Teil. Ein Agent, der ohne alles aufwacht – ohne Verlauf, ohne Einstellungen, ohne Zuhörer – ist es noch nicht wert, geweckt zu werden.
4. Kontextlebensdauer verwalten
Informationen sind zu unterschiedlichen Zeiten an unterschiedlichen Orten verfügbar
Ein Agent speichert Fakten an verschiedenen Stellen: im Chatverlauf, im Sitzungsstatus, in Dateien (Artefakten) oder im Langzeitgedächtnis des Nutzers. Jeder Ort ist unterschiedlich lange verfügbar.
Wenn Sie Informationen an der falschen Stelle eingeben:
- Es verschwindet zu schnell: Wichtige Details werden gelöscht, wenn der Chat beendet wird.
- Es bleibt zu lange bestehen: Temporäre Fakten verunreinigen das Langzeitgedächtnis und verwirren zukünftige Unterhaltungen.
Sie müssen jede Information dem richtigen Speicherort und der richtigen Lebensdauer zuordnen.
Code laden
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN.
👉💻 Führen Sie im Terminal 1 Folgendes aus:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
Was hat sich seit dem letzten Schritt geändert?
Datei | Was hat sich geändert? |
|
|
| etwas Neues. Unser eigener Speicher |
| Drei weitere Tools in der Liste und die Anweisung, dass der Agent sie verwenden soll |
👉💻 Starten Sie den Agent in Terminal 1 neu:
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"
Zwei Flags, die Sie noch nicht verwendet haben:
Flag | Beschreibung |
| Das ADK schreibt jede Nachricht, jeden Tool-Aufruf und jede Statusänderung als Zeilen in die Datenbank (SQLite-Datenbank). |
| Dateien, die der Agent speichert.(Dateien) |
Ohne sie speichert das ADK alles im Arbeitsspeicher und verwirft es, wenn Sie den Prozess beenden. Diese beiden Einstellungen sorgen dafür, dass die Änderungen beibehalten werden.
Die Datenbank sessions.db enthält eine simulierte frühere Unterhaltung (two-days-ago) mit 13 aufgezeichneten Ereignissen:
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?
Eine Erinnerungsdatei mit drei vergangenen Buchungen, memory/userx.md, die Sie in einem Editor öffnen können:
- 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.
Deshalb sind die bernsteinfarbenen Zeilen an Wochentagen so wichtig. Eine Vorstellung am Abend unter der Woche ist 10 $ pro Platz günstiger, es sind noch bessere Plätze verfügbar und sie steht ganz oben auf der Liste. Trotzdem ist sie die falsche Antwort für diese Person. Der einzige Grund, warum der KI-Agent das wissen kann, ist, dass er eine Datei nach der letzten Buchung geschrieben hat. Ob diese Tatsache bestehen bleibt, wird in den Schritten 4 und 5 geprüft.
Die drei neuen Tools

note_companion erfasst, wer kommt und was sie einschränkt:
# 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}
Füge sie hinzu, weil „Sam kann abends unter der Woche nicht“ auch nächste Woche noch zutreffen muss. Schreiben Sie es in user:prefs, damit das passiert, und sehen Sie zu, wie der user:-Teil des Namens die Arbeit erledigt. Dazu kommen wir unten noch einmal.
recall
und
remember: Eine Textdatei, memory/userx.md, wird gelesen und geschrieben. Halte sie einen Moment lang.
👉 Sehen Sie sich nun die beiden Stellen an, an denen der Agent seinen Kontext speichert:
Standort | Inhalt |
Bundesland | den Speicher dieser Sitzung als Schlüssel/Wert-Paare – hier sind Sie in Schritt 4 |
Artefakte | Dateien, die der Agent gespeichert hat, die aber nie in den Prompt aufgenommen werden |
Änderungen in ansehen
get_seatmap
. Früher wurde die gesamte Sitzplatzkarte zurückgegeben. Die Sitzplatzkarte wird nun in einer Datei gespeichert und es wird eine kurze Zusammenfassung zurückgegeben:
# 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
Wir erklären, warum diese Änderung wichtig ist.
Die Nutzlast jeder Tool-Antwort wird an den Unterhaltungsverlauf angehängt und in allen nachfolgenden Zügen an das Modell zurückgesendet. Wenn Sie große JSON-Objekte (z. B. einen Sitzplan mit 6 KB) zurückgeben, erhöht sich der Tokenverbrauch bei jedem nachfolgenden Zug.
Speichern Sie es stattdessen in einer Datei und bezahlen Sie einen Dateinamen. Lassen Sie den Kundenservicemitarbeiter mit load_artifact nach den Details fragen, wenn er sie benötigt. In den meisten Fällen wird das jedoch nicht der Fall sein.
Speicherort der Datei: der Artefaktdienst
Sehen Sie sich die Zeile in get_seatmap noch einmal an, in agent/concert/tools.py:
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context stammt aus dem ADK, das es als Argument an Ihr Tool übergibt, und save_artifact ist eine Methode dafür. In dieser Zeile übergibt Ihr Tool die Byte an das ADK.
Das ADK schreibt sie dann mit dem Artefaktdienst, mit dem es gestartet wurde. Sie haben das mit dem Flag ausgewählt:
--artifact_service_uri="file://$WORKSHOP/artifacts"
Das ADK bietet drei Speicherorte:
URI | Wo die Byte landen |
| RAM Verschwindet, wenn Sie den Vorgang beenden. Dies ist die Standardeinstellung, wenn Sie kein Flag übergeben. |
| einen Ordner auf der Festplatte. Was Sie derzeit verwenden |
| Daten in Cloud Storage zu übertragen. Was der bereitgestellte Agent im letzten Schritt verwendet |
👉💻 Hier wählen wir Dateien aus:
ls -R ~/monstertix/artifacts
Wenn Sie die Daten von Ihrem Laptop in Cloud Storage verschieben möchten, ändern Sie file:// in gs://. (Das sehen wir uns in Schritt 10 an.)
Unser Erinnerungsspeicher

Das ADK bietet eine Schnittstelle für den Langzeitspeicher namens BaseMemoryService. Sie hat zwei Methoden:
# 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
Das ADK enthält drei Implementierungen:
Klasse | URI | Voraussetzungen |
|
| Es geschieht nichts. Verliert alles, wenn Sie den Vorgang beenden |
|
| ein Vertex AI-RAG-Korpus |
|
| eine Agent Engine-Ressource |
Wir schreiben unsere eigenen für diesen Workshop. Schließen Sie InMemoryMemoryService aus, da dort alles vergessen wird, wenn Sie den Vorgang beenden. Schließen Sie auch die anderen beiden aus, da sie Cloud-Ressourcen benötigen, deren Erstellung mehrere Minuten dauert. Wählen Sie stattdessen eine Datei aus, da Sie sie im Voraus mit Daten füllen und in einem Editor lesen können. memory.py ist also Ihre eigene Implementierung. Wir speichern alles in einer Markdown-Datei:
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 und MemoryEntry sind ADK-Typen und Memory Bank gibt dieselben Objekte zurück. Hinter einem steht ein verwalteter Dienst. Unseres hat eine Datei.
Sie können eine Datei aus einem bestimmten Grund auswählen: Sie können sie öffnen und lesen. Das gesamte Langzeitgedächtnis Ihres Agents ist eine Textseite.
Wohin richten Sie sie also?
In dieser Workshop-Implementierung werden zwei der drei Dienste in der Befehlszeile ausgewählt. Für die dritte haben wir aus dem oben genannten Grund eine eigene Implementierung vorgenommen:
Dienst | Wo Sie die Einstellung vornehmen | Dieser Workshop |
Sitzung |
|
|
artifact |
|
|
Arbeitsspeicher | nirgends in der Befehlszeile |
|
memory.py erstellt den Dienst also selbst aus einer Umgebungsvariable:
# 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 exportiert beides. Mit „Get MEMORY_DIR“ wird festgelegt, wo der Agent seinen Speicher liest, und mit MEMORY_USER wird die Datei bestimmt.
Orte finden
Sie haben jetzt vier Orte, an denen Sie eine Fakteninformation unterbringen können. Sehen Sie sich alle vier an.
Zuerst Ihre eigene Sitzung
👉 Klicken Sie in der oberen Leiste auf Neue Sitzung.
👉✨ Sag drei Dinge nacheinander:
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?
Diese dritte Nachricht ist wichtig. Dadurch wird der Zug beendet, in dem die Sitzplatzkarte gelesen wurde. Wenn ein Zug beendet wird, verschwindet temp:.
Drei Dinge wurden an drei verschiedenen Orten gespeichert:
Was wurde gespeichert? | Welches Tool hat | Wo ist es hin? | |
1 | Sam kann abends unter der Woche nicht |
|
|
2 | den vollständigen Sitzplan (6 KB) |
| den Artifact Service |
3 | eine einzeilige Zusammenfassung des Sitzplans |
| nirgends |
👉 Jetzt prüfen Der Tab State (Status) und der Tab Artifacts (Artefakte) befinden sich links von adk web.


Auf dem Tab State (Status) sehen Sie, dass temp:seatmap ausgelassen wurde. Zustandsschlüssel mit dem Präfix temp: sind nur während des aktiven Aufrufs vorhanden und werden vom ADK automatisch entfernt, bevor der Sitzungsstatus in der Datenbank gespeichert wird.
Danach die ältere Unterhaltung
👉 Wechseln Sie oben im Drop-down-Menü für die Sitzung zu two-days-ago.
Diese Sitzung enthält einen vorab ausgefüllten Unterhaltungsverlauf in der Datenbank.

👉 Sehen Sie sich den Ereignisstream (mittlere Spalte) an. Ältere Abbiegungen wurden durch einen zusammengefassten Ereignisblock ersetzt.

👉 Sehen Sie sich den Tab Status an. Dort befinden sich drei Arten von Schlüsseln:
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
Mit dem Präfix user: (z.B. user:prefs) wird der Status für alle Sitzungen eines bestimmten Nutzers festgelegt. Im Gegensatz dazu Schlüssel ohne Präfix (
target_event_id
,
party_size
) stellen den einfachen Sitzungsstatus dar, der streng auf die aktive Sitzung beschränkt ist und gelöscht wird, wenn die Sitzung endet.
👉 Vergleichen Sie nun die beiden Tabs „Status“. Während user:prefs und gespeicherte Artefakte erhalten bleiben, ist temp:seatmap in beiden Sitzungen nicht vorhanden. Status-Schlüssel mit dem Präfix temp: sind ausschließlich temporär und werden nie in der Datenbank gespeichert.
Dann die Datei
👉💻 Im Editor öffnen:
cloudshell edit ~/monstertix/memory/userx.md
Drei Buchungen, die bis in den Januar zurückreichen. Der Agent liest sie, wenn er recall() aufruft. Sie ist auch dann noch vorhanden, wenn die Sitzungsdaten verloren gegangen sind.
Die vier Orte
Wo befindet sich die Funktion? | Wird die nächste Nachricht übermittelt? | Wird ein Neustart überstanden? | Bleibt die Einstellung erhalten, wenn die Sitzung gelöscht wird? |
| no | Nein | Nein |
| Ja | Ja | Nein |
| Ja | Ja | ja |
| Ja | Ja | Ja, und sie bleibt auch nach dem Löschen der Datenbank erhalten. |
ein Artefakt | Ja | Ja | abhängig vom Dienst |
In dieser Tabelle geht es um die Container, nicht um ihren Inhalt.memory/userx.md übersteht alles und enthält nur das, was remember()dort hineingetan hat. Ihre aktuelle Unterhaltung wird nicht in den Kontext aufgenommen. Dass eine Datei eine Sitzung überdauert, ist also nicht dasselbe wie wenn sich der Agent daran erinnert, worüber Sie gerade gesprochen haben. In Schritt 5 wird dieser Unterschied deutlich.

Was der Zustand ist
Stellen Sie sich den Zustand als Dictionary vor, das mit der Sitzung übertragen wird. Sie können die Notiz über tool_context.state in Ihren Tools lesen und schreiben lassen und das ADK speichert sie zusammen mit der Unterhaltung.
Das ADK weist den Präfixen die Bedeutung zu. Sie werden nirgends definiert. Das ADK liest den ersten Teil des Schlüsselnamens und entscheidet anhand dessen, wohin der Wert geht und wie lange er gültig ist:
Ein Schlüssel namens | ADK speichert sie | Dauer |
| mit der Sitzung | bis die Sitzung gelöscht wird |
| gegen den Nutzer | in jeder Sitzung, die der Nutzer hat |
| gegen die App | für alle Nutzer |
| nirgends | ein Aufruf |
Ein Aufruf ist ein einzelner Anfrage-Antwort-Zyklus, der alle Modellüberlegungen, die Ausführung von Tools und die endgültige Antwort umfasst.
Wenn ein Aufruf beendet wird, schreibt das ADK den neuen Status in die Datenbank und entfernt dabei alle temp:-Schlüssel.
Eine Sache ist noch ungeklärt
In two-days-ago haben Sie eine Zusammenfassung gesehen, die an der Stelle angezeigt wurde, an der sich früher die ersten Züge einer Person befanden. Das ADK hat die Zusammenfassung automatisch erstellt, als die Unterhaltung lang genug wurde.
Fakten haben eine begrenzte Gültigkeit
5. Kontextverlust und Verdichtung
Zusammenfassungen vergessen die Details
Wenn eine Unterhaltung zu lang wird, fasst das System ältere Nachrichten zusammen, um Speicherplatz und Tokens zu sparen.
Zusammenfassungen eignen sich gut, um den allgemeinen Tenor zu erfassen und Token zu sparen.

👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Zum Code dieses Schritts wechseln:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
Was hat sich seit dem letzten Schritt geändert?
Datei | Was hat sich geändert? |
| Der Agent ist in |
Alles andere | nicht geändert |
agent.py endet nicht mehr mit root_agent. Sie endet so:
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
),
)
adk web sucht zuerst nach einer Variablen namens app und greift dann auf root_agent zurück. Schließen Sie den Agenten in App ein, wenn Sie etwas benötigen, das sich auf die gesamte Anwendung und nicht auf einen einzelnen Agenten bezieht, z. B. die Komprimierung hier und die Wiederaufnahmefähigkeit in den nächsten Schritten.
compaction_interval=3 bedeutet, dass ADK nach jeweils drei Zügen zusammenfasst. Ein Turn ist, wenn Sie etwas sagen und der Agent seine Antwort beendet, unabhängig davon, wie viele Tools er auf dem Weg dorthin aufgerufen hat. Drei ist eine kleine Zahl, daher wird die Benachrichtigung während eines Workshops ausgelöst. Eine echte App würde eine viel größere Zahl verwenden.
overlap_size=1 bedeutet, dass jede Zusammenfassung eine Runde früher beginnt als unbedingt nötig.
Das ADK fasst die Züge 1, 2 und 3 zusammen. Drei Züge später fasst es den nächsten Batch zusammen. Mit overlap_size=1 beginnt die zweite Zusammenfassung wieder bei Zug 3 und nicht bei Zug 4:

Beachten Sie, dass Runde 3 zweimal zusammengefasst wird. Das ist Absicht. Wenn Sie in Runde 3 eine Frage gestellt haben und der Kundenservicemitarbeiter in Runde 4 geantwortet hat, wird bei einer sauberen Aufteilung die Frage in einer Zusammenfassung und die Antwort in der anderen platziert. Beide sind dann verwirrend. Das Wiederholen einer Äußerung ist eine kostengünstige Versicherung gegen das Abschneiden eines Gedankens in der Mitte.
Legen Sie beide Felder fest. Für beide gibt es im ADK keinen Standardwert.
Kompaktierung und Ihr Speicherdienst treffen sich nie
Das sollte explizit erwähnt werden, da die beiden Begriffe ähnlich klingen, die Systeme aber nicht.
EventsCompactionConfig befindet sich auf der App in agent.py, neben root_agent. Es ist eine Eigenschaft der Anwendung, nicht eines Dienstes:
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
Bei einer Zusammenfassung wird die Unterhaltung neu formuliert. Die Datei wird nicht neu geschrieben.
Das ist der einzige Grund, warum remember() als bewusster Aufruf existiert. Alles, was im Transkript verbleibt, wird schließlich als Paraphrase gelesen, die jemand anderes geschrieben hat. Alles, was in die Datei geschrieben wird, wird morgen und nächste Woche genau so gelesen, wie es gespeichert wurde. Am Ende dieses Schritts sehen Sie, wie sich der Unterschied auswirkt.
Wo wird die Zusammenfassung gespeichert?
In sessions.db wird in der Tabelle events ein Verdichtungsdatensatz in eine Ereigniszeile geschrieben. Er hat einen Zeitstempel wie alle anderen Turn-Ereignisse.
Ein normales Ereignis enthält das Feld content und die Zusammenfassung befindet sich unter actions.compaction.
👉💻 In Ihrer eigenen Datenbank ansehen:
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%';"
So sieht die Form des Ereignisses aus:
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
Der Ereignisstream bezieht jedes Ereignis aus seinem Inhalt. Die Zusammenfassung in der Datenbank wird bei jedem Zug an das Modell gesendet.
Der zweite Agent
budget_split ist ein separater Agent und kein Tool:
# 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)],
)
Warum ein KI-Agent und nicht eine einfache Funktion? Weil für die Arbeit ein Modell erforderlich ist. „Welche dieser Abschnitte passen zu einem Budget von 200 € für vier Personen und wie hoch ist die Gesamtsumme?“ ist ein Beispiel für Arithmetik, die in eine Entscheidung eingebettet ist. Die Antwort muss für Menschen lesbar sein. Lassen Sie eine Python-Funktion multiplizieren und bitten Sie sie nicht, zu entscheiden, welche Optionen erwähnenswert sind.
Warum sollte das nicht der Hauptagent übernehmen? In der nächsten Zeile include_contents="none" bedeutet das, dass dieser Agent keinen Teil der Unterhaltung erhält. Kein Gruppenchat, keine Vorlieben, keine 40 Tourdaten. Sie erhält die Anfrage und nichts weiter. Dieses Flag ist nur in einem Agent vorhanden. Das ist der eigentliche Grund, warum es sich um einen Agenten und nicht um eine Funktion handelt.

Bevorzugen Sie den kurzen Prompt: Er ist günstiger, schneller und es ist viel schwieriger, ihn zu entgleisen. Die Arithmetik ist immun gegen etwas, das vor 20 Zügen gesagt wurde, weil sie es nie sieht.
Warum?
AgentTool
? Umschließen Sie den Agenten damit, damit der Haupt-Agent ihn wie jedes andere Tool aufrufen kann. Aus Sicht des Modells ist budget_split identisch mit search_events.
Und
Mit output_key="budget_plan" wird die Antwort unter diesem Namen im Sitzungsstatus gespeichert, anstatt sie lose in der Unterhaltung zu lassen. Hier erfahren Sie, warum das wichtig ist.
Ausführen
👉💻 Drücken Sie in Terminal 1 Strg+C und starten Sie den Server mit denselben Flags neu:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 Neue Sitzung starten und sich unterhalten Fünf oder sechs Umdrehungen. Das funktioniert:
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?
👉 Öffnen Sie den Tab Status.
Dort finden Sie budget_plan mit der vollständigen Antwort des Arithmetik-Agents.

👉✨ Ändere nun die Zahlen:
what if 10 people come and I can only do $120 each?
👉 Sieh dir budget_plan noch einmal an. Sie wurde überschrieben, nicht angehängt. Das liegt daran, dass output_key die neueste Version enthält.

Was Sie nicht sehen können: die Zusammenfassung
Irgendwann hat ADK die vorherigen zusammengefasst.
👉💻 Führen Sie in einem neuen Terminal folgenden Befehl aus, um die Datei direkt zu lesen:
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
Antwort lesen
Hier ist ein echtes Beispiel in gekürzter Form:
── 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`
Die Preise, Abschnitte, Städte und Präferenzen wurden beibehalten und es wurde korrekt darauf hingewiesen, dass noch nichts entschieden wurde.
Daraus ergeben sich drei Dinge:
Es handelt sich um eine Paraphrase. Nichts davon hat irgendjemand gesagt. Es ist eine Darstellung dessen, was gesagt wurde, die durch denselben Prozess erstellt wird, der gelegentlich auch für alles andere verantwortlich ist, was das Modell falsch macht.
Hinweis: Zusammenfassungsstrings werden in internen Sitzungsereignisdatensätzen gespeichert und nicht direkt als separate UI-Tabs angezeigt.
Alle drei Züge in einer Unterhaltung, die teilweise aus früheren Zusammenfassungen besteht.
Versuchen Sie, zu buchen, ohne sich zu wiederholen.
In einer neuen Sitzung: Starten Sie keine neue Sitzung. Die Zusammenfassung, die Sie gerade gelesen haben, basiert auf den Ereignissen dieser Sitzung. Das ist der einzige Grund, warum der Agent noch etwas über Amsterdam weiß.
👉✨ Gib ihm nichts Neues:
Book us something.
Sehen Sie sich an, wie der Agent fragt, welche Show, welcher Abschnitt und wie viele Personen. Markieren Sie das als richtig: In der Zusammenfassung steht, dass der Nutzer nichts bestätigt hat, daher hat der Agent nachgefragt. Kompaktierung berücksichtigen
Auch wenn die Verdichtung erfolgreich ist, kann die Zusammenfassung vorheriger Züge zu Unklarheiten führen, die es erforderlich machen, dass der Agent die Einschränkungen mit dem Nutzer noch einmal bestätigt.
👉✨ Antworte und lasse die Buchung durchgehen:
Amsterdam Saturday, section C, 8 people
Die Bestellung wird noch einmal gelesen, bevor etwas ausgegeben wird:
„Du möchtest 8 Tickets für The Midnight Signal in Amsterdam am Samstag, 14. November, im Bereich C buchen. Die Gesamtkosten belaufen sich auf 760 $. Soll ich die Tickets kaufen?"
👉✨ Bestätigen:
yes
Jetzt wird angezeigt, dass es funktioniert hat:
„Ihr Kauf von 8 Tickets für The Midnight Signal in Amsterdam am Samstag, 14. November, im Block C zum Gesamtpreis von 760 $ ist abgeschlossen. Ihre Bestell-ID lautet ord_227877fe.“
Prüfen Sie, ob das der Fall ist.
👉 Wechseln Sie zum Veranstaltungsort-Bereich. Verlassen Sie sich nicht auf die Aussagen des Kundenservicemitarbeiters.

Auf dem Kanal | Was Sie sehen sollten |
Gekaufte Tickets |
|
die darunterliegende Bestellung |
|
Die Tour, Amsterdam, Samstag | Abschnitt C ist um 8 Zeilen nach unten verschoben |
Aktivität |
|
Im Bereich finden Sie Informationen zum Ticketverkauf.
Warum user:prefs die richtige Wahl war
Erinnern Sie sich an note_companion, in dem Sams Einschränkung in user:prefs geschrieben wurde, und an den offensichtlichen Einwand: Warum sollte man sich mit einem Tool beschäftigen, wenn der Agent sich einfach daran erinnern könnte?
Das hat folgende Gründe: Der Status wird nicht in der Unterhaltung berücksichtigt und auch nicht in der Zusammenfassung. Prüfen Sie user:prefs und budget_plan und stellen Sie fest, dass sie unverändert sind. Sehen Sie sich dann das Transkript an.
Bei einer Zusammenfassung bleibt die allgemeine Form erhalten, die Details gehen jedoch verloren. Speichern Sie alles, was Sie nicht verlieren möchten, an einem anderen Ort als in der Unterhaltung.
6. Fortsetzung der Verarbeitung
40 Minuten warten, ohne online zu bleiben
Wenn ein Kundenservicemitarbeiter in einer 40-minütigen Ticketwarteschlange warten muss, ist es schwierig und teuer, einen Python-Prozess am Laufen zu halten.
So gehen Sie mit langen Wartezeiten richtig um:
- Status in einer Datenbank speichern: Speichern Sie, wo der Agent pausiert wurde, damit der Prozess sicher heruntergefahren werden kann.
- Auf Abruf reaktivieren: Wenn die Wartezeit abgelaufen ist, wird der Agent durch ein externes Signal neu gestartet und die Ausführung wird an der Stelle fortgesetzt, an der sie unterbrochen wurde.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Zum Code dieses Schritts wechseln:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
Ausführen
Was hat sich seit dem letzten Schritt geändert?
Datei | Was hat sich geändert? |
| hat |
|
|
join_queue gibt sofort ein Ticket zurück, anstatt zu blockieren, und ResumabilityConfig lässt den Lauf pausieren und später fortsetzen.

👉💻 Terminal 1 – der unveränderte Agent:
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"
👉✨ KI-Agenten bitten, etwas zu kaufen:
Get us two tickets to the Amsterdam show.
Es ruft recall, dann search_events und dann join_queue auf und antwortet mit etwas in dieser Art:
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
Beachten Sie, dass Samstag ausgewählt wurde, ohne dass Sie gefragt wurden. Es gibt zwei Shows in Amsterdam. Die am 17. ist an einem Dienstag und in der Erinnerungsdatei aus dem letzten Schritt steht, dass Sam nicht an Wochentagen auftritt. Das ist eine Tatsache aus einer früheren Buchung, die eine Frage in dieser Buchung beantwortet.
Außerdem hat es sich bevor nach einem einzelnen Sitzplatz oder Preis gesucht wurde in die Warteschlange eingereiht und nichts dazu gesagt. Das ist die Regel des Veranstaltungsorts. Du kaufst die Tickets, bis du an der Reihe bist. Das Einzige, was sich lohnt, ist, früh da zu sein, um in der Schlange einen guten Platz zu bekommen. Welche Plätze wir nehmen, ist eine Frage für in 40 Minuten.
👉 Sehen Sie sich das Steuerfeld an. Das Banner lautet:

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
Die Warteschlange ist wirklich 40 Minuten lang und wird während der Arbeit nicht kürzer.
👉💻 Agent beenden:
Ctrl-C
👉💻 Starten Sie es noch einmal in Terminal 1:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉✨ Fragen Sie den KI-Agenten in derselben Sitzung, wo er sich in der Zeile befindet:
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Vergleiche das Ticket und beobachte, wie die Position immer weiter sinkt, obwohl nichts ausgeführt wurde. Der Agent wusste nicht, dass er beendet wurde. Es wurde check_queue mit dem Ticket aufgerufen, das vor dem Neustart abgerufen wurde, da dieses Ticket auf der Festplatte gespeichert war.
👉💻 Gründe:
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
Jeder Zug jeder Unterhaltung ist eine Zeile. Der Zug selbst, wer ihn gesagt hat, welches Tool aufgerufen wurde, was zurückgegeben wurde und der Hinweis, dass es sich um JSON innerhalb von event_data handelt.
👉💻 So finden Sie Ihr Warteschlangenticket:
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
Wenn join_queue zurückgegeben wird, wird das Ticket direkt in der Datenbank gespeichert. Beim Neustart lädt ADK die vorherigen Züge und Tool-Ergebnisse von der Festplatte, sodass der Agent dort weitermachen kann, wo er aufgehört hat.
Achten Sie darauf, was nicht neu begonnen wurde. Der Agent wird direkt fortgesetzt, anstatt den Workflow von Anfang an neu zu starten. Da LongRunningFunctionTool und ResumabilityConfig den Ausführungsstatus auf der Festplatte speichern, weiß ADK, dass der Agent bereits in der Warteschlange ist. So wird verhindert, dass er noch einmal nach Tourdaten sucht oder ein Duplikatticket anfordert.

Warum join_queue und nicht check_queue?
Beide Funktionen geben Millisekunden zurück. Der Unterschied besteht darin, ob der zugrunde liegende Job abgeschlossen ist, wenn die Funktion zurückgegeben wird.
Was wird zurückgegeben? | Ist die Arbeit erledigt? | |
| Ticket in der Warteschlange und erste Position | Nein. Die Verarbeitung wird asynchron im externen System fortgesetzt. |
| Aktueller Status der Warteschlange | Ja. Gibt eine sofortige Antwort zu einem bestimmten Zeitpunkt zurück. |
LongRunningFunctionTool ist für Vorgänge konzipiert, die ein Handle für ausstehende Aufgaben zurückgeben. Durch das Umschließen von join_queue kann der KI-Agent die Ausführung unterbrechen, während die externe Warteschlange voranschreitet.
check_queue ist dagegen synchron und vollständig. Wenn der Agent den Vorgang umschließt, würde er die Ausführung jedes Mal fälschlicherweise unterbrechen, wenn er seinen Status prüft – auch wenn er an der Reihe ist und den Kauf sofort abschließen muss.

Was hat funktioniert?
Drei Komponenten ermöglichen das Aussetzen und Wiederaufnehmen der Ausführung:
# 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: Markiert ein Tool als nicht blockierend und gibt ein aktives Handle zurück, damit der Agent warten kann, ohne den Thread aufzuhängen.ResumabilityConfig: Weist die Laufzeit an, den Ausführungslebenszyklus bei einer unvollständigen Aufgabe zu pausieren und vom letzten aufgezeichneten Status aus fortzusetzen.Session Storage URI (--session_service_uri): Der Status wird in einem externen Speicher (z.B. SQLite, Cloud SQL) und nicht im Prozessspeicher gespeichert, um Datenverlust bei Neustarts zu verhindern.
7. Mit veralteten Daten arbeiten
Informationen sind veraltet, während Sie warten
Der gespeicherte Unterhaltungsverlauf ist kein Echtzeitstatus. Bei langen Wartezeiten können sich externe Daten wie Ticketverfügbarkeit oder Preise ändern, ohne dass der Kundenservicemitarbeiter davon weiß.
Ein Agent mit fehlenden Informationen pausiert, um um Klärung zu bitten. Ein Agent mit veralteten Daten reagiert jedoch sofort auf der Grundlage veralteter Annahmen. Um ungültige Aktionen zu verhindern, müssen zeitkritische Daten unmittelbar vor der Ausführung wichtiger Vorgänge noch einmal überprüft werden.
Laden Sie noch keinen neuen Code. Bleiben Sie beim Agenten des vorherigen Schritts (er kann Tickets kaufen, hat aber derzeit keine Sicherheitsvorkehrungen für veraltete Daten oder Idempotenz).
👉🔴 Drücke auf ORT ZURÜCKSETZEN und starte dann eine neue Sitzung in adk web – mit der Schaltfläche + Neue Sitzung über dem Chat.
Starten Sie eine neue Sitzung, um das vorherige Warteschlangenticket und den Status „Auftrag wird bearbeitet“ zu löschen. Für diesen Schritt ist ein sauberer Ausführungskontext erforderlich, um die Verarbeitung von veralteten Daten richtig zu demonstrieren.
Der Snapshot liegt
👉✨ Nach derselben Sendung wie zuvor fragen:
Get us two tickets to the Amsterdam show.
👉✨ Während das Tool in der Warteschlange ist, kannst du nach Sitzplätzen fragen:
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.
👉🔴 Klicke auf DIE BESTEN PLÄTZE VERKAUFEN. Abschnitt A fällt auf null.
👉🔴 Klicke auf SKIP THE WAIT (WARTEN ÜBERSPRINGEN), um den Kundenservicemitarbeiter an den Anfang der Warteschlange zu stellen.

👉✨ Sagen Sie, dass Sie etwas kaufen möchten:
Buy the two section A seats.
purchase(section="A") wird ausgeführt, ohne die Verfügbarkeit von Sitzplätzen noch einmal zu prüfen. Die Venue API gibt daher eine Fehlermeldung zurück:
{"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.

Der Agent hat den Sitzplan erst nach Erhalt einer API-Fehlerantwort noch einmal angefragt. Die Ursache ist ein veralteter Kontext: Zu Beginn der Sitzung hat der Agent „Abschnitt A: 400 verfügbar“ aufgezeichnet und dieser Zug blieb im Konversationsverlauf, der an das Modell übergeben wurde.
Machen Sie deutlich, wo die alten Informationen gespeichert sind: Sie bleiben im Unterhaltungsverlauf erhalten, der durch Statuspräfixe wie temp: nicht überschrieben werden kann.

Die Lösung
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Laden Sie jetzt den Code für diesen Schritt:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👉💻 Drücken Sie in Terminal 1 Strg+C und starten Sie den Vorgang neu:
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"
Änderungen:
Datei | Was hat sich geändert? |
| etwas Neues. Wird unmittelbar vor einem Kauf ausgeführt und kann ihn blockieren |
|
|
|
|
fence.py ist ein Callback, der vom ADK unmittelbar vor einem Tool-Aufruf ausgeführt wird:
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
Das will ich sehen
👉🔴 VENUE ZURÜCKSETZEN Stelle dich in der Warteschlange an, drücke während des Wartens auf DIE GUTEN PLÄTZE VERKAUFEN und sage dann, dass du Bereich A kaufen möchtest.
Dieses Mal stoppt es von selbst. Der Agent sagt dann zum Beispiel:
„Es sieht so aus, als wäre Abschnitt A gerade ausverkauft. Ich muss mir den Sitzplan noch einmal ansehen.“
Sehen Sie dann zu, wie get_seatmap noch einmal aufgerufen wird, ohne dass Sie dazu aufgefordert werden, und wie stattdessen Abschnitt C angeboten wird.
👉🔴 Lies den Aktivitätsfeed des Veranstaltungsorts sorgfältig, da er beide Hälften anzeigt:
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

Im Dashboard wird ein Anruf angezeigt, aber der Zähler für Tickets bleibt bei null. Das liegt daran, dass fence.py als Schutz vor der Ausführung des Tools dient:
- Der Agent versucht, das Kauf-Tool aufzurufen.
fence.pyfängt die Anfrage zuerst ab und prüft die Verfügbarkeit von Live-Plätzen.- Da die Plätze ausverkauft sind, gibt
fence.pydirekt einen Fehler zurück und bricht den Kauf ab, bevor die API aufgerufen wird.
Durch Prompt-Anweisungen kann nicht garantiert werden, dass ein LLM nicht versucht, ausverkaufte Plätze zu kaufen. Code-Guards (before_tool_callback) sorgen dafür, dass der betrügerische Kauf nie in der realen Welt stattfindet.
👉🔴 Klicke auf ORT ZURÜCKSETZEN und dann auf NÄCHSTEN KAUF ABBRECHEN.
👉✨ Bitten Sie den Assistenten, den Artikel zu kaufen, und lassen Sie ihn es so oft wie nötig versuchen.

Die Anzahl der Bestellungen bleibt bei 1, da der Veranstaltungsort bei der Wiederholung denselben Idempotenzschlüssel erhalten und die ursprüngliche Bestätigung zurückgegeben hat, anstatt eine doppelte Bestellung zu erstellen.
8. Autonomer Workflow
LLMs können starre Regeln nicht immer einhalten
LLMs eignen sich hervorragend für Entscheidungen, z. B. für die Auswahl einer Serie oder Sendung basierend auf den Nutzerpräferenzen. Wenn Sie sich jedoch darauf verlassen, dass ein LLM strikte Schritt-für-Schritt-Regeln befolgt (z. B. „immer der Warteschlange beitreten, bevor Sie die Sitzplätze prüfen“), werden gelegentlich Schritte übersprungen oder die Reihenfolge durcheinandergebracht.
Um das Problem zu beheben, teilen Sie den Workflow in zwei Arten von Schritten auf:
- Feste Regeln (Code-Funktionen): Schritte, die jedes Mal auf genau dieselbe Weise ausgeführt werden müssen (z. B. ein Warteschlangenticket ziehen, Position prüfen), werden als normale Codeknoten geschrieben.
- Entscheidungen (Agent-Knoten): Schritte, die eine Entscheidung erfordern (z. B. Auswahl von Sitzplatzbereichen, Überprüfung von Nutzerpräferenzen), werden dem KI-Agenten überlassen.
Sehen Sie sich an, was tatsächlich in diesem Ablauf enthalten ist. Einige davon enthalten überhaupt keine Wertung:
Schritt | Urteil? |
genau ein Warteschlangenticket ziehen | Nein. Immer nur eine und immer vor dem Kauf |
Prüfen, ob Sie an der Reihe sind | Nein. Frage stellen, Antwort vorlesen |
Briefing für den Käufer erstellen | Nein. Immer derselbe Satz, immer dieselbe Form. |
Welche Show, welcher Abschnitt | Ja. Preis, Tag, wer kommt, was sie hassen |
Nutzerpräferenz und Einschränkung | Ja. |
Für strikte Verfahren sollte kein LLM verwendet werden, da probabilistische Modelle ohne Überwachung irgendwann starre Sequenzen nicht mehr einhalten.
Das Workflow des ADK trennt die deterministische Ausführung von der KI-Schlussfolgerung:
- Funktionsknoten erzwingen jedes Mal konsistent feste Regeln, z. B. das Eintreten in eine Warteschlange oder das Prüfen der Position.
- Agent-Knoten verarbeiten kontextbezogene Entscheidungen, z. B. die Auswahl von Sitzplätzen basierend auf Nutzerpräferenzen.
- Wait-Knoten unterbrechen die Ausführung, ohne einen Prozess im Arbeitsspeicher aktiv zu halten.
Der ursprüngliche Agent wird nicht ersetzt. Er wird zum letzten Knoten im Workflow-Diagramm und behält seine Tools, Aktualitätsprüfungen und Idempotenzschutzmaßnahmen bei. Das Diagramm steuert die Ausführungsreihenfolge, während der KI-Agent die Kaufentscheidung trifft.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN.
👉💻 Zum Code dieses Schritts wechseln:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
Ausführen
👉💻 Drücken Sie in Terminal 1 Strg+C und starten Sie neu. Der Befehl ist wie immer:
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"
Was hat sich seit dem letzten Schritt geändert?
Datei | Was hat sich geändert? |
| etwas Neues. Die unbeaufsichtigte Ausführung als ADK 2- |
|
|
| Außerdem hat er |
Diagramm ausführen – drei Nachrichten in der richtigen Reihenfolge
Führen Sie alle folgenden Aktionen in der Nightly-Python-Version aus (dort befindet sich das Diagramm). Senden Sie in jedem Zug dieselbe Eingabe, um zu sehen, wie der Workflow-Graph die verschiedenen Status durchläuft.
1. Achten Sie darauf, mit wem Sie gerade sprechen.
👉 Laden Sie die ADK-Web-UI neu.
# 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
adk web wird für das Objekt ausgeführt, das root_agent zugewiesen ist. Hier verweist root_agent auf das nightly-Workflowdiagramm, wobei buyer_agent als letzter Knoten im Diagramm konfiguriert ist.
2. Starten
👉✨ Geben Sie Folgendes ein und drücken Sie die Eingabetaste:
go
Hinweis: In einem automatisierten Workflowdiagramm beginnt die erste Ausführung, ohne auf Nutzereingaben zu warten. Wenn Sie eine Nachricht in adk web senden, wird die erste Ausführung ausgelöst.
👉 Sehen Sie sich nun den Webbrowser an. Im linken Bereich wird das Diagramm gezeichnet und die Knoten werden während der Ausführung eingefärbt. Im rechten Bereich sehen Sie den Ereignisstream und können nachvollziehen, wie die Entscheidungen getroffen werden:

queue_up schreibt queue_ticket und queue_event_id in den Sitzungsstatus.

Sie sollten einen Funktionsaufruf mit dem Namen adk_request_input sehen, der auf eine Antwort wartet. Dann wird es still und der Lauf wird beendet. Die Grafik hat check_front erreicht, Sie wurden auf Platz 14.203 gefunden und die Suche wurde beendet.
👉 Prüfe das Bereich „Veranstaltungsort“: Es sollte nur ein Warteschlangenticket vorhanden sein und nichts ausgeführt werden.

3. Aktivieren Sie es – und achten Sie darauf, in welches Feld Sie schreiben
Scrollen Sie im Ereignisstream zum Ereignis adk_request_input und suchen Sie das Eingabefeld, das mit diesem Ereignis verknüpft ist:

Es gibt jetzt zwei Stellen, an denen Sie Text eingeben können, und sie haben unterschiedliche Funktionen.
Wo Sie Text eingeben | Was passiert dann? |
das Feld | auf die Unterbrechung reagiert. Der geparkte Lauf wird ab |
das Hauptchatfeld unten | startet einen neuen Aufruf. Die Grafik wird ab |
👉✨ Geben Sie etwas in das Feld Enter your response... ein und drücken Sie den Pfeil.
👉 Terminal 1:
[check_front] #14,197 — not yet. Pausing.
Eine Zeile: Es wurde keine zweite Show ausgewählt und es wurde kein zweites Warteschlangenticket erstellt, wodurch du am Ende einer Warteschlange mit 14.203 Personen gestanden hättest.
Wenn [open] und [pick_show] noch einmal ausgeführt werden, haben Sie etwas in das Chatfeld eingegeben. Drücken Sie auf ORT ZURÜCKSETZEN und beginnen Sie den Schritt noch einmal.
👉 Sehen Sie sich das Venue-Panel noch einmal an: Es ist immer noch ein Ticket und die Position ist etwas gesunken, weil die Uhr des Veranstaltungsorts weitergelaufen ist, während Ihre Uhr stehen geblieben ist.

4. Bis zum Anfang gehen und dann fertigstellen
👉🔴 Drücke im Bereich für den Veranstaltungsort auf WARTEN ÜBERSPRINGEN.

👉✨ Antworte ein letztes Mal auf das neueste adk_request_input-Ereignis – wieder in seinem Feld, nicht im Chatfeld. 
Dieses Mal ist check_front vorne und fällt durch. Sehen Sie zu, wie die beiden letzten Knoten im linken Bereich aufleuchten brief und concert.

👉 Browser ansehen Hier wird der Lauf beendet. Es gibt vier Dinge, die es sich anzusehen lohnt.

Das Diagramm ist vollständig grün und die Kanten werden gezählt. brief und concert sind jetzt beleuchtet und die Kanten zu pick_show, queue_up und check_front haben jetzt ein Symbol.
2x
ist die Anzahl der Durchläufe des Graphen entlang dieser Kante, einmal pro Aktivierung. Die beiden oben genannten sind der Preis für das Fortsetzen: Der Scheduler tritt jedes Mal von Anfang an wieder in das Diagramm ein. Das bedeutet nicht, dass die Arbeit dreimal ausgeführt wurde. Das Veranstaltungsort-Panel ist der Beweis: ein Warteschlangenticket, nicht zwei. @node(rerun_on_resume=False) ist der Grund dafür.
Die Grafik

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
Beachten Sie, dass zwei Knoten Agents und zwei Funktionen sind. Die Frage, welcher Knoten was ist, ist die gesamte Designfrage.
Knoten | Warum diese Form? |
| agent. Die Auswahl einer Serie aus dem Verlauf einer Person ist eine Entscheidung. Es ruft |
| Funktion Das Hinzufügen zu einer Warteschlange ist eine Regel. Eine Regel sollte nicht um 3 Uhr morgens ausgewürfelt werden. |
| Funktion „Bin ich vorne?“ hat eine richtige Antwort und der Veranstaltungsort hat sie |
| agent. Es ist |
Sehen Sie sich den letzten Knoten an und suchen Sie nach dem Agent, den Sie erstellt haben. Keine Kopie, keine Umformulierung:
# agent/concert/nightly.py
from .agent import buyer_agent
Alles ist dabei: jedes Tool, die erneute Lektüre des Sitzplans in fence.py und der Idempotenzschlüssel in purchase. Es gibt nur ein Exemplar.
Warum wurde die Wiedergabe beendet, anstatt zu warten?
Hier ist die Version, die niemand schreiben sollte:
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
Es hält einen Prozess 40 Minuten lang am Laufen, ohne etwas zu erreichen, wird mit dem Terminal beendet und kann nicht fortgesetzt werden. check_front macht Folgendes:
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput ist eine Unterbrechung. Der Graph wird angehalten, die Anfrage wird an den Sitzungsspeicher gesendet und der Prozess kann beendet werden.
Der Lauf kostet nichts, während er wartet, und er endet an genau einer Stelle: gekaufte Tickets. Es kann aber auch die ganze Nacht und mehrere Wachphasen dauern, bis du dort ankommst.
9. Mit einem Mitarbeiter sprechen
Geben Sie kein Geld aus, ohne sich vorher mit einer Person abzustimmen.
Ein autonomer Agent, der über Nacht ausgeführt wird, sollte keine teuren finanziellen Entscheidungen auf der Grundlage von Vermutungen oder mehrdeutigen Präferenzen aus dem Chatverlauf treffen.
Bevor der Agent risikoreiche Aktionen ausführt (z. B. Geld für Tickets ausgibt), muss er pausieren, den Nutzer bitten, sein Budget laut zu bestätigen, und diese ausdrückliche Bestätigung speichern, bevor er Käufe tätigt.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Zum Code dieses Schritts wechseln:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
Ausführen
👉💻 Drücken Sie in Terminal 1 Strg+C und starten Sie neu. Gleicher Befehl wie zuvor:
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"
Was hat sich seit dem letzten Schritt geändert?
Datei | Was hat sich geändert? |
| Enthält das, was die Person vereinbart hat, als den Satz, den sie gesagt hat. |
|
|
| ein neuer erster Knoten, |
Nicht modellieren. Speichern Sie den Satz.
Öffnen Sie agent/concert/budget.py und lesen Sie den gesamten Text:
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"
Im Knoten wird geprüft, ob die Person das gesagt hat und ob sie es bestätigt hat.
Anschließend wird der Satz im Sitzungsstatus und nicht unter user: gespeichert. Für eine Buchung wird ein Budget vereinbart. „250 $, wenn sie gut sind“ wurde an diesem Abend über diese Band gesagt und hat nichts mit der Ticketbuchung zu tun.
Das gilt auch für alles andere, was sie gesagt haben.
Nicht beim Budget aufhören. Nutzer ändern ihre Meinung während der Entscheidungsfindung. Erfassen Sie daher alles, wonach sie fragen, anstatt sich darauf zu verlassen, was der Kundenservicemitarbeiter sich merkt.
"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)
Bei Nur von Nutzer erstellte Ereignisse werden nur
text
Teile aus dem function_response, damit die Frage und Antwort zum Budget hier nicht wiederholt werden. Keine Duplikate:
Hinweis: agree_budget ruft die Funktion bei jedem Lauf auf, nicht nur beim ersten. Eine Korrektur, die eingegeben wird, während der Agent in der Warteschlange steht, wird also auch berücksichtigt. open_the_night Gib dem Picker dann beide Sätze, die Anfrage und das Budget, und lass ihn sie vorlesen.
Das Diagramm, ein Knoten länger
Beginnen wir mit einer Unterhaltung:
# 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)],
)

Zwei Knoten halten an und warten. Es handelt sich um denselben Mechanismus. check_front wartet auf einen Ort und wird von einer Uhr beantwortet. agree_budget wartet auf eine Person und kann nur von einer Person beantwortet werden. Diese Differenz ist der gesamte Schritt 10.
Schrittweise ausführen
1. Starten
👉 Laden Sie die ADK-Web-UI neu und wählen Sie den concert-Agenten aus.
👉✨ Sagen Sie, was Sie wirklich möchten:
Get us two tickets to the Amsterdam show.

In Schritt 8 gab es keinen Nutzer im Diagramm. Daher wurde alles, was Sie eingegeben haben, verworfen und go hat genauso gut abgeschnitten wie alles andere. Hier ruft agree_budget in der ersten Zeile what_they_asked_for(ctx) auf, wodurch alles, was Sie eingegeben haben, erfasst und in state["request"] gespeichert wird. open_the_night übergibt es dann zusammen mit dem Budget an den Kommissionierer.
Du hast keinen Tag angegeben und in Amsterdam gibt es zwei Shows. Achte also darauf, dass die Auswahl auf recall() zurückgesetzt wird, suche nach „Sam bails on weeknights“ und wähle den Samstag aus.
Im Browser wird der Lauf bei einem adk_request_input-Ereignis beendet:
Ein Diagramm hat Ihnen gerade eine Frage gestellt. Die Unterbrechung ist dieselbe wie beim Warten in der Warteschlange in Schritt 8, aber jetzt wird eine Person genannt, die die Frage beantworten soll.
2. Antworten Sie so, wie Menschen tatsächlich antworten.
👉✨ Geben Sie im Feld Enter your response... (nicht im Chatfeld unten) Folgendes ein:
About a 100 for the cheap seats. 250 if they're the good ones.
So können Sie sehen, dass die Wiedergabe beendet wird:
[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. Bestätigen
👉✨ Gleicher Karton:
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.
Vier Knoten wurden mit einem Wort ausgeführt. 👉 Öffnen Sie den Tab State (Status) und suchen Sie nach budget – Ihrem Satz im Wortlaut.

Kein Präfix. Das ist der Sitzungsstatus. Wenn Sie eine neue Sitzung starten und das Budget aufgebraucht ist, werden Sie noch einmal gefragt.
4. An die Spitze kommen und das Rennen beenden
👉🔴 Drücke im Bereich für den Veranstaltungsort auf WARTEN ÜBERSPRINGEN.
👉✨ Beantworte die neueste adk_request_input – wieder im entsprechenden Feld:
go

👉 Prüfen Sie den Bereich „Veranstaltungsort“: eine Bestellung zu einem Preis, der dem vereinbarten Preis für diese Art von Sitzplatz entspricht.

Wie ein Diagramm einer Person eine Frage stellt
Öffnen Sie nightly.py und lesen Sie agree_budget. Wenn drei Dinge gleichzeitig zutreffen und bei jedem einzelnen das gleiche Problem auftritt, nämlich dass die Anfrage nicht beantwortet wird,
@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?",
)
...
| Andernfalls wird der Knoten als erledigt behandelt und übersprungen. | |
eine stabile | Standardmäßig wird für jeden Lauf eine neue UUID generiert. Es gibt also nichts, wonach gesucht werden kann. | |
| wo die Antwort landet. Nicht der Rückgabewert von | |
Grafik
Es ist das Diagramm aus Schritt 8 mit einem zusätzlichen Knoten am Anfang. 
Vom Nutzer bestätigte Einschränkungen gelten nur auf Sitzungsebene und laufen ab, wenn die Unterhaltung beendet wird. Wenn temporäre Limits sitzungsübergreifend beibehalten werden, besteht das Risiko, dass einmalige Entscheidungen zu unbeabsichtigten permanenten Standardeinstellungen werden.
10. Der Cloud-Stack
Laptops: Ruhezustand und lokale Dateien verschwinden
Auf Ihrem Laptop ist ein Agent davon abhängig, dass Ihr Computer aktiv bleibt, lokale SQLite-Datenbanken und lokale Dateien vorhanden sind. Wenn Sie den Laptopdeckel schließen oder in einem Cloud-Container bereitstellen, der neu gestartet wird, gehen lokale Dateien und der Arbeitsspeicher verloren.
Damit sie zuverlässig in der Produktion ausgeführt werden können:
- Cloud Compute: Führen Sie den Agent in einem Container (z. B. Cloud Run) aus, der online bleibt.
- Durable Cloud Databases: Ersetzen Sie lokale Dateien und SQLite durch Cloud-Dienste (Cloud SQL für Sitzungsdaten, Cloud Storage für Dateien).
- Cloud-Timer: Verwenden Sie verwaltete Scheduler wie Cloud Scheduler und Pub/Sub, damit der Agent auch dann aktiviert wird, wenn Ihr Laptop ausgeschaltet ist.
👉🔴 Drücken Sie im Bereich „Veranstaltungsort“ auf VERANSTALTUNGSORT ZURÜCKSETZEN. Jeder Schritt beginnt in derselben Welt: 8 Shows, alle Plätze verfügbar, Uhr auf 1×.
👉💻 Zum endgültigen Code wechseln:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
Datei | Was hat sich geändert? |
| etwas Neues. Ein FastAPI, das die Chatseite und den Endpunkt bereitstellt, den ein Scheduler aufruft |
| etwas Neues. Gewöhnliches python-slim und uvicorn. Nichts davon ist ADK-spezifisch. |
| kopiert, sodass die Seite im selben Container wie der Agent ausgeliefert wird. |
| unverändert. Byte für Byte der Ordner, mit dem Sie Schritt 9 abgeschlossen haben |
Die Seite, die Sie bereits erstellt haben
Die** ADK-Web-UI** ist ausschließlich eine lokale Debugging-Schnittstelle zum Prüfen von Status, Ereignisstreams und Workflow-Diagrammen während der Entwicklung. Es ist nicht für die Bereitstellung in der Produktion vorgesehen.
Verwenden Sie für die Produktion das eigenständige Chat-Frontend, das in Schritt 3 erstellt wurde. Über den /wake-Endpunkt werden Nutzernachrichten und Interaktionsunterbrechungen verarbeitet, ohne dass interne Debugging-Steuerelemente verfügbar sind.
Lokalen Status zu Clouddiensten migrieren
Halten Sie den Server an und sehen Sie sich an, was er geschrieben hat:
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
Lokale Dateien eignen sich zwar gut für die Entwicklung, aber nicht für die Produktion, da Cloud Run-Container flüchtigen, instanzlokalen Speicher verwenden.
Bei der Ausführung in Cloud Run gilt Folgendes:
- Instanzen werden auf null skaliert:Wenn der Traffic ausbleibt, wird der Container beendet und sein lokales Dateisystem wird gelöscht.
- Instanzen werden unabhängig horizontal skaliert:Mehrere gleichzeitige Instanzen teilen sich keinen Speicherplatz. Jede wird mit einem eigenen isolierten Dateisystem ausgeführt.
Wenn ein Kundenservicemitarbeiter ein Warteschlangenticket oder einen Sitzungsstatus in einer lokalen SQLite-Datei (sessions.db) speichert, wird bei nachfolgenden Anfragen, die an eine neue oder andere Containerinstanz weitergeleitet werden, eine leere Datenbank gefunden. Damit der Status beim Skalieren und Neustarten erhalten bleibt, müssen lokale Dateien durch verwaltete externe Dienste ersetzt werden.
Stattdessen benötigen Sie einen Status, der länger als ein einzelner Container bestehen bleibt. Drei Dienste, einer für jede Datei:
Auf dem Laptop | In der Cloud | Beschreibung |
| Cloud SQL | einer verwalteten Postgres-Datenbank. Sie ist unabhängig davon vorhanden, ob etwas ausgeführt wird. |
| Cloud Storage | einem Bucket. Dateien als Objekte, die von jeder Instanz aus gleichzeitig erreichbar sind |
| Cloud Storage | im selben Bucket |
| Cloud Scheduler | ein Cron-Job, der von Google ausgeführt wird. Ihr Terminal ist kein Wecker |
Erstellen
Stellen Sie eine PostgreSQL-Datenbank, einen Datenbanknutzer und einen Cloud Storage-Bucket bereit. Das Erstellen der Cloud SQL-Instanz dauert einige Minuten und wurde im Hintergrund während setup.sh initiiert.
👉💻 Sammle es und mache den Rest:
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
Durch das Skript wurden vier Elemente erstellt, eines pro Zeile der Tabelle oben, und zwei URIs wurden in .env geschrieben:
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
Agent mit verwalteten geplanten Triggern bereitstellen
Wenn die Dienste bereit sind, geben Sie dem Agenten sein permanentes Zuhause.
👉💻 Drei bis vier Minuten. So gehts:
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
Das Script erstellt ein Container-Image aus agent/, stellt es in Cloud Run bereit und stellt die unterstützende ereignisgesteuerte Infrastruktur bereit.
Die vollständige Bereitstellung besteht aus fünf verschiedenen Komponenten:
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 als Triggerquelle
Cloud Scheduler ersetzt lokale Ausführungstimer (clock.py) durch einen verwalteten Cron-Dienst, der unabhängig von lokalen Terminalsitzungen bestehen bleibt, automatische Wiederholungen verarbeitet und die Planung nach Zeitzone unterstützt.
Pub/Sub als Message-Entkopplungsebene
Wenn Sie ein Pub/Sub-Thema zwischen Cloud Scheduler und Cloud Run platzieren, ergeben sich drei architektonische Vorteile:
- Nachrichtensicherheit: Eingehende Triggerereignisse werden in die Warteschlange gestellt, wenn der Cloud Run-Dienst kalt gestartet, neu bereitgestellt oder vorübergehend nicht verfügbar ist.
- Integrierte Wiederholungsrichtlinien: Vorübergehende Übermittlungsfehler werden automatisch wiederholt, ohne dass eine benutzerdefinierte Wiederholungslogik auf Anwendungsebene erforderlich ist.
- Fan-Out-Skalierbarkeit: Ermöglicht es mehreren Downstream-Diensten, dieselben Zeitplanereignisse zu abonnieren, ohne dass der Scheduler neu konfiguriert werden muss.

Durch die Konfiguration von trigger_sources=["pubsub"] wird ein Push-Endpunkt registriert, der mit Standard-Pub/Sub-Envelope-Nutzlasten kompatibel ist. Eingehende Nachrichten werden direkt in Agent-Aufrufereignisse übersetzt.
→ 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 * * *
Mit ihr sprechen
👉 Öffnen Sie die URL des Agents in einem Browser. 👉✨ Bitten Sie Gemini, eine Buchung vorzunehmen:
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
Sie sollten gefragt werden, wie viel Sie ausgeben möchten.
👉✨ Antworte so, wie du es laut tun würdest, und bestätige dann:
up to 250 a seat for the good ones
Der Lauf wird derzeit in der Warteschlange gespeichert.
👉🔴 Drücke im Bereich für den Veranstaltungsort auf WARTEN ÜBERSPRINGEN, damit die Warteschlange bereit ist, wenn die Nachricht eingeht.
Damit der Job nach einem echten Zeitplan ausgeführt wird, muss er aktiviert sein. jobs run wird für einen pausierten Job mit Job.state must be ENABLED abgelehnt:
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 Innerhalb weniger Sekunden führt der Agent den Kauf innerhalb der bestätigten Budgetgrenzen aus und gibt automatisch eine Bestellbestätigung zurück.
👉🔴 Prüfen Sie den Bereich „Veranstaltungsort“. Eine Bestellung.
So können Sie festlegen, dass die Funktion alle 5 Minuten ausgeführt wird:
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
Bereinigen
Es werden zwei Dinge bereitgestellt: der Veranstaltungsort und der Agent daneben. Beide werden auf null skaliert, sodass für eine im Leerlauf befindliche Instanz keine Kosten anfallen. Löschen Sie sie trotzdem.
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
Cloud SQL und der Bucket werden bewusst nicht berücksichtigt, da sie Ihre Daten enthalten und beide abgerechnet werden. Wenn Sie sicher sind, dass Sie fertig sind:
./destroy-agent.sh --all
Alles andere ist lokal. Löschen Sie den Ordner, wenn Sie fertig sind, oder behalten Sie ihn bei und führen Sie ihn so lange wie gewünscht für Ihr eigenes Projekt aus.
Der Agent, seine Tools, seine Rückrufe und das Budget haben sich nicht geändert. Zwei URIs und eine Umgebungsvariable.
11. Systemübersicht und Anleitung zur Fehlerbehebung
Umgebungsunterschiede: Lokale Architektur im Vergleich zur Cloud-Architektur
Auf Ihrem Laptop | In der Cloud | Was hat sich geändert? |
| Cloud SQL | Verbindungsstring |
|
| ein URI |
|
| ein Pfad |
| ein Cloud Run-Dienst |
|
| Cloud Scheduler → Pub/Sub |
|
Sie an der Tastatur |
| einen Standardwert, der pro Anfrage überschrieben wird |
Die Kernimplementierung des Agents, einschließlich root_agent, Tool-Definitionen, Callback-Guards und Budgetdurchsetzung, bleibt unverändert. Der Vergleich von solutions/step10_deploy/concert/ mit Schritt 9 zeigt eine genaue Byte-für-Byte-Übereinstimmung. Dies bestätigt, wie durch die Dienstabstraktion des ADK die Infrastrukturkonfiguration von der Agent-Logik getrennt wird.
Wenn Sie nicht weiterkommen
Sie können in jedem Schritt auf den vollständigen funktionierenden Code zugreifen:
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
Wenn etwas kaputt geht
Symptom | Problembehebung |
| Falsche ID. |
Modell gibt 404 zurück |
|
| Etwas hat bereits Port 8000 oder 8090. |
Der KI-Agent kann den Veranstaltungsort nicht erreichen |
|
Warteschlange wird nicht fortgesetzt | Drücken Sie auf SKIP THE WAIT (Warten überspringen). Bei 1× erreicht die Warteschlange nicht von selbst den Anfang. |
Ein zweites Warteschlangenticket wird im Bereich angezeigt. | Sie haben im Chatfeld anstelle des Felds |
Das Drop-down-Menü ist leer oder die App wird nicht geladen |
|
| Erwartet. |
Alles ist seltsam | Drücken Sie auf ORT ZURÜCKSETZEN und dann auf |
Speicherdatei mit doppelten Buchungen |
|
|
|
Zeitüberschreitung bei Cloud Shell | Starten Sie |