Tudo o que você precisa para criar agentes de longa duração no Google Cloud

1. Antes de começar

A maioria dos sites de venda de ingressos coloca os compradores em uma fila para evitar falhas no servidor e gerenciar a alta demanda. Você começa a compra e fica atrás de 14 mil pessoas. A fila se move por 40 minutos. Quando chegar a sua vez, você terá um curto período para escolher assentos e pagar. Se você perder esse período, seu lugar na fila será perdido.

Muitas tarefas do mundo real seguem este padrão: uma tarefa que leva muito mais tempo do que uma única conversa, executada em um sistema externo que você não controla. Por exemplo, aguardar o reabastecimento de um fornecedor, a aprovação de uma reivindicação ou em uma fila.

Os agentes de IA padrão ficam em uma única rodada de chat, que dura segundos e é puramente reativa. Elas não podem esperar 40 minutos nem agir de forma autônoma. Para resolver isso, você precisa de um agente de longa duração: um sistema que fica inativo durante longas esperas, mantém o contexto em reinicializações de processos e é retomado automaticamente quando ativado por um gatilho externo.

Neste workshop, você vai aprender a criar um agente de fila de ingressos para um show, abordando os principais padrões de arquitetura necessários para agentes de longa duração no Google Cloud.

O que você vai criar

O que você está criando

Neste workshop, você vai criar um agente de reserva de passagens de longa duração e aprender padrões importantes de software de agentes:

  • Suspensão e retomada de processos: estacione uma invocação de agente durante longos períodos de espera e retome a execução por gatilhos externos.
  • Armazenamento de estado durável: persista o estado e os artefatos da conversa em reinicializações de processos usando SQLite/Cloud SQL e Cloud Storage.
  • Proteções contra obsolescência e idempotência: verifique dados dinâmicos (como disponibilidade de assentos) antes de agir e use chaves de idempotência para evitar transações duplicadas.
  • Gráficos de fluxo de trabalho do ADK: estruturam a execução usando nós de código deterministas para regras procedimentais e nós de agente para tomada de decisões contextuais.
  • Aprovação humana: pause a execução para coletar e manter limites de gastos explícitos do usuário.
  • Implantação na nuvem: crie contêineres e implante o agente no Cloud Run com gatilhos do Cloud Scheduler e do Pub/Sub.

Começar a configuração

Primeiro, prepare seu ambiente e verifique se você tem acesso ao Google Cloud. Recomendamos executar esse comando no Cloud Shell, que já tem todas as ferramentas necessárias. Verifique se você tem espaço suficiente e se sua conta pode criar projetos.

👉💻 Clone o repositório e execute a configuração:

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

Ele vai perguntar isso uma 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]:

Em seguida, ele faz o seguinte, em ordem:

virtualenv

cria .venv e instala o ADK 2 nele

sign-in

gcloud auth application-default login se você ainda não tiver credenciais

project

aponta gcloud para seu projeto e o define como o projeto de cota.

APIs

que permite os dez recursos necessários para o workshop. aiplatform, run, cloudbuild, artifactregistry, storage, pubsub, cloudscheduler, cloudtrace, logging e sqladmin.

.env

grava o ID do projeto, da região e do modelo para que todos os scripts e adk web os coletem.

Verificação de modelo

faz uma chamada real para o Gemini, então um ID de projeto ou modelo ruim falha aqui

semente

cria uma sessão que já tem dois dias, que a etapa 3 abre

Cloud SQL

inicia uma instância do Postgres em segundo plano

venue

implanta seu próprio vendedor de ingressos simulado no Cloud Run

👉💻 Ative o ambiente:

cd ~/monstertix
source .venv/bin/activate

Seu comando agora começa com (.venv).

Visão geral do cenário

Neste cenário de workshop, seu agente reserva dois ingressos para um show ("The Midnight Signal" em Amsterdã). As preferências do usuário armazenadas na memória histórica incluem:

  • Precisa ser um show de fim de semana (Sam não pode ir em noites de semana).
  • Evite os assentos da parte de cima do Ziggo Dome (visibilidade ruim).
  • O preço desejado é de aproximadamente US $200 por ingresso, com um limite máximo de US $250.

The Midnight Signal, em turnê

Verificar a instalação

👉💻 Um comando, oito tiques:

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

👉 Abra outras guias do navegador:

Onde

O que é

$VENUE_URL/panel

O painel de controle, seus botões. setup.sh imprimiu este URL e o colocou em .env

2. Comandos para autonomia?

Os comandos não podem ser iniciados por conta própria

Um comando é apenas um texto transmitido a um modelo de IA quando uma chamada acontece. Ele não pode ser executado sozinho, se ativar ou iniciar uma nova chamada.

Se você pedir para um agente "voltar em 10 minutos", nada vai acontecer depois que o turno atual terminar. O modelo não tem um relógio ou loop interno. Para tornar um agente autônomo, algo fora dele precisa ativá-lo e chamá-lo.

Onde esta etapa se encaixa

Iniciar o equipamento

Vamos começar testando as ferramentas de desenvolvedor. adk web inicia a interface da web do ADK, a interface de desenvolvimento fornecida com o ADK. Ele permite conversar com o agente que você está criando sem escrever um front-end primeiro e mostra o que uma janela de chat não pode mostrar, como cada chamada de ferramenta e seus argumentos, o estado da sessão à medida que muda, os artefatos salvos pelo agente e os eventos brutos abaixo. Você não o exporia em produção. É a melhor janela para um agente em execução enquanto você aprende. Na etapa 3, você cria o front-end que será exposto, e na etapa final, você o implanta.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Terminal 1: carregue o código desta etapa e inicie a interface da web do ADK:

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

O use-solution.sh copia o código concluído de uma etapa para agent/, que é onde você trabalha. Cada etapa começa com ele e imprime quais arquivos foram alterados para que você saiba o que ler.

O que você precisa para começar:

Arquivo

O que é

agent.py

o agente: um nome, um modelo, uma instrução, uma lista de ferramentas

tools.py

search_events e get_seatmap

venue.py

um cliente HTTP para o local. Nada interessante

config.py

qual modelo usar e uma verificação de que suas credenciais funcionam

. ./set_env.sh ativa o virtualenv, carrega seu projeto e modelo e exporta $WORKSHOP, o caminho absoluto para ~/monstertix. Execute-o em todos os terminais que você abrir. Ele imprime o que foi definido:

  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

👉 Abra as primeiras guias do navegador:

Onde

O que é

Interface da Web do ADK

localhost:8000: seu agente e as guias "Estado", "Eventos" e "Artefatos"

Fale com ele

👉✨ Na interface da web do ADK, digite:

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

Ele pesquisa o tour, lê os mapas de assentos e considera seu orçamento e sua empresa. Esse é um assistente realmente competente. No entanto, quando a pré-venda começa às 10h, o agente não é executado automaticamente porque nenhum processo ou acionador em segundo plano está em execução.

Leia o que acabou de acontecer

O meio da tela não é apenas um registro de chat. Cada etapa da vez é numerada, incluindo as chamadas de ferramenta:

  ▸ 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

👉 Clique em uma linha ⚡. O painel à esquerda mostra os argumentos escolhidos pelo modelo. Clique no ✓ abaixo dele para ver o JSON que foi retornado.

As informações do tour não estão presentes nos dados de treinamento ou no comando do modelo. O agente recuperou esses dados executando a ferramenta search_events, que consultou a API Venue.

Abra o código que você acabou de executar

Você viu o funcionamento e o rastreamento. Agora, analise o código.

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

👉💻 Abra agent/concert/agent.py. Todo o agente está na parte de baixo do arquivo:

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

Um nome, um modelo, uma descrição de como o agente deve se comportar e uma lista de funções do Python. Isso é tudo que um Agent é. O adk web o encontrou porque o arquivo se chama agent.py e a variável se chama root_agent.

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

Uma função comum. Sem decorador, sem registro, sem esquema para escrever. O ADK lê a assinatura e a docstring e cria a definição de ferramenta que o modelo vê. Por isso, essa docstring é importante, e excluir um argumento mudou o comportamento do modelo.

O modelo nunca executa seu código. Quando você viu search_events({"city": "Amsterdam"}), o modelo estava pedindo essa ligação. O ADK executou a função, devolveu o resultado e o modelo escreveu uma frase sobre isso. O loop é:

