VibeStudio: Flujo de trabajo de agentes multimodal con el ADK, edición de Halloween

1. Introducción

VibeStudio, Haunted Edition

Tienes un canal de contenido acogedor y espeluznante en VibeTube: un Short cada noche hasta Halloween. El público quiere magia pequeña en habitaciones pequeñas y te la envía de todas las formas posibles: como comentarios, notas de voz, fotos de sus propios cajones embrujados y clips de sus porches al atardecer. La lista de tareas pendientes está llena de ideas. Todas las noches se dedican a las mismas tareas: investigar qué es lo que está de moda en el feed, revisar el contenido pendiente, leer lo que envió el público, elegir una dirección, verificarla con la política del canal, escribir el guion, dibujar el póster, renderizar el clip y publicar.

Los modelos generativos pueden realizar cada tarea por sí solos. Gemini investiga, escucha, mira y escribe; el modelo de imagen dibuja; Veo renderiza. Lo que no pueden hacer solos es dirigir la noche en orden, detenerse para la única decisión que es tuya, rechazar una mala dirección antes de que cueste dinero, esperar minutos para un renderizado sin mantener nada abierto y dejar un registro.

Este codelab convierte esa tarde en un ADK Workflow: un grafo dibujado de nodos y aristas que se ejecuta por sí solo, te pregunta solo dónde se necesita tu criterio y finaliza cada ejecución con un clip y su póster en VibeTube.

Una instrucción no puede prometer un orden, una detención o una verificación. Un gráfico sí puede.

Qué aprenderás

  • Un agente y el modelo de imagen como herramienta. Un Agent es un modelo, una instrucción y herramientas. La tercera herramienta llama al modelo de imagen y la primera ejecución finaliza con un póster. Luego, la oración que impulsa el resto del lab: una instrucción no puede prometer orden, una detención o una verificación.
  • Son las estructuras que componen un grafo. Un Workflow es una lista de aristas. Crearás la distribución ramificada y la unión (lectores paralelos que se encuentran en un JoinNode) y un router que nombra su propia salida, con un agente en modo de tarea detrás de la salida rechazada que limpia una dirección en un bucle de llamadas a herramientas y se vuelve a unir a la línea principal.
  • Una canalización de transferencia multimodal. Cuatro tipos de cosas que envió el público, un lector por tipo: Gemini transcribe las notas de voz, agrega subtítulos a las fotos, mira los clips y escucha sus bandas sonoras, y cada lector escribe la misma forma de línea en un corpus de RAG Engine. Un tercer lector en la distribución recupera por significado y devuelve el contenido multimedia con los pasajes.
  • Dos tipos de suspensión, un mecanismo. Una carrera se detiene para una persona (RequestInput: cuatro tarjetas, tú respondes) y para una herramienta lenta (Veo detrás de LongRunningFunctionTool: un recibo). Ambas son lo mismo: una llamada abierta en el almacén de sesiones, reanudada por su ID, desde el formulario o desde otro proceso. Las dos ramas de la mesa de medios comparten el póster por nombre, en el estado.
  • Memoria en varias noches. Dos devoluciones de llamada de ciclo de vida en Memory Bank: before_model_callback recupera el gusto del creador antes de que el proponente lea, after_agent_callback recuerda la elección de esta noche después del guionista.
  • La versión El gráfico dentro de una app con un Runner de ADK, eventos enviados por el servidor a la página, Cloud Run con afinidad de sesión y un POST de varias partes que coloca el clip en VibeTube con el póster como miniatura.

Qué compilarás

Una app en ejecución en tu navegador: VibeStudio Workbench, una página complementaria con un editor de código in-page, verificadores que leen los archivos en el disco y las sesiones que escribieron tus ejecuciones, y un adk web incorporado. Sus siete sesiones son las siete páginas de este codelab, con los mismos números y las mismas letras de las partes, para que ambos estén sincronizados. Cada página se abre en una banda que indica la sesión en una oración y las tres cosas que haces allí: editar, ejecutar y verificar. Lee una sección aquí y, luego, realiza la edición en el entorno de trabajo. Cada edición termina con un panel de verificación que indica lo que realmente sucedió.

Qué se ejecuta y dónde: VibeStudio Workbench, tu backend y los servicios de Google Cloud

Se ejecutan tres cosas: el Workbench (la página en el puerto 4600, el editor, los verificadores), tu backend (el Workflow en agent/ y una app de zona de pruebas por sesión en stage0_prompt/ a través de stage6_video/, que entrega adk web) y Google Cloud (Gemini, el modelo de imagen, el motor de RAG, Veo, Memory Bank, a los que se accede a través de Vertex AI con tus propias credenciales).

Las siete sesiones

Sesión

Qué compilarás

Tus cambios

1 · La historia

Aún nada: la noche y el gráfico en el que se convierte

—

2 · Generación de imágenes

Un agente dibuja el primer póster

POSTER_TOOL

3 · Workflows

La noche como un grafo: la distribución ramificada y la unión, el router y su bucle

FANOUT, ROUTER

4 · Transferencia multimodal

Lo que el público envió, leído en un corpus; el tercer lector

INGEST_READERS, RAG_MEDIA

5 · Generación de videos

Dos tipos de suspensión; el póster compartido por nombre en el estado

GATE, VIDEO_TOOL, DELIVER_RESPONSE, STATE_SHARE

6 - Memoria

Qué recuerda el canal entre noches

MEMORY_RECALL, MEMORY_REMEMBER

7 - Implementa y publica

VibeStudio en Cloud Run; la noche en VibeTube

el código de evento de la sala y publicar

Cómo usar este codelab Se prioriza el concepto: cada sesión explica la idea, luego muestra el código tal como se lee una vez que se aplica el cambio y lo analiza. Nunca te pide que edites un archivo aquí; la edición se realiza en el banco de trabajo, que te presenta una edición a la vez, una sugerencia junto a ella y un panel de verificación debajo. Lee la sesión y, luego, deja que la página te guíe.

Dónde se ubica

El gráfico, el tema y la sala de VibeTube provienen de la edición embrujada de Lin de VibeStudio. Este es el corte multimodal, que se centra en lo que envió el público y en los dos tipos de suspensión, en el formato interactivo de una página de los labs de Agent Valley. El estudio es el escenario; cada mecanismo que se encuentra debajo es el habitual que usarías para una canalización de asistencia, un flujo de revisión de contenido o cualquier otra cosa que deba detenerse para una persona y esperar un trabajo lento.

