1. Antes de comenzar
La mayoría de los sitios de venta de entradas ponen a los compradores en una fila para evitar fallas en el servidor y administrar la alta demanda. Comienzas tu compra y te encuentras detrás de 14,000 personas. La fila se mueve durante cuarenta minutos. Cuando llegue tu turno, tendrás un breve período para elegir asientos y pagar. Si no lo haces, perderás tu lugar en la fila.
Muchas tareas del mundo real siguen este patrón: una tarea que lleva mucho más tiempo que una sola conversación y que se ejecuta en un sistema externo que no controlas. Algunos ejemplos incluyen esperar a que un proveedor reponga el stock, esperar la aprobación de un reclamo o esperar en una fila.
Los agentes de IA estándar se encuentran dentro de un solo turno de chat, que dura segundos y es puramente reactivo. No pueden esperar 40 minutos ni actuar de forma autónoma. Para resolver este problema, necesitas un agente de larga ejecución: un sistema que permanece inactivo durante las esperas prolongadas, conserva el contexto entre los reinicios del proceso y se reanuda automáticamente cuando se activa por un disparador externo.
En este taller, se explica cómo compilar un agente de filas de entradas para conciertos y se abarcan los patrones arquitectónicos principales necesarios para los agentes de ejecución prolongada en Google Cloud.
Qué compilarás

En este taller, compilarás un agente de reserva de boletos de larga duración mientras aprendes patrones de software basados en agentes clave:
- Procesamiento de suspensión y reanudación: Detiene la invocación de un agente durante largos períodos de espera y reanuda la ejecución a través de activadores externos.
- Almacenamiento de estado duradero: Conserva el estado y los artefactos de la conversación en los reinicios del proceso con SQLite/Cloud SQL y Cloud Storage.
- Protección contra datos obsoletos y protección contra idempotencia: Verifica los datos dinámicos (como la disponibilidad de asientos) antes de actuar y usa claves de idempotencia para evitar transacciones duplicadas.
- Gráficos de flujo de trabajo del ADK: Estructura la ejecución con nodos de código determinísticos para reglas de procedimiento y nodos de agentes para la toma de decisiones contextual.
- Aprobación con interacción humana: Pausa la ejecución para recopilar y conservar los límites de inversión explícitos del usuario.
- Implementación en la nube: Conteneriza e implementa el agente en Cloud Run con activadores de Cloud Scheduler y Pub/Sub.
Prepárate
Primero, prepara tu entorno y asegúrate de tener acceso a Google Cloud. Te recomiendo que ejecutes este comando en Cloud Shell, que ya tiene todas las herramientas que necesitas. Solo asegúrate de tener suficiente espacio y de que tu cuenta pueda crear proyectos.
👉💻 Clona el repo y ejecuta la configuración:
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
Se te preguntará lo siguiente una vez:
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
Luego, hace lo siguiente, en orden:
virtualenv | crea | |
sign-in |
| |
project | apunta | |
API | permite que los diez participantes que necesita el taller. | |
| Escribe el ID de tu proyecto, región y modelo, de modo que cada secuencia de comandos y | |
Verificación del modelo | Realiza una llamada real a Gemini, por lo que un ID de proyecto o modelo incorrecto falla aquí. | |
semilla | compila una sesión que ya tiene dos días, que se abre en el paso 3 | |
Cloud SQL | Inicia una instancia de Postgres en segundo plano. | |
venue | Implementa tu propio vendedor de entradas simulado en Cloud Run. | |
👉💻 Activa el entorno:
cd ~/monstertix
source .venv/bin/activate
Ahora, tu instrucción comienza con (.venv).
Descripción general de la situación
En este caso práctico del taller, tu agente reserva dos entradas para un concierto ("The Midnight Signal" en Ámsterdam). Las preferencias del usuario almacenadas en la memoria histórica incluyen lo siguiente:
- Debe ser un programa de fin de semana (Sam no puede asistir a los programas de días laborales).
- Evita los asientos de la parte superior del estadio Ziggo Dome (mala visibilidad).
- El precio objetivo es de aproximadamente USD 200 por entrada, con un límite máximo de USD 250.

Verifica tu instalación
👉💻 Un comando, ocho marcas:
./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.
👉 Abre segundas pestañas del navegador:
Dónde | Qué es |
| El panel de control, tus botones. |
2. ¿Puedes lograr la autonomía con instrucciones?
Las instrucciones no se pueden iniciar por sí solas
Una instrucción es solo texto que se pasa a un modelo de IA cuando se realiza una llamada. No puede ejecutarse por sí solo, activarse ni iniciar una llamada nueva.
Si le dices a un agente en su instrucción que "vuelva en 10 minutos", no sucede nada después de que finaliza el turno actual. El modelo no tiene un reloj ni un bucle internos. Para que un agente sea autónomo, algo externo a él debe activarlo y llamarlo.