👉 Os ícones na borda esquerda mudam o que o painel mostra. O segundo é o gráfico de agentes. Teste e você vai encontrar duas ferramentas:

Gráfico do agente mostrando as ferramentas search_events e get_seatmap

Então diga para ele não dormir

O que você faria para corrigir isso? Tente o que a maioria das pessoas pega primeiro. Relembre INSTRUCTION da etapa 1, a prosa entregue ao modelo a cada vez, e reescreva.

👉💻 Abra agent/concert/agent.py. Abaixo de INSTRUCTION, há outro já escrito para você. Se quiser, mude você mesmo:

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

👉💻 No terminal 1, reinicie:

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

👉✨ Conte o plano exatamente como você faria com uma pessoa:

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.

Ele vai concordar e parecer que é verdade.

👉 Agora, aguarde. Não digite mais nada. Assista o painel.

Aguarde um minuto. NADA ACONTECE!

Por que esse comando nunca ia funcionar

Leia de novo com um novo olhar. Monitorar. No momento em que ele for aberto. Continue verificando. Cada uma delas descreve algo que acontece ao longo do tempo.

Agora, lembre-se do que instruction= realmente é: uma string transmitida ao modelo como parte da solicitação no momento em que alguém envia uma mensagem. Nada lê entre as jogadas.

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

Uma instrução não pode invocar a função em que foi escrita.

Variações de comando que falham

A tentativa de forçar a execução autônoma por instruções de comando falha, não importa a redação. Tentativas comuns de comandos, como:

  • "Defina um timer para as 10h de terça-feira e compre os ingressos."
  • "Verifique a cada cinco minutos se a pré-venda começou."
  • "Agir de forma autônoma sem esperar a entrada do usuário."

Todos falham pelo mesmo motivo: os comandos são somente leitura quando uma invocação ocorre. Sem um caller externo, nenhum código é executado entre as rodadas.

3. Inatividade orientada por eventos

Os agentes não ficam acordados para sempre

Deixar um processo em execução fazendo polling contínuo desperdiça memória, gasta dinheiro e causa falhas se o notebook entrar em modo de espera ou for reiniciado.

Em vez de manter um agente em execução sem parar enquanto ele espera, um agente de longa duração deve ficar completamente inativo. Um evento externo (como um timer, um webhook ou uma mensagem) ativa o agente somente quando há trabalho real a ser feito.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Carregue o código desta etapa:

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

Observe o que foi instalado: monstertix/, o servidor, o relógio e mais uma coisa que você ainda não viu. Verifique se ele deixou agent/ sozinho, porque nada sobre o agente muda nesta etapa. Guarde isso, é o ponto principal.

Arquivo

O que é

monstertix/server.py

a cada conversa. Um servidor da Web com um Runner atrás dele

monstertix/clock.py

a cada conversa. Algo que sabe a hora e chama o servidor

Ele tem duas metades, que ficam em dois arquivos até a nuvem: O gatilho e o acionador, mantidos separados

se torna, na etapa 10

o gatilho

um endpoint que pode executar o agente

Cloud Run

o acionador

algo com um relógio que o chama

Cloud Scheduler

O gatilho: seu próprio servidor da Web

Sim, um servidor da Web. Algo precisa estar ouvindo antes que qualquer coisa possa chamar. A interface da Web do ADK das etapas anteriores é uma interface de desenvolvimento que pode executar um agente.

👉💻 Abra 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"])]),
    ):
        ...

Um Agent não é executado por conta própria. É uma descrição: um nome, um modelo, uma instrução, uma lista de ferramentas. Algo precisa receber uma mensagem recebida, encontrar a sessão certa, entregar ao modelo a conversa até o momento, executar as ferramentas solicitadas, enviar os resultados de volta, fazer um loop até parar de chamar ferramentas e gravar cada etapa no armazenamento. Esse algo é o Runner.

O que um Runner faz com seu agente

A interface da Web do ADK tem uma padrão para você. Aqui, você cria por conta própria, e essa é toda a diferença entre a interface da Web do ADK e algo que você pode implantar.

👉💻 Terminal 2: inicie-o:

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

Leia a última linha. O gatilho está em execução e o agente está carregado, mas nada acontece. Separe as duas ideias: ser chamável e ser chamado.

E uma porta da frente, já que estamos aqui

Embora adk web seja uma ótima ferramenta de desenvolvimento, ela não deve ser usada em produção. Não ative esse recurso ao implantar na produção, e não o indique a um cliente se ele estiver ativado. Portanto, o mesmo servidor também veicula uma página.

👉 Abra http://127.0.0.1:8090 em um navegador (o mesmo endereço).

Conheça a MonsterTix. Uma janela de chat, um nome no canto, dois botões. Ele faz uma postagem em /wake e renderiza o que é retornado. Além disso, funciona sem alterações em qualquer endpoint que responda da mesma forma.

Na página

O que está por baixo

a conversa

POST /wake, uma mensagem por vez

+ Nova sessão

um ID de sessão novo: nova conversa e, mais tarde, um novo orçamento

Redefinir memória

exclui todas as sessões e o arquivo de memória. Ele pergunta primeiro e mostra exatamente onde esse estado fica.

👉✨ Diga algo para ele — What Amsterdam dates are there? — e observe o terminal 2. Mesmo Runner, mesmo agente, mesmas linhas de registro. Apenas o autor da chamada mudou.

O acionador: algo que sabe a hora

👉💻 Abra monstertix/clock.py. Analise o 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

É um mecanismo muito simples: sleep, POST, talvez repetir. Todos os agendadores que você usou fazem o mesmo, além de novas tentativas, fusos horários e uma garantia de que ele vai sobreviver a uma reinicialização.

👉💻 Terminal 3: disparar uma vez, daqui a 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

👉 Agora pare de digitar. Sem dúvida. Assista ao terminal 2.

Um minuto depois, sem ninguém no 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.

Essa é a resposta da etapa 2. Algo fora do agente o chamou, e esse algo era um sleep e uma solicitação HTTP.

👉💻 Ctrl-C quando você já assistiu algumas vezes. Use --every 30 primeiro se quiser que ele se repita.

Agora leia o que ele disse

"Comprei dois ingressos para The Midnight Signal em Amsterdã, seção A, US$ 420."

👉🔴 Confira o painel do local para ver se o ingresso foi comprado.

Então, o que ainda está errado?

Ele comprou algo. Veja o que ele teve que adivinhar. Ele escolheu Amsterdã e a seção A porque foram as primeiras opções encontradas. Você pediu para o agente comprar diretamente, e ele não sabe que Sam não pode ir durante a semana. Um agente que age com base em palpites às 3h é pior do que um que não faz nada.

Acordar é a parte fácil. Um agente que acorda sem nada (sem histórico, sem preferências, sem ninguém ouvindo) ainda não vale a pena ser ativado.

4. Gerenciar o ciclo de vida do contexto

As informações ficam em lugares diferentes por períodos diferentes

Um agente armazena fatos em vários lugares: na conversa por chat, no estado da sessão, em arquivos (artefatos) ou na memória de longo prazo do usuário. Cada lugar dura um tempo diferente.

Se você colocar informações no lugar errado:

  • Ele desaparece muito rápido: detalhes importantes são apagados quando o chat termina.
  • Ele fica por muito tempo: fatos temporários contaminam a memória de longo prazo e confundem conversas futuras.

Você precisa corresponder cada informação ao local e ao tempo de armazenamento corretos.

Carregar o código

👉🔴 No painel do local, pressione REDEFINIR O LOCAL.

👉💻 No terminal 1, execute:

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

O que mudou desde a última etapa:

Arquivo

O que mudou

tools.py

note_companion adicionado. O get_seatmap agora salva o mapa de assentos em um arquivo

memory.py

a cada conversa. Nosso próprio repositório de memória

agent.py

mais três ferramentas na lista, e a instrução diz ao agente para usá-las

👉💻 Reinicie o agente no 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"

Duas flags que você nunca usou:

Sinalização

O que é

session_service_uri

O ADK grava cada mensagem, chamada de ferramenta e mudança de estado como linhas (banco de dados SQLite).

artifact_service_uri

Arquivos que o agente salva.(Arquivos)

Sem eles, o ADK mantém tudo na memória e descarta quando você interrompe o processo. Essas duas configurações são o que faz com que ele persista.

O banco de dados sessions.db contém uma conversa anterior inicializada (two-days-ago) com 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?