2. 🧰 Configuración

  • Qué: una cuenta que puede pagar, un proyecto, el lab instalado, el modelo que se demostró que responde y el entorno de trabajo que se ejecuta en segundo plano en un puerto.
  • Por qué: un proceso en un puerto: la página, el editor, los verificadores, /api/* y adk web comparten un origen, por lo que nada en este lab necesita una segunda terminal o un segundo puerto.
  • Cómo: clonar, dos secuencias de comandos. La segunda realiza una llamada real al modelo, inicia el entorno de trabajo y comienza a crear el corpus de RAG y el banco de memoria mientras lees, para que las sesiones 4 y 6 los encuentren listos.

Todo en este lab se ejecuta en Cloud Shell: gcloud, Python, Node, ffmpeg y git ya están instalados allí, y se puede acceder a Gemini, el modelo de imagen, Veo, RAG Engine y Memory Bank a través de Vertex AI con tus propias credenciales. No hay ninguna clave de API en este lab ni nada que instalar en tu laptop. Una laptop con el SDK de Google Cloud, Python 3.12 y Node 20 o una versión posterior funciona de la misma manera.

Hay algo que este lab no puede darte: un proyecto de Google Cloud con la facturación habilitada. En el paso 1, obtienes una cuenta que puede pagar por el proyecto; en el paso 4, conviertes esa cuenta en un proyecto. Hazlo en ese orden: cada paso después del 4 lee el proyecto que se establece en el paso 4.

1 · Obtén una cuenta con facturación (~1 min)

Accede con una Cuenta de Google que tenga habilitada la facturación. Cualquiera de estas cuentas funciona. Si no es así, inicia una prueba gratuita, que te proporcionará una cuenta de facturación, que es todo lo que necesitas para el paso 4. Si estás en un lab con instructor, primero canjea los créditos de esta sesión, ya que esto creará la cuenta de facturación por ti.

2 · Abre Cloud Shell (~2 min)

👉🖥 Haz clic en Activar Cloud Shell en la parte superior de la consola de Google Cloud (es el ícono con forma de terminal que se encuentra en el encabezado).

Activar Cloud Shell desde el encabezado de la consola de Google Cloud

👉🖥 Haz clic en Abrir editor (el ícono de lápiz). Esto abrirá el editor de Cloud Shell con un explorador de archivos a la izquierda, que es donde debes ir cada vez que quieras leer un archivo completo.

Abre el Editor de Cloud Shell

👉🖥 Si se te solicita que autorices en algún momento, haz clic en Autorizar para continuar.

👉🖥 Abre una terminal dentro del editor, es decir, la pestaña 1, y cada paso 👉💻 se ejecuta allí. La pestaña 2 es el workbench en sí, y la abrirás en el paso 7. Si la terminal aún no está en la parte inferior de la pantalla, ábrela con Ver → Terminal:

Abre una terminal nueva en el editor de Cloud Shell.

👉💻 Verifica con qué cuenta accediste. Debería ser la del paso 1:

gcloud auth list

3 · Obtén el lab (~1 min)

👉💻 Clona el repositorio:

cd ~
git clone https://github.com/cuppibla/vibe-studio-multimodal
cd vibe-studio-multimodal

4 · Crea tu proyecto (~2 min)

👉💻 Una secuencia de comandos encuentra tu cuenta de facturación, crea un proyecto en ella, vincula la facturación y dirige Cloud Shell a ese proyecto:

cd ~/vibe-studio-multimodal
./setup_project.sh

Espera un ✓ para la cuenta de facturación que eligió, un ✓ para el proyecto que creó y un final como el siguiente:

Project ready: vibe-studio-4271
  Next:  ./setup_codelab.sh

vibe-studio-4271 es el ID de tu proyecto (el tuyo tendrá dígitos diferentes). Allí ocurren cuatro cosas:

facturación

Toma la cuenta de [YYYY-MM-DD] GDP Credit: más reciente si tienes una y, de lo contrario, cualquier otra cuenta de facturación abierta.

project

crea vibe-studio-XXXX y le vincula esa cuenta de facturación

selects

escribe el ID en ~/project_id.txt y ejecuta gcloud config set project, por lo que no se te pedirá que lo escribas después de esto

esperas

realiza sondeos hasta que el proyecto pueda entregar contenido, durante un máximo de dos minutos

👉🖥 El mismo proyecto ahora está en la consola; su ID se encuentra en la tarjeta Información del proyecto del panel:

El ID del proyecto en el panel de la consola

5 · Instala y configura el lab (~4 min)

👉💻

cd ~/vibe-studio-multimodal
./setup_codelab.sh

Una secuencia de comandos que se puede volver a ejecutar de forma segura: una segunda ejecución conserva todas las respuestas que diste la primera vez. Se hacen dos preguntas cerca del inicio, y la tecla Intro toma el valor predeterminado para ambas:

código de evento

la sala de VibeTube en la que se publicará tu video en la sesión 7. Escribe el código de la sala que te proporcionó el instructor o presiona Intro para sandbox, que está abierta para cualquier persona en cualquier momento. Los códigos distinguen mayúsculas de minúsculas, y una sala solo acepta cargas dentro de su ventana. La secuencia de comandos te indica si la sala que nombraste está abierta.

nombre visible

el nombre con el que te identifica la sala, o Enter para usar uno creado desde tu cuenta.

Ambos se guardan en .env y se pueden cambiar más adelante, en .env o en la página de publicación. Luego, hace el resto por su cuenta, en orden:

uv env

instala uv si falta, luego uv venv y uv sync instalan exactamente lo que uv.lock fija — google-adk, google-genai, vertexai, fastapi, uvicorn — en .venv

apis

Habilita seis APIs en el proyecto seleccionado en el paso 4 y confirma que cada una responda antes de continuar: aiplatform (Gemini, el modelo de imagen, Veo, Memory Bank, RAG Engine), vectorsearch (el almacén detrás de un corpus de RAG), run, cloudbuild, artifactregistry (sesión 7), cloudtrace (los seguimientos de la app)

cuenta

se asegura de que exista la cuenta de servicio de Compute predeterminada del proyecto y de que tenga los roles que necesitan la compilación y el servicio de la sesión 7

.env

escribe .env para dirigir cada llamada a Vertex AI en tu proyecto (Gemini en el extremo global, Veo, Memory Bank y RAG Engine en us-central1) y sin clave de API en ningún lugar; un .env anterior se conserva como .env.bak y todo lo que agregaste manualmente se mantiene

veo · room

confirma que veo-3.1-fast-generate-001 se entrega en us-central1 (una lectura de metadatos, sin renderización) y le pregunta a VibeTube si tu sala está abierta

model

realiza una llamada real a Gemini, por lo que un proyecto incorrecto falla aquí en el momento de la configuración y no dentro de una sesión

starter

Coloca los nueve archivos que editarás en su lugar desde starter/, manteniendo sus líneas TODO, solo las que faltan; tus ediciones nunca se reemplazan.

página

Instala y compila web/, la página del entorno de trabajo, y, luego, inicia el servidor en segundo plano en el puerto 4600.

cloud

comienza a crear el corpus de RAG Engine y el banco de memoria en segundo plano, por lo que las sesiones 4 y 6 los encuentran listos

preflight

ejecuta scripts/preflight.py y muestra el vínculo para abrir

6 · Lee la comprobación preliminar (aprox. 1 min)

Lo último que se imprimió en el paso 5 es la verificación previa: cada línea es un sondeo real, no una suposición:

  ✓ python 3.12
  ✓ auth path A: Vertex via ADC (STUDIO_VERTEX=1)
  ✓ Google Cloud ADC (project vibe-studio-4271)
  ✓ Memory Bank SDK
  ✓ stage0_prompt loads
  ✓ stage1_fanout loads (0 edges)
  ✓ stage3_router loads (... edges)
  ✓ stage4_memory loads (... edges)
  ✓ stage5_rag loads (... edges)
  ✓ stage6_video loads (... edges)
  ✓ node and npm (build the learning center's page)
  ✓ learning center page built (web/dist)
  ✓ aiplatform.googleapis.com enabled (Gemini, Veo, Memory Bank, RAG Engine)
  ...
  - Memory Bank: still being created in the background
  - RAG corpus: still being created in the background
  ✓ session 7 runtime account 123456789-compute@developer.gserviceaccount.com has its roles
  ✓ learning center running on port 4600

PREFLIGHT GREEN

Open session 1 here:  https://4600-cs-...cloudshell.dev/step/story

stage1_fanout loads (0 edges) tiene razón: ese gráfico se envía vacío y, en la sesión 3, lo completas. Las dos líneas - también son correctas: un corpus tarda uno o dos minutos en aparecer y el banco unos veinte segundos, y no necesitarás ninguno de los dos antes de la sesión 4.

👉💻 Ejecútalo de nuevo en cualquier momento para ver dónde estás. Lee tus archivos, tu proyecto y el servidor en ejecución, no una lista de tareas:

cd ~/vibe-studio-multimodal
uv run python scripts/preflight.py

7. Abre el entorno de trabajo (~1 min)

El workbench ya se está ejecutando. En el paso 5, se inició en segundo plano un proceso de uvicorn en el puerto 4600 que entrega la página, cada ruta de /api/* y adk web activado en /workbench. No hay nada que iniciar, y nada en este lab te pide que lo reinicies: una edición se aplica en tu próxima ejecución.

👉🖥 Haz clic en el vínculo que se imprimió en la verificación previa o, en Cloud Shell, ve a Vista previa en la Web → Cambiar puerto → 4600. Llamemos a esa pestaña 2. Se abrirá en Sesión 1: La historia.

Los tres comandos que puedes necesitar más adelante, todos de ~/vibe-studio-multimodal:

scripts/stop.sh

detiene el workbench

scripts/start.sh

lo inicia en primer plano; Ctrl + C lo detiene, junto con todos los procesos que inició

scripts/restart.sh --pull

git pull, luego se reinicia en segundo plano, vuelve a sincronizar el venv y vuelve a compilar la página solo cuando se cambia

Diseño del repositorio

vibe-studio-multimodal/
├── agent/                  # the workflow and the platform clients
│   ├── graph.py            # the graph: three readers, the proposer, the gate, the router, the script, the desk
│   ├── desk.py             # render_desk and render_submit, the LongRunningFunctionTool
│   ├── deliver.py          # the delivery console: find the pending call, wait for Veo, answer by id
│   ├── schemas.py          # Direction, Directions, CleanedDirection, Script, each with a poster idea
│   ├── cleanup_tools.py    # find_policy_hits, suggest_replacement: the task agent's tools
│   ├── policy_words.txt    # the policy, as data; policy_replacements.txt, the approved stand-ins
│   ├── backlog.txt         # the creator's Halloween ideas; comments.md, the audience's comments
│   ├── audience/           # what else the audience sent: voice/*.mp3, photo/*.jpg, clip/*.mp4
│   ├── media/past/         # six past posters and captions.md, what Gemini sees in each
│   └── platform/           # config, audience (the readers), rag (RAG Engine), images, videogen (Veo), memory (Memory Bank)
├── stage0_prompt/ ... stage6_video/   # one sandbox app per session, runnable in adk web
├── server/ & web/          # the VibeStudio Workbench (FastAPI + React, port 4600)
├── vibestudio/             # the app deployed in session 7, with its own copy of agent/
├── checks/ & scripts/      # the hole registry, the verifiers, preflight, start/stop/restart
└── starter/                # the nine files you edit, as students receive them

3. 1 · La historia: la noche y el gráfico en el que se convierte

👉🖥 La pestaña 2 se abre en Sesión 1: La historia. Léelo: seis tareas dispuestas en un anillo alrededor de una persona (investigar la tendencia, revisar el trabajo pendiente, elegir una dirección, verificar la política, escribir el guion, renderizar el clip) y el anillo se repite todas las noches. Gemini puede encargarse de la investigación, la lista de tareas pendientes y el guion por sí solo; Veo puede renderizar por sí solo; la persona se encarga de elegir y verificar.

Debajo del anillo, se encuentra el gráfico que se compila en el resto del lab, trece nodos que van desde una idea hasta un clip y su póster. Coloca el cursor sobre un nodo para ver qué hace y la sesión que lo compila. No hay nada que editar aquí; el botón Genera el primer póster de la página es la sesión 2.

El vocabulario, para que las siguientes seis páginas se lean sin detenerse:

palabra

lo que significa aquí

agente

un Agent de ADK: un modelo, una instrucción y herramientas. Sesión 2

nodo

un paso del grafo: una función de Python, un agente, una unión, un router

perimetral

una tupla de nodos que se ejecutan en orden; el grafo es una lista de ellos. Sesión 3

session

el registro de una ejecución en runs/sessions.db: sus eventos, su estado y cualquier llamada que haya quedado abierta. Sesión 5

state

Un dict que los nodos comparten por nombre de clave; un nodo escribe Event(state=...), otro declara un parámetro con el mismo nombre. Sesión 5

memoria

lo que sobrevive a una sesión: Memory Bank, a través de devoluciones de llamada. Sesión 6

4. 2 · Generación de imágenes: un agente y el modelo de imagen como herramienta

Sesión 2 · Generación de imágenes

👉🖥 En el entorno de trabajo, abre Sesión 2: Generación de imágenes. Antes del grafo, lo más simple que podría funcionar: un Agent cuya instrucción es toda la noche en prosa, tres funciones de Python como sus manos y una ejecución que termina con una imagen.

Un agente es un modelo, una instrucción y herramientas

stage0_prompt/agent.py define root_agent con tres de los campos de un agente del ADK. model es el modelo de Gemini de agent/platform/config.py. instruction es la instrucción del sistema: seis trabajos de una sola vez, el último de ellos una imagen. tools contiene tres funciones.

root_agent = Agent(
    name="solo_channel", model=config.MODEL,
    tools=[check_trends, read_backlog, make_poster],
    instruction=(
        "You run the creator's cozy-spooky short-video channel, alone, every "
        "night until Halloween.\n"
        "When the creator gives you an idea (or nothing), do ALL of this:\n"
        "check what is trending. look at your backlog of ideas. propose a "
        "direction and agree on it with the creator. refuse blacklisted "
        "subjects (competitor, hateful, gore). then describe the video you "
        "would make: a title (<=60 chars) and 3 shots, one visual sentence "
        "each. then draw its poster: call make_poster once with one line that "
        "names the single image that sells the video, and tell the creator "
        "where the poster is.\n"
        "Ask the creator to confirm the direction before describing the "
        "video."))

Un modelo es un mundo cerrado: razona sobre sus pesos y los tokens en su contexto. Una herramienta es una función de Python que el modelo puede solicitar. El ADK lee la firma y la cadena de documentación de cada función y se las declara al modelo. Cuando el modelo quiere una, emite un evento function_call, el entorno de ejecución ejecuta el código de Python y escribe un evento function_response en el contexto, y el modelo continúa con el resultado que tiene delante.

Tres funciones, y una de ellas dibuja

Dos de los tres leyeron la investigación. En el tercero, se le pide al modelo de imagen un póster:

def check_trends() -> dict:
    """Ten formats trending on the platform right now, with a heat score each."""
    from agent.trends import sample_trends
    return {"trends": sample_trends()}


def read_backlog() -> dict:
    """The creator's backlog: ideas they noted down to make someday."""
    from agent.graph import backlog_notes
    return {"backlog": backlog_notes()}


def make_poster(subject: str) -> dict:
    """Draw tonight's poster: one image of `subject` in the channel's cozy
    low-poly style. Returns where the PNG landed (path, url) in a few seconds."""
    ...

make_poster llama a agent/platform/images.py, el modelo de imagen que se une como una función simple de la forma en que videogen.py une Veo: un prefijo de estilo de casa (el aspecto acogedor de Halloween de baja poligonización del canal, naranja calabaza y crema de luz de vela contra ciruela de medianoche), el sujeto, un marco vertical de 9:16, un PNG en runs/media/posters, que se entrega en /static/posters. Con STUDIO_REAL_IMAGES=0, se copia un póster de marcador de posición y se marca el resultado prebaked, por lo que el lab se ejecuta en cualquier lugar.

MODEL = os.environ.get("STUDIO_IMAGE_MODEL", "gemini-2.5-flash-image")
ASPECT = "9:16"

def generate(subject: str, visual_style: str = "", name: str | None = None) -> dict:
    """One poster for `subject`. Returns where it landed: status, path, url,
    prompt, and prebaked=True when the stand-in was used instead of the model."""

La misma llamada es un nodo en el gráfico de la sesión 5.

Práctica: La edición definitiva

Tal como se envía, make_poster devuelve un dict que indica que se omitió el póster. 👉🖥 En el editor de Workbench, haz que llame al modelo de imagen, en lugar de la línea TODO:

    from agent.platform import images
    return images.generate(subject, name="solo")

Guardar. Las filas de verificación leen el archivo: tres herramientas en el agente, la llamada al modelo de imagen en su lugar. 👉🖥 Haz clic en Check the workflow loads, luego en Open adk web y envía lo siguiente:

tonight's idea: a tiny ghost learning to haunt a sock drawer

Qué esperar y por qué

  • Dos eventos de herramienta antes de la respuesta y, luego, una pregunta. check_trends y read_backlog se ejecutan porque la instrucción decía que se verificara y se buscara, y el modelo no tenía ninguna de las dos acciones en sus ponderaciones. La respuesta propone una dirección y te pide que confirmes, ya que la instrucción solicitaba un acuerdo primero.
  • El segundo mensaje recibe todo. Envía skip the questions, describe the video and draw the poster. El modelo describe el video, llama a make_poster una vez con un asunto que escribió por sí mismo y te indica dónde se guardó el PNG. En el panel de verificación, se muestra el póster junto a las filas: una escena de Halloween con el estilo del canal, dibujada en unos ocho segundos.
  • Por qué no es un canal. La investigación se incorpora en una prosa que no se puede citar. El modelo que escribió la instrucción certificó la política, y el póster se dibujó antes de que se verificara. Se habló del acuerdo en un mensaje. El texto de la instrucción no es una barrera. En el resto del lab, se desglosan estos seis trabajos en un gráfico dibujado.

La página termina con De qué está hecho un agente del ADK, una referencia a la que puedes volver: el modelo, el contexto con el que razona, las herramientas y los colaboradores a través de los que actúa, las devoluciones de llamada que unen cada llamada, y la sesión y la memoria que viven fuera de él.

5. 3 · Workflows: fan-out y join, router y loop

Sesión 3: Flujos de trabajo

👉🖥 En el entorno de trabajo, abre Session 3 · Workflows, partes 3a y 3b. Cada parte se compila en una app de zona de pruebas para que puedas ejecutarla en adk web por sí sola: la distribución ramificada en stage1_fanout, el router y su bucle en stage3_router.

Fan-out y unión (3a)

Un ADK Workflow es una lista de bordes. Cada entrada es una cadena, una tupla de nodos que se ejecutan en orden. Dos cadenas que salen de START se ejecutan en paralelo. Una cadena que termina en un JoinNode informa a este, y la unión espera a que cada cadena que termina allí antes de continuar, entregando un dict con clave por nombre de nodo. Un destino escrito como un dict asigna los nombres de ruta de un router a los nodos. En la página 3a del cuaderno de ejercicios, se dibujan las formas y hay un ejercicio para escribir los bordes.

Tipo de nodo

Se escribe como

Qué hace

Nodo de función

def scan_trends(node_input): ... return Event(output=...)

Python simple. También puede escribir el estado compartido con Event(state=...).

Nodo de unión

JoinNode(name="join_research")

Espera hasta que se hayan informado todas las cadenas entrantes y, luego, pasa todos sus resultados como un diccionario.

Nodo de agente

Agent(...)

Una llamada al modelo en modo single_turn, una respuesta estructurada, sin conversación.

Router

return Event(output=..., route="OK")

Un nodo de función cuyo valor de retorno asigna un nombre al borde saliente.

Nodo de intervención humana

yield RequestInput(...)

Suspende el gráfico hasta que una persona responda un formulario. Sesión 5.

La distribución de la investigación: dos lectores desde START hasta una unión

El primer gráfico es la investigación de la sesión 2 como nodos. scan_trends y read_backlog en agent/graph.py son los dos lectores; read_backlog también escribe la idea de esta noche en el estado compartido. stage1_fanout/agent.py define la unión y compila su Workflow a partir de una lista llamada EDGES, que se envía vacía (el 0 edges de la verificación previa). 👉🖥 Completa la lista con las dos cadenas:

EDGES = [(START, scan_trends, join_research),
         (START, read_backlog, join_research)]

👉🖥 Ejecútalo en adk web. Dos nodos se iluminan juntos y, luego, se unen; abre el evento de join_research y lee un dict con una clave scan_trends y una clave read_backlog. No se realizó ninguna llamada al modelo. Nada cambia el formato del diccionario de la unión: el nodo que le sigue, en 3b, es un agente, y un nodo de agente recibe su node_input como su mensaje, por lo que un diccionario llega como JSON que el modelo lee directamente.

El router y el bucle (3b)

Un Agent dentro de un Workflow se ejecuta en modo single_turn: recibe el resultado del nodo anterior como su mensaje, realiza una llamada al modelo y responde una vez. Con output_schema, la respuesta se valida con un modelo de Pydantic antes de que el siguiente nodo la vea, por lo que el grafo pasa objetos con tipo, no prosa. propose_directions lee el diccionario de investigación de la unión y propone cuatro candidatos, cada uno con una escena de Halloween. El candidato 4 es el cebo: su título contiene una palabra que el canal rechaza, por lo que el router tiene algo que capturar. Cada candidato lleva un poster_idea, una línea que nombra la única imagen que vende el video; el guionista lo conserva y el equipo de medios lo dibuja en la sesión 5.

Un enrutador es un nodo de función cuyo valor de retorno nombra la arista que toma la ejecución. Sin llamada al modelo: policy_check lee agent/policy_words.txt en el momento de la decisión, compara palabras completas con el título y el ángulo de la dirección, y lo enruta antes de que se escriba cualquier guion o se gaste dinero. La política son datos: edita el archivo y la próxima ejecución lo leerá.

def policy_check(node_input):
    """The policy gate: a router. Refused words in the direction send it to
    quarantine; a clean one goes to the scripter. Code, not a model."""
    chosen = _as_dict(node_input)
    bad = policy_hits(f"{chosen.get('title', '')} {chosen.get('angle', '')}")
    return Event(output=node_input, route="BLOCK" if bad else "OK")

La puerta de enlace de la política: un router con dos salidas etiquetadas

stage3_router/agent.py es la distribución más propose_directions y choose_direction, un sustituto de la puerta humana que creas en la sesión 5: elige al candidato cuyo número finaliza la idea de esta noche (..., pick 4), de lo contrario, el primero, y escribe la elección en el estado. scripter es un agente de single_turn que convierte una dirección aprobada en un Script: un título, una descripción, etiquetas, una línea de apertura, tres tomas y una idea para un póster, y cada toma es una imagen de Halloween con el estilo del canal.

El bucle detrás de la salida rechazada. quarantine se envía como un agente en modo de tarea. Un Agent tiene un mode: chat es el bucle conversacional que un agente raíz ejecuta con una persona, single_turn es una llamada con una respuesta estructurada y task es un bucle de llamadas a herramientas que el agente ejecuta por su cuenta hasta que llama al finish_task integrado, cuyos argumentos el ADK deriva de output_schema. Quarantine llama a find_policy_hits en la dirección rechazada, le pide a suggest_replacement el sustituto aprobado del canal para cada palabra (agent/policy_replacements.txt, datos de nuevo), reescribe, vuelve a verificar y finaliza con un CleanedDirection escrito. Léelo en la página; no lo escribas.

quarantine = Agent(
    name="quarantine",
    model=config.MODEL,
    mode="task",
    instruction=QUARANTINE_INSTRUCTION,
    tools=[find_policy_hits, suggest_replacement],
    output_schema=CleanedDirection)

Tal como se envía, la cadena se ejecuta desde la selección directamente hasta el generador de secuencias de comandos. 👉🖥 Coloca el router entre ellos, con sus dos salidas y el camino de regreso de la cuarentena; tres líneas reemplazan a una:

           (join_research, propose_directions, choose_direction, policy_check),
           (policy_check, {"OK": scripter, "BLOCK": quarantine}),
           (quarantine, scripter),

👉🖥 Ejecuta ambas rutas. Enviar la idea tal como está: choose_direction toma el candidato 1, policy_check emite OK y el guionista escribe un guion estructurado. Haz clic en Nueva sesión y vuelve a enviar la idea con , pick 4 al final: el candidato 4 es el cebo, policy_check emite BLOCK, la cuarentena llama a sus herramientas, reescribe las palabras rechazadas, llama a finish_task y la dirección limpia vuelve al guionista. Las filas de verificación se leen desde el almacén de sesiones.

Cada oración de la instrucción de la sesión 2 ahora tiene un nodo: la investigación son dos lectores y una unión, el rechazo es un enrutador y un agente de tareas, el guion es un nodo de agente. En todo el gráfico, la selección de reemplazo son dos nodos de función, direction_gate y persist_direction, que verás que se ejecutan directamente hasta que la sesión 5 detiene la puerta. El acuerdo con la creadora aún es provisional, al igual que la investigación, que todavía no ha escuchado al público.

6. 4 · Ingesta multimodal: lo que el público envió, leído en un corpus

Sesión 4: Transferencia multimodal

👉🖥 En el entorno de trabajo, abre Session 4 · Multimodal ingestion, partes 4a y 4b. El público no solo escribió. Un lector por tipo convierte todo lo que envió en texto con la misma forma, un corpus de RAG Engine lo contiene todo y un tercer lector en la distribución lo busca por significado y devuelve el contenido multimedia.

Qué envió el público (4a)

agent/comments.md contiene doce comentarios escritos en los seis videos de Halloween del canal. agent/audience/ contiene el resto: ocho notas de voz (voice/*.mp3), catorce fotos de las habitaciones de los espectadores (photo/*.jpg) y seis clips (clip/*.mp4), una panorámica lenta sobre una foto con la voz en off del espectador. La página del 4a se abre con todo el contenido: se reproducen todas las notas de voz, se muestran todas las fotos y se reproducen todos los clips.

La cuadrícula de público después de la transferencia: cada tarjeta muestra lo que Gemini escuchó o vio

RAG Engine recupera texto. Una nota de voz, una foto o un clip solo pueden unirse al corpus de la misma manera que lo hicieron los carteles anteriores en rag.caption_posters: un modelo lo convierte en una línea de texto que nombra su archivo, la línea se convierte en un pasaje y, cuando el pasaje regresa, el archivo regresa con él. agent/platform/audience.py tiene un lector por tipo. Cada uno envía un archivo y una instrucción a Gemini, y escribe una línea por archivo:

VOICE_PROMPT = ("Transcribe this voice note word for word inside double quotes. Then add: mood: and one word. "
                "Plain text on one line, nothing else.")
PHOTO_PROMPT = ("Two sentences: what is in the picture, the palette, the mood. Then: tags: and three short tags. "
                "Plain text on one line, no headings, no markdown.")
CLIP_PROMPT = ("Describe the shots in two sentences, then quote what you HEAR on the soundtrack as: heard: \"...\". "
               "Plain text on one line, no headings, no markdown.")


def _ask(path: pathlib.Path, mime: str, prompt: str) -> str:
    """One media file in, one line of text out."""
    from google.genai import types as gt
    client = images._client()
    resp = client.models.generate_content(
        model=config.MODEL,
        contents=[gt.Part.from_bytes(data=path.read_bytes(), mime_type=mime), prompt],
        config=gt.GenerateContentConfig(automatic_function_calling=gt.AutomaticFunctionCallingConfig(disable=True)))
    return " ".join((resp.text or "").split())


def transcribe_voice() -> pathlib.Path:
    """voice note -> text: Gemini listens."""
    files = sorted((AUDIENCE / "voice").glob("*.mp3"))
    lines = []
    for p in files:
        said = _ask(p, "audio/mpeg", VOICE_PROMPT)
        lines.append(f"- voice: {p.name} — {said}")
    return _write(AUDIENCE / "voice" / "transcripts.md", "voice", "What Gemini heard in the audience's voice notes", lines)

caption_photos hace lo mismo con image/jpeg y PHOTO_PROMPT; watch_clips con video/mp4 y CLIP_PROMPT, y Gemini mira la imagen y escucha la banda sonora en una sola llamada. Cada lector escribe la misma forma de línea, por lo que la división, la incorporación y la recuperación nunca saben de qué tipo proviene un pasaje:

- voice: note-03.mp3 — "I want a whole series of the cat judging costumes ..." mood: delighted
- photo: photo-01-ghost-drawer.jpg — A cream wooden drawer, striped socks, a paper-cup ghost ... tags: drawer, socks, cozy
- clip:  clip-05.mp4 — A hallway floor, a snail in a sheet and a round vacuum ... heard: "The snail won."
- poster: sock-ghost.png — A sheet ghost one inch above a striped sock ...

Un lector por tipo, una forma de línea y un corpus

El corpus. Vertex AI RAG Engine es la recuperación de documentos: un corpus se divide en pasajes de alrededor de 120 tokens, cada pasaje se incorpora con text-embedding-005 y una consulta se incorpora de la misma manera y se responde con los pasajes más cercanos por significado, no por palabras compartidas. "cozy little hauntings at home" encuentra el comentario que pide un poco de magia en habitaciones pequeñas, la nota de voz sobre el murciélago que abraza la bombilla y el clip cuya banda sonora dice que el fantasma es un vaso de papel.

corpus = rag.create_corpus(
    display_name="vibestudio-feedback",
    backend_config=rag.RagVectorDbConfig(
        rag_embedding_model_config=rag.RagEmbeddingModelConfig(
            vertex_prediction_endpoint=rag.VertexPredictionEndpoint(
                publisher_model="publishers/google/models/text-embedding-005"))))

rag.upload_file(
    corpus_name=corpus.name, path="agent/comments.md", display_name="comments.md",
    transformation_config=rag.TransformationConfig(
        chunking_config=rag.ChunkingConfig(chunk_size=120, chunk_overlap=20)))

La configuración ya comenzó a crear el corpus; la fila de conexión de la página 4a indica si está activo y lo crea si la configuración no pudo hacerlo. Desde los botones de la página o la terminal: python -m agent.platform.rag crea el corpus y lo registra en runs/ragcorpus.json; python -m agent.platform.rag load --audience ejecuta cada lector en audience.READERS y, luego, sube lo que escribieron junto a los comentarios y las leyendas de los pósteres, cada archivo con su propio nombre, y espera a que el índice responda; python -m agent.platform.rag query "cozy little hauntings at home" muestra la recuperación de pasajes que no comparten ninguna palabra con la consulta.

La edición. Tal como se envía, READERS solo contiene read_comments; los tres lectores de medios se encuentran detrás de un # . 👉🖥 Ejecuta la transferencia una vez tal como se envía: el resultado dice not read (no reader wired in audience.READERS): voice, photo, clip, y solo se incluyen los comentarios y los subtítulos. Luego, borra la # :

    transcribe_voice, caption_photos, watch_clips,

👉🖥 Guarda y vuelve a ejecutar la transferencia. Los lectores tardan unos tres minutos en leer los cuarenta elementos; la consola dice heard, saw y watched una línea a la vez, y cada tarjeta de la cuadrícula se completa con su línea. Las filas de verificación leen el archivo, el resultado de los lectores y el corpus: cuatro lectores conectados, ocho notas de voz escuchadas, catorce fotos vistas, seis clips vistos, cinco archivos en el corpus.

El tercer lector (4b)

Los comentarios del público son investigaciones que se comparten en cada ejecución, por lo que ingresan al grafo donde lo hace la investigación: como un tercer lector en la unión. read_feedback en agent/graph.py toma la idea de esta noche, le pide al corpus los pasajes más cercanos y se los informa a join_research, que espera tres. La arista (START, read_feedback, join_research) se envía en stage5_rag/agent.py, la forma que se enseñó en la sesión 3; tu edición está dentro del nodo. Recuperación, no una llamada al modelo, y determinística: la misma idea encuentra los mismos pasajes, mientras que el proponente varía.

def read_feedback(node_input):
    """The third reader: what the audience sent - comments, and what Gemini
    heard and saw in their voice notes, photos and clips - the passages
    nearest to tonight's idea, and the media those passages came with.
    Retrieval, not a model call."""
    from .platform import rag
    idea = idea_text(node_input)
    query = idea or "what viewers liked and what they complained about"
    try:
        hits = rag.retrieve(query)
    except Exception as e:
        print(f"  [rag] feedback unavailable ({str(e)[:80]})")
        return Event(output={"query": query, "feedback": [], "media": [],
                             "note": "no corpus connected - run: python -m agent.platform.rag"})
    return Event(output={"query": query, "feedback": [h["text"] for h in hits]})  # TODO: RAG_MEDIA

Cada línea del corpus que proviene de una nota de voz, una foto, un clip o un póster nombra su archivo. rag.media_in(hits) lee esos nombres de los pasajes y los convierte en URLs que la página puede mostrar y reproducir. 👉🖥 Devuelve el contenido multimedia con los pasajes, de modo que lo que envió el público viaje con lo que dijeron:

    return Event(output={"query": query, "feedback": [h["text"] for h in hits],
                         "media": rag.media_in(hits)})

👉🖥 Ejecuta stage5_rag con una idea. Abre read_feedback: la consulta es tu idea, el resultado tiene los cinco pasajes más cercanos y una lista media con una URL por archivo. Abrir join_research: tres teclas. Abre propose_directions: los candidatos del 1 al 3 citan feedback en sus evidencias. El panel de verificación muestra lo que el lector miró debajo de las filas: un clip, tres notas de voz y un póster del fantasma en la luz del refrigerador.

Qué miró el tercer lector: el contenido multimedia que acompañaba a los pasajes recuperados

7. 5 · Generación de video: Dos tipos de suspensión, un mecanismo

Sesión 5: Generación de videos

👉🖥 En el entorno de trabajo, abre Session 5 · generación de video, partes 5a y 5b, compiladas en stage6_video, todo el grafo. El clip tarda minutos y el póster tarda segundos, y la ejecución se detiene dos veces en el camino: una para ti y otra para Veo. Las dos paradas son un mecanismo, y las dos ramas del escritorio comparten lo que hicieron por nombre.

Dos tipos de suspensión (5a)

Nada espera. Una ejecución que debe detenerse escribe una llamada abierta en su sesión y finaliza; quien responda esa llamada, una persona o un proceso, la continúa. La página se abre en una tabla:

para una persona · direction_gate

para una herramienta lenta · render_desk

quién detiene la ejecución

direction_gate genera un RequestInput

render_submit devuelve {status: pending}; LongRunningFunctionTool hace que sea una parada

lo que espera

una persona: una de cuatro tarjetas

un trabajo lento: la operación de Veo, minutos

lo que se encuentra en la sesión

una llamada abierta de adk_request_input: el mensaje, el formulario y la carga útil

una llamada render_submit abierta: el recibo con el nombre de la operación

cómo se reanuda

un function_response con el ID de esa llamada, que lleva tu selección

un function_response con el ID de esa llamada, que lleva la URL del clip

quién lo envía

adk web, cuando envíes el formulario

python -m agent.deliver, desde otro proceso, más tarde

donde vive mientras tanto

runs/sessions.db · un ID de sesión · no se ejecuta nada

la misma fila, el mismo ID, no se ejecuta nada

Dos tipos de suspensión, una tabla

Para una persona En una sola instrucción, "pedirle al creador que confirme" es un consejo, y la sesión 2 lo ignoró en un mensaje. En un flujo de trabajo, la pausa es del motor. Un nodo que genera RequestInput suspende la ejecución: el ADK escribe una llamada abierta llamada adk_request_input en la sesión con tus message, response_schema y payload, le asigna un interrupt_id y vuelve al emisor. No hay esperas de subprocesos. Lo único que lo reanuda es un function_response que lleva ese ID, validado con tu esquema; un mensaje de chat al flujo de trabajo es un turno nuevo, no una respuesta. El esquema también es el formulario: adk web lo renderiza, y la app de VibeStudio renderiza el mismo esquema como cuatro tarjetas. direction_gate ya escribe los candidatos en el estado. 👉🖥 Detén el proceso:

    yield RequestInput(
        message="Pick tonight's direction: 1, 2, 3 or 4.",
        response_schema={
            "type": "object",
            "properties": {
                "pick": {"type": "string", "enum": ["1", "2", "3", "4"]}}},
        payload={"candidates": cands})

Tu respuesta llega a persist_direction como su node_input, un diccionario con una clave, pick. Los cuatro candidatos llegan de otra manera: la puerta de enlace los escribió en el estado compartido en candidates, y el ADK vincula una clave de estado a un parámetro con el mismo nombre. El nodo resuelve la selección en un candidato y escribe direction, angle, hook y user:prefs con Event(state=...). Las claves con el prefijo user: persisten en todas las sesiones de ese usuario.

Para una herramienta lenta. Un renderizado de Veo tarda minutos, y un gráfico no debería esperar tanto. render_submit en agent/desk.py inicia el trabajo y devuelve de inmediato un recibo cuyo estado es pending:

def render_submit(prompt: str) -> dict:
    """Submit one Veo render of `prompt`. Returns at once with a pending
    receipt; the clip is delivered later, to this call, by id."""
    receipt = videogen.start(f"{prompt} {videogen.NO_TEXT}")
    return {"status": "pending", "operation": receipt["operation"], "prompt": receipt["prompt"]}

Encapsulado en LongRunningFunctionTool, ese recibo significa algo para el ADK: el turno del agente finaliza, el flujo de trabajo se suspende en render_desk y la llamada abierta con su ID y recibo es una fila en runs/sessions.db. 👉🖥 En stage6_video/agent.py, une la herramienta:

    tools=[LongRunningFunctionTool(render_submit)])

Reanuda por ID, desde otro proceso. agent/deliver.py busca la llamada pendiente en el almacén de sesiones, sondea Veo hasta que exista el clip, escribe el resultado en runs/state.json y envía a la misma sesión un function_response con el ID de esa llamada. 👉🖥 Crea la parte de la respuesta:

    part = Part(function_response=FunctionResponse(
        id=row["call_id"], name=row["name"], response=response))

👉🖥 Ejecuta stage6_video hasta que se detenga dos veces. Envía una idea: la ejecución se detiene en un formulario creado a partir de tu esquema, con los cuatro candidatos en su carga útil. Escribe 2 en pick y envíalo. El gráfico se reanudará en persist_direction. Prueba la prueba de finalización aquí: suspende en el formulario, detén el servidor (scripts/stop.sh en la pestaña 1), vuelve a iniciarlo (scripts/restart.sh) y vuelve a abrir la sesión. El formulario sigue en pie, porque una pausa es una fila, no un hilo. Después de script_ready, render_desk llama a render_submit, obtiene el recibo, responde WAITING y finaliza la ejecución. La sesión conserva el ID de llamada. Ejecuta la entrega desde la consola de la página o la pestaña 1:

cd ~/vibe-studio-multimodal
uv run python -m agent.deliver status   # the pending renders, by session, call id and operation
uv run python -m agent.deliver          # wait for Veo, answer the newest pending call by id

👉🖥 Haz clic en Refresh adk web: function_response sigue la llamada pendiente, join_media se libera y store_release se ejecuta. Ocho filas de verificación leen las dos paradas de la sesión: la llamada de la puerta de enlace y tu respuesta, la recepción de la renderización y su entrega.

Indica el estado y el escritorio (5b)

La mesa de medios es la estructura de 3a de nuevo, una distribución y una unión, con una rama lenta. script_ready envía el guion de dos maneras. render_desk envía el clip y se suspende la ejecución. make_poster es la herramienta de la sesión 2 como un nodo: el poster_idea del guion, el estilo del canal y lo que Memory Bank sabe sobre el gusto del creador en cuanto a pósteres (vinculado desde el estado como memory_facts, sesión 6) van al modelo de imagen, y un PNG de 9:16 regresa en segundos. join_media espera a la lenta y store_release escribe lo que encontró en el estado.

Comparte por nombre. store_release nunca recibe el póster. Declara un parámetro llamado poster_url, y el ADK lo vincula desde el estado compartido, porque un nodo en la otra rama escribió esa clave. No se cruza ninguna carga útil entre las ramas:

def store_release(node_input, poster_url: str = "", poster_path: str = ""):
    """`poster_url` and `poster_path` are not passed by anyone: ADK binds them
    from shared STATE because the parameter names match the keys make_poster
    wrote (the STATE_SHARE edit). The two branches of the desk never hand each
    other payloads; they share what they made by name."""
    render = state.load().get("render") or {}          # the delivered clip, written by deliver
    url, status = render.get("url") or "", render.get("status", "")
    ...
    if not poster_url:
        said += " · poster: not in state (did make_poster write poster_url to state? the STATE_SHARE edit)"
    yield Event(output={"status": status, "url": url, "poster_url": poster_url},
                state={"render_url": url, "render_status": status, "poster_url": poster_url},
                message=f"video: {said} · poster: {poster_url or 'none'}")

Tal como se envía, make_poster genera el póster como resultado y un mensaje, y store_release dice poster: not in state. 👉🖥 Agrega la escritura de estado a su resultado final:

                state={"poster_url": rec.get("url", ""), "poster_path": rec.get("path", "")},
                message=f"poster: {said}")

👉🖥 Ejecuta una vez más y entrega. Abre el evento de make_poster: la URL del póster en su resultado y un delta de estado con poster_url. Abre store_release: su mensaje indica el nombre de quien lo publicó. El bloque This run's session de la página muestra la misma fila que muestra la pestaña State de adk web: el ID de sesión que necesita un currículum, la llamada que está abierta y las claves que escribieron los nodos, entre ellas, poster_url. En el panel de verificación, se muestran el clip y el póster uno al lado del otro.

El clip y su póster, uno al lado del otro, después de la publicación

8. 6 · Memoria: lo que el canal recuerda entre noches

Sesión 6: Memoria

👉🖥 En el entorno de trabajo, abre Session 6 · Memory, partes 6a y 6b, creadas en stage4_memory. La sesión 5 trató sobre el estado de una ejecución. Esta sesión trata sobre lo que el canal recuerda entre ejecuciones: lo que el grafo escucha desde el exterior, a través de dos devoluciones de llamada.

Memory Bank (6a)

Vertex AI Memory Bank es una memoria administrada a largo plazo sobre una persona. Los hechos se encuentran en un alcance, el nombre de la app y el ID de usuario, y en los temas que definas. Cuando le entregas un intercambio con memories.generate, extrae lo que se ajusta a un tema, lo incorpora y lo consolida con lo que ya tiene: se crea un hecho nuevo, se actualiza uno modificado y se combina uno repetido. memories.retrieve devuelve los hechos de un permiso. Ambas llamadas se realizan en agent/platform/memory.py.

SCOPE = {"app_name": config.APP, "user_id": config.USER}
TOPICS = {
    "CREATOR_TASTE": "Which video directions this creator picks and passes on, "
                     "and how that preference changes over time.",
    "CHANNEL_RULES": "Standing instructions the creator states for every video "
                     "(style, subjects to avoid, format rules).",
    "VISUAL_STYLE": "What this creator approves and rejects in a poster: palettes, "
                    "framing, props and mood of the posters they shipped.",
}

El tercer tema es el gusto del canal en cuanto a las imágenes. La versión escribió en ella en la sesión 5: se describe un póster enviado en una oración y se recuerda, y el siguiente póster se dibuja con esa oración en su instrucción.

La configuración ya comenzó a crear el banco; la fila de conexión de la página 6a indica si está activo. Desde los botones de la página o la terminal: python -m agent.platform.bank crea el banco y sus temas, y almacena en caché el nombre del recurso en runs/memorybank.json; python -m agent.platform.bank load genera cuatro sesiones anteriores (dos eras de animales con una regla de estilo, una era de gadgets y una era de fantasía reciente); python -m agent.platform.bank list muestra en qué se convirtieron las transcripciones: declaraciones de hechos consolidadas, no transcripciones.

Devoluciones de llamada (6b)

La memoria no es un nodo. Una devolución de llamada es una función que pasas a un Agent; el ADK la llama en un momento fijo con el contexto en vivo, y devolver None permite que ocurra lo normal. Tres pares: alrededor del turno del agente (before_agent_callback, after_agent_callback, con un CallbackContext), alrededor de cada llamada al modelo (before_model_callback con el LlmRequest, after_model_callback con el LlmResponse), alrededor de cada llamada a la herramienta. Las barreras de seguridad, la telemetría, las cachés y la inserción de contexto se incluyen aquí, sin un nodo en el grafo.

En stage4_memory/agent.py, propose_directions lee el banco antes de su llamada al modelo: recall_taste recupera los hechos, los agrega a la solicitud como un bloque MEMORY, primero los más antiguos, y le indica al modelo que incline los candidatos del 1 al 3 hacia el gusto actual y que trate las reglas como restricciones. También escribe los hechos en el estado como memory_facts, donde make_poster los encuentra. 👉🖥 Conecta los cables:

    output_schema=Directions,
    before_model_callback=recall_taste)

El guionista escribe lo que el creador eligió después de su turno: remember_pick lee direction del estado, escribe una oración sobre la elección y se la entrega a memories.generate. 👉🖥 Conecta los cables:

    output_schema=Script,
    after_agent_callback=remember_pick)

👉🖥 Ejecuta el código con un mensaje vacío y elige una tarjeta. Abre el evento propose_directions: la solicitud incluye las tres eras, y los candidatos del 1 al 3 se inclinan por el gusto actual, mientras que las tendencias sugieren otra cosa. Después de la secuencia de comandos, haz clic en Mostrar el banco: se cambió o se agregó un recuerdo para la selección de esta noche. Ejecuta una segunda noche: recuerda la primera.

9. 7 · Implementa y publica: el grafo dentro de una app, la noche en VibeTube

Sesión 7: Implementa y publica

👉🖥 En el entorno de trabajo, abre Sesión 7: Implementa y publica, partes 7a y 7b.

Implementa VibeStudio (7a)

En las zonas de pruebas, adk web impulsó el gráfico. VibeStudio lo controla con el Runner del ADK, en un subproceso de trabajo con su propio bucle de eventos, y pliega cada evento en un estado de ejecución que la página lee a través de eventos enviados por el servidor: los lectores, tu elección de cuatro cartas, el póster cuando llega y la renderización cuando se entrega. La respuesta de la puerta de enlace y la entrega de la renderización son la misma llamada, una parte de function_response, y la app sondea a Veo en lugar de esperar a la consola.

self._svc = DatabaseSessionService(db_url=config.DB_URL)
self._runner = Runner(app_name=config.APP, agent=wf, session_service=self._svc)

async for ev in self._runner.run_async(user_id=config.USER, session_id=run_id, new_message=message):
    self._absorb(ev)    # fold the ADK event into the run state, publish one app event

part = Part(function_response=FunctionResponse(id=call_id, name=name, response=response))

vibestudio/server/agent/ es una copia byte por byte del agent/ del lab, que verifica checks/verify_app.py. La lista completa de bordes en agent/graph.py es cada estructura de este codelab en un solo lugar, trece nodos; se envía completa, para que la leas, no para que la escribas:

        (START, scan_trends, join_research),
        (START, read_backlog, join_research),
        (START, read_feedback, join_research),
        (join_research, propose_directions, direction_gate,
         persist_direction, policy_check),
        (policy_check, {"OK": scripter, "BLOCK": quarantine}),
        (quarantine, scripter),
        (scripter, script_ready),
        (script_ready, render_desk, join_media),
        (script_ready, make_poster, join_media),
        (join_media, store_release),

👉🖥 Implementa con el botón Deploy del entorno de trabajo o 👉💻 desde la pestaña 1:

cd ~/vibe-studio-multimodal
uv run python vibestudio/deploy.py

que se ejecuta con los valores de tu .env completados:

gcloud run deploy vibestudio --source vibestudio \
  --project $GOOGLE_CLOUD_PROJECT --region us-central1 \
  --labels dev-tutorial-codelab=vibetube --allow-unauthenticated \
  --memory 2Gi --cpu 2 --timeout 3600 --concurrency 40 \
  --max-instances 1 --min-instances 1 --session-affinity \
  --set-env-vars GOOGLE_CLOUD_PROJECT=...,STUDIO_VERTEX=1,STUDIO_MEMORY_BANK=...,STUDIO_RAG_CORPUS=...,VIBETUBE_URL=...,VIBETUBE_EVENT=...,VIBETUBE_NAME=...,VIBETUBE_PROJECT=...

--source compila el contenedor con Cloud Build y lo implementa en un solo paso. La primera compilación tarda entre cuatro y cinco minutos. --session-affinity con una instancia activa mantiene una ejecución suspendida en la instancia que la contiene: el almacén de la sesión 5, una vez más. En una implementación más grande, el almacén de sesiones se traslada a VertexAiSessionService y la afinidad desaparece. La compilación y el servicio se ejecutan como la cuenta de servicio de procesamiento predeterminada del proyecto, cuya configuración otorgó los roles que necesita.

👉🖥 Abre la URL del servicio y ejecuta una noche en la app: una idea, una selección, espera el clip y el póster. En la pestaña Historial, se conservan ambos.

Publica (7b)

La publicación es una solicitud POST de varias partes de la app al extremo de la sala en VibeTube, en vibestudio/server/platform/publish.py. El clip es el video, el póster es la miniatura y el perfil te nombra a ti y a la sala:

url = f"{prof['platform_url']}/api/events/{prof['event_code']}/videos"
data = {"title": state.script["title"], "description": state.script["description"],
        "duration": "0:08", "displayName": prof["display_name"],
        "projectId": prof["project_id"]}        # one video per project and room: publishing again replaces it
files = {"videoFile": (clip.name, clip.open("rb"), "video/mp4"),
         "thumbnailFile": (poster.name, poster.open("rb"), "image/png")}   # the poster, not a frame
r = httpx.post(url, data=data, files=files, timeout=120)
video_id = r.json()["id"]                       # watch it at {platform}/e/{event}?v={video_id}

VibeTube conserva un video por projectId y sala, y el proyecto que envía la app es tu proyecto de Google Cloud, por lo que volver a publicar reemplaza tu video en lugar de agregar uno. Nunca se sube un póster de reemplazo; el real es la miniatura, byte por byte.

La última edición es una configuración, no una línea de código. 👉🖥 En la página 7b: la URL de VibeStudio (completada a partir de la implementación de 7a), el código de evento de la sala, exactamente como lo escribe VibeTube (el que proporcionaste en la configuración, a menos que tu instructor haya abierto otro) y tu nombre. Presiona Publicar. El banco de trabajo retransmite el clic a la app, la app publica la noche y las filas de abajo leen el resultado de la app y de VibeTube: la app responde, la noche se termina con un póster real, el nombre interno nombra una sala abierta, VibeTube devolvió un ID de video, el video está en el muro y su miniatura es tu póster. La tarjeta de resultados muestra la miniatura en VibeTube junto al póster en la app.

Publicado: La miniatura en VibeTube es el póster que dibujó el modelo de imagen

10. Qué compilaste

👉🖥 El final de la página 7b dibuja todo el grafo, trece nodos desde una idea hasta un clip y su póster; coloca el cursor sobre un nodo para ver qué hace y la sesión que lo creó.

Sesión

Qué enseñó

Se escribe como

2 · Generación de imágenes

Un agente: modelo, instrucción, herramientas; el modelo de imagen como herramienta; lo que una instrucción no puede prometer

Agent(tools=[...]), function_call / function_response, images.generate

3 · Workflows

La lista de bordes, la distribución ramificada y la unión, un router con un bucle de modo de tarea detrás

Workflow, START, JoinNode, Event(route=...), mode="task" y finish_task

4 · Transferencia multimodal

Un lector por tipo, una forma de línea, un corpus; un tercer lector por significado, con los medios

generate_content con partes de audio, imagen y video, rag.upload_file, rag.retrieve, media_in

5 · Generación de videos

Dos tipos de suspensión, un mecanismo; el almacén de sesiones; el póster compartido por nombre

RequestInput, LongRunningFunctionTool, FunctionResponse(id=...), Event(state=...), vinculación de parámetros

6 - Memoria

Memoria durante las noches, dos devoluciones de llamada

memories.generate / retrieve, before_model_callback, after_agent_callback

7 - Implementa y publica

El gráfico dentro de una app, en Cloud Run; la noche en VibeTube

Runner(agent=wf), run_async, eventos enviados por el servidor, un POST de varias partes

Cinco reglas que sigue el gráfico:

  1. Suspende en lugar de esperar. Una pausa es una fila en el almacén de sesiones, no un subproceso.
  2. Reanuda todo de la misma manera: por el ID de la llamada abierta, ya sea que responda una persona o un proceso.
  3. Comparte datos por nombre en el estado, no los pases por cada carga útil.
  4. Decide con código antes de pagar por un modelo. La política es un archivo de texto y un router, y se ejecuta antes de la renderización.
  5. Lee todo lo que no sea texto en texto antes de que llegue al corpus, con el nombre del archivo dentro de la línea para que el archivo pueda volver.

Una instrucción no puede prometer un orden, una detención o una verificación. Un gráfico sí puede.

Próximos pasos

  • VertexAiSessionService para las sesiones que sobreviven a una instancia y descarta --session-affinity.
  • Un webhook que responde a la llamada de renderización en lugar de un poller: el mismo FunctionResponse(id=...), pero un remitente diferente.
  • Un segundo RequestInput antes de publicar: el esquema es el formulario, por lo que es un nodo y no hay código de página.
  • Los nuevos comentarios, notas de voz y clips del público que se agregaron al corpus después de cada lanzamiento, junto con el póster con el que se enviaron.

Limpia

Todo lo que se creó en este lab se encuentra en el proyecto que se creó en el paso 4, por lo que la forma más económica de realizar la limpieza es borrar el proyecto. En Cloud Shell:

gcloud projects delete $(cat ~/project_id.txt)

Para conservar el proyecto y quitar solo las partes pagadas, haz lo siguiente: el servicio de Cloud Run (gcloud run services delete vibestudio --region us-central1), el corpus de RAG y el banco de memoria (sus nombres de recursos están en runs/ragcorpus.json y runs/memorybank.json, y las páginas de Agent Engine y RAG Engine de la consola los borran) y la imagen de contenedor en Artifact Registry.