Cómo iniciar la plataforma
Comenzaremos probando las herramientas para desarrolladores. adk web inicia la IU web del ADK, la interfaz de desarrollo que se incluye con el ADK. Te permite hablar con el agente que estás creando sin escribir primero un frontend y te muestra lo que una ventana de chat no puede mostrar, como cada llamada a la herramienta y sus argumentos, el estado de la sesión a medida que cambia, los artefactos que guardó el agente y los eventos sin procesar subyacentes. No lo expondrías en producción. Es la mejor ventana para ver un agente en ejecución mientras aprendes. En el paso 3, compilarás el frontend que expondrías realmente y, en el último paso, lo implementarás.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Terminal 1: Carga el código de este paso y, luego, inicia la IU web del ADK:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
use-solution.sh copia el código terminado de un paso en agent/, que es donde trabajas. Cada paso comienza con él y muestra los archivos que cambió para que sepas qué leer.
Con qué comienzas:
Archivo | Qué es |
| El agente: un nombre, un modelo, una instrucción y una lista de herramientas |
|
|
| un cliente HTTP para el lugar. No hay nada interesante |
| qué modelo usar y una verificación de que tus credenciales funcionan |
. ./set_env.sh activa el entorno virtual, carga tu proyecto y modelo, y exporta $WORKSHOP, la ruta de acceso absoluta a ~/monstertix. Ejecútalo en cada terminal que abras. Imprime lo que configuró:
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
👉 Abre las primeras pestañas del navegador:
Dónde | Qué es |
IU web del ADK |
|
Háblale
👉✨ En la IU web del ADK, escribe lo siguiente:
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
Busca en el recorrido, lee los planos de asientos y razona sobre tu presupuesto y tu empresa. Este es un asistente realmente capaz. Sin embargo, cuando la preventa se abre a las 10:00 a.m., el agente no se ejecuta automáticamente porque no se está ejecutando ningún proceso ni activador en segundo plano.
Lee lo que acaba de ocurrir
El centro de la pantalla no es solo un registro de chat. Cada paso del turno está numerado, incluidas las llamadas a herramientas:
▸ 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
👉 Haz clic en una fila con el ícono de ⚡. En el panel izquierdo, se muestran los argumentos que eligió el modelo. Haz clic en la ✓ que aparece debajo del JSON que se devolvió.
La información del tour no está presente en los datos de entrenamiento ni en la instrucción del modelo. El agente recuperó estos datos ejecutando la herramienta search_events, que consultó la API de lugares.
Abre el código que acabas de ejecutar
Viste cómo funciona y viste el registro. Ahora, observa el código en sí.
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
👉💻 Abre agent/concert/agent.py. Todo el agente se encuentra en la parte inferior del archivo:
INSTRUCTION = """
You help someone plan a trip to see a band on tour.
You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""
root_agent = Agent(
name="concert",
model=MODEL,
instruction=INSTRUCTION,
tools=[search_events, get_seatmap],
)
Un nombre, un modelo, una descripción de cómo debe comportarse el agente y una lista de funciones de Python. Eso es todo lo que es un Agent. adk web lo encontró porque el archivo se llama agent.py y la variable se llama root_agent.
👉💻 Ahora 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)
Es una función ordinaria. Sin decorador, sin registro y sin esquema para escribir. El ADK lee la firma y la cadena de documentación, y compila la definición de la herramienta que ve el modelo, por lo que esa cadena de documentación es fundamental y por eso borrar un argumento cambió el comportamiento del modelo.
El modelo nunca ejecuta tu código. Cuando viste search_events({"city": "Amsterdam"}), el modelo estaba solicitando esa llamada. El ADK ejecutó la función, devolvió el resultado y el modelo escribió una oración sobre él. El bucle es el siguiente:
👉 Los íconos que se encuentran en el extremo izquierdo cambian lo que muestra el panel. El segundo es el gráfico del agente. Pruébalo y deberías ver dos herramientas:

Así que dile que no se suspenda.
¿Qué harías para solucionarlo? Prueba con lo que la mayoría de las personas buscan primero. Recuerda INSTRUCTION del paso 1, la prosa que se le entrega al modelo en cada turno, y reescríbela.
👉💻 Abre agent/concert/agent.py. Debajo de INSTRUCTION, hay otra, ya escrita para ti. Si quieres, puedes cambiarlo tú mismo:
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],
)
👉💻 En la terminal 1, reinicia:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ Explícale el plan, exactamente como se lo dirías a una persona:
I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.
Estará de acuerdo y sonará como si lo sintiera.
👉 Ahora espera. No escribas nada más. Mira el panel.
Espera un minuto. NO PASA NADA
Por qué esa instrucción nunca iba a funcionar
Vuelve a leerlo con una perspectiva diferente. Supervisa. El momento en que se abre Sigue revisando. Cada una de ellas describe algo que sucede con el tiempo.
Ahora recuerda qué es instruction= en realidad. Es una cadena que se entrega al modelo como parte de la solicitud en el momento en que alguien envía un mensaje. Nada lo lee entre turnos.
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
Una instrucción no puede invocar la función dentro de la que está escrita.
Variaciones de instrucciones que fallan
El intento de aplicar la ejecución autónoma a través de instrucciones de la instrucción falla independientemente de la redacción. Intentos de instrucciones comunes, como los siguientes:
- "Establece un temporizador para las 10 a.m. del martes y compra las entradas en ese momento".
- "Comprueba cada cinco minutos si se abrió la preventa".
- "Actúa de forma autónoma sin esperar la entrada del usuario".
Todos fallan por el mismo motivo: las instrucciones son de solo lectura cuando se produce una invocación. Sin un llamador externo, no se ejecuta ningún código entre turnos.
3. Inactividad basada en eventos
Los agentes no permanecen despiertos para siempre
Dejar un proceso en ejecución que sondea continuamente desperdicia memoria, consume dinero y falla si la laptop se suspende o reinicia.
En lugar de mantener un agente en ejecución sin parar mientras espera, un agente de larga duración debería quedar completamente inactivo. Un evento externo (como un temporizador, un webhook o un mensaje) activa el agente solo cuando hay trabajo real que hacer.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Carga el código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
Observa lo que se instaló: monstertix/, que sostiene el servidor, el reloj y una cosa más que aún no viste. Comprueba que no haya modificado agent/, ya que nada del agente cambia en este paso. Quédate con eso, es el punto.
Archivo | Qué es |
| nuevo Un servidor web con un Runner detrás |
| nuevo Algo que conoce la hora y llama al servidor |
Tiene dos mitades, y ambas permanecen en dos archivos hasta la nube: 
se convierte en, en el paso 10 | |||
el activador | un endpoint que puede ejecutar el agente | Cloud Run | |
el activador | algo con un reloj, que lo llama | Cloud Scheduler | |
El activador: tu propio servidor web
Sí, un servidor web. Algo tiene que estar escuchando antes de que se pueda llamar. La IU web del ADK de los pasos anteriores es una interfaz de desarrollo que puede ejecutar un agente.
👉💻 Abre monstertix/server.py:
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent
session_service = InMemorySessionService()
runner = Runner(
agent=root_agent,
app_name="concert",
session_service=session_service,
)
@app.post("/wake")
async def wake(payload: dict = Body(default={})):
session_id = f"wake-{uuid.uuid4().hex[:8]}" # brand new, every time
await session_service.create_session(...)
async for event in runner.run_async(
user_id=USER_ID, session_id=session_id,
new_message=types.Content(role="user",
parts=[types.Part(text=payload["message"])]),
):
...
Un Agent no se ejecuta por sí solo. Es una descripción: un nombre, un modelo, una instrucción y una lista de herramientas. Algo tiene que tomar un mensaje entrante, encontrar la sesión correcta, entregarle al modelo la conversación hasta el momento, ejecutar las herramientas que solicite, devolver los resultados, repetir el proceso hasta que deje de llamar a las herramientas y escribir cada paso en el almacenamiento. Ese algo es el Runner.

La IU web del ADK tiene una predeterminada para ti. Aquí lo compilas tú mismo, y esa es la diferencia entre la IU web del ADK y algo que puedes implementar.
👉💻 Terminal 2: Iníciala:
Inicia el 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.
Lee esa última línea. El activador se está ejecutando, el agente se cargó y no sucede nada. Separa las dos ideas: ser invocable y ser invocado.
Y una puerta principal, ya que estamos aquí
A pesar de que adk web es una excelente herramienta de desarrollo, no debería ser algo que envíes. No debes habilitarlo cuando implementes la producción, y no se lo mostrarías a un cliente si lo hicieras. Por lo tanto, el mismo servidor también entrega una página.
👉 Abre http://127.0.0.1:8090 en un navegador (la misma dirección).
Conoce MonsterTix. Una ventana de chat, un nombre en la esquina y dos botones. Publica en /wake y renderiza lo que se devuelve, y funcionaría sin cambios en cualquier extremo que responda de la misma manera.
En la página | Qué hay debajo |
la conversación |
|
+ Nueva sesión | Un ID de sesión nuevo: una conversación nueva y, luego, un presupuesto nuevo |
Restablecer la memoria | borra cada sesión y el archivo de memoria. Primero, te pregunta y te muestra exactamente dónde se encuentra ese estado. |
👉✨ Dile algo —What Amsterdam dates are there?— y mira la terminal 2. El mismo Runner, el mismo agente y las mismas líneas de registro. Solo cambió la persona que llamó.
El activador: Algo que sabe la hora
👉💻 Abre monstertix/clock.py. Observa el código.
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
Es un mecanismo muy simple: dormir, POST, tal vez repetir. Todos los programadores que usaste hacen lo mismo, además de reintentos, zonas horarias y una garantía de que sobrevivirá a un reinicio.
👉💻 Terminal 3: Se activará una vez, dentro de 10 segundos:
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
👉 Ahora deja de escribir. Sin lugar a dudas. Mira la terminal 2.
Un minuto después, sin nadie en el teclado:
[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.
Esa es la respuesta al paso 2. Algo externo al agente lo llamó, y ese algo fue un sleep y una solicitud HTTP.
👉💻 Ctrl-C en ambos casos cuando lo hayas mirado un par de veces. Usa --every 30 primero si quieres verla repetirse.
Ahora, lee lo que dijo
"Compré dos entradas para The Midnight Signal en Ámsterdam, sección A, USD 420".
👉🔴 Revisa el panel de la sede y deberías ver que se compró la entrada.
Entonces, ¿qué sigue mal?
Compró algo. Mira lo que tuvo que adivinar. Eligió Ámsterdam y la sección A porque fueron lo primero que encontró, y le dijiste al agente que lo comprara directamente, y no tiene idea de que Sam no puede ir los días de semana por la noche. Un agente que actúa en función de suposiciones a las 3 a.m. es peor que uno que no hace nada.
Despertarse es la mitad fácil. Un agente que se activa sin nada (sin historial, sin preferencias, sin que nadie lo escuche) aún no vale la pena activarlo.
4. Administrar la vida útil del contexto
La información se encuentra en diferentes lugares y en diferentes momentos
Un agente almacena hechos en varios lugares: en la conversación de chat, en el estado de la sesión, en archivos (artefactos) o en la memoria a largo plazo del usuario. Cada lugar dura una cantidad de tiempo diferente.
Si colocaste información en el lugar incorrecto, haz lo siguiente:
- Desaparece demasiado pronto: Los detalles importantes se borran cuando finaliza el chat.
- Se queda demasiado tiempo: Los hechos temporales contaminan la memoria a largo plazo y confunden las conversaciones futuras.
Debes hacer coincidir cada fragmento de información con el lugar de almacenamiento y el ciclo de vida correctos.
Carga el código
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR.
👉💻 En la terminal 1, ejecuta lo siguiente:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
Qué cambió desde el último paso:
Archivo | Qué cambió |
| Se agregó |
| nuevo Nuestro propio almacén de memoria |
| Tres herramientas más en la lista, y la instrucción le indica al agente que las use |
👉💻 Reinicia el agente en la 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"
Dos marcas que no usaste antes:
Marcar | Qué es |
| El ADK escribe cada mensaje, llamada a herramienta y cambio de estado en él como filas (base de datos SQLite). |
| Archivos en los que guarda el agente (Files) |
Sin ellos, el ADK mantiene todo en la memoria y lo descarta cuando detienes el proceso. Estos dos parámetros de configuración son los que hacen que persista.
La base de datos sessions.db contiene una conversación pasada inicializada (two-days-ago) con 13 eventos registrados:
you We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you Sam can't do weeknights though, bailed on every single one.
you Priya's out, she's away that weekend. Just me and Sam.
agent Two it is. Still Saturday the 14th, still Amsterdam.
you You said the upper bowl at Ziggo was bad last time?
Un archivo de memoria de tres reservas anteriores, memory/userx.md, que puedes abrir en un editor:
- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.
Por eso, las filas ámbar de los días laborables son importantes. Un espectáculo entre semana cuesta USD 10 menos por asiento, tiene mejores asientos disponibles y se encuentra en la parte superior de la lista, pero es la respuesta incorrecta para esta persona. La única razón por la que el agente puede saber que es un archivo que escribió después de la última reserva. Los pasos 4 y 5 indican si ese hecho persiste.
Las tres herramientas que agregamos

note_companion registra quién llega y qué lo limita:
# 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}
Agrégala porque "Sam no puede los días de semana" debe seguir siendo cierto la próxima semana. Escríbelo en user:prefs para que eso suceda y observa cómo la parte user: del nombre hace el trabajo. Volveremos a este tema más adelante.
recall
y
remember lee y escribe un archivo de texto, memory/userx.md. Mantén la posición un momento.
👉 Ahora observa los dos lugares en los que el agente almacena su contexto:
ubicación | Qué contiene |
Estado | La memoria de esta sesión, como pares clave-valor. Te encuentras aquí en el paso 4. |
Artefactos | Archivos que guardó el agente y que nunca se ingresan en la instrucción |
Mira los cambios en
get_seatmap
. Antes, devolvía todo el mapa de asientos. Ahora, guarda el mapa de asientos en un archivo y devuelve un breve resumen:
# 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
A continuación, te explicamos por qué es importante este cambio.
Cada carga útil de devolución de la herramienta se agrega al historial de conversación y se vuelve a enviar al modelo en todos los turnos posteriores. Devolver objetos JSON grandes (como un mapa de asientos de 6 KB) aumenta el consumo de tokens en cada turno posterior.
En su lugar, guárdalo en un archivo y paga un nombre de archivo. Permite que el agente vuelva a solicitar el detalle con load_artifact si lo necesita, y espera que, la mayoría de las veces, no lo haga.
Dónde va el archivo: el servicio de artefactos
Vuelve a mirar la línea en get_seatmap, en agent/concert/tools.py:
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context proviene del ADK, que lo pasa a tu herramienta como un argumento, y save_artifact es un método en él. En esa línea, tu herramienta entrega los bytes al ADK.
Luego, el ADK los escribe con el servicio de artefactos con el que se inició. Elegiste esa opción con la marca:
--artifact_service_uri="file://$WORKSHOP/artifacts"
El ADK tiene tres ubicaciones para almacenarlos:
URI | Dónde van los bytes |
| RAM Desaparecen cuando detienes el proceso. Este es el valor predeterminado si no pasas ninguna marca. |
| una carpeta en el disco. Lo que usas ahora |
| a Cloud Storage. Qué usa el agente implementado en el último paso |
👉💻 Aquí elegimos archivos:
ls -R ~/monstertix/artifacts
Para pasar de tu laptop a Cloud Storage, cambia file:// por gs://. (Veremos esto en el paso 10).
Nuestro almacén de memoria

El ADK te proporciona una interfaz para la memoria a largo plazo llamada BaseMemoryService. Tiene dos métodos:
# 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
El ADK incluye tres implementaciones:
Clase | URI | Qué necesita |
|
| desde cero Se pierde todo cuando detienes el proceso |
|
| Un corpus de RAG de Vertex AI |
|
| Un recurso de Agent Engine |
Escribiremos la nuestra para este taller. Descarta InMemoryMemoryService, que olvida todo cuando detienes el proceso, y descarta los otros dos, que necesitan recursos de la nube que tardan minutos en crearse. Elige un archivo, ya que puedes agregarlo con anticipación y leerlo en un editor. Así que toma memory.py como tu propia implementación. Almacenaremos todo en un archivo 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 y MemoryEntry son tipos del ADK, y Memory Bank devuelve los mismos objetos. Uno tiene un servicio administrado detrás. El nuestro tiene un archivo.
Elige un archivo por un motivo: puedes abrirlo y leerlo. Toda la memoria a largo plazo de tu agente es una página de texto.
Entonces, ¿hacia dónde la apuntas?
Para recapitular la implementación de este taller, se eligen dos de los tres servicios en la línea de comandos. La tercera la implementamos nosotros mismos por el motivo anterior:
Servicio | Dónde la configuras | Este taller |
sesión |
|
|
artefacto |
|
|
memoria | en ningún lugar de la línea de comandos |
|
Por lo tanto, memory.py construye el servicio en sí, a partir de una variable de entorno:
# 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 exporta ambos. Obtener los conjuntos de MEMORY_DIR desde donde el agente lee su memoria y MEMORY_USER determina el archivo
Ve y encuentra dónde están las cosas
Ahora tienes cuatro lugares donde puedes colocar un hecho. Mira las cuatro.
Primero, tu propia sesión
👉 Haz clic en Nueva sesión en la barra superior.
👉✨ Di tres cosas, una tras otra:
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?
Ese tercer mensaje es importante. Finaliza el turno en el que se leyó el mapa de asientos, y finalizar un turno es lo que hace que desaparezca temp:.
Se guardaron tres cosas en tres lugares diferentes:
Qué se almacenó | ¿Qué herramienta | Dónde se fue | |
1 | Sam no puede los días de semana |
|
|
2 | El mapa de asientos completo, 6 KB |
| el servicio de artefactos |
3 | un resumen del mapa de asientos en una línea |
| nowhere |
👉 Ahora, verifica. Las pestañas State y Artifacts se encuentran a la izquierda de adk web.


En la pestaña Estado, observa que se omitió temp:seatmap. Las claves de estado con un prefijo temp: solo existen durante la invocación activa y el ADK las quita automáticamente antes de persistir el estado de la sesión en la base de datos.
Luego, la conversación anterior
👉 Cambia a two-days-ago en el menú desplegable de la sesión que se encuentra en la parte superior.
Esta sesión contiene un historial de conversaciones prepropagado en la base de datos.

👉 Mira el flujo de eventos (la columna del medio). Observa que los turnos anteriores se reemplazaron por un bloque de eventos resumidos.

👉 Mira la pestaña Estado. Hay tres tipos de llaves juntas:
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
El prefijo user: (p.ej., user:prefs) limita el estado en todas las sesiones para un usuario determinado. En cambio, las claves sin prefijo (
target_event_id
,
party_size
) representan el estado de la sesión simple, que se limita estrictamente a la sesión activa y se borra cuando finaliza la sesión.
👉 Ahora compara las dos pestañas de Estado. Si bien user:prefs y los artefactos almacenados persisten, temp:seatmap no está presente en ninguna de las sesiones. Las claves de estado con un prefijo temp: son estrictamente efímeras y nunca se conservan en la base de datos.
Luego, el archivo
👉💻 Ábrelo en el editor:
cloudshell edit ~/monstertix/memory/userx.md
Tres reservas, que se remontan a enero. El agente lo lee cuando llama a recall(), y seguirá allí incluso si perdimos los datos de la sesión.
Los cuatro lugares
Dónde se encuentra | ¿Sobrevive al siguiente mensaje? | ¿Sobrevive a un reinicio? | ¿Sobrevive al borrado de la sesión? |
| no | no | no |
| sí | sí | no |
| sí | sí | yes |
| sí | sí | Sí, y sobrevive al borrado de la base de datos. |
un artefacto | sí | sí | depende del servicio |
Lee esa tabla como si se tratara de los contenedores, no de lo que hay en ellos.memory/userx.md sobrevive a todo y solo contiene lo que remember()decidió poner allí. Nada escribe tu conversación actual en él. Por lo tanto, que el archivo sobreviva a una sesión no es lo mismo que que el agente recuerde de qué estabas hablando, y el paso 5 es donde se nota esa distinción.

Qué estado es
Piensa en el estado como un diccionario que viaja con la sesión. Léelo y escríbelo desde tus herramientas a través de tool_context.state, y permite que el ADK lo guarde junto con la conversación.
El ADK asigna el significado de los prefijos. No los defines en ningún lugar. El ADK lee la primera parte del nombre de la clave y la usa para decidir dónde va el valor y cuánto dura:
Una clave llamada | El ADK lo almacena | Duración |
| con la sesión | hasta que se borre la sesión |
| contra el usuario | en cada sesión que tiene el usuario |
| contra la app | en todos los usuarios |
| en ningún lugar | una invocación |
Una invocación es un solo ciclo de solicitud y respuesta, que incluye todo el razonamiento del modelo, la ejecución de la herramienta y la respuesta final.
Cuando finaliza una invocación, el ADK escribe el nuevo estado en la base de datos y quita cada clave temp: durante el trayecto.
Una cosa que quedó sin explicar
En two-days-ago, viste un resumen en el lugar donde solían estar los primeros turnos de alguien. El ADK la escribió automáticamente cuando la conversación se hizo lo suficientemente larga.
Cada hecho tiene una vida útil
5. Degradación y compactación del contexto
Resúmenes: Olvídate de los detalles
Cuando una conversación se vuelve demasiado larga, el sistema resume los mensajes más antiguos para ahorrar espacio y tokens.
Los resúmenes son útiles para comprender la idea general y ahorrar el uso de tokens.

👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Ve al código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
Qué cambió desde el último paso:
Archivo | Qué cambió |
| El agente se incluye en un |
Todo lo demás | sin modificar |
agent.py ya no termina con root_agent. Termina con lo siguiente:
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
),
)
Ten en cuenta que adk web primero busca una variable llamada app y, luego, recurre a root_agent. Encapsula el agente en un App siempre que necesites algo que se aplique a toda la aplicación en lugar de a un agente: compactación aquí y reanudación más adelante en los próximos pasos.
compaction_interval=3 significa que el ADK genera un resumen después de cada tres turnos. Un turno es cuando dices algo y el agente termina su respuesta, sin importar cuántas herramientas haya llamado en el camino. Tres es un número pequeño, por lo que se activa durante un taller. Una app real usaría un número mucho mayor.
overlap_size=1 significa que cada resumen comienza un turno antes de lo que estrictamente necesita.
El ADK resume los turnos 1, 2 y 3. Tres turnos después, resume el siguiente lote. Con overlap_size=1, ese segundo resumen comienza de nuevo en el turno 3, no en el 4:

Observa que el turno 3 se resume dos veces. Eso es deliberado. Si hiciste una pregunta en el turno 3 y el agente respondió en el turno 4, una división clara coloca la pregunta en un resumen y la respuesta en el otro, lo que hace que ambos sean confusos. Repetir un turno es una garantía económica contra la interrupción de un pensamiento.
Configura ambos campos. El ADK no tiene ningún valor predeterminado para ninguno de los dos.
La compactación y tu servicio de memoria nunca se encuentran
Vale la pena ser explícito, ya que las dos palabras suenan relacionadas, pero los sistemas no lo son.
EventsCompactionConfig se encuentra en App en agent.py, junto a root_agent. Es una propiedad de la aplicación, no de ningún servicio:
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
Un resumen reescribe la conversación. No vuelve a escribir el archivo.
Ese es el motivo por el que remember() existe como una llamada deliberada. Todo lo que quede en la transcripción se leerá como una paráfrasis que escribió otra persona; todo lo que se escriba en el archivo se leerá exactamente como se almacenó, mañana y la próxima semana. El final de este paso es donde observas esa diferencia.
Dónde se guarda el resumen
En sessions.db, en la tabla events, el agente escribe un registro de compactación en una fila de eventos, con una marca de tiempo como cualquier otro evento de turno.
Un evento normal incluye un campo content y el resumen se encuentra en actions.compaction.
👉💻 Consulta tu propia base de datos:
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%';"
Así se ve la forma del evento:
events row
└── event_data (JSON)
├── id, author, timestamp, invocation_id ← like every other event
├── content ← ABSENT on this one
└── actions
└── compaction
├── start_timestamp ← the window it replaced
├── end_timestamp
└── compacted_content.parts[0].text ← the summary itself
El flujo de eventos extrae cada evento de su contenido. El resumen en la base de datos y que se envía al modelo en cada turno.
El segundo agente
budget_split es un agente completamente independiente, no una herramienta:
# 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)],
)
¿Por qué un agente y no una función simple? Porque el trabajo necesita un modelo. Lee "¿Cuáles de estas secciones se ajustan a un presupuesto de USD 200 para cuatro personas y cuál es el total?" como aritmética incluida en un juicio, con una respuesta que una persona debe poder leer. Permite que una función de Python multiplique y no le pidas que decida qué opciones vale la pena mencionar.
Entonces, ¿por qué no dejar que lo haga el agente principal? La siguiente línea include_contents="none" significa que a este agente no se le entrega ninguna parte de la conversación. Sin chat en grupo, sin preferencias, sin cuarenta fechas de gira. Obtiene la solicitud y nada más. Esa marca solo existe en un Agent, que es la razón real por la que esto es un agente en lugar de una función.

Prefiere la instrucción breve: es más económica, rápida y difícil de desviar. Confía en que la aritmética es inmune a algo que se dijo hace veinte turnos, porque nunca lo ve.
Por qué
AgentTool
? Envuelve el agente con él para que el agente principal pueda llamarlo como cualquier otra herramienta. Desde el punto de vista del modelo, lee budget_split como idéntico a search_events.
And
output_key="budget_plan" coloca la respuesta en el estado de la sesión con ese nombre en lugar de dejarla suelta en la conversación. Mira por qué es importante.
Ejecución
👉💻 En la terminal 1, presiona Ctrl+C y reinicia con las mismas marcas:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 Inicia una Nueva sesión y ten una conversación real. Cinco o seis vueltas. Este funciona:
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?
👉 Abre la pestaña Estado.
Busca budget_plan allí, que contiene la respuesta completa del agente aritmético.

👉✨ Ahora cambia los números:
what if 10 people come and I can only do $120 each?
👉 Vuelve a mirar budget_plan. Se reemplazó, no se agregó. Esto se debe a que output_key contiene el más reciente.

Lo que no puedes ver: el resumen
En algún momento de esas respuestas, la ADK resumió las anteriores.
👉💻 En una terminal nueva, ejecuta el siguiente comando para leerlo directamente:
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
Leer lo que escribió
Aquí hay una real, abreviada:
── 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`
Conservó los precios, las secciones, las ciudades y las preferencias, y señaló correctamente que aún no se había decidido nada.
De esto se desprenden tres cosas:
Es una paráfrasis. Nada de lo que se dice aquí es lo que dijo alguien. Es un recuento de lo que se dijo, producido por el mismo tipo de proceso que produce todo lo demás que el modelo a veces interpreta mal.
Nota: Las cadenas de resumen se almacenan en registros de eventos de sesión internos y no se exponen directamente como pestañas de IU separadas.
Cada tres turnos, en una conversación que ahora se compone en parte de resúmenes anteriores.
Intenta reservar sin repetir la información.
En una sesión nueva No inicies una nueva. El resumen que acabas de leer se encuentra en los eventos de esta sesión, y ese es el único motivo por el que el agente aún sabe algo sobre Ámsterdam.
👉✨ No le des nada nuevo:
Book us something.
Mira cómo el agente pregunta qué programa, qué sección y cuántas personas. Marque que es correcta: El resumen en sí indica que el usuario nunca confirmó nada, por lo que el agente preguntó. Dale crédito a la compactación.
Incluso cuando la compactación se realiza correctamente, resumir los turnos anteriores puede generar ambigüedad, lo que requiere que el agente vuelva a confirmar las restricciones con el usuario.
👉✨ Responde y permite que se complete la reserva:
Amsterdam Saturday, section C, 8 people
Lee el pedido antes de gastar nada:
"Quieres reservar 8 entradas en la sección C para The Midnight Signal en Ámsterdam el sábado 14 de noviembre. El costo total será de USD 760. ¿Compro estas entradas?"
👉✨ Confírmalo:
yes
Ahora te indica que funcionó:
"Se completó tu compra de 8 entradas para la sección C de The Midnight Signal en Ámsterdam el sábado 14 de noviembre por un total de USD 760. Tu ID de pedido es ord_227877fe".
Ve y comprueba si lo hizo.
👉 Cambia al panel de la sede. No te quedes solo con la palabra del agente.

En el panel | Qué deberías ver |
Entradas compradas |
|
el orden debajo |
|
El tour, sábado en Ámsterdam | La sección C bajó 8 |
Activity |
|
Consulta el panel para conocer la venta de entradas.
Por qué user:prefs fue la elección correcta
Recuerda que note_companion escribe la restricción de Sam en el último paso de user:prefs y la objeción obvia: ¿por qué molestarse con una herramienta cuando el agente podría simplemente recordarla?
Estos son los motivos. No mantengas el estado en la conversación y ningún resumen lo toca. Comprueba user:prefs y budget_plan en función de cómo entraron y verás que no cambiaron. Luego, revisa la transcripción.
Un resumen conserva la forma general y pierde los detalles. Guarda todo lo que no te puedas permitir perder en un lugar que no sea la conversación.
6. Reanudación del proceso
Esperar 40 minutos sin permanecer en línea
Cuando un agente tiene que esperar en una fila de tickets de 40 minutos, mantener activo y en espera un proceso de Python es frágil y costoso.
Para manejar las esperas largas de forma adecuada, haz lo siguiente:
- Guardar el estado en una base de datos: Almacena el lugar donde se detuvo el agente para que el proceso se pueda cerrar de forma segura.
- Activación a pedido: Cuando finaliza la espera, un indicador externo reinicia el agente y reanuda la ejecución justo donde la dejó.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Ve al código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
Ejecución
Qué cambió desde el último paso:
Archivo | Qué cambió |
| Se agregaron |
|
|
join_queue devuelve de inmediato un ticket en lugar de bloquearse, y ResumabilityConfig permite que la ejecución se detenga en él y se reanude después.

👉💻 Terminal 1: El agente, sin cambios:
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"
👉✨ Pídele al agente que compre:
Get us two tickets to the Amsterdam show.
Llama a recall, luego a search_events y, por último, a join_queue, y responde con algo parecido a lo siguiente:
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
Observa que eligió sábado sin preguntarte. Hay dos shows en Ámsterdam, el 17 es martes y el archivo de memoria del último paso dice que Sam no trabaja los días de semana. Es un hecho de una reserva anterior que decide una pregunta en esta.
También observa que se unió a la fila antes de ver un solo asiento o precio, y no dijo nada sobre ninguno de los dos. Esa es la regla del lugar. Compras las entradas hasta que llegas al frente, por lo que lo único que vale la pena tener temprano es un lugar en la fila. Qué asientos tomar es una pregunta para dentro de cuarenta minutos.
👉 Observa el panel de control. El banner dice lo siguiente:

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
La fila dura cuarenta minutos y no se moverá mientras trabajas.
👉💻 Detén el agente.:
Ctrl-C
👉💻 Vuelve a iniciarlo en la 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"
👉✨ En la misma sesión, pregúntale al agente dónde está en la fila:
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

Compara el ticket y observa cómo la posición sigue disminuyendo mientras no se ejecutaba nada. Observa que el agente no sabía que lo habían matado. Llamó a check_queue con el ticket que obtuvo antes del reinicio, ya que ese ticket estaba en el disco.
👉💻 Descubre por qué:
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
Observa que cada turno de cada conversación es una fila, y el turno en sí, quién lo dijo, qué herramienta llamó, qué devolvió y ten en cuenta que es JSON dentro de event_data.
👉💻 Busca tu ticket de fila allí:
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
Cuando join_queue regresa, su ticket se guarda directamente en la base de datos. Cuando se reinicia, ADK vuelve a cargar los resultados de las herramientas y los turnos anteriores desde el disco, lo que permite que el agente continúe donde lo dejó.
Y observa que no se reinició. El agente se reanuda directamente en lugar de reproducir el flujo de trabajo desde el principio. Dado que LongRunningFunctionTool y ResumabilityConfig conservan el estado de ejecución en el disco, el ADK sabe que el agente ya está en la fila, lo que evita que vuelva a buscar fechas de la gira o solicite un boleto duplicado.

Por qué join_queue y no check_queue
Ambas funciones devuelven el resultado en milisegundos. La distinción radica en si el trabajo subyacente se completa cuando la función devuelve un valor.
Qué devuelve | ¿Se completó el trabajo? | |
| Ticket de la fila y posición inicial | No. El procesamiento continúa de forma asíncrona en el sistema externo. |
| Estado actual de la fila | Sí. Devuelve una respuesta inmediata en un momento determinado. |
LongRunningFunctionTool está diseñado para operaciones que devuelven un identificador para el trabajo pendiente. Encapsular join_queue permite que el agente suspenda la ejecución mientras avanza la cola externa.
En cambio, check_queue es síncrono y completo. Si lo hiciera, el agente suspendería la ejecución de forma incorrecta cada vez que verifique su estado, incluso cuando llegue al principio de la fila y deba completar la compra de inmediato.

Qué hizo que funcionara
Tres componentes permiten la suspensión y la recuperación de la ejecución:
# 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: Marca una herramienta como no bloqueante y devuelve un identificador activo para que el agente pueda esperar sin bloquear el subproceso.ResumabilityConfig: Indica al tiempo de ejecución que pause el ciclo de vida de ejecución en una tarea incompleta y que reanude desde el último estado registrado.Session Storage URI (--session_service_uri): Dirige la persistencia del estado al almacenamiento externo (p.ej., SQLite, Cloud SQL) en lugar de la memoria del proceso, lo que evita la pérdida de datos durante los reinicios.
7. Cómo actuar en función de los datos inactivos
La información se desactualiza mientras esperas
El historial de conversaciones persistente no es un estado en tiempo real. Durante los intervalos de espera largos, los datos externos, como la disponibilidad o los precios de los boletos, pueden cambiar sin que el agente lo sepa.
Un agente con información faltante hace una pausa para pedir aclaraciones, pero un agente con datos desactualizados actúa de inmediato según suposiciones obsoletas. Para evitar acciones no válidas, los datos sensibles al tiempo deben volver a verificarse inmediatamente antes de ejecutar operaciones críticas.
No cargues código nuevo aún. Quédate en el agente del paso anterior (puede comprar boletos, pero actualmente no tiene medidas de seguridad para la obsolescencia o la idempotencia).
👉🔴 Presiona RESET THE VENUE y, luego, inicia una sesión nueva en adk web con el botón + New Session que se encuentra sobre el chat.
Inicia una nueva sesión para borrar el ticket de la fila anterior y el estado de compra pendiente. Este paso requiere un contexto de ejecución limpio para demostrar el control de la obsolescencia de forma adecuada.
La instantánea miente
👉✨ Pide el mismo programa que antes:
Get us two tickets to the Amsterdam show.
👉✨ Ahora, mientras espera en la fila, pregunta por los asientos:
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.
👉🔴 Presiona VENDE LOS MEJORES ASIENTOS. La sección A disminuye a cero.
👉🔴 Presiona OMITIR LA ESPERA para enviar al agente al principio de la fila.

👉✨ Dile que compre:
Buy the two section A seats.
Ejecuta purchase(section="A") sin volver a verificar la disponibilidad de asientos, lo que hace que la API del lugar muestre una respuesta de error:
{"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.

Observa que el agente solo volvió a consultar el mapa de asientos después de recibir una respuesta de error de la API. La causa raíz es el contexto obsoleto: al principio de la sesión, el agente registró "sección A: 400 disponibles", y ese turno permaneció en el historial de conversaciones que se pasó al modelo.
Indica claramente dónde se almacena la información anterior: persiste en el historial de conversaciones, que los prefijos de estado como temp: no pueden anular.

La solución
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Ahora, carga el código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👉💻 En la terminal 1, presiona Ctrl+C y reinicia:
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"
Los cambios que se aplicaron:
Archivo | Qué cambió |
| nuevo Se ejecuta inmediatamente antes de una compra y puede bloquearla. |
|
|
|
|
fence.py es una devolución de llamada que el ADK ejecuta en el momento anterior a cualquier llamada a herramienta:
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
Demuéstralo
👉🔴 RESTABLECE EL LUGAR. Haz fila, presiona VENDE LOS MEJORES ASIENTOS mientras esperas y, luego, dile que compre la sección A.
Esta vez, se detiene solo. Observa cómo el agente dice algo como lo siguiente:
"Parece que la sección A se agotó. Necesito volver a leer el mapa de asientos".
Luego, observa cómo vuelve a llamar a get_seatmap sin que se lo pidas y regresa ofreciendo la sección C.
👉🔴 Lee con atención el feed de actividad del lugar, ya que muestra ambas mitades:
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

En el panel, ves la compra de llamadas, pero el contador de tickets permanece en cero. Esto sucede porque fence.py actúa como un guardia antes de que se ejecute la herramienta:
- El agente intenta llamar a la herramienta de compra.
fence.pycapta la solicitud primero y verifica la disponibilidad de asientos en vivo.- Al ver que los asientos están agotados,
fence.pydevuelve un error directamente y cancela la compra antes de que se llame a la API.
Las instrucciones de la instrucción no pueden garantizar que un LLM no intente comprar asientos agotados. Los protectores de código (before_tool_callback) garantizan que la compra incorrecta nunca llegue al mundo real.
👉🔴 Presiona RESET THE VENUE y, luego, BREAK THE NEXT PURCHASE.
👉✨ Pídele que compre y déjalo que lo intente tantas veces como quiera.

El recuento de pedidos se mantiene en 1 porque el comercio recibió la misma clave de idempotencia en el reintento y devolvió la confirmación original en lugar de crear un pedido duplicado.
8. Flujo de trabajo autónomo
Los LLMs no son buenos para seguir reglas rígidas todo el tiempo
Los LLM son excelentes para tomar decisiones (como elegir un programa según las preferencias del usuario). Sin embargo, confiar en un LLM para que siga reglas estrictas paso a paso (como "siempre únete a la fila antes de verificar los asientos") significa que, en ocasiones, omitirá pasos o confundirá el orden.
Para solucionar este problema, divide el flujo de trabajo en dos tipos de pasos:
- Reglas fijas (funciones de código): Los pasos que deben realizarse de la misma manera cada vez (tomar un ticket de la fila, verificar la posición) se escriben como nodos de código comunes.
- Decisiones (nodos de agentes): Los pasos que requieren juicio (elegir secciones de asientos, verificar las preferencias del usuario) se asignan al agente de IA.
Mira lo que realmente hay en este flujo. Algunas de ellas no tienen ningún juicio:
Paso | ¿Juicio? |
tomar exactamente un ticket de la fila | No. Siempre una, siempre antes de comprar |
comprobar si es tu turno | No. Haz la pregunta y lee la respuesta. |
escribir el resumen para el comprador | No. La misma oración, la misma forma, siempre |
Qué programa, qué sección | Sí. Precio, día, quiénes vendrán y qué odian |
Preferencias y limitaciones del usuario | Sí. |
Los procedimientos estrictos no deben depender de un LLM, ya que los modelos probabilísticos eventualmente no seguirán secuencias rígidas cuando no se supervisen.
El Workflow del ADK separa la ejecución determinística del razonamiento de la IA:
- Los nodos de función aplican reglas fijas (como unirse a una fila o verificar la posición) de manera coherente cada vez.
- Los nodos de agentes controlan las decisiones contextuales (como elegir asientos según las preferencias del usuario).
- Los nodos de espera suspenden la ejecución sin mantener un proceso activo en la memoria.
No se reemplaza el agente original. Se convierte en el nodo final del gráfico de flujo de trabajo y conserva sus herramientas, verificaciones de obsolescencia y medidas de seguridad de idempotencia. El gráfico controla el orden de ejecución, mientras que el agente ejecuta la decisión de compra.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR.
👉💻 Ve al código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
Ejecución
👉💻 En la terminal 1, presiona Ctrl + C y reinicia. El mismo comando de siempre:
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"
Qué cambió desde el último paso:
Archivo | Qué cambió |
| nuevo La ejecución desatendida como un grafo de |
|
|
| También perdió |
Ejecuta el gráfico: tres mensajes, en orden
Ejecuta todas las siguientes acciones en el Python de versión nocturna (aquí es donde se encuentra el gráfico). Envía la misma entrada en cada turno para observar la transición del gráfico de flujo de trabajo a través de sus estados.
1. Observa con qué estás hablando ahora
👉 Vuelve a cargar la IU web del 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
Ten en cuenta que adk web ejecuta cualquier objeto que se asigne a root_agent. Aquí, root_agent apunta al grafo de flujo de trabajo nightly, con buyer_agent configurado como el nodo final del grafo.
2. Comenzar
👉✨ Escribe lo siguiente y presiona Intro:
go
Nota: En un gráfico de flujo de trabajo automatizado, la ejecución inicial comienza sin esperar la entrada del usuario. Enviar un mensaje en adk web activa la ejecución inicial.
👉 Ahora mira el navegador web. El panel izquierdo dibuja el gráfico y colorea los nodos a medida que se ejecutan. El panel derecho es el flujo de eventos, en el que se observa cómo se toman las decisiones:

Observa cómo queue_up escribe queue_ticket y queue_event_id en el estado de la sesión.

Deberías ver una llamada a función llamada adk_request_input, que espera una respuesta. Luego, se detiene y finaliza la carrera. El gráfico alcanzó el valor check_front, te encontró en el puesto 14,203 y se detuvo.
👉 Consulta el panel de la sala: Un ticket en la fila y nada en ejecución.

3. Actívala y ten cuidado en qué cuadro escribes.
Desplázate por el flujo de eventos hasta el evento adk_request_input y busca el cuadro de entrada asociado a ese evento específico:

Ahora hay dos lugares en los que puedes escribir, y hacen cosas diferentes.
Dónde escribes | Consecuencia |
La casilla | Responde la interrupción. La carrera estacionada continúa desde |
el cuadro de chat principal en la parte inferior | Inicia una invocación nueva. El gráfico se reproduce desde |
👉✨ Escribe lo que quieras en el cuadro Enter your response... y presiona la flecha.
👉 Terminal 1:
[check_front] #14,197 — not yet. Pausing.
Una línea. No eligió un segundo programa ni tomó un segundo ticket de fila, lo que te habría puesto al final de una fila de 14,203 personas.
Si ves que [open] y [pick_show] se ejecutan de nuevo, significa que escribiste en el cuadro de chat. Presiona RESET THE VENUE y vuelve a comenzar el paso.
👉 Vuelve a revisar el panel de la sede: Sigue habiendo una entrada, y la posición bajó un poco porque el reloj de la sede siguió funcionando mientras que el tuyo no.

4. Llega al frente y, luego, termina
👉🔴 Presiona SKIP THE WAIT en el panel del lugar.

👉✨ Responde el evento adk_request_input más reciente por última vez, de nuevo en su cuadro, no en el cuadro de chat. 
Esta vez, el reloj check_front te encontrará en la parte delantera y se caerá. Observa cómo se iluminan los paneles izquierdo brief y concert, los dos últimos nodos.

👉 Y mira el navegador. Esta es la finalización de la ejecución, y hay cuatro cosas en ella que vale la pena leer.

El gráfico es completamente verde y se cuentan las aristas. brief y concert se iluminaron, y las aristas hacia pick_show, queue_up y check_front ahora tienen una insignia.
2x
es la cantidad de veces que el grafo recorrió esa arista, una vez por activación. Los 2 de la parte superior son el precio de la reanudación: el programador vuelve a ingresar al gráfico desde el principio cada vez. No significa que el trabajo se realizó tres veces, y el panel de la sala es la prueba: un ticket de fila, no 2. @node(rerun_on_resume=False) es la respuesta.
El gráfico

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
Ten en cuenta que dos nodos son agentes y dos son funciones, y considera cuál es cuál como la pregunta de diseño completa.
Nodo | Por qué esa forma |
| agent. Elegir un programa del historial de alguien es un juicio. Llama a |
| función. Unirse a una fila es una regla. Una regla no debe ser un lanzamiento de moneda a las 3 a.m. |
| función. "¿Estoy al frente?" tiene una respuesta correcta y el lugar la tiene. |
| agent. Es |
Mira el último nodo y busca el agente que creaste. No es una copia ni una reescritura:
# agent/concert/nightly.py
from .agent import buyer_agent
Todo se incluye: cada herramienta que tiene, la nueva lectura del mapa de asientos en fence.py y la clave de idempotencia en purchase. Solo hay uno.
Por qué se detuvo en lugar de esperar
Esta es la versión que nadie debería escribir:
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
Mantiene un proceso activo durante cuarenta minutos sin lograr nada, se cierra con la terminal y no se puede reanudar. Esto es lo que hace check_front en su lugar:
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput es una interrupción. El gráfico se detiene donde está, la invocación va al almacén de sesiones y el proceso puede salir.
La ejecución no cuesta nada mientras espera y termina en un solo lugar: los boletos comprados. Es posible que te lleve toda la noche y varias veces que te despiertes.
9. Trabajar con una persona
No gastes dinero sin consultar con otra persona.
Un agente autónomo que se ejecuta durante la noche no debería tomar decisiones financieras costosas basadas en preferencias ambiguas o supuestas del historial de chat.
Antes de ejecutar acciones importantes (como gastar dinero en boletos), el agente debe detenerse, pedirle al usuario que confirme su presupuesto en voz alta y guardar esa confirmación explícita antes de realizar cualquier compra.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Ve al código de este paso:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
Ejecución
👉💻 En la terminal 1, presiona Ctrl + C y reinicia. El mismo comando que antes:
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"
Qué cambió desde el último paso:
Archivo | Qué cambió |
| Contiene lo que la persona acordó, como la oración que dijo. |
| Se agregó |
| Un nuevo primer nodo, |
No lo modeles. Almacena la oración.
Abre agent/concert/budget.py y lee todo el contenido:
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"
El nodo verifica dos cosas: que la persona lo haya dicho y que lo haya confirmado.
Luego, mantiene la oración en el estado de la sesión en lugar de en user:, a propósito. Se acuerda un presupuesto para una reserva. "USD 250 si son buenos" se dijo sobre esa banda, esa noche, y no tiene nada que ver con la reserva de entradas.
Lo mismo sucede con todo lo demás que dijeron.
No te detengas en el presupuesto. Las personas cambian de opinión mientras deciden, así que registra todo lo que solicitan aquí en lugar de confiar en lo que el agente recuerda.
"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)
Leer Solo los eventos que creó el usuario se ve como Solo
text
partes del function_response, por lo que la pregunta y la respuesta sobre el presupuesto no se repiten aquí. Sin duplicados.
Ten en cuenta que agree_budget la llama en cada ejecución, no solo en la primera, por lo que también se aplica una corrección escrita mientras el agente está en cola. Luego, open_the_night le entrega al selector ambas oraciones, la pregunta y el presupuesto, y el selector las lee.
El gráfico, un nodo más largo
Comencemos con una conversación:
# 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)],
)

Dos nodos se detienen y esperan, y son el mismo mecanismo. check_front espera un lugar y responde con un reloj. agree_budget espera a una persona y solo una persona puede responderla. Esa diferencia es el paso 10 en su totalidad.
Ejecútalo paso a paso
1. Comenzar
👉 Vuelve a cargar la IU web del ADK y elige el agente concert.
👉✨ Pide lo que realmente quieres:
Get us two tickets to the Amsterdam show.

En el paso 8, el gráfico no tenía ningún usuario, por lo que se descartó todo lo que escribiste y go funcionó tan bien como cualquier otra cosa. Aquí agree_budget llama a what_they_asked_for(ctx) en su primera línea, que recopila todo lo que escribiste y lo coloca en state["request"]. Luego, open_the_night se la entrega al comprador junto con el presupuesto.
No mencionaste un día, y Ámsterdam tiene dos espectáculos. Así que mira cómo el selector vuelve a recall() para eso, busca "Sam se va los días de semana" y toma el sábado.
En el navegador, la ejecución se detiene en un evento adk_request_input:
Un gráfico acaba de hacerte una pregunta. Es la misma interrupción que la espera en la fila del paso 8, pero ahora se señala a una persona para que responda la pregunta.
2. Responde de la forma en que lo hacen las personas
👉✨ En el cuadro Enter your response..., no en el cuadro de chat de la parte inferior, haz lo siguiente:
About a 100 for the cheap seats. 250 if they're the good ones.
Mira cómo se detiene y confirma lo siguiente:
[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. Confirmar
👉✨ Misma caja:
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.
Cuatro nodos se ejecutaron con una sola palabra. 👉 Abre la pestaña Estado y busca budget, tu oración, palabra por palabra.

Sin prefijo. Ese es el estado de la sesión, y es una decisión que vale la pena tomar a propósito si inicias una sesión nueva y el presupuesto se agotará, por lo que se te preguntará de nuevo.
4. Llega al frente y termina
👉🔴 Presiona SKIP THE WAIT en el panel del lugar.
👉✨ Responde la adk_request_input más reciente, de nuevo en su cuadro:
go

👉 Revisa el panel del lugar: Un pedido, a un precio dentro de lo que acordaste para ese tipo de asiento.

Cómo un gráfico le hace una pregunta a una persona
Abre nightly.py y lee agree_budget. Haz que tres cosas sean verdaderas juntas y espera el mismo error cuando falte alguna: pregunta para siempre.
@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?",
)
...
| Sin él, el nodo se trata como completado y se omite. | |
un estable | El valor predeterminado es un UUID nuevo por ejecución, por lo que no hay nada por lo que buscar la respuesta. | |
| dónde se encuentra la respuesta. No es el valor de retorno de | |
El gráfico
Es el gráfico del paso 8 con un nodo adicional al frente. 
Las restricciones confirmadas por el usuario se limitan a la sesión y vencen cuando finaliza la conversación. Si se conservan los límites temporales en las sesiones, se corre el riesgo de que las decisiones únicas se conviertan en valores predeterminados permanentes no deseados.
10. La pila de Cloud
Las laptops se suspenden y desaparecen los archivos locales
En tu laptop, un agente depende de que la computadora permanezca activa, de las bases de datos locales de SQLite y de los archivos locales. Si cierras la tapa de la laptop o implementas un contenedor en la nube que se reinicia, los archivos locales y la memoria desaparecen.
Para que se ejecute de forma confiable en producción, haz lo siguiente:
- Cloud Compute: Ejecuta el agente en un contenedor (como Cloud Run) que permanece en línea.
- Bases de datos duraderas en la nube: Reemplaza los archivos locales y SQLite por servicios en la nube (Cloud SQL para datos de sesión y Cloud Storage para archivos).
- Temporizadores de Cloud: Usa programadores administrados (como Cloud Scheduler y Pub/Sub) para que el agente se active incluso cuando la laptop esté apagada.
👉🔴 En el panel del lugar, presiona RESTABLECER EL LUGAR. Cada paso comienza en el mismo mundo: 8 programas, todos los asientos disponibles y el reloj en 1×.
👉💻 Ve al código final:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
Archivo | Qué cambió |
| nuevo Un FastAPI que entrega la página de chat y el extremo al que llama un programador |
| nuevo Python-slim y Uvicorn comunes. No tiene nada de específico del ADK. |
| copiado, por lo que la página se envía en el mismo contenedor que el agente |
| sin cambios. Byte a byte, la carpeta con la que terminaste el paso 9 |
La página que ya creaste
La** IU web del ADK** es estrictamente una interfaz de depuración local para inspeccionar el estado, los flujos de eventos y los gráficos de flujo de trabajo durante el desarrollo. No está empaquetado para la implementación en producción.
Para la producción, usa el frontend de chat independiente que se compiló en el paso 3. Maneja la mensajería del usuario y las interrupciones de interacción a través del extremo /wake sin exponer los controles internos de depuración.
Migración del estado local a los servicios en la nube
Detén el servidor y observa lo que escribió:
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
Si bien los archivos locales funcionan bien para el desarrollo, fallan en la producción porque los contenedores de Cloud Run usan almacenamiento efímero y local para la instancia.
Cuando se ejecuta en Cloud Run:
- Las instancias se ajustan a cero: Cuando se detiene el tráfico, el contenedor finaliza y se borra su sistema de archivos local.
- Las instancias se escalan horizontalmente de forma independiente: Las múltiples instancias simultáneas no comparten espacio en disco, y cada una se ejecuta con su propio sistema de archivos aislado.
Si un agente guarda un ticket de fila o un estado de sesión en un archivo SQLite local (sessions.db), las solicitudes posteriores que se enruten a una instancia de contenedor nueva o diferente encontrarán una base de datos vacía. Para mantener un estado duradero en los reinicios y el ajuste de escala, los archivos locales deben reemplazarse por servicios externos administrados.
En cambio, lo que necesitas es un estado que sobreviva a cualquier contenedor. Tres servicios, uno para cada archivo:
En tu laptop | En la nube | Qué es |
| Cloud SQL | una base de datos de Postgres administrada. Existe independientemente de si se está ejecutando algo |
| Cloud Storage | un bucket. Archivos como objetos, accesibles desde cada instancia a la vez |
| Cloud Storage | el mismo bucket |
| Cloud Scheduler | un cron que ejecuta Google. La terminal no es un despertador |
Cómo crearlos
Aprovisiona una base de datos de PostgreSQL, un usuario de la base de datos y un bucket de Cloud Storage. Ten en cuenta que la creación de la instancia de Cloud SQL tarda varios minutos y se inició en segundo plano durante setup.sh.
👉💻 Recopila la información y crea el resto:
cd ~/monstertix
./setup-cloud-state.sh
→ instance RUNNABLE
→ database adk created
→ user adk created
→ bucket created
→ memory memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env
Esa secuencia de comandos creó cuatro elementos (uno por cada fila de la tabla anterior) y escribió dos URIs en .env:
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
Implementa el agente con activadores programados administrados
Con los servicios listos, asígnale al agente su hogar permanente.
👉💻 Tres o cuatro minutos Inícialo y sigue leyendo:
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
La secuencia de comandos compila una imagen de contenedor a partir de agent/, la implementa en Cloud Run y aprovisiona la infraestructura compatible basada en eventos.
La implementación completa consta de cinco componentes distintos:
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 como fuente del activador
Cloud Scheduler reemplaza los temporizadores de ejecución locales (clock.py) por un servicio cron administrado que persiste independientemente de las sesiones de terminal locales, controla los reintentos automatizados y admite la programación de zonas horarias.
Pub/Sub como capa de desacoplamiento de mensajes
Colocar un tema de Pub/Sub entre Cloud Scheduler y Cloud Run proporciona tres ventajas arquitectónicas:
- Durabilidad de los mensajes: Pone en cola los eventos de activación entrantes si el servicio de Cloud Run se está iniciando en frío, se está volviendo a implementar o no está disponible temporalmente.
- Políticas de reintento integradas: Se reintentan automáticamente las fallas de entrega transitorias sin necesidad de lógica de reintento personalizada a nivel de la aplicación.
- Escalabilidad de fan-out: Permite que varios servicios descendentes se suscriban a los mismos eventos de programación sin necesidad de volver a configurar el programador.

La configuración de trigger_sources=["pubsub"] registra un extremo de envío compatible con las cargas útiles de sobre estándar de Pub/Sub, lo que traduce los mensajes entrantes directamente en eventos de invocación del agente.
→ agent https://concert-you-xxxx.run.app
→ topic presale-you created
→ iam concert-trigger@your-project... may invoke concert-you
→ push → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *
Háblale
👉 Abre la URL del agente en un navegador. 👉✨ Pídele que reserve:
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
Debería preguntarte cuánto quieres gastar
👉✨ Responde como lo harías en voz alta y, luego, confirma:
up to 250 a seat for the good ones
La ejecución está en la fila en este momento.
👉🔴 Presiona OMITIR LA ESPERA en el panel del lugar para que la fila esté lista cuando llegue el mensaje.
Para ejecutarlo en un programa real, el trabajo debe estar habilitado. jobs run rechaza uno pausado con Job.state must be ENABLED:
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 En cuestión de segundos, el agente ejecuta la compra dentro de los límites de presupuesto confirmados y devuelve automáticamente una confirmación del pedido.
👉🔴 Consulta el panel del lugar. Un pedido.
Sigue estos pasos para que se ejecute cada 5 minutos:
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
Limpia
Cuenta dos elementos implementados: el lugar y el agente que está junto a él. Ambos se reducen a cero, por lo que uno inactivo no cuesta nada. Bórralos de todas formas.
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
Ten en cuenta que, deliberadamente, dejamos Cloud SQL y el bucket intactos, ya que contienen tus datos y ambos mantienen la facturación. Cuando tengas la certeza de que terminaste, haz lo siguiente:
./destroy-agent.sh --all
Todo lo demás es local. Borra la carpeta cuando termines o consérvala y ejecútala en tu propio proyecto durante el tiempo que quieras.
El agente, sus herramientas, sus devoluciones de llamada y el presupuesto no cambiaron. Dos URIs y una variable de entorno lo hicieron.
11. Guía de solución de problemas y descripción general del sistema
Diferencias de entorno: arquitectura local vs. en la nube
Su laptop | En la nube | Qué cambió |
| Cloud SQL | una cadena de conexión |
|
| Un URI |
|
| una ruta |
| Un servicio de Cloud Run | un |
| Cloud Scheduler → Pub/Sub |
|
tú, en el teclado |
| Un valor predeterminado que se anula por solicitud |
La implementación del agente principal, incluidos root_agent, las definiciones de herramientas, las protecciones de devolución de llamadas y la aplicación del presupuesto, no cambia en absoluto. La comparación de solutions/step10_deploy/concert/ con el paso 9 muestra una coincidencia exacta byte por byte, lo que valida cómo la abstracción de servicio del ADK separa la configuración de la infraestructura de la lógica del agente.
Si te quedaste atascado
Puedes acceder al código de trabajo completo en cualquier paso:
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
Cuando algo se rompe
Síntoma | Corregir |
| ID incorrecto. |
El modelo devuelve el error 404 | Los alias de |
| Algo ya tiene el puerto 8000 u 8090. |
El agente no puede llegar al lugar |
|
La fila nunca avanza | Presiona OMITIR LA ESPERA. En 1×, la fila no llegará al frente por sí sola. |
Aparecerá un segundo ticket de la fila en el panel. | Respondiste en el cuadro de chat en lugar de en el cuadro de |
El menú desplegable está vacío o la app no se carga |
|
| Es lo esperado. |
Todo es extraño | Presiona RESTABLECER EL LUGAR y, luego, |
Archivo de memoria lleno de reservas duplicadas |
|
|
|
Se agotó el tiempo de espera de Cloud Shell | Vuelve a iniciar |