Um arquivo de memória de três reservas anteriores, memory/userx.md, que você pode abrir em um 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 isso que as linhas de semana em âmbar são importantes. Um show em uma noite da semana custa US $10 a menos por assento, tem lugares melhores disponíveis e fica no topo da lista, mas é a resposta errada para essa pessoa. O único motivo para o agente saber isso é um arquivo que ele gravou após a última reserva. As etapas 4 e 5 vão determinar se esse fato vai sobreviver.

As três ferramentas que adicionamos

As três ferramentas e onde cada uma escreve

O note_companion registra quem está chegando e quais são os limites:

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

Adicione porque "Sam não pode durante a semana" ainda precisa ser verdade na semana que vem. Escreva em user:prefs para que isso aconteça e observe a parte user: do nome fazer o trabalho. Vamos voltar a esse assunto abaixo.

recall

e

remember lê e grava um arquivo de texto, memory/userx.md. Mantenha essa posição por um momento.

👉 Agora, observe os dois lugares em que o agente armazena o contexto:

local

O que ele contém

Estado

a memória desta sessão, como pares de chave-valor. Você está aqui na etapa 4.

Artefatos

arquivos salvos pelo agente, que nunca entram no comando

Confira o que mudou em

get_seatmap

. Antes, ele retornava todo o mapa de assentos. Agora ele salva o mapa de assentos em um arquivo e retorna um breve resumo:

# 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

Confira por que essa mudança é importante.

Cada payload de retorno de ferramenta é anexado ao histórico da conversa e reenviado ao modelo em todos os turnos subsequentes. Retornar objetos JSON grandes (como um mapa de assentos de 6 KB) aumenta o consumo de tokens em cada interação subsequente.

Salve em um arquivo e pague por um nome de arquivo. Deixe o agente pedir o detalhe de volta com load_artifact se precisar, mas espere que na maioria das vezes isso não aconteça.

Onde o arquivo vai: o serviço de artefato

Confira a linha em get_seatmap novamente, em agent/concert/tools.py:

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

tool_context vem do ADK, que o transmite para sua ferramenta como um argumento, e save_artifact é um método dele. É nessa linha que sua ferramenta entrega os bytes ao ADK.

Em seguida, o ADK os grava usando o serviço de artefato com que foi iniciado. Você escolheu isso com a flag:

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

O ADK tem três locais para armazená-los:

URI

Para onde os bytes vão

memory://

RAM. Desaparece quando você interrompe o processo. Esse é o padrão se você não transmitir nenhuma flag.

file://...

uma pasta no disco. O que você está usando agora

gs://bucket

para o Cloud Storage. O que o agente implantado usa na última etapa

👉💻 Aqui escolhemos os arquivos:

ls -R ~/monstertix/artifacts

Para mover do laptop para o Cloud Storage, mude file:// para gs://. Vamos ver isso na etapa 10.

Nosso repositório de memória

Memória como uma interface, com um arquivo por trás

O ADK oferece uma interface para memória de longo prazo chamada BaseMemoryService. Ele tem dois 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

O ADK oferece três implementações:

Turma

URI

O que é necessário

InMemoryMemoryService

memory://

do zero. Perde tudo quando você interrompe o processo

VertexAiRagMemoryService

rag://

um corpus de RAG da Vertex AI

VertexAiMemoryBankService

agentengine://

um recurso do Agent Engine

Vamos escrever o nosso para este workshop. Descarte o InMemoryMemoryService, que esquece tudo quando você interrompe o processo, e os outros dois, que precisam de recursos na nuvem que levam minutos para serem criados. Escolha um arquivo, porque você pode inserir dados nele com antecedência e ler em um editor. Considere memory.py como sua própria implementação. Estamos armazenando tudo em um arquivo Markdown:

class MarkdownMemoryService(BaseMemoryService):

    async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
        path = self._path(user_id)                      # memory/userx.md
        return SearchMemoryResponse(memories=[
            MemoryEntry(content=types.Content(
                role="user", parts=[types.Part(text=path.read_text())]))])

SearchMemoryResponse e MemoryEntry são tipos do ADK, e o Memory Bank retorna os mesmos objetos. Um deles tem um serviço gerenciado por trás. O nosso tem um arquivo.

Escolha um arquivo por um motivo: você pode abrir e ler. Toda a memória de longo prazo do seu agente é uma página de texto.

Então, para onde você aponta?

Para recapitular a implementação deste workshop, dois dos três serviços são escolhidos na linha de comando. O terceiro foi implementado por nós mesmos pelo motivo acima:

Serviço

Onde você o definiu

Este workshop

sessão

--session_service_uri em adk web

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

artifact

--artifact_service_uri em adk web

file://$WORKSHOP/artifacts

memória

em nenhum lugar na linha de comando

MEMORY_DIR, lida em agent/concert/memory.py

Assim, memory.py cria o próprio serviço de uma variável de ambiente:

# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)

. ./set_env.sh exporta os dois. O Get MEMORY_DIR define onde o agente lê a memória, e MEMORY_USER determina o arquivo.

Encontre onde as coisas estão

Agora você tem quatro lugares para armazenar um fato. Analise as quatro.

Primeiro, sua própria sessão

👉 Clique em Nova sessão na barra superior.

👉✨ Diga três coisas, uma após a outra:

Sam can't do weeknights, by the way.

Nova sessão, primeira vez

Show me the seat map for the Amsterdam Saturday show.

Nova sessão, segunda vez

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

Essa terceira mensagem é importante. Ele encerra o turno que leu o mapa de assentos, e encerrar um turno é o que faz temp: desaparecer.

Três coisas foram armazenadas em três lugares diferentes:

O que foi armazenado

Qual ferramenta fez isso?

Para onde ele foi

1

Sam não pode à noite durante a semana

note_companion

user:prefs, no estado da sessão

2

o mapa de assentos completo, 6 KB

get_seatmap, via save_artifact

o serviço de artefato

3

um resumo do mapa de assentos em uma linha

get_seatmap, em temp:seatmap

nowhere

👉 Agora confira. As guias Estado e Artefatos estão à esquerda de adk web.

Guia "Estado"

Guia "Artefatos"

Na guia Estado, observe que temp:seatmap foi omitido. As chaves de estado com um prefixo temp: existem apenas durante a invocação ativa e são removidas automaticamente pelo ADK antes de persistir o estado da sessão no banco de dados.

Depois, a conversa mais antiga

👉 Mude para two-days-ago no menu suspenso de sessão na parte de cima.

Essa sessão contém o histórico de conversas pré-preenchido no banco de dados.

A sessão de dois dias atrás

👉 Confira o fluxo de eventos (coluna do meio). As conversas mais antigas foram substituídas por um bloco de eventos resumidos.

Turnos compactados

👉 Confira a guia Estado. Três tipos de chaves estão juntos:

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

O prefixo user: (por exemplo, user:prefs) define o escopo do estado em todas as sessões de um determinado usuário. Por outro lado, chaves sem prefixo (

target_event_id

,

party_size

) representam o estado simples da sessão, que é limitado estritamente à sessão ativa e excluído quando ela termina.

👉 Agora compare as duas guias "Estado". Enquanto user:prefs e os artefatos armazenados persistem, temp:seatmap está ausente das duas sessões. As chaves de estado com um prefixo temp: são estritamente temporárias e nunca são persistidas no banco de dados.

Em seguida, o arquivo

👉💻 Abra no editor:

cloudshell edit ~/monstertix/memory/userx.md

Três reservas, desde janeiro. O agente lê esse cookie quando chama recall(), e ele ainda estará lá mesmo que os dados da sessão sejam perdidos.

Os quatro lugares

Onde ele fica

A próxima mensagem vai sobreviver?

Sobrevive a uma reinicialização?

Sobrevive à exclusão da sessão?

temp:seatmap

no

não

não

party_size: estado de sessão simples, em two-days-ago

sim

sim

não

user:prefs

sim

sim

sim

memory/userx.md

sim

sim

sim, e sobrevive à exclusão do banco de dados

um artefato

sim

sim

depende do serviço

Leia essa tabela como sendo sobre os contêineres, não sobre o que está neles.memory/userx.md sobrevive a tudo e só contém o que remember() escolheu colocar lá. Nada grava sua conversa atual nele. Portanto, o arquivo sobreviver a uma sessão não é o mesmo que o agente lembrar do que você estava falando. A etapa 5 é onde essa distinção se torna importante.

Cinco lugares em que um fato pode ficar e quanto tempo cada um dura

O que é o estado

Pense no estado como um dicionário que viaja com a sessão. Leia e grave usando suas ferramentas com tool_context.state, e deixe o ADK salvar junto com a conversa.

O ADK atribui o significado dos prefixos. Você não os define em nenhum lugar. O ADK lê a primeira parte do nome da chave e a usa para decidir onde o valor vai e quanto tempo ele dura:

Uma chave chamada

O ADK armazena

Durações

party_size

com a sessão

até que a sessão seja excluída

user:prefs

contra o usuário

em todas as sessões que o usuário tem

app:tour_id

contra o app

em todos os usuários

temp:seatmap

em nenhum lugar

uma invocação

Uma invocação é um único ciclo de solicitação-resposta, incluindo todo o raciocínio do modelo, a execução da ferramenta e a resposta final.

Quando uma invocação termina, o ADK grava o novo estado no banco de dados e remove todas as chaves temp: no caminho.

Uma coisa ficou sem explicação

Em two-days-ago, você viu um resumo no lugar das primeiras falas de alguém. O ADK escreveu automaticamente quando a conversa ficou longa o suficiente.

Todo fato tem uma vida útil

5. Degradação e compactação de contexto

Resumos: esqueça os detalhes

Quando uma conversa fica muito longa, o sistema resume as mensagens mais antigas para economizar espaço e tokens.

Os resumos são bons para entender o contexto geral e economizar o uso de tokens.

A compactação funciona na sessão. A memória é um repositório separado

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Acesse o código desta etapa:

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

O que mudou desde a última etapa:

Arquivo

O que mudou

agent.py

o agente está envolvido em um App, com a compactação ativada. Um segundo agente, budget_split, faz cálculos com os tíquetes.

todo o restante

inalterado

agent.py não termina mais com root_agent. Ele termina assim:

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

Saiba que adk web procura primeiro uma variável chamada app e volta para root_agent. Encapsule o agente em um App sempre que precisar de algo que se aplique a todo o aplicativo, e não a um agente: compactação aqui e capacidade de retomada mais tarde nas próximas etapas.

compaction_interval=3 significa que o ADK faz um resumo a cada três turnos. Um turno é você dizer algo e o agente terminar a resposta, não importa quantas ferramentas ele tenha chamado ao longo do caminho. Três é pequeno, então é acionado durante um workshop. Um app real usaria um número muito maior.

overlap_size=1 significa que cada resumo começa um turno antes do necessário.

O ADK resume as conversas 1, 2 e 3. Três turnos depois, ele resume o próximo lote. Com overlap_size=1, o segundo resumo começa novamente na rodada 3, não na 4:

Dois resumos, sobrepostos em uma rodada

Observe que a rodada 3 é resumida duas vezes. Isso é proposital. Se você fez uma pergunta na rodada 3 e o agente respondeu na rodada 4, uma divisão limpa coloca a pergunta em um resumo e a resposta em outro, deixando os dois confusos. Repetir uma vez é uma garantia barata contra o corte de um pensamento pela metade.

Defina os dois campos. O ADK não tem um padrão para nenhum dos dois.

Compactação e seu serviço de memória nunca se encontram

É importante ser explícito, porque as duas palavras parecem relacionadas, mas os sistemas não são.

EventsCompactionConfig está no App em agent.py, ao lado de root_agent. É uma propriedade do aplicativo, não de um serviço:

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

Um resumo reescreve a conversa. Ele não reescreve o arquivo.

É por isso que remember() existe como uma chamada deliberada. Tudo o que sobrar na transcrição será lido como uma paráfrase escrita por outra pessoa. Tudo o que for gravado no arquivo será lido exatamente como foi armazenado, amanhã e na próxima semana. O final desta etapa é quando você vê essa diferença acontecer.

Onde o resumo é armazenado

Em sessions.db, na tabela events, um registro de compactação é gravado em uma linha de evento pelo agente, com um carimbo de data/hora como qualquer outro evento de turno.

Um evento comum tem um campo content, e o resumo fica em actions.compaction.

👉💻 Veja no seu próprio banco de dados:

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

Esta é a aparência da forma do 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

O fluxo de eventos extrai cada evento do conteúdo dele. O resumo no banco de dados e sendo enviado ao modelo a cada vez.

O segundo agente

budget_split é um agente totalmente separado, não uma ferramenta:

# 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 que um agente e não uma função simples? Porque o trabalho precisa de um modelo. Leia "qual destas seções se encaixa em um orçamento de US $200 para quatro pessoas e qual é o total" como uma operação aritmética envolta em julgamento, com uma resposta que uma pessoa precisa conseguir ler. Deixe uma função do Python multiplicar e não peça para ela decidir quais opções valem a pena mencionar.

Então, por que não deixar o agente principal fazer isso? A próxima linha include_contents="none" significa que o agente não recebe nada da conversa. Sem grupo de chat, sem preferências, sem 40 datas de turnê. Ele recebe a solicitação e nada mais. Essa flag só existe em um Agent, que é o motivo real de ser um agente em vez de uma função.

Um subagente que vê a solicitação e nada mais

Prefira o comando curto: mais barato, mais rápido e muito mais difícil de desviar. Confie na aritmética, que é imune a algo dito há 20 rodadas porque nunca viu isso.

Por que

AgentTool

? Inclua o agente para que o principal possa chamá-lo como qualquer outra ferramenta. Do lado do modelo, leia budget_split como idêntico a search_events.

E

output_key="budget_plan" coloca a resposta no estado da sessão com esse nome, em vez de deixá-la solta na conversa. Assista para entender por que isso é importante.

Executar

👉💻 No terminal 1, pressione Ctrl-C e reinicie com as mesmas flags:

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

👉 Inicie uma Nova sessão e tenha uma conversa real. Cinco ou seis voltas. 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?

👉 Abra a guia Estado.

Encontre budget_plan lá, com a resposta completa do agente aritmético.

budget_plan após a primeira pergunta sobre orçamento

👉✨ Agora mude os números:

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

👉 Veja budget_plan de novo. Ele foi substituído, não anexado. Isso porque output_key contém o mais recente.

budget_plan após a segunda pergunta sobre orçamento

O que você não pode ver: o resumo

Em algum momento, o ADK resumiu as conversas anteriores.

👉💻 Em um novo Terminal, execute para ler diretamente:

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

Leia o que ele escreveu

Confira um exemplo real e resumido:

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

Ele manteve os preços, as seções, as cidades e as preferências, e observou corretamente que nada havia sido decidido ainda.

Três coisas decorrem disso:

É uma paráfrase. Nada aqui é o que alguém disse. É um relato do que foi dito, produzido pelo mesmo tipo de processo que produz tudo o que o modelo erra ocasionalmente.

Observação: as strings de resumo são armazenadas em registros de eventos de sessão internos e não são expostas diretamente como guias separadas da interface.

A cada três turnos, em uma conversa que agora é parcialmente composta de resumos anteriores.

Tente fazer uma reserva sem repetir o que já foi dito

Em uma nova sessão: não inicie uma nova. O resumo que você acabou de ler está nos eventos desta sessão, e esse é o único motivo pelo qual o agente ainda sabe algo sobre Amsterdã.

👉✨ Não dê nada de novo:

Book us something.

Assista o agente perguntar qual programa, qual seção e quantas pessoas. Marque como correto: o próprio resumo diz que o usuário nunca confirmou nada, então o agente perguntou. Dê crédito à compactação.

Mesmo quando a compactação é bem-sucedida, resumir turnos anteriores pode introduzir ambiguidade, o que exige que o agente reconfirme as restrições com o usuário.

👉✨ Responda e deixe o agendamento ser concluído:

Amsterdam Saturday, section C, 8 people

Ele lê o pedido antes de gastar qualquer coisa:

"Você quer reservar oito ingressos na seção C para The Midnight Signal em Amsterdã no sábado, 14 de novembro. O custo total será de US $760. Posso comprar essas passagens?"

👉✨ Confirme:

yes

Agora ele informa que funcionou:

Sua compra de oito ingressos na seção C para The Midnight Signal em Amsterdã no sábado, 14 de novembro, no valor total de US $760, foi concluída. O ID do seu pedido é ord_227877fe."

Confira se ele fez isso.

👉 Mude para o painel de locais. Não acredite só no que o agente diz.

A ordem no painel do local

No painel

O que você vai ver

Ingressos comprados

1 e correto: uma reserva, um pedido

o pedido abaixo

ord_xxxxxxxx — 8× section C · 760

O tour, sábado em Amsterdã

a seção C diminuiu em 8

Atividade

ord_xxxxxxxx — 8x section C @ 95 = 760

Confira no painel para saber quando começa a venda de ingressos.

Por que user:prefs foi a escolha certa

Lembre-se de note_companion escrever a restrição de Sam na última etapa de user:prefs e a objeção óbvia: por que se preocupar com uma ferramenta quando o agente pode simplesmente se lembrar dela?

Confira o motivo. Não inclua estado na conversa, e nenhum resumo o menciona. Verifique user:prefs e budget_plan em relação a como eles entraram e veja que não mudaram. Em seguida, confira a transcrição.

Um resumo mantém o formato geral e perde os detalhes. Armazene em outro lugar tudo o que você não pode perder.

6. Retomada do processo

Esperar 40 minutos sem ficar on-line

Quando um agente precisa esperar em uma fila de tíquetes de 40 minutos, manter um processo Python ativo e aguardando é frágil e caro.

Para lidar com longas esperas de maneira adequada:

  1. Salvar estado em um banco de dados: armazene onde o agente pausou para que o processo possa ser encerrado com segurança.
  2. Ativação sob demanda: quando a espera termina, um sinal externo reinicia o agente e retoma a execução de onde parou.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Acesse o código desta etapa:

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

Executar

O que mudou desde a última etapa:

Arquivo

O que mudou

tools.py

join_queue e check_queue foram adicionados

agent.py

join_queue está envolvido em LongRunningFunctionTool, e o App ativa ResumabilityConfig

join_queue retorna imediatamente com um tíquete em vez de bloquear, e ResumabilityConfig permite que a execução seja pausada e retomada depois.

O que a capacidade de retomada adiciona

👉💻 Terminal 1: o agente, sem alterações:

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"

👉✨ Peça ao agente para comprar:

Get us two tickets to the Amsterdam show.

Ele chama recall, depois search_events, depois join_queue e responde com algo parecido com isto:

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

Perceba que ele escolheu sábado sem perguntar. Há dois shows em Amsterdã, o dia 17 é uma terça-feira, e o arquivo de memória da última etapa diz que Sam não trabalha à noite durante a semana. Esse é um fato de uma reserva anterior que decide uma questão nesta.

Observe também que ele entrou na fila antes de analisar um único assento ou preço e não disse nada sobre eles. Essa é a regra do local. Você compra os ingressos até chegar à frente. Portanto, a única coisa que vale a pena ter cedo é um lugar na fila. Quais assentos escolher é uma questão para daqui a 40 minutos.

👉 Consulte o painel de controle. O banner diz:

A fila, ainda em movimento enquanto nada seu está em execução

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

A fila tem 40 minutos de espera e não vai diminuir enquanto você trabalha.

👉💻 Encerrar o agente:

Ctrl-C

👉💻 Inicie novamente no 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"

👉✨ Na mesma sessão, pergunte ao agente qual é a posição na fila:

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

O mesmo bilhete, após a reinicialização

Compare o tíquete e observe como a posição continua caindo enquanto nada estava sendo executado. O agente não sabia que tinha sido morto. Ele chamou check_queue com o tíquete que recebeu antes da reinicialização, porque esse tíquete estava no disco.

👉💻 Saiba o motivo:

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

Cada turno de cada conversa é uma linha, e o turno em si, quem disse, qual ferramenta foi chamada, o que foi retornado e observe que é JSON dentro de event_data.

👉💻 Encontre seu ingresso para a fila:

sqlite3 sessions.db "select event_data from events;" \
  | grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203

Quando join_queue retorna, o tíquete é salvo diretamente no banco de dados. Ao reiniciar, o ADK recarrega as conversas e os resultados de ferramentas anteriores do disco, permitindo que o agente continue de onde parou.

E observe o que não foi reiniciado. O agente retoma diretamente em vez de reproduzir o fluxo de trabalho desde o início. Como LongRunningFunctionTool e ResumabilityConfig persistem o estado de execução no disco, o ADK sabe que o agente já está esperando na fila, evitando que ele pesquise novamente as datas da turnê ou solicite um ingresso duplicado.

O mesmo ingresso, em um processo que morreu

Por que join_queue e não check_queue

As duas funções retornam em milissegundos. A distinção é se o job subjacente está concluído quando a função retorna.

o que ele retorna

O trabalho foi concluído?

join_queue

Tíquete na fila e posição inicial

Não.O processamento continua de forma assíncrona no sistema externo.

check_queue

Status atual da fila

Sim. Retorna uma resposta imediata e pontual.

LongRunningFunctionTool foi projetado para operações que retornam um identificador para trabalho pendente. O encapsulamento de join_queue permite que o agente suspenda a execução enquanto a fila externa avança.

Em contraste, check_queue é síncrono e completo. Encapsular faria com que o agente suspendesse a execução incorretamente sempre que verificasse o status, mesmo quando chegasse ao início da fila e precisasse concluir a compra imediatamente.

Qual chamada ainda está pendente quando ela retorna

O que fez isso funcionar

Três componentes permitem a suspensão e a recuperação da execução:

# 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 uma ferramenta como não bloqueadora, retornando um identificador ativo para que o agente possa esperar sem travar a linha de execução.
  • ResumabilityConfig: instrui o ambiente de execução a pausar o ciclo de vida de execução em uma tarefa incompleta e retomar do último estado registrado.
  • Session Storage URI (--session_service_uri): direciona a persistência de estado para armazenamento externo (por exemplo, SQLite, Cloud SQL) em vez de memória de processo, evitando a perda de dados em reinicializações.

7. Como lidar com dados desatualizados

As informações ficam desatualizadas enquanto você espera

O histórico de conversas persistente não é um estado em tempo real. Durante longos intervalos de espera, dados externos, como disponibilidade ou preços de passagens, podem mudar sem que o agente saiba.

Um agente com informações ausentes faz uma pausa para pedir esclarecimentos, mas um agente com dados desatualizados age imediatamente com base em premissas desatualizadas. Para evitar ações inválidas, os dados sensíveis ao tempo precisam ser verificados novamente imediatamente antes da execução de operações críticas.

Não carregue um novo código ainda. Permaneça no agente da etapa anterior. Ele pode comprar ingressos, mas não tem salvaguardas contra obsolescência ou idempotência.

👉🔴 Pressione REDEFINIR O LOCAL e inicie uma nova sessão em adk web. Para isso, clique no botão + Nova sessão acima da conversa.

Inicie uma nova sessão para limpar o tíquete da fila anterior e o estado de compra pendente. Essa etapa exige um contexto de execução limpo para demonstrar o tratamento de dados desatualizados corretamente.

O snapshot fica

👉✨ Peça o mesmo programa de antes:

Get us two tickets to the Amsterdam show.

👉✨ Agora, enquanto espera na fila, pergunte sobre os assentos:

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.

👉🔴 Clique em VENDER OS MELHORES LUGARES. A seção A cai para zero.

👉🔴 Clique em PULAR A ESPERA para enviar o agente para o início da fila.

Na frente da fila

👉✨ Diga para comprar:

Buy the two section A seats.

Ele executa purchase(section="A") sem verificar novamente a disponibilidade de assentos, fazendo com que a API do local retorne uma resposta de erro:

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

Erro da API Venue: a seção A está esgotada

O agente só consultou o mapa de assentos depois de receber uma resposta de erro da API. A causa principal é um contexto desatualizado: no início da sessão, o agente registrou "seção A: 400 disponíveis", e esse turno permaneceu no histórico da conversa transmitido ao modelo.

Deixe claro onde as informações antigas estão armazenadas: elas permanecem no histórico de conversas, que prefixos de estado como temp: não podem substituir.

O mapa de assentos que o agente ainda está usando

A correção

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Agora carregue o código desta etapa:

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

👉💻 No terminal 1, pressione Ctrl-C e reinicie:

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"

O que mudou:

Arquivo

O que mudou

fence.py

a cada conversa. Executa imediatamente antes de uma compra e pode bloqueá-la

agent.py

before_tool_callback=refresh_before_purchase

tools.py

purchase agora envia um cabeçalho Idempotency-Key

fence.py é um callback que o ADK executa no momento antes de qualquer chamada de ferramenta:

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

Prove

👉🔴 REDEFINIR O LOCAL. Coloque na fila, pressione VENDER OS MELHORES LUGARES enquanto espera e diga para comprar a seção A.

Desta vez, ele para sozinho. Assista o agente dizer algo como:

"Parece que a seção A acabou de esgotar. Preciso ler o mapa de assentos de novo."

Em seguida, observe o app chamar get_seatmap novamente, sem solicitação, e voltar oferecendo a seção C.

👉🔴 Leia com atenção o feed de atividades do local, porque ele mostra as duas partes:

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

O feed de locais: a tentativa e a recusa

No painel, você vê a compra de chamadas, mas o contador de tíquetes permanece em zero. Isso acontece porque fence.py atua como uma proteção antes da execução da ferramenta:

  1. O agente tenta chamar a ferramenta de compra.
  2. fence.py captura a solicitação primeiro e verifica a disponibilidade de assentos ao vivo.
  3. Como os assentos estão esgotados, fence.py retorna um erro diretamente, cancelando a compra antes que a API seja chamada.

As instruções de comando não garantem que um LLM não vai tentar comprar assentos esgotados. As proteções de código (before_tool_callback) garantem que a compra inválida nunca chegue ao mundo real.

👉🔴 Clique em REDEFINIR O LOCAL e em CANCELAR A PRÓXIMA COMPRA.

👉✨ Peça para comprar e deixe que ele tente quantas vezes quiser.

Ainda é um pedido, não importa quantas vezes você tente

A contagem de pedidos permanece em 1 porque o local recebeu a mesma chave de idempotência na nova tentativa e retornou a confirmação original em vez de criar um pedido duplicado.

8. Fluxo de trabalho autônomo

Os LLMs não são bons em seguir regras rígidas sempre

Os LLMs são ótimos para tomar decisões, como escolher um programa com base nas preferências do usuário. Mas confiar em um LLM para seguir regras estritas (como "sempre entre na fila antes de verificar os assentos") significa que ele vai pular etapas ou confundir a ordem de vez em quando.

Para corrigir isso, divida o fluxo de trabalho em dois tipos de etapas:

  • Regras fixas (funções de código): etapas que precisam acontecer exatamente da mesma forma todas as vezes (pegar uma senha, verificar a posição) são escritas como nós de código comuns.
  • Decisões (nós do agente): as etapas que exigem julgamento (escolher seções de assentos, verificar preferências do usuário) são atribuídas ao agente de IA.

Confira o que realmente está nesse fluxo. Alguns deles não têm julgamento algum:

Etapa

Julgamento?

retirar exatamente um tíquete de fila

Não.Sempre uma, sempre antes de comprar

verificar se é sua vez

Não. Pergunte e leia a resposta.

escrever o briefing para o comprador

Não.Mesma frase, mesma forma, sempre

que mostram qual seção

Sim. Preço, dia, quem vai, o que não gostam

preferência e limitação do usuário

Sim.

Procedimentos rigorosos não devem depender de um LLM porque os modelos probabilísticos acabam não seguindo sequências rígidas quando não são monitorados.

O Workflow do ADK separa a execução determinística do raciocínio de IA:

  • Os nós de função aplicam regras fixas (como entrar em uma fila ou verificar a posição) de maneira consistente sempre.
  • Os nós do agente processam decisões contextuais, como escolher assentos com base nas preferências do usuário.
  • Os nós de espera suspendem a execução sem manter um processo ativo na memória.

O agente original não é substituído. Ele se torna o nó final no gráfico de fluxo de trabalho, mantendo as ferramentas, as verificações de obsolescência e as proteções de idempotência. O gráfico controla a ordem de execução, enquanto o agente executa a decisão de compra.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL.

👉💻 Acesse o código desta etapa:

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

Executar

👉💻 No terminal 1, pressione Ctrl-C e reinicie. O mesmo comando de sempre:

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

O que mudou desde a última etapa:

Arquivo

O que mudou

nightly.py

a cada conversa. A execução autônoma como um gráfico do ADK 2 Workflow

agent.py

root_agent agora é o gráfico. O agente com quem você estava conversando ainda está lá, renomeado como buyer_agent, e é um nó dentro dele.

agent.py

ele também perdeu join_queue e check_queue: o gráfico agora é o proprietário da fila.

Executar o gráfico: três mensagens, em ordem

Execute todas as ações a seguir no Python noturno (onde o gráfico está). Envie a mesma entrada em cada turno para observar a transição do gráfico de fluxo de trabalho pelos estados.

1. Observe com o que você está falando agora

👉 Recarregue a interface da web do 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

Observe que adk web executa qualquer objeto atribuído a root_agent. Aqui, root_agent aponta para o gráfico de fluxo de trabalho nightly, com buyer_agent configurado como o nó final no gráfico.

2. Começar

👉✨ Digite isso e pressione Enter:

go

Observação: em um gráfico de fluxo de trabalho automatizado, a execução inicial começa sem esperar a entrada do usuário. Enviar uma mensagem em adk web aciona a execução inicial.

👉 Agora olhe para o navegador da Web. O painel esquerdo mostra o gráfico e colore os nós à medida que eles são executados. O painel à direita é o fluxo de eventos, que mostra como as decisões estão sendo tomadas:

O gráfico, estacionado em check_front

Assista queue_up gravar queue_ticket e queue_event_id no estado da sessão.

O gráfico, estacionado em check_front

Você vai ver uma chamada de função chamada adk_request_input, aguardando uma resposta. Em seguida, ele fica em silêncio e a execução termina. O gráfico chegou a check_front, encontrou você na posição 14.203 e parou.

👉 Confira o painel do local: um ingresso na fila e nada em execução.

O gráfico, estacionado em check_front

3. Ative o dispositivo e preste atenção em qual caixa você digita.

Role o fluxo de eventos até o evento adk_request_input e localize a caixa de entrada associada a ele:

O gráfico, estacionado em check_front

Agora há dois lugares para digitar, e eles fazem coisas diferentes.

Onde você digita

O que acontece

a caixa Enter your response... no evento adk_request_input

responde à interrupção. A execução estacionada continua de check_front

a caixa de chat principal na parte de baixo

inicia uma nova invocação. O gráfico é reproduzido de START, escolhe um programa novamente e pega um segundo tíquete da fila

👉✨ Digite qualquer coisa na caixa Enter your response... e pressione a seta.

👉 Terminal 1:

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

Uma linha. Ele não escolheu um segundo programa nem pegou um segundo ingresso para a fila, o que teria colocado você no final de uma fila de 14.203 pessoas.

Se você vir [open] e [pick_show] sendo executados novamente, isso significa que você digitou na caixa de chat. Pressione REDEFINIR O LOCAL e comece a etapa novamente.

👉 Verifique o painel do local novamente: ainda há um ingresso, e a posição caiu um pouco porque o relógio do local continuou funcionando enquanto o seu não.

Acordou uma vez e ainda tem um ingresso

4. Chegue à frente e termine

👉🔴 Pressione PULAR A ESPERA no painel do local.

O gráfico concluído, com todos os nós em verde

👉✨ Responda ao evento adk_request_input mais recente pela última vez — de novo na caixa dele, não na caixa de chat. O gráfico concluído, com todos os nós em verde

Desta vez, o relógio check_front vai encontrar você na frente e cair. Observe o painel esquerdo acender brief e concert, os dois últimos nós.

O gráfico concluído, com todos os nós em verde

👉 E olhe para o navegador. Este é o fim da execução, e há quatro coisas que valem a pena ler.

O gráfico concluído, com todos os nós em verde

O gráfico está totalmente verde, e as bordas são contadas. brief e concert estão iluminados, e as bordas em pick_show, queue_up e check_front agora têm um selo.

2x

é o número de vezes que o gráfico percorreu essa aresta, uma vez por despertar. Os dois na parte de cima são o preço da retomada: o programador entra novamente no gráfico desde o início a cada vez. Isso não significa que o trabalho aconteceu três vezes, e o painel do local é a prova: um ingresso para a fila, não dois. @node(rerun_on_resume=False) é o motivo.

O gráfico

O gráfico: funções para regras, agentes para julgamento

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

Observe que dois nós são agentes e dois são funções, e trate qual é qual como a questão de design completa.

Nó

Por que esse formato

pick_show

agent. Escolher um programa do histórico de alguém é um julgamento. Ele chama recall, lê o tour e retorna um Plan validado.

queue_up

função. Entrar em uma fila é uma regra. Uma regra não pode ser um cara ou coroa às 3h da manhã

check_front

função. "Estou na frente?" tem uma resposta certa, e o local tem essa informação.

buy_it

agent. É buyer_agent, o que root_agent costumava ser, sem modificações. A seção A esgotou durante a espera. Você escolhe B, C ou não escolhe nada? Esse é o julgamento que você ensinou no módulo 4.

Confira o último nó e encontre o agente que você criou. Não é uma cópia nem uma reescrita:

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

Tudo isso vem junto: todas as ferramentas, a releitura do mapa de assentos em fence.py, a chave de idempotência em purchase. Só existe um.

Por que ele parou em vez de esperar

Esta é a versão que ninguém deve escrever:

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

Ele mantém um processo ativo por 40 minutos sem fazer nada, é encerrado com o terminal e não pode ser retomado. Veja o que a check_front faz:

# agent/concert/nightly.py — check_front
if not status["ready"]:
    return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")

RequestInput é uma interrupção. O gráfico para onde está, a invocação vai para o armazenamento de sessão, e o processo pode ser encerrado.

A execução não custa nada enquanto espera e termina em um só lugar: ingressos comprados. Pode levar a noite toda e vários despertares para chegar lá.

9. Trabalhar com um humano

Não gaste dinheiro sem confirmar com uma pessoa

Um agente autônomo que é executado durante a noite não deve fazer escolhas financeiras caras com base em preferências ambíguas ou presumidas do histórico de chat.

Antes de executar ações importantes (como gastar dinheiro em ingressos), o agente precisa pausar, pedir que o usuário confirme o orçamento em voz alta e salvar essa confirmação explícita antes de fazer qualquer compra.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Acesse o código desta etapa:

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

Executar

👉💻 No terminal 1, pressione Ctrl-C e reinicie. O mesmo comando de 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"

O que mudou desde a última etapa:

Arquivo

O que mudou

budget.py

Contém o que a pessoa concordou, como a frase que ela disse. load() lê o texto em voz alta, e someone_is_there() responde se há alguém por perto para fazer perguntas.

tools.py

adicionou set_budget para que o agente possa gravar um limite acordado no estado da sessão depois que uma pessoa o confirmar.

nightly.py

um novo primeiro nó, agree_budget, que pergunta e lê de volta antes de qualquer outra coisa ser executada. Ele captura o orçamento e o restante da solicitação, tanto como texto livre quanto SEATS como uma constante.

Não o modele. Armazene a frase.

Abra agent/concert/budget.py e leia tudo:

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"

O nó verifica duas coisas: se a pessoa disse isso e se ela confirmou.

Em seguida, ele mantém a frase no estado da sessão em vez de user:, de propósito. Um orçamento é acordado para uma reserva. "US$ 250 se forem os bons" foi dito sobre essa banda, naquela noite, e não tem nada a ver com a reserva de ingressos.

O mesmo vale para tudo o mais que eles disseram

Não pare no orçamento. As pessoas mudam de ideia enquanto decidem. Por isso, capture tudo o que elas pedem aqui, em vez de confiar no que o agente lembra.

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

Leia Somente eventos criados pelo usuário, que mostra Somente

text

peças do function_response, para que a pergunta e resposta sobre o orçamento não sejam repetidas aqui. Sem duplicatas.

O agree_budget chama o cada execução, não apenas a primeira. Portanto, uma correção digitada enquanto o agente está na fila também é aplicada. Em seguida, open_the_night entregue ao picker as duas frases, o pedido e o orçamento, e peça para ele ler.

O gráfico, um nó mais longo

Vamos começar com uma conversa:

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

O mesmo gráfico, com agree_budget na frente

Dois nós param e esperam, e eles são o mesmo mecanismo. check_front espera um local e é respondido por um relógio. agree_budget espera uma pessoa e só pode ser respondido por uma pessoa. Essa diferença é toda a etapa 10.

Executar etapa a etapa

1. Começar

👉 Atualize a interface da web do ADK e escolha o agente concert.

👉✨ Peça o que você realmente quer:

Get us two tickets to the Amsterdam show.

A execução é interrompida e pergunta quanto você vai gastar

Na etapa 8, o gráfico não tinha usuário, então tudo o que você digitou foi descartado, e go teve o mesmo desempenho de qualquer outra coisa. Aqui, agree_budget chama what_they_asked_for(ctx) na primeira linha, que coleta tudo o que você digitou e coloca em state["request"]. Em seguida, open_the_night entrega ao seletor junto com o orçamento.

Você não especificou um dia, e Amsterdã tem dois shows. Então, assista o seletor voltar para recall(), encontre "Sam bails on weeknights" e escolha o sábado.

No navegador, a execução para em um evento adk_request_input:

Um gráfico acabou de fazer uma pergunta. A mesma interrupção da espera na fila da etapa 8, mas agora apontando para uma pessoa responder à pergunta.

2. Responda como as pessoas realmente respondem

👉✨ Na caixa Enter your response..., não na caixa de chat na parte de baixo:

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

Assista até ele parar e confirme:

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

Ele lê o número de volta antes de confirmar

3. Confirmar

👉✨ Mesma caixa:

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.

Quatro nós foram executados com uma palavra. 👉 Abra a guia Estado e encontre budget, sua frase na íntegra.

orçamento na guia "Estado", com suas próprias palavras

Nenhum prefixo. Esse é o estado da sessão, e é uma decisão que vale a pena tomar de propósito se você iniciar uma Nova sessão e o orçamento acabar. Assim, o sistema vai perguntar de novo.

4. Chegue à frente e termine

👉🔴 Pressione PULAR A ESPERA no painel do local.

👉✨ Responda à adk_request_input mais recente, de novo na caixa dela:

go

Acordado novamente, e não pergunta duas vezes

👉 Confira o painel do local: um pedido, a um preço dentro do que você concordou para esse tipo de assento.

Um pedido, dentro do que você concordou

Como um gráfico faz uma pergunta a uma pessoa

Abra nightly.py e leia agree_budget. Faça três coisas serem verdadeiras ao mesmo tempo e espere a mesma falha quando uma delas estiver faltando: ela pergunta para sempre.

@node(rerun_on_resume=True)              # 1. run again when woken
def agree_budget(node_input, ctx):
    answers = ctx.resume_inputs or {}    # 3. where the answer arrives

    said = answers.get(ASK_BUDGET)
    if not said:
        return RequestInput(
            interrupt_id=ASK_BUDGET,     # 2. STABLE id, not the default
            message="What are you willing to spend?",
        )
    ...

rerun_on_resume=True

sem ele, o nó é tratado como concluído e ignorado.

um interrupt_id estável

o padrão é um UUID novo por execução, então não há nada para pesquisar a resposta

ctx.resume_inputs[id]

onde a resposta aparece. Não o valor de retorno de RequestInput: o ADK itera um nó em vez de enviar para ele.

O gráfico

É o gráfico da etapa 8 com um nó extra na frente. O mesmo gráfico, com agree_budget na frente

As restrições confirmadas pelo usuário têm escopo de sessão e expiram quando a conversa termina. Manter limites temporários em várias sessões pode transformar decisões únicas em padrões permanentes não intencionais.

10. A pilha de nuvem

Laptops entram em modo de espera e arquivos locais desaparecem

No seu notebook, um agente depende de que o computador fique ativo, de bancos de dados SQLite locais e de arquivos locais. Se você fechar a tampa do laptop ou implantar em um contêiner de nuvem que reinicia, os arquivos locais e a memória vão desaparecer.

Para executar de maneira confiável na produção:

  1. Computação em nuvem: execute o agente em um contêiner (como o Cloud Run) que permanece on-line.
  2. Bancos de dados duráveis na nuvem: substitua arquivos locais e SQLite por serviços de nuvem (Cloud SQL para dados de sessão, Cloud Storage para arquivos).
  3. Timers do Cloud: use programadores gerenciados, como o Cloud Scheduler e o Pub/Sub, para que o agente seja ativado mesmo quando o notebook estiver desligado.

👉🔴 No painel do local, pressione REDEFINIR O LOCAL. Cada etapa começa no mesmo mundo: oito programas, todos os assentos disponíveis, relógio em 1×.

👉💻 Acesse o código final:

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

Arquivo

O que mudou

main.py

a cada conversa. Um FastAPI que atende a página de chat e o endpoint chamado por um programador

Dockerfile

a cada conversa. Python-slim e uvicorn comuns. Nada disso é específico do ADK

monstertix/

copiada, para que a página seja enviada no mesmo contêiner que o agente

concert/

não mudou. Byte a byte, a pasta em que você concluiu a etapa 9

A página que você já criou

A <strong>interface da Web do ADK</strong> é estritamente uma interface de depuração local para inspecionar estado, fluxos de eventos e gráficos de fluxo de trabalho durante o desenvolvimento. Ele não está empacotado para implantação em produção.

Para produção, use o front-end de chat independente criado na etapa 3. Ele processa mensagens e interrupções de interação do usuário pelo endpoint /wake sem expor controles de depuração internos.

Migrar o estado local para serviços de nuvem

Pare o servidor e veja o que ele gravou:

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

Embora os arquivos locais funcionem bem para desenvolvimento, eles falham na produção porque os contêineres do Cloud Run usam armazenamento efêmero e local da instância.

Ao executar no Cloud Run:

  • As instâncias são escalonadas para zero:quando o tráfego para, o contêiner é encerrado e o sistema de arquivos local é excluído.
  • As instâncias escalonam horizontalmente de forma independente:várias instâncias simultâneas não compartilham espaço em disco. Cada uma é executada com um sistema de arquivos isolado.

Se um agente salvar um tíquete de fila ou um estado de sessão em um arquivo SQLite local (sessions.db), as solicitações subsequentes encaminhadas para uma instância de contêiner nova ou diferente vão encontrar um banco de dados vazio. Para manter um estado durável em escalonamentos e reinicializações, os arquivos locais precisam ser substituídos por serviços externos gerenciados.

Em vez disso, você precisa de um estado que dure mais que um contêiner. Três serviços, um para cada arquivo:

No laptop

Na nuvem

O que é

sessions.db

Cloud SQL

um banco de dados Postgres gerenciado. Ele existe independente de algo estar em execução ou não.

memory/userx.md

Cloud Storage

políticas do IAM com um bucket. Arquivos como objetos, acessíveis de todas as instâncias ao mesmo tempo

artifacts/

Cloud Storage

o mesmo bucket

clock.py

Cloud Scheduler

um cron que o Google executa. Seu terminal não é um despertador

Criar

Provisione um banco de dados PostgreSQL, um usuário do banco de dados e um bucket do Cloud Storage. A criação da instância do Cloud SQL leva vários minutos e foi iniciada em segundo plano durante setup.sh.

👉💻 Colete e faça o restante:

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

Esse script criou quatro coisas, uma por linha da tabela acima, e gravou dois URIs em .env:

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

Como implantar o agente com gatilhos programados gerenciados

Com os serviços prontos, dê ao agente um lar permanente.

👉💻 Três ou quatro minutos. Comece e continue lendo:

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

O script cria uma imagem de contêiner de agent/, implanta no Cloud Run e provisiona a infraestrutura de suporte orientada a eventos.

A implantação completa consiste em 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 origem do gatilho

O Cloud Scheduler substitui os timers de execução local (clock.py) por um serviço cron gerenciado que persiste independente das sessões de terminal local, processa novas tentativas automatizadas e oferece suporte ao agendamento de fuso horário.

Pub/Sub como camada de dissociação de mensagens

Colocar um tópico do Pub/Sub entre o Cloud Scheduler e o Cloud Run oferece três vantagens arquitetônicas:

  • Durabilidade da mensagem: enfileira eventos de acionamento recebidos se o serviço do Cloud Run estiver sendo inicializado a frio, reimplantado ou temporariamente indisponível.
  • Políticas de novas tentativas integradas: repetem automaticamente falhas de entrega temporárias sem exigir uma lógica personalizada de novas tentativas no nível do aplicativo.
  • Escalonabilidade de fan-out: permite que vários serviços downstream se inscrevam nos mesmos eventos de programação sem reconfigurar o programador.

texto alternativo

A configuração de trigger_sources=["pubsub"] registra um endpoint de push compatível com payloads de envelope padrão do Pub/Sub, traduzindo mensagens recebidas diretamente em eventos de invocação do 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 * * *

Fale com ele

👉 Abra o URL do agente em um navegador. 👉✨ Peça para ele fazer uma reserva:

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

Ele vai perguntar quanto você quer gastar

👉✨ Responda como se estivesse falando em voz alta e confirme:

up to 250 a seat for the good ones

A execução está na fila no momento.

👉🔴 Clique em PULAR A ESPERA no painel do local para que a fila esteja pronta quando a mensagem chegar.

Para executar em uma programação real, o job precisa estar ativado. O jobs run se recusa a executar um job pausado com Job.state must be ENABLED:

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

👉 Em alguns segundos, o agente executa a compra dentro dos limites de orçamento confirmados e retorna uma confirmação do pedido automaticamente.

👉🔴 Confira o painel do local. Um pedido.

Veja como fazer isso:

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

Limpar

Conte duas coisas implantadas: o local e o agente ao lado dele. Ambos têm redução da escala a zero, então um ocioso não custa nada. Exclua mesmo assim.

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

Esses comandos deixam o Cloud SQL e o bucket intactos, porque eles contêm seus dados e ambos continuam gerando faturamento. Quando tiver certeza de que terminou:

./destroy-agent.sh --all

Todo o resto é local. Exclua a pasta quando terminar ou mantenha e execute no seu próprio projeto pelo tempo que quiser.

O agente, as ferramentas, os callbacks e o orçamento não mudaram. Dois URIs e uma variável de ambiente fizeram isso.

11. Visão geral do sistema e guia de solução de problemas

Diferenças de ambiente: arquitetura local x na nuvem

Seu laptop

Na nuvem

O que mudou

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

Cloud SQL

uma string de conexão

--artifact_service_uri=file://...

gs://bucket

um URI

memory/userx.md

gs://bucket/memory/

um caminho

adk web

um serviço do Cloud Run

um Dockerfile e um main.py

monstertix/clock.py

Cloud Scheduler → Pub/Sub

trigger_sources=["pubsub"]

você no teclado

someone_is_there()

um padrão, substituído por solicitação

A implementação do agente principal, incluindo root_agent, definições de ferramentas, proteções de callback e aplicação de orçamento, permanece completamente inalterada. Comparar solutions/step10_deploy/concert/ com a etapa 9 mostra uma correspondência exata byte a byte, validando como a abstração de serviço do ADK separa a configuração da infraestrutura da lógica do agente.

Se você não entendeu alguma coisa

Acesse o código completo e funcional em qualquer etapa:

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

Quando algo quebra

Sintoma

Correção

✗ Cannot access project

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

O modelo retorna 404

Os aliases -latest são exclusivos do AI Studio. Use ADK_MODEL=gemini-latest-flash

address already in use

Algo já tem a porta 8000 ou 8090. lsof -ti:8000 | xargs kill -9

O agente não consegue chegar ao local

curl $VENUE_URL/health ou ./deploy-venue.sh novamente

A fila nunca avança

Pressione PULAR A ESPERA. Em 1×, a fila não vai chegar à frente sozinha

Um segundo tíquete de fila aparece no painel.

Você respondeu na caixa de chat em vez da caixa Enter your response... no evento adk_request_input. A caixa de chat inicia uma nova execução

O menu suspenso está vazio ou o app não vai carregar

O adk web trata todas as pastas em agent/ como um app e falha em toda a lista se uma delas não for. ls agent/ precisa mostrar concert/ e nada mais. Executar ações de dentro de agent/ pode deixar um memory/ perdido. ./use-solution.sh 8 --force organiza

[EXPERIMENTAL] avisos

Esperado. EventsCompactionConfig e ResumabilityConfig estão em pré-GA na versão 2.6.2

Tudo é estranho

Pressione REDEFINIR O LOCAL e, em seguida, rm sessions.db.

Arquivo de memória cheio de reservas duplicadas

O remember() anexa todas as execuções. ./use-solution.sh N --force o redefine ou cp seed/memory/default.md memory/userx.md

adk: command not found

source .venv/bin/activate: toda nova guia precisa dele

O Cloud Shell expirou

Inicie adk web novamente. sessions.db sobreviveu, que é a etapa 6, acontecendo com você sem custos financeiros