<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Laura Bolaños</title>
    <description>The latest articles on DEV Community by Laura Bolaños (@reinalau).</description>
    <link>https://dev.to/reinalau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3629379%2F49e3cfe8-aac1-4f47-b540-d042f1e6d241.jpg</url>
      <title>DEV Community: Laura Bolaños</title>
      <link>https://dev.to/reinalau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reinalau"/>
    <language>en</language>
    <item>
      <title>Una novedad en Serverless bajo la lupa: AWS Lambda Web Functions</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:52:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/una-novedad-en-serverless-bajo-la-lupa-aws-lambda-web-functions-nm4</link>
      <guid>https://dev.to/aws-builders/una-novedad-en-serverless-bajo-la-lupa-aws-lambda-web-functions-nm4</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;AWS anunció Lambda Web Functions, una forma de ejecutar aplicaciones web y APIs sobre Lambda ejecutando directamente un servidor HTTP de Node.js, sin adaptadores ni handlers por evento. Aquí te describo mi propio análisis.&lt;br&gt;
Este artículo no pretende ser una guía de cómo usar Lambda Web Functions. Hago un recorrido corto por las características que importan, referencio blogs de colegas y muestro mi propio enfoque de cuándo utilizarla y cuando No.&lt;br&gt;
Para no quedarme en la teoría, armé un pequeño playground con ejemplos: desde un agente de IA con streaming hasta microservicios simulando alto tráfico. Además, hice algunos experimentos de concurrencia y comportamiento de escalado.&lt;/p&gt;
&lt;h2&gt;
  
  
  Qué es una Lambda Web Function
&lt;/h2&gt;

&lt;p&gt;Una Lambda Web Function es cómputo serverless que corre un &lt;strong&gt;servidor HTTP normal&lt;/strong&gt;. Tu aplicación escucha en el host y puerto que Lambda te pasa en la variable &lt;code&gt;AWS_LAMBDA_HTTP_ENDPOINT&lt;/code&gt;, exactamente el mismo código que correrías con &lt;code&gt;node index.js&lt;/code&gt; en tu máquina. AWS te da el endpoint HTTPS, el TLS y el escalado; de tu parte toca escribir Express (o el framework que uses) sin adaptadores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NO&lt;/strong&gt; es un reemplazo universal de la Lambda de siempre que devuelve un event handler. Esta sigue siendo la opción natural para lo que es dirigido por eventos: procesar mensajes de una SQS, reaccionar a un evento de S3, consumir un stream. Tampoco es un servidor que tengas "siempre encendido": escala a cero cuando no hay tráfico. Y por ahora solo soporta runtime &lt;strong&gt;Node.js&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lo que cambia de verdad
&lt;/h3&gt;

&lt;p&gt;Muchos de los protocolos que vas a ver en las demos (REST, SSE, streaming, webhooks) ya se podían hacer antes de otra forma. Lo que cambia no son los protocolos, sino la &lt;strong&gt;forma&lt;/strong&gt; de la Lambda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modelo de programación.&lt;/strong&gt; Antes escribías un handler que recibía un evento; ahora escribís un servidor HTTP común que escucha en un puerto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrencia.&lt;/strong&gt; En una Event Function, cada entorno atiende una petición a la vez. En una Web Function, un mismo entorno atiende varias en simultáneo, como un servidor Node común. Para algo que pasa casi todo el tiempo esperando (una base de datos, una API, un LLM), eso significa que muchas peticiones comparten un proceso en lugar de ocupar un entorno cada una.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parámetros importantes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parámetro&lt;/th&gt;
&lt;th&gt;descripción&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tope por entorno, 1–128, se configura en la &lt;strong&gt;revisión&lt;/strong&gt;. El límite real puede ser menor, nunca mayor. Cambiarlo crea una nueva revisión&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxEnvironments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tope de entornos por región, se configura en el &lt;strong&gt;endpoint&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rateLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;peticiones por segundo que acepta el &lt;strong&gt;endpoint&lt;/strong&gt;, por región. Lo que supera rateLimit recibe 429. Lo mismo ocurre si no puede escalar más, pero mientras un entorno nuevo se inicializa, las requests esperan en cola. rateLimit se aplica por región, y sus valores deben ser múltiplos de 100 (hasta 1.000) o de 1.000 (por encima).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;maxEnvironments&lt;/code&gt; y &lt;code&gt;rateLimit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;se cambian en caliente con &lt;code&gt;aws lambda-web update-web-function-endpoint&lt;/code&gt;, sin redeployar; en cambio &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt; sí obliga a hacer una nueva revisión de la Lambda.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entrada y salida: HTTP crudo, no un payload.&lt;/strong&gt; Una Event Function recibe un &lt;code&gt;event&lt;/code&gt; (un JSON ya parseado) y devuelve otro JSON; nunca tocás HTTP a mano. Una Web Function recibe y devuelve &lt;strong&gt;HTTP/1.1 crudo&lt;/strong&gt;: Lambda reenvía a tu app el método, el path, la query string, el body y casi todos los headers, y tu servidor (Express, Fastify, Hono o el framework de Node que prefieras) ve un &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; normal. Soporta GET, HEAD, POST, PUT, PATCH, OPTIONS y DELETE, solo HTTP/1.1 (un HTTP/1.0 lo rechaza con 505), y agrega headers de contexto como &lt;code&gt;lambda-web-request-id&lt;/code&gt;. Es justo lo que te deja usar tu framework sin adaptador, pero el parseo y la validación del request quedan de tu lado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad: el endpoint es público, el modo de auth define quién valida.&lt;/strong&gt; Todo endpoint es alcanzable desde internet, tenga el auth que tenga. Hay dos modos: con &lt;code&gt;IamAuth&lt;/code&gt;, Lambda valida la firma SigV4 y remueve el header &lt;code&gt;Authorization&lt;/code&gt; antes de reenviar (la autorización la hace la plataforma); con &lt;code&gt;ApplicationManaged&lt;/code&gt;, Lambda no auténtica nada y reenvía todo a tu app, así que conocer la URL alcanza para llegar y es necesario implementar la autenticación. Sumá a eso que algunos headers no son de fiar (&lt;code&gt;X-Forwarded-Host&lt;/code&gt; llega sin validar; el cliente real está en el último valor de &lt;code&gt;X-Forwarded-For&lt;/code&gt;) y que validar input, tamaño de request y abuso es responsabilidad de tu código o de un front door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facturación:&lt;/strong&gt; Pagás &lt;strong&gt;CPU activa&lt;/strong&gt; y &lt;strong&gt;memoria&lt;/strong&gt; mientras el entorno exista. No se cobra tiempo de reloj. Una petición que espera a una base de datos o a una API externa consume poca CPU, y esa espera casi no se cobra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proceso de larga vida.&lt;/strong&gt; El entorno no se congela entre peticiones → hay estado global, conexiones reutilizadas y tareas en segundo plano.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Para el detalle de cada protocolo, el recorrido de &lt;a href="https://edjgeek.com/blog/why-i-m-excited-about-lambda-web-functions/" rel="noopener noreferrer"&gt;edjgeek&lt;/a&gt; lo cubre muy bien, y el repositorio de ejemplos que acompaña ese blog muestra REST, SSE, NDJSON y webhooks funcionando.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dónde NO lo usaría
&lt;/h3&gt;

&lt;p&gt;Hay varios escenarios donde esta tipo de Lambda no aplica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request/response simple y de bajo volumen.&lt;/strong&gt; Un formulario que dispara un mail, o una Lambda que devuelve un JSON corto cada tanto. Ahí una Event Function con su function URL es más simple y suficiente; migrar no te da ningún beneficio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trabajo dirigido por eventos.&lt;/strong&gt; Procesar una cola SQS, reaccionar a un archivo en S3, consumir un stream. Eso es territorio de las Event Functions, y la propia documentación lo recomienda así.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tareas largas.&lt;/strong&gt; El timeout de una petición llega hasta 900 s (15 min). Si tu proceso puede tardar más, no entra en una sola petición: hay que partirlo o usar otro mecanismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets o HTTP/2.&lt;/strong&gt; no  mantiene conexiones bidireccionales persistentes de tipo TCP Upgrade abiertas directamente hacia el entorno de ejecución de la Lambda de forma continua. Si este es tu caso no se resuelve por aquí.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;En Tareas multi-paso elegimos Lambda durable functions.&lt;/strong&gt; Pensadas para flujos de trabajo resilientes de varios pasos que pueden durar hasta un año, con checkpoints, reintentos y estados de espera que no consumen cómputo mientras aguardan. Casos de uso: procesar pagos, orquestar pedidos, flujos con aprobación humana o workflows de IA de varios pasos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ La regla práctica: si tu carga no es "un servidor HTTP que atiende tráfico concurrente", probablemente Lambda Web Functions no corresponda a tu caso de uso.&lt;/p&gt;
&lt;h2&gt;
  
  
  ¿Reemplaza a API Gateway / function URLs?
&lt;/h2&gt;

&lt;p&gt;Una Web Function ya trae su propio endpoint HTTPS. Pero reemplaza en los casos simples, no es un reemplazo directo.&lt;/p&gt;

&lt;p&gt;Pensemos primero en las &lt;strong&gt;function URLs&lt;/strong&gt;, la URL pública de una Lambda común. Desde el lado del cliente, para un request/response sencillo, casi no hay diferencia: hacés una petición HTTP y recibís una respuesta. Si lo que tenés es una Lambda con su URL devolviendo un JSON corto, migrar a Web Functions no te cambia nada visible. Donde empieza a tener sentido es cuando aparece concurrencia con mucha espera, streaming, o una app con framework y varias rutas.&lt;/p&gt;

&lt;p&gt;Con &lt;strong&gt;API Gateway&lt;/strong&gt; la cuestión es diferente, porque son de distinta naturaleza: una Web Function es cómputo con un endpoint incluido, y API Gateway es una puerta de entrada que se pone delante del cómputo.&lt;/p&gt;

&lt;p&gt;Si tenés una app con su propio router (Express, Hono, Fastify), antes necesitabas API Gateway para exponerla; ahora el endpoint ya viene con HTTPS, autenticación y un límite global de peticiones. Para APIs internas o servicios entre equipos, eso suele alcanzar.&lt;/p&gt;

&lt;p&gt;Pero API Gateway sigue teniendo sentido como &lt;em&gt;front door&lt;/em&gt;. La propia documentación de buenas prácticas recomienda poner &lt;strong&gt;CloudFront o API Gateway delante&lt;/strong&gt; del endpoint cuando necesitás dominio propio, inspección con WAF, caché o controles centralizados en el borde. Y hay features que API Gateway te da y la Web Function no: planes de uso y API keys por cliente, authorizers (JWT, Cognito), cuotas por consumidor. El &lt;strong&gt;límite de peticiones de la Web Function es un tope por endpoint y región, no por cliente&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⭐ Para una app o microservicio con su propio router, Lambda Web Functions te AHORRA una capa. Para una API pública con múltiples consumidores, cuotas y políticas en el borde, seguís necesitando API Gateway o CloudFront según el caso.&lt;/p&gt;
&lt;h2&gt;
  
  
  Casos de Uso del playground
&lt;/h2&gt;

&lt;p&gt;Construí ejemplos sencillos que pueden servirte para llevar a la práctica rápidamente tus casos de uso.&lt;/p&gt;

&lt;p&gt;📦 Aquí vas encontrar ejemplos con dos maneras de desplegar: manualmente por la CLi o via SAM : &lt;a href="https://github.com/reinalau/lambda-web-functions-playground" rel="noopener noreferrer"&gt;github.com/reinalau/lambda-web-functions-playground&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lambda Web Functions ya es compatible con IaC:&lt;br&gt;
&lt;strong&gt;AWS CloudFormation&lt;/strong&gt; (&amp;nbsp;"AWS::Lambda::WebFunction&amp;nbsp; resource").&lt;br&gt;
&lt;strong&gt;AWS CDK&lt;/strong&gt; (L2 constructs available)&lt;br&gt;
&lt;strong&gt;AWS SAM&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Un agente strands harness con streaming
&lt;/h3&gt;

&lt;p&gt;Es un agente de IA hecho con strands harness, que responde preguntas sobre el clima y ayuda a planificar viajes. Recibe un mensaje, llama a herramientas que consultan una API pública (Open-Meteo), razona sobre los datos y devuelve la respuesta al navegador &lt;strong&gt;token a token&lt;/strong&gt; por SSE. Mantiene la conversación entre turnos, recuerda lo que ya le dijiste.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;%%{init: {"theme": "base", "themeVariables": {"fontSize": "30px"}}}%%
flowchart LR
    Client["Client / browser"] --&amp;gt;|"POST /chat"| Agent["weather-agent&amp;lt;br /&amp;gt;Lambda Web Function (Express)&amp;lt;br /&amp;gt;Strands harness"]
    Agent --&amp;gt;|"SSE: stream tokens"| Client
    Agent --&amp;gt;|"InvokeModelWithResponseStream"| Bedrock["Amazon Bedrock&amp;lt;br /&amp;gt;Nova 2 Lite"]
    Agent --&amp;gt;|"tools (no API key)"| OpenMeteo["Open-Meteo API&amp;lt;br /&amp;gt;geocode + forecast"]
    Agent --&amp;gt;|"SessionManager&amp;lt;br /&amp;gt;by conversationId"| S3[("Amazon S3&amp;lt;br /&amp;gt;session state")]

    style Client fill:#2563eb,stroke:#1e40af,color:#ffffff
    style Agent fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Bedrock fill:#15803d,stroke:#0f5027,color:#ffffff
    style OpenMeteo fill:#64748b,stroke:#475569,color:#ffffff
    style S3 fill:#4ade80,stroke:#16a34a,color:#063d1e&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Por qué con Web Functions y no con otra cosa →&lt;/strong&gt; Este caso pasa casi todo el tiempo &lt;em&gt;esperando&lt;/em&gt; al modelo y a las herramientas, consumiendo muy poca CPU, y esa espera casi no se cobra. Además:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El modelo de servidor HTTP me deja usar el harness de Strands y Express tal cual, sin reescribir nada como handler ni armar un adaptador.&lt;/li&gt;
&lt;li&gt;El streaming de tokens por SSE es nativo: devuelvo la respuesta a medida que se genera, sin esperar a tenerla completa.&lt;/li&gt;
&lt;li&gt;La concurrencia por entorno significa que muchas conversaciones conviven en el mismo proceso mientras esperan al LLM, en vez de ocupar un entorno entero cada una (como pasaría con una Event Function).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En una Event Function con function URL pagaría el tiempo de reloj completo de cada conversación y ocuparía un entorno por request. Acá no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La implementación:&lt;/strong&gt; Es Express corriendo como Web Function. El agente usa el &lt;strong&gt;harness&lt;/strong&gt; de Strands (TypeScript) con &lt;code&gt;builtinTools: []&lt;/code&gt; para dejar solo las dos herramientas de clima (nada de shell ni acceso a archivos: menor superficie), sobre un modelo &lt;strong&gt;Amazon Nova&lt;/strong&gt; en Bedrock.&lt;/p&gt;

&lt;p&gt;El punto fino es el estado: como el entorno es efímero y no hay afinidad de sesión, la conversación &lt;strong&gt;no puede vivir en memoria&lt;/strong&gt;; se persiste fuera, en S3, usando el &lt;code&gt;SessionManager&lt;/code&gt; del SDK. Cada conversación se identifica por un id, y cualquier entorno puede retomarla. El detalle completo lo encontrás en el README del ejemplo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un BFF con microservicios (pedidos + inventario)
&lt;/h3&gt;

&lt;p&gt;Este ejemplo consta de dos Lambda Web Functions que modelan un patrón BFF + microservicios, sin IA: solo API y DynamoDB. Un servicio &lt;code&gt;orders&lt;/code&gt; (el BFF, público) recibe un pedido, reserva stock llamando a un servicio &lt;code&gt;inventory&lt;/code&gt; (privado) y persiste el pedido. &lt;code&gt;inventory&lt;/code&gt; expone el stock y lo reserva con un &lt;code&gt;UpdateItem&lt;/code&gt; condicional que nunca lo deja en negativo (devuelve &lt;code&gt;409&lt;/code&gt; si no alcanza).&lt;br&gt;&lt;br&gt;
Este caso es de un "backend de alto tráfico que espera mucho I/O": cada pedido llama a otro servicio y a una base de datos, y pasa la mayor parte del tiempo esperando esas dos cosas.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;%%{init: {"theme": "base", "themeVariables": {"fontSize": "28px"}}}%%
flowchart LR
    Client["Client"] --&amp;gt;|"POST /orders (no signing)"| Orders["orders BFF&amp;lt;br /&amp;gt;Lambda Web Function&amp;lt;br /&amp;gt;ApplicationManaged"]
    Orders --&amp;gt;|"SigV4-signed call&amp;lt;br /&amp;gt;service: lambda&amp;lt;br /&amp;gt;x-amz-content-sha256"| Inventory["inventory&amp;lt;br /&amp;gt;Lambda Web Function&amp;lt;br /&amp;gt;IamAuth (private)"]
    Orders --&amp;gt;|"PutItem"| OrdersDB[("DynamoDB&amp;lt;br /&amp;gt;orders table")]
    Inventory --&amp;gt;|"conditional UpdateItem&amp;lt;br /&amp;gt;reserve stock or 409"| InventoryDB[("DynamoDB&amp;lt;br /&amp;gt;inventory table")]

    style Client fill:#22c55e,stroke:#15803d,color:#ffffff
    style Orders fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Inventory fill:#f59e0b,stroke:#b45309,color:#ffffff
    style OrdersDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style InventoryDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Algunas características:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El servidor HTTP deja usar Express con su router tal cual; &lt;code&gt;orders&lt;/code&gt; enruta por dentro y no necesito una función por ruta ni API Gateway delante.&lt;/li&gt;
&lt;li&gt;La concurrencia por entorno deja que muchas peticiones convivan en el mismo proceso mientras esperan a &lt;code&gt;inventory&lt;/code&gt; y a DynamoDB.&lt;/li&gt;
&lt;li&gt;Cada servicio escala por separado: un pico en el BFF no obliga a sobredimensionar el inventario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Comunicación entre servicios con IamAuth.&lt;/strong&gt; La mayoría de las reseñas lo pasa por alto: &lt;code&gt;inventory&lt;/code&gt; es privado (&lt;code&gt;IamAuth&lt;/code&gt;), así que &lt;code&gt;orders&lt;/code&gt; no lo invoca con un SDK →  tiene que &lt;strong&gt;firmar la petición HTTP con SigV4&lt;/strong&gt; usando el servicio &lt;code&gt;lambda&lt;/code&gt;, e incluir siempre la cabecera &lt;code&gt;x-amz-content-sha256&lt;/code&gt; (Lambda la usa como hash del payload en vez de leer el body).&lt;/p&gt;

&lt;p&gt;El rol de &lt;code&gt;orders&lt;/code&gt; necesita el permiso &lt;code&gt;lambda:InvokeWebFunctionEndpoint&lt;/code&gt; sobre el endpoint de &lt;code&gt;inventory&lt;/code&gt;, y Lambda elimina la cabecera &lt;code&gt;Authorization&lt;/code&gt; antes de reenviar la request a la aplicación. Para endpoints multi-región la firma pasa a SigV4a.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La implementación:&lt;/strong&gt; Dos servicios Express en TypeScript, cada uno su propia Web Function con su rol de mínimo privilegio. &lt;code&gt;inventory&lt;/code&gt; guarda el stock en DynamoDB y hace la reserva atómica; &lt;code&gt;orders&lt;/code&gt; firma su llamada a &lt;code&gt;inventory&lt;/code&gt;, y si la reserva sale bien persiste el pedido en otra tabla. El detalle (despliegue ordenado, IAM y la receta de firma) está en el README del ejemplo.&lt;br&gt;
Hice dos experimentos de caga sobre el BFF. Los números salen de los eventos de CloudWatch (&lt;code&gt;invokeReport&lt;/code&gt; / &lt;code&gt;initReport&lt;/code&gt;).&lt;/p&gt;

&lt;h4&gt;
  
  
  Experimento 1 ➡ Carga: ¿cuánta CPU se cobra?
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Objetivo:&lt;/em&gt; analizar con tráfico concurrente qué fracción del tiempo del entorno se factura como CPU. 500 pedidos, 50 en paralelo (no 500 a la vez: 500 en total).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Valor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pedidos (todos &lt;code&gt;201&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo de reloj&lt;/td&gt;
&lt;td&gt;15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entornos creados&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pico de concurrencia por entorno&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU activa de &lt;em&gt;serving&lt;/em&gt; (toda la flota)&lt;/td&gt;
&lt;td&gt;~49 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU de arranque (toda la flota)&lt;/td&gt;
&lt;td&gt;~22 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vida de servicio sumada&lt;/td&gt;
&lt;td&gt;~24.461 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Análisis:&lt;/em&gt; la CPU de serving (~49 s) es &lt;strong&gt;~0,2%&lt;/strong&gt; de la vida de servicio sumada (~24.461 s). Cuidado con la lectura: ese 99,8% restante &lt;strong&gt;no es todo "espera de I/O"&lt;/strong&gt;. La carga duró 15 s, pero cada entorno siguió vivo ~400 s hasta escalar a cero, así que la mayor parte es &lt;strong&gt;ocio&lt;/strong&gt; del entorno después de la carga, no espera por request. El punto importante, ese tiempo no se cobra como CPU (la memoria sí se cobra mientras el entorno exista). Medir la espera de I/O por request requiere los eventos &lt;code&gt;requestComplete&lt;/code&gt; (nivel DEBUG), que no lo activé acá.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3Kmt03rgqsf2FHLA1ytgI7Sofw0%2Fexp1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3Kmt03rgqsf2FHLA1ytgI7Sofw0%2Fexp1.webp" title="Experimento 1" alt="Experiment 1" width="799" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;em&gt;Un detalle de la concurrencia:&lt;/em&gt; 50 pedidos en paralelo deberían caber en pocos entornos (uno atiende hasta 64), pero se abrieron 61 con un pico de solo 2 por entorno. No es un parámetro que faltó setear como &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt; porque solo fija el techo. Por debajo, Lambda usa un &lt;strong&gt;límite dinámico por entorno&lt;/strong&gt; que ajusta según el uso de recursos, y lo baja cuando cada request consume CPU (acá, la firma SigV4). Con poca concurrencia por entorno, Lambda abre más entornos, y cada uno paga su arranque: eso explica el costo del Experimento 2. La ventaja de "menos entornos por concurrencia compartida" frente a una Event Function es real, pero el reparto lo decide la plataforma, no tu config.&lt;/p&gt;

&lt;h4&gt;
  
  
  Experimento 2 ➡ ¿Cuánto cuesta un pedido?
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Objetivo:&lt;/em&gt; separar el costo de arranque del trabajo real por pedido. Dos corridas: 1 pedido en un entorno frío, y 200 pedidos secuenciales en el mismo entorno caliente.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Corrida&lt;/th&gt;
&lt;th&gt;Pedidos&lt;/th&gt;
&lt;th&gt;CPU total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entorno frío&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~780 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entorno caliente&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;~2.600 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Análisis:&lt;/em&gt; el costo marginal por pedido es &lt;code&gt;(2.600 − 780) / 199 ≈ 9 ms&lt;/code&gt;. O sea, un pedido en caliente cuesta &lt;strong&gt;~9 ms&lt;/strong&gt; de CPU (SigV4 + JSON + la parte de CPU del I/O), pero el &lt;strong&gt;primer&lt;/strong&gt; pedido de un entorno frío cuesta &lt;strong&gt;~780 ms&lt;/strong&gt; (arranque de Node + carga del SDK + primera conexión), unas 85 veces más. En una Web Function, lo que domina el costo de CPU es &lt;strong&gt;cuántos entornos se abren&lt;/strong&gt;, no cuántos pedidos se atienden.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3KmtM9C913xa1FVk9URIkO3eh0q%2Fexp2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3KmtM9C913xa1FVk9URIkO3eh0q%2Fexp2.webp" title="Experiment 2" alt="Experiment 2" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;Lambda Web Functions no viene a reemplazar a la Lambda de siempre, suma una forma más, junto a las Event Functions y las durable functions. Su enfoque es claro: cargas que son, en esencia, un servidor HTTP atendiendo tráfico concurrente que pasa la mayor parte del tiempo esperando I/O. Ahí el modelo encaja muy bien.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Las ventajas que comprobé en el playground&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Código de servidor común (Express) sin adaptadores, que corre igual en local y en AWS.&lt;/li&gt;
&lt;li&gt;Concurrencia por entorno: cada entorno atiende varias peticiones mientras esperan I/O.&lt;/li&gt;
&lt;li&gt;Facturación por CPU activa: en la prueba del BFF, la CPU de serving fue ~0,2% de la vida de los entornos; la espera de I/O casi no se cobra.&lt;/li&gt;
&lt;li&gt;Streaming nativo (el SSE del agente) sin configuración extra.&lt;/li&gt;
&lt;li&gt;Controles en el endpoint (&lt;code&gt;rateLimit&lt;/code&gt;, &lt;code&gt;maxEnvironments&lt;/code&gt;) para proteger a las dependencias que están detrás, como una base de datos.&lt;/li&gt;
&lt;li&gt;Comunicación privada entre servicios con &lt;code&gt;IamAuth&lt;/code&gt;, aunque exige firmar las peticiones a mano con SigV4.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Sobre migrar&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si ya tenés una app Express o Hono corriendo en Lambda con un adaptador, la migración es mayormente quitar el adaptador y escuchar en &lt;code&gt;AWS_LAMBDA_HTTP_ENDPOINT&lt;/code&gt;. Si viene de handlers por evento, hay que reescribirla como servidor. En los dos casos no conviene cambiar todo de golpe: las revisiones y los pesos por revisión del endpoint permiten hacer un canary (mandar un porcentaje del tráfico a la versión nueva y volver atrás si algo falla), que es lo que recomienda la guía de buenas prácticas para producción. Y antes de migrar, conviene preguntarse si hay algo para ganar: un endpoint de bajo volumen con request/response simple no mejora.&lt;/p&gt;

&lt;p&gt;Un punto importante, aunque lo nombre muchas veces en estas notas, es que hoy solo ofrece runtime con node.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Medir antes de decidir&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Los números del playground salen de una simulación. Sirven para entender el modelo, no para cotizar. La documentación recomienda hacer pruebas de carga con las mismas dependencias que en producción, subiendo la concurrencia hasta ver qué se agota primero (latencia, CPU, memoria o la base de datos), y revisar antes las cuotas de la cuenta y de la región. Con esos resultados se ajustan &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt;, &lt;code&gt;maxEnvironments&lt;/code&gt; y &lt;code&gt;rateLimit&lt;/code&gt;. El costo real hay que verlo en la factura: la memoria se cobra durante toda la vida del entorno, y lo que más mueve la CPU no es el trabajo por pedido (~9 ms en caliente en el BFF) sino cuántos entornos se abren, porque cada uno paga su arranque (~780 ms el primer pedido de un entorno frío).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lecciones aprendidas&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;La plataforma importa tu módulo a través de un wrapper, no lo ejecuta como entrypoint: el servidor tiene que arrancar al importarse.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ping&lt;/code&gt; está reservado por la plataforma; para el health check hay que usar otra ruta.&lt;/li&gt;
&lt;li&gt;Toda dependencia que use el código, incluido el AWS SDK, tiene que estar declarada en el &lt;code&gt;package.json&lt;/code&gt;; si no, falla recién en runtime.&lt;/li&gt;
&lt;li&gt;No hay afinidad de sesión y el entorno puede desaparecer en cualquier momento: el estado va afuera (S3, DynamoDB).&lt;/li&gt;
&lt;li&gt;Los log groups se crean sin expiración; conviene fijar la retención.&lt;/li&gt;
&lt;li&gt;Con SAM, &lt;code&gt;CodeUri&lt;/code&gt; tiene que ser un objeto en un bucket S3 versionado: &lt;code&gt;sam build&lt;/code&gt; no empaqueta código local para este recurso.&lt;/li&gt;
&lt;li&gt;Es un servicio nuevo: hacen falta versiones recientes del AWS CLI y de SAM CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3KeP5Ow0BZUP6u2G9PPjskuCtPi/lambda-web-functions-because-your-express-app-deserves-better-than-a-lambda-adapter" rel="noopener noreferrer"&gt;Lambda Web Functions: because your Express app deserves better than a Lambda adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://edjgeek.com/blog/why-i-m-excited-about-lambda-web-functions/" rel="noopener noreferrer"&gt;Why I'm Excited About AWS Lambda Web Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/web-functions.html" rel="noopener noreferrer"&gt;AWS Lambda Web Functions — documentación oficial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-webfunction.html" rel="noopener noreferrer"&gt;AWS::Serverless::WebFunction — recurso de AWS SAM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>lambda</category>
      <category>http</category>
    </item>
    <item>
      <title>Sumergiéndonos en Strands Agents Harness 🐬</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Sat, 10 Oct 2026 19:36:27 +0000</pubDate>
      <link>https://dev.to/reinalau/sumergiendonos-en-strands-agents-harness-4dkp</link>
      <guid>https://dev.to/reinalau/sumergiendonos-en-strands-agents-harness-4dkp</guid>
      <description>&lt;p&gt;Un recorrido práctico por Strands Agents Harness, la forma de crear agentes óptimos y listos para producción en una sola llamada. Según sus benchmarks, logra rendimiento de vanguardia con un coste de token un 28 % menor. Pero ¿qué pasa cuando lo sacás del ejemplo de la doc y lo ponés a trabajar en un caso más real? 🧐&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Hoy, por todos lados, nos enseñan a crear un agente en dos líneas. Y es cierto: con Strands harness, &lt;code&gt;create_harness()&lt;/code&gt; te devuelve un agente que ya trae shell, herramientas de archivo, acceso web, memoria y sesiones. Con una pequeña tarea funciona e impresiona.&lt;/p&gt;

&lt;p&gt;Pero cuando dejás el ejemplo de demo y te ponés a construir algo real, aparece todo lo que esas dos líneas tapan. Un agente no es una llamada y una respuesta: es un proceso que corre &lt;strong&gt;por turnos&lt;/strong&gt;, que decide por su cuenta cuántas veces usar sus herramientas antes de contestar, que &lt;strong&gt;recuerda&lt;/strong&gt; conversaciones previas si le das una sesión, que arrastra un &lt;strong&gt;contexto&lt;/strong&gt; que crece y hay que administrar, y que puede tener &lt;strong&gt;memoria de largo plazo&lt;/strong&gt; que a veces ayuda y a veces estorba. Entender cómo encajan turnos, sesiones, memoria, contexto es la diferencia entre un demo y un prototipo que se sostiene.&lt;/p&gt;

&lt;p&gt;Descubrí que &lt;strong&gt;el harness empareja las condiciones, pero se comporta diferente según el modelo.&lt;/strong&gt; Algunos de estos hallazgos los plasmo en estas notas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conceptos clave de Strands harness
&lt;/h2&gt;

&lt;p&gt;Strands harness es un &lt;em&gt;harness&lt;/em&gt; de agente ya ensamblado: con &lt;strong&gt;una sola llamada&lt;/strong&gt; (&lt;code&gt;create_harness()&lt;/code&gt; en Python, &lt;code&gt;createHarness()&lt;/code&gt; en TypeScript) obtenés un agente listo para trabajar, sin tener que cablear las piezas. Está construido sobre el Strands Harness SDK, y lo que te devuelve es un &lt;code&gt;strands.Agent&lt;/code&gt; estándar optimizado: no hay wrapper ni abstracción oculta, así que cualquier default se puede sobreescribir, extender o reemplazar.&lt;/p&gt;

&lt;p&gt;La idea de fondo: en vez de armar un agente pieza por pieza (modelo, herramientas, memoria, sesiones, gestión de contexto, system prompt), arrancás desde un agente ya optimizado y solo ajustás lo que tu caso de uso necesita.&lt;/p&gt;

&lt;p&gt;Es diferente a &lt;strong&gt;Strands Harness SDK&lt;/strong&gt; que es la capa de mas bajo nivel. El SDK te da las piezas sueltas (herramientas, proveedores de modelos, memoria, sesiones, hooks) y armás el agente desde cero: elegís cada componente, cómo se relacionan y controlás el ciclo de vida completo. &lt;strong&gt;El harness&lt;/strong&gt;, en cambio, ya tiene ensambladas todas esas piezas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tres formas de comenzar a crear tu Agente.
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Configurar el harness mediante la CLI
&lt;/h4&gt;

&lt;p&gt;Esta opción te permite generar el agente y testearlo → prototipar rápido. Aún le falta madurar, a la fecha de la prueba algunas opciones me las deja como no elegidas en el harness cuando intentaba exportar el agente, pero seguramente lo vayan puliendo pronto. Te invito a experimentar.&lt;/p&gt;

&lt;p&gt;Básicamente instalás la CLI (en mi caso para Python)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @strands-agents/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;En tu terminal iniciás la CLI con la palabra &lt;code&gt;strands&lt;/code&gt;, te abrirá una interfaz similar. Aquí podés customizar el harness, activando o desactivando lo que desees, indicarle nombre-system prompt, etc. Si tenés modelos locales vía Ollama te los detecta (los podés setear), si tenés en tu máquina configurada credenciales de AWS te permite elegir cualquier modelo soportado por Bedrock y por supuesto configurar el modelo vía API key:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqznfqniso52iyyripplb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqznfqniso52iyyripplb.jpg" alt="Strands CLI" width="799" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Luego podés probar el agente en el chat de la CLI o acceder a cualquier settings con &lt;code&gt;/&lt;/code&gt; (notar que aquí tengo seleccionado "Nova 2 Lite"):&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynw9t8nte8kffmd424pa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fynw9t8nte8kffmd424pa.jpg" alt="Strands CLI" width="758" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Podés ver &lt;a href="https://strandsagents.com/_astro/strands-cli-export-demo.NZBG3Eo0.mp4" rel="noopener noreferrer"&gt;este video que hace un tour por la CLI&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configurar el harness mediante tu agente de Código (kiro, claudecode, codex...)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://strandsagents.com/docs/user-guide/harness/quickstart/#set-up-with-your-coding-agent" rel="noopener noreferrer"&gt;Esta opción&lt;/a&gt; básicamente es un prompt predefinido que copiás/pegás. Lo probé con Kiro y te guía para crear el harness sin importar el caso de uso. Conviene en la primera interacción indicarle tu listado de requerimientos para el agent harness y subagentes que quieras crear + el prompt predefinido.&lt;/p&gt;

&lt;p&gt;Siempre es útil, si vas a empezar a crear agentes con Strands Agents Harness, que configures el &lt;strong&gt;MCP server de Strands&lt;/strong&gt; en tu agente de código. Le da acceso en vivo a la documentación oficial, así genera código con las últimas versiones y tu asistente no lo saca de su conocimiento. Necesita &lt;code&gt;uv&lt;/code&gt; instalado y se ejecuta con &lt;code&gt;uvx strands-agents-mcp-server&lt;/code&gt;. En Kiro, se agrega en &lt;code&gt;~/.kiro/settings/mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strands-agents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"strands-agents-mcp-server@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para otros agentes de código (Cursor, Claude Code, Codex, VS Code) la idea es la misma; cambia la ubicación del archivo de configuración.&lt;/p&gt;

&lt;h4&gt;
  
  
  Usalo como una librería (library)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;harness&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y comenzás a escribir tu harness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands_harness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_harness&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Research the three most common strategies for versioning a REST API, compare their tradeoffs, and write a recommendation to api-versioning.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Qué trae el agente por defecto
&lt;/h3&gt;

&lt;p&gt;Sin pasar ningún argumento, &lt;code&gt;create_harness()&lt;/code&gt; ya te entrega un agente que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corre sobre el modelo que elijas&lt;/strong&gt;, con proveedores: Amazon Bedrock (default), Anthropic, OpenAI, Google y Ollama (local). Con razonamiento activado por defecto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sigue un system prompt afinado&lt;/strong&gt;: explorar antes de actuar, confirmar antes de algo irreversible, y verificar antes de dar una tarea por terminada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trae herramientas listas&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;shell&lt;/strong&gt; (shell virtual pensado para agentes). Aquí hay que tener cuidado: ¡es peligroso dejarlo activado si no lo vas a usar!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Herramientas de archivo&lt;/strong&gt;: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceso web&lt;/strong&gt;: &lt;code&gt;web_fetch&lt;/code&gt; (siempre activo) y &lt;code&gt;web_search&lt;/code&gt; (según el proveedor).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;programmatic_tool_caller&lt;/code&gt;&lt;/strong&gt;: un sandbox donde el agente escribe código que encadena, repite y paraleliza sus otras herramientas para orquestaciones más complejas.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gestiona su propia ventana de contexto&lt;/strong&gt;: resume turnos viejos y mueve resultados voluminosos a almacenamiento, dejando una referencia corta para recuperarlos. Cachea las partes reutilizadas de cada request donde el proveedor lo soporta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recuerda&lt;/strong&gt;: cada conversación se guarda en disco y es reanudable por &lt;code&gt;session id&lt;/code&gt;; además tiene memoria de largo plazo que destila hechos entre conversaciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delega subtareas&lt;/strong&gt; a un subagente integrado (lo explico más adelante).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carga Agent Skills&lt;/strong&gt; cuando están presentes: podés reutilizar las existentes y también crear nuevas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El modelo por defecto es un modelo de frontera (Claude Opus en Bedrock). Esto es importante: &lt;strong&gt;el harness está afinado para modelos frontera&lt;/strong&gt;, y eso explica varias de las limitaciones que aparecen con modelos más chicos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cómo se ejecuta un agente: el agent-loop y los turnos
&lt;/h3&gt;

&lt;p&gt;Un detalle que conviene tener claro desde el principio: cuando le das una tarea al agente, no hace una sola llamada al modelo y responde. Entra en un &lt;strong&gt;bucle&lt;/strong&gt; (el &lt;em&gt;agent-loop&lt;/em&gt;): el modelo piensa, decide usar una herramienta, recibe el resultado, vuelve a pensar, usa otra herramienta, y así hasta que considera que terminó. Una sola petición tuya puede disparar varias idas y vueltas al modelo. Esto es lo normal y lo deseable, es lo que le permite leer un archivo, razonar sobre él y después escribir. Pero también significa que &lt;strong&gt;un "turno" puede costar varias llamadas&lt;/strong&gt; (y en modelos con cuota limitada, eso se nota).&lt;/p&gt;

&lt;p&gt;Cada agente corre su propio agent-loop, incluidos los subagentes. Un subagente sin herramientas suele resolver en una sola iteración; el agente principal, que lee, delega y escribe, hace el bucle largo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sesiones, memoria y contexto: lo que el agente arrastra entre turnos
&lt;/h3&gt;

&lt;p&gt;Tres piezas distintas que es fácil confundir:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sesión:&lt;/strong&gt; el historial de la conversación. El harness lo persiste en disco y lo puede reanudar si le das un &lt;code&gt;session id&lt;/code&gt; estable. Es la "memoria de corto plazo" de &lt;em&gt;esta&lt;/em&gt; historia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memoria de largo plazo:&lt;/strong&gt; hechos que el agente destila entre conversaciones (preferencias, datos del proyecto), guardados aparte. Útil para que recuerde cosas &lt;em&gt;entre&lt;/em&gt; sesiones distintas; prescindible y a veces contraproducente, si tu estado ya vive en archivos propios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contexto (la ventana del modelo):&lt;/strong&gt; el texto que el modelo lee de una sola vez. Crece con cada turno, así que el harness lo administra: resume lo viejo y mueve resultados grandes a almacenamiento, dejando una referencia para recuperarlos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entender estas tres capas es lo que separa un demo de un prototipo que se sostiene entre ejecuciones.&lt;/p&gt;

&lt;h3&gt;
  
  
  La interfaz &lt;code&gt;create_harness(...)&lt;/code&gt; y cómo podemos acceder
&lt;/h3&gt;

&lt;p&gt;Todos los argumentos son opcionales. Los más relevantes para empezar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Argumento&lt;/th&gt;
&lt;th&gt;Para qué sirve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"proveedor/nombre"&lt;/code&gt;, un id de Bedrock a secas, o una instancia &lt;code&gt;Model&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;effort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nivel de razonamiento: &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;minimal&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;instructions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Texto de dominio que se añade al system prompt (identidad, reglas, alcance).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tus herramientas propias, junto a las integradas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;builtin_tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Edita el set de herramientas integradas (una lista fija los nombres; un mapa agrega/quita/configura).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;True&lt;/code&gt; \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Memoria de largo plazo: &lt;code&gt;True&lt;/code&gt;, un dict de config, o &lt;code&gt;False&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;caching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cachea system prompt, tools e historial donde se soporta.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp_servers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conecta servidores MCP (ruta a un JSON o el mapping inline).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interventions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Puertas de aprobación sobre las llamadas a herramientas.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ejemplo de cómo elegir modelo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic/claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# Anthropic directo
&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-5.6-sol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# OpenAI
&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google/gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# Google
&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock/global.anthropic.claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# el default, explícito
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada proveedor necesita su extra: &lt;code&gt;pip install "strands-harness[gemini]"&lt;/code&gt; (o &lt;code&gt;[anthropic]&lt;/code&gt;, &lt;code&gt;[openai]&lt;/code&gt;, &lt;code&gt;[ollama]&lt;/code&gt;, &lt;code&gt;[litellm]&lt;/code&gt;). Bedrock viene de base.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observar logs del modelo y Evaluar el agente (evals)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Si usás algún modelo vía Amazon Bedrock, podés activar los logs en la consola de AWS y revisar cómo se está invocando tu modelo y cómo resuelve las instrucciones incrustadas por el harness:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa10lglrao76pll8g1y8u.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa10lglrao76pll8g1y8u.JPG" alt="CloudWatch" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un agente que "parece que anda" no es un agente que anda. Antes de confiar en él conviene &lt;strong&gt;evaluarlo&lt;/strong&gt;: ¿elige bien las herramientas?, ¿sigue las instrucciones?, ¿comunica los fallos en vez de inventar que todo salió bien? Strands incluye un SDK de evaluaciones (&lt;code&gt;/evals&lt;/code&gt;) justamente para esto —validar el comportamiento del agente antes de darlo por bueno— con evaluadores de calidad, de seguimiento de instrucciones, de selección de herramientas y más.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Primeras pruebas: lo que la documentación no te cuenta
&lt;/h2&gt;

&lt;p&gt;Esta es la parte más valiosa del artículo: lo que la doc no te cuenta y aparece al ejecutarlo de verdad en Windows con modelos chicos. La tarea de prueba era sencilla: investigar estrategias de versionado de una REST API y escribir una recomendación en un &lt;code&gt;.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Antes de entrar en los tropiezos, conviene recordar algo que la propia doc deja claro: &lt;strong&gt;el harness está optimizado para modelos grandes o de frontera&lt;/strong&gt; (su default es Claude Opus en Bedrock). Su system prompt afinado (el que está en el harness, no el que le incrustamos nosotros), el agent-loop, la gestión de contexto y el uso de herramientas asumen un modelo con buena capacidad de razonamiento y de seguimiento de instrucciones. Con un modelo chico el harness igual arranca, pero empiezan las fricciones: el modelo se enreda con las herramientas, inventa rutas, no sabe cerrar la tarea. No son bugs del harness en sí; es el desajuste entre un andamiaje pensado para modelos capaces y un modelo que no llega. Es importante que lo tengas presente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nova-2-lite (Amazon Bedrock)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bucle de &lt;code&gt;web_fetch&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;global.amazon.nova-2-lite-v1:0&lt;/code&gt; &lt;strong&gt;no tiene web_search nativo&lt;/strong&gt;. Sin búsqueda real, el agente intentó "investigar" trayendo página tras página con &lt;code&gt;web_fetch&lt;/code&gt;: llegó a &lt;strong&gt;229 llamadas seguidas&lt;/strong&gt; sin cerrar la tarea. Se arregla dándole búsqueda de verdad con &lt;code&gt;builtin_tools={"web_search": "exa"}&lt;/code&gt; y/o acotando la tarea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rutas de Windows "unixificadas":&lt;/strong&gt; la herramienta &lt;code&gt;write&lt;/code&gt; exige &lt;strong&gt;ruta absoluta&lt;/strong&gt;. Nova-2-lite convertía la ruta &lt;code&gt;C:\Users\...&lt;/code&gt; a formato Unix &lt;code&gt;/C/Users/...&lt;/code&gt;, así que escribía al vacío. El archivo nunca aparecía donde debía. El modelo razona como si estuviera en Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No reporta bien los errores:&lt;/strong&gt; afirmaba "escribí el archivo" cuando no era cierto. Lección: &lt;strong&gt;verificar en disco, no confiar en lo que el agente dice.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gemini (Google)
&lt;/h3&gt;

&lt;p&gt;Cambiar a Gemini resolvió lo de las rutas, pero trajo límites propios del proveedor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cuota de la capa gratuita (429 &lt;code&gt;RESOURCE_EXHAUSTED&lt;/code&gt;):&lt;/strong&gt; el harness hace muchas llamadas por minuto (razonar, buscar, procesar, escribir) y supera el límite RPM gratuito enseguida. Una llamada simple de prueba sí funcionaba; la ráfaga del harness no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No se puede combinar búsqueda nativa + function calling (400 &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;):&lt;/strong&gt; Gemini rechaza usar &lt;code&gt;google_search&lt;/code&gt; junto con el resto de herramientas en la misma petición. Solución: &lt;code&gt;builtin_tools={"web_search": False}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Thinking level is not supported" (400):&lt;/strong&gt; el harness activa razonamiento por defecto (&lt;code&gt;effort="auto"&lt;/code&gt;), y &lt;code&gt;gemini-2.5-flash&lt;/code&gt; rechazó el nivel que le llegaba. Solución: &lt;code&gt;effort="off"&lt;/code&gt;. Probé también &lt;code&gt;low&lt;/code&gt; y &lt;code&gt;medium&lt;/code&gt; y los rechazó igual: con este modelo, apagar el razonamiento fue la única salida.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A veces no llama a &lt;code&gt;write&lt;/code&gt;:&lt;/strong&gt; con el razonamiento apagado, el modelo pequeño respondía el texto en el chat en lugar de invocar la herramienta de escritura.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Algunas soluciones que podés adoptar
&lt;/h3&gt;

&lt;p&gt;Después de varias iteraciones, la conclusión fue: &lt;strong&gt;confiar en que un modelo pequeño invoque &lt;code&gt;write&lt;/code&gt; con la ruta correcta es frágil&lt;/strong&gt;. La herramienta &lt;code&gt;write&lt;/code&gt; funciona; lo que falla es que el modelo chico la use bien.&lt;/p&gt;

&lt;p&gt;Patrón robusto: dejar que el &lt;strong&gt;agente genere el contenido&lt;/strong&gt; y que &lt;strong&gt;Python escriba el archivo&lt;/strong&gt; con &lt;code&gt;str(result)&lt;/code&gt;. Así se saca de la ecuación la parte que falla (que el modelo construya bien la ruta de Windows) y se garantiza el resultado. Ejemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...devuelve solo el documento markdown...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruta_absoluta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inconvenientes con Unicode en consola (Windows)
&lt;/h3&gt;

&lt;p&gt;Comenté algunos tropiezos que tuve pero vale la pena mencionar por si recién estás empezando en este mundo. En Windows la consola usa &lt;code&gt;cp1252&lt;/code&gt; y &lt;strong&gt;crashea al imprimir emojis&lt;/strong&gt; (p. ej. el ✅ que el agente pone al confirmar). Eso tiraba el proceso con &lt;code&gt;UnicodeEncodeError&lt;/code&gt; antes de terminar. Se arregla forzando UTF-8 al inicio del script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sesiones: mantener una conversación entre ejecuciones
&lt;/h2&gt;

&lt;p&gt;Por defecto el harness &lt;strong&gt;persiste cada conversación&lt;/strong&gt; en disco bajo &lt;code&gt;./.agent/sessions&lt;/code&gt;. Pero &lt;strong&gt;no reanuda solo&lt;/strong&gt;: sin id, genera uno nuevo en cada corrida (por eso aparecen carpetas con nombres aleatorios). Para continuar una conversación, hay que pasar un id estable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-design&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;¿Dónde quedamos?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un ejemplo en dos ejecuciones separadas (dos corridas del programa, una después de la otra, cerrando la primera antes de abrir la segunda):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primera corrida:&lt;/strong&gt; se construye el agente con &lt;code&gt;session={"id": "api-design"}&lt;/code&gt; y se le pide, por ejemplo, que compare tres estrategias de versionado de una API. Se cierra el programa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segunda corrida:&lt;/strong&gt; se vuelve a construir el agente con el &lt;strong&gt;mismo id&lt;/strong&gt; (&lt;code&gt;"api-design"&lt;/code&gt;) y se le pregunta "¿cuál de &lt;em&gt;esas&lt;/em&gt; elegirías...?". Aunque es un proceso nuevo, el agente responde correctamente &lt;strong&gt;sin que le repitas cuáles eran las opciones&lt;/strong&gt;: recuerda la conversación anterior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso funciona porque el estado quedó guardado en &lt;code&gt;./.agent/sessions/session/api-design/&lt;/code&gt; como &lt;code&gt;snapshot_latest.json&lt;/code&gt;, y el harness lo &lt;strong&gt;rehidrata&lt;/strong&gt; al construir un agente con ese mismo id. Si no pasás un id, cada corrida genera uno nuevo y arranca de cero.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session={"dir": ...}&lt;/code&gt; cambia dónde vive el estado.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session=False&lt;/code&gt; desactiva la persistencia (corrida desechable).&lt;/li&gt;
&lt;li&gt;Para &lt;strong&gt;multi-host&lt;/strong&gt;, se pasa un &lt;code&gt;SessionManager&lt;/code&gt; propio (por ejemplo &lt;code&gt;S3SessionManager&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto último merece una aclaración, es clave si pensás llevar el agente a producción. Por defecto el harness guarda las sesiones en el &lt;strong&gt;disco local&lt;/strong&gt; de la máquina donde ejecuta. Eso alcanza mientras ejecutes siempre en el mismo lugar. Pero cuando el agente vive en &lt;strong&gt;varias máquinas a la vez&lt;/strong&gt; o en funciones serverless (que se levantan y apagan), el disco local deja de servir. La solución es guardar las sesiones en un lugar &lt;strong&gt;compartido&lt;/strong&gt; por todas las máquinas, y para eso se usa &lt;code&gt;S3SessionManager&lt;/code&gt;, que guarda el estado en &lt;strong&gt;Amazon S3&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.session.s3_session_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3SessionManager&lt;/span&gt;
&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;S3SessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-design&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mi-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Dato para tener en cuenta: si desplegás en Amazon Bedrock AgentCore Runtime, las sesiones guardadas en disco &lt;strong&gt;se pierden&lt;/strong&gt; cuando la microVM se reinicia. Ahí es donde podemos usar S3 para no perder el estado.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Un caso de uso completo: Narrador de Historias
&lt;/h2&gt;

&lt;p&gt;Armé un narrador de historias (aún en construcción), inspirado en Sherpa de Pocket FM pero reducido a lo esencial, solo texto, en español, sin audio ni imágenes. Invocás al agente y te entrevista qué clase de historia querés escribir (formato, género, tono, personajes, premisa) y se pone a trabajar.&lt;/p&gt;

&lt;p&gt;No es un solo agente respondiendo, sino un conjunto de agentes trabajando como un pequeño taller literario. Un orquestador (el narrador) conduce la conversación y escribe la prosa, y delega el trabajo de revisión en tres subagentes especialistas, uno que chequea la continuidad contra la "biblia" de la historia, otro que audita la estructura y el ritmo, y un bibliotecario que mantiene esa biblia actualizada. Cada uno hace su parte sin ensuciar el contexto principal.&lt;/p&gt;

&lt;p&gt;📦 GitHub: &lt;a href="https://github.com/reinalau/strands-harness-narrador" rel="noopener noreferrer"&gt;github.com/reinalau/strands-harness-narrador&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  La arquitectura: un orquestador y tres subagentes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F981t2d829wna0zx1pp7p.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F981t2d829wna0zx1pp7p.jpg" alt="Architecture-Story Creator" width="800" height="684"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Subagentes trabajando para un orquestador
&lt;/h4&gt;

&lt;p&gt;Un &lt;strong&gt;subagente&lt;/strong&gt; es un agente al que el agente principal puede llamar &lt;strong&gt;como si fuera una herramienta&lt;/strong&gt;. Lo importante es que la subtarea corre en su &lt;strong&gt;propia conversación fresca&lt;/strong&gt; y solo &lt;strong&gt;vuelve su respuesta final&lt;/strong&gt; — el trabajo intermedio no inunda el contexto principal. Hay dos formas de usarlos, y conviven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El generalista integrado (built-in).&lt;/strong&gt; El harness activa por defecto &lt;strong&gt;un&lt;/strong&gt; subagente llamado &lt;code&gt;generalist&lt;/code&gt;, expuesto al agente como herramienta. Es un agente harness completo que &lt;strong&gt;hereda la configuración del padre&lt;/strong&gt; (modelo, razonamiento, caching, gestión de contexto, herramientas integradas, plugins, subagentes, interventions y sandbox), pero corre con un prompt de rol genérico en lugar de tus &lt;code&gt;instructions&lt;/code&gt; (es el prompt que hace a la identidad del agente, no le decimos system prompt porque el harness lo compone: contrato del harness + tus instructions). Arranca de una conversación en blanco, así que el modelo debe meter en la llamada todo lo que la subtarea necesita, y devuelve una única respuesta autocontenida. No hay que crearlo, viene activado y corre en &lt;strong&gt;segundo plano&lt;/strong&gt;. Se usa cuando una subtarea &lt;em&gt;inundaría&lt;/em&gt; el contexto principal (buscar entre muchos archivos, un cambio multi-paso, exploración abierta donde solo necesitás la conclusión). Para desactivarlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builtin_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subagent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;# sin generalista
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;En el narrador el generalista &lt;strong&gt;no se usa&lt;/strong&gt;: al configurar &lt;code&gt;builtin_tools={"*": False, "read": True, "write": True, "edit": True}&lt;/code&gt; partimos de cero y solo habilitamos las herramientas que nombramos, así que el &lt;code&gt;subagent&lt;/code&gt; integrado queda apagado. La delegación la hacen los tres especialistas de abajo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Agentes especialistas (Agents as Tools).&lt;/strong&gt; En este caso de uso creé agentes especialistas: se pasan instancias de &lt;code&gt;Agent&lt;/code&gt; envueltas con &lt;code&gt;as_tool()&lt;/code&gt; (Python) / &lt;code&gt;asTool()&lt;/code&gt; (TS). Cada uno se convierte en una herramienta nombrada según el &lt;code&gt;name&lt;/code&gt; del agente, así que conviene darle un &lt;code&gt;name&lt;/code&gt; y &lt;code&gt;description&lt;/code&gt; claros para que el modelo sepa cuándo usarla.&lt;br&gt;
Ejemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands_harness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_harness&lt;/span&gt;

&lt;span class="n"&gt;researcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;researcher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Investiga un tema y devuelve un resumen conciso y con fuentes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Investiga el tema dado y devuelve un resumen conciso y con fuentes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_tool&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada llamada corre desde el estado base del especialista (conversación fresca), así que &lt;strong&gt;no acumula estado entre llamadas&lt;/strong&gt;. El &lt;code&gt;name&lt;/code&gt; del subagente debe ser único en el set de herramientas, igual que cualquier tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;En el narrador, los tres revisores (continuidad, estructura/ritmo, bibliotecario) son especialistas de este tipo: trabajan sobre el capítulo sin ensuciar la conversación principal, y solo devuelven su reporte o su JSON.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Ojo con el modelo: &lt;code&gt;Agent&lt;/code&gt; crudo necesita una instancia de modelo
&lt;/h4&gt;

&lt;p&gt;Acá me comí un error que vale la pena contar, porque es fácil de cometer. Al orquestador lo creás con &lt;code&gt;create_harness(model="google/gemini-3.5-flash")&lt;/code&gt;, y ese string con prefijo —&lt;code&gt;"google/..."&lt;/code&gt;, &lt;code&gt;"bedrock/..."&lt;/code&gt;— funciona porque &lt;strong&gt;lo resuelve el harness&lt;/strong&gt;: traduce el atajo a una instancia de modelo del proveedor.&lt;/p&gt;

&lt;p&gt;Pero los subagentes especialistas son &lt;code&gt;Agent&lt;/code&gt; &lt;strong&gt;crudos&lt;/strong&gt; del SDK, no harness. Y si a un &lt;code&gt;Agent&lt;/code&gt; crudo le pasás ese mismo string, falla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValidationException ... The provided model identifier is invalid.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;Agent&lt;/code&gt; del SDK no entiende el prefijo &lt;code&gt;"google/..."&lt;/code&gt;: lo interpreta como un id de Bedrock a secas y revienta. La solución es pasarle una &lt;strong&gt;instancia de modelo&lt;/strong&gt; del SDK, no el string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models.bedrock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&lt;/span&gt;

&lt;span class="n"&gt;continuidad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;continuidad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revisa la continuidad de un capítulo contra la biblia.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sos un revisor de continuidad...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-lite-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regla para recordar: &lt;strong&gt;el string &lt;code&gt;"proveedor/nombre"&lt;/code&gt; es &lt;em&gt;syntactic sugar&lt;/em&gt; del harness&lt;/strong&gt; (un atajo de sintaxis que resuelve solo &lt;code&gt;create_harness()&lt;/code&gt;); fuera de él (en un &lt;code&gt;Agent&lt;/code&gt; del SDK) tenés que instanciar el modelo vos mismo con &lt;code&gt;GeminiModel(...)&lt;/code&gt;, &lt;code&gt;BedrockModel(...)&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  La biblia con structured output
&lt;/h3&gt;

&lt;p&gt;La "biblia" de la historia (personajes, relaciones, qué sabe cada uno, hilos abiertos, objetos, línea de tiempo) vive en &lt;code&gt;bible.json&lt;/code&gt;, y la mantiene el subagente bibliotecario. El primer impulso es pedirle al modelo "devolveme el JSON actualizado, sin&lt;br&gt;
&lt;br&gt;
 ``&lt;code&gt;" y después parsearlo. Funciona a veces: un modelo chico mete un backtick de más, un comentario, una coma colgada, y el&lt;/code&gt;json.loads` explota.&lt;/p&gt;

&lt;p&gt;El harness (vía el SDK) tiene una forma mejor: &lt;strong&gt;structured output&lt;/strong&gt;. Definís el esquema con un modelo &lt;strong&gt;Pydantic&lt;/strong&gt; y se lo pasás a la invocación; el SDK se encarga de que la respuesta salga &lt;strong&gt;validada&lt;/strong&gt; contra ese esquema. En lugar de esperar con incertidumbre, obtenés un objeto que ya cumple la forma esperada.&lt;/p&gt;

&lt;p&gt;En el narrador, la biblia es un modelo Pydantic (&lt;code&gt;Biblia&lt;/code&gt;, con submodelos para personajes, relaciones, etc.), y el bibliotecario se invoca pidiéndole esa forma:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
from pydantic import BaseModel

class Biblia(BaseModel):
    meta: Meta
    personajes: list[Personaje]
    relaciones: list[Relacion]
    # ... hilos_abiertos, objetos, linea_de_tiempo

# dentro de la tool del bibliotecario:
result = _bibliotecario_agent(prompt, structured_output_model=Biblia)
biblia: Biblia = result.structured_output        # objeto validado, no texto suelto
return biblia.model_dump_json(indent=2)           # el orquestador lo escribe en disco


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El valor validado se lee en &lt;code&gt;result.structured_output&lt;/code&gt;. Después lo serializo con &lt;code&gt;model_dump_json()&lt;/code&gt; y es el &lt;strong&gt;orquestador&lt;/strong&gt; quien lo escribe en &lt;code&gt;bible.json&lt;/code&gt;, el bibliotecario solo genera y valida, nunca toca disco. Así la biblia &lt;strong&gt;siempre&lt;/strong&gt; tiene la forma correcta, sin importar lo prolijo que sea el modelo que la generó.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human-in-the-loop: un capítulo por turno
&lt;/h3&gt;

&lt;p&gt;Cuando pensé en que intervenga una persona con el agente, mi primer reflejo fue buscar un mecanismo del harness: &lt;code&gt;interventions&lt;/code&gt;, que &lt;strong&gt;gatea&lt;/strong&gt; las llamadas a herramientas y pide aprobación antes de ejecutarlas. Pero eso resuelve otra cosa (aprobar o no una acción riesgosa), no lo que yo quería. Lo que buscaba era más simple: que el narrador escriba &lt;strong&gt;un&lt;/strong&gt; capítulo, lo guarde, y &lt;strong&gt;pare&lt;/strong&gt;, que no siga de largo con el capítulo 2, para que yo pueda leerlo, editarlo, y recién entonces decirle cómo seguir.&lt;/p&gt;

&lt;p&gt;Y esa pausa no necesita ningún mecanismo especial: &lt;strong&gt;es el chat por turnos&lt;/strong&gt;. El agente termina su turno después de guardar el capítulo, y el control vuelve a mí. La regla la fijé en el system prompt: &lt;em&gt;escribí un capítulo por turno; después de guardarlo, pará y esperá a que te pidan "revisar" o "continuar"; no empieces el siguiente por tu cuenta&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;La pieza que hace que esto sea sólido es que &lt;strong&gt;los archivos son la fuente de verdad&lt;/strong&gt;. Si yo edito a mano &lt;code&gt;story/capitulo-01.md&lt;/code&gt; o &lt;code&gt;bible.json&lt;/code&gt; entre turnos, el agente no se entera por arte de magia: tiene que &lt;strong&gt;releer desde disco&lt;/strong&gt; antes de actuar. Así que el prompt también le ordena que, al pedirle "revisar" o "continuar", &lt;strong&gt;vuelva a leer&lt;/strong&gt; los archivos en vez de confiar en lo que tiene en su contexto (que puede estar desactualizado respecto a mis ediciones). El flujo es así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Le pido un capítulo → lo escribe, lo guarda, y &lt;strong&gt;para&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Lo leo. Si quiero, edito el &lt;code&gt;.md&lt;/code&gt; o la biblia a mano.&lt;/li&gt;
&lt;li&gt;Le digo "revisá" → relee desde disco, delega en continuidad y estructura, y me reporta.&lt;/li&gt;
&lt;li&gt;Le digo "continuá" → relee la biblia y el último capítulo, y escribe el siguiente.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Dos modelos, un mismo harness: cada uno con su personalidad
&lt;/h3&gt;

&lt;p&gt;El diseño terminó con &lt;strong&gt;dos modelos&lt;/strong&gt;: el orquestador (que escribe la prosa y en disco) y los subagentes de análisis (que no tocan disco, así que pueden correr en un modelo más barato). Pero llegar a esa repartición no fue elegir en un papel, fue &lt;strong&gt;probar modelo tras modelo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El recorrido para el &lt;strong&gt;orquestador&lt;/strong&gt; fue así:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nova 2 Lite (Bedrock):&lt;/strong&gt; barato y disponible, pero en Windows &lt;strong&gt;inventa rutas estilo Linux&lt;/strong&gt; (&lt;code&gt;/C/...&lt;/code&gt;, &lt;code&gt;/workspace/...&lt;/code&gt;). La herramienta &lt;code&gt;write&lt;/code&gt; exige ruta absoluta y el modelo construye una que no existe, así que el archivo nunca se graba. Encima afirmaba haber guardado cuando no era cierto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 2.5 Flash (Google):&lt;/strong&gt; resolvió las rutas, pero trajo lo suyo. Rechazó el nivel de razonamiento que el harness activa por defecto (&lt;code&gt;Thinking level is not supported&lt;/code&gt;, hay que forzar &lt;code&gt;effort="off"&lt;/code&gt;), no deja combinar su búsqueda nativa con el resto de herramientas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nova Pro (Bedrock):&lt;/strong&gt; más capaz, probé si manejaba mejor la escritura. Dos sorpresas: primero un &lt;code&gt;ValidationException: User messages cannot contain reasoning content&lt;/code&gt; (el harness le mandaba bloques de &lt;em&gt;reasoning&lt;/em&gt; que el modelo rechaza); y cuando logró escribir, &lt;strong&gt;también unixificó la ruta&lt;/strong&gt; (&lt;code&gt;/workspace/prueba2.md&lt;/code&gt;), confirmado leyendo el log de invocación en CloudWatch. Mismo problema de rutas que su hermano chico.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3.5 Flash (Google):&lt;/strong&gt; el que mejor se portó —construye rutas correctas y escribe donde debe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Por eso terminó teniendo sentido la repartición en dos modelos: dejar la escritura y la prosa en manos de un modelo que se porta bien con el sistema de archivos, y mandar el análisis (que solo lee y devuelve texto, sin tocar rutas) a uno más barato y chico.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué queda por explorar
&lt;/h2&gt;

&lt;p&gt;Para no estirar el artículo, dejé afuera varias piezas del harness que quedan para una próxima. Las menciono por si querés seguir el camino por tu cuenta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Skills:&lt;/strong&gt; habilidades que el agente carga desde &lt;code&gt;./.agent/skills&lt;/code&gt;, con sus instrucciones y archivos de apoyo. Útil para empaquetar "cómo hacer X" sin inflar el system prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interventions:&lt;/strong&gt; las puertas de aprobación del harness (&lt;code&gt;"ask"&lt;/code&gt;, &lt;code&gt;"smart"&lt;/code&gt;, políticas en lenguaje natural, Cedar). Las nombré al hablar del human-in-the-loop, pero no las usé en el narrador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de contexto y caching en detalle:&lt;/strong&gt; el harness los administra por defecto; queda pendiente mirar de cerca cómo resume, qué cachea y cómo se afina.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export desde la CLI y despliegue en AgentCore:&lt;/strong&gt; llevar el prototipo de la laptop a un entorno gestionado (y ahí sí, resolver la persistencia durable de sesiones).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;Volvamos a la promesa del principio: crear un agente en dos líneas. Es verdad, y no es poco: &lt;code&gt;create_harness()&lt;/code&gt; te ahorra ensamblar a mano el modelo, las herramientas, la memoria, las sesiones y la gestión de contexto. Para empezar, es un salto enorme.&lt;/p&gt;

&lt;p&gt;Pero construir algo que se sostenga pide entender lo que esas dos líneas tapan. Que un turno no es una llamada sino un &lt;strong&gt;bucle&lt;/strong&gt; que puede disparar varias. Que las &lt;strong&gt;sesiones&lt;/strong&gt; hacen reanudable la conversación, pero el id lo gestiona tu código, no el modelo. Que la &lt;strong&gt;memoria&lt;/strong&gt; de largo plazo a veces ayuda y a veces estorba, en el narrador la apagué, porque el estado real vivía mejor en archivos. Que el &lt;strong&gt;contexto&lt;/strong&gt; crece y el harness lo administra por nosotros. Y que delegar en &lt;strong&gt;subagentes&lt;/strong&gt; mantiene limpio el hilo principal porque cada uno trabaja aparte y solo devuelve su conclusión.&lt;/p&gt;

&lt;p&gt;Otro punto importante es la elección de modelos. Si tenemos la posibilidad de usar los modelos de Claude, a priori sabemos que la mayoría de las tareas las van a resolver con eficacia, pero creo que el aprendizaje pasa por la experimentación. Si estás dando tus primeros pasos, te invito a que uses todos los modelos que posean free tier. También experimentar con Docker + Ollama, bajando a tu laptop modelos pequeños y entender en qué tareas de subagentes son más óptimos.&lt;/p&gt;

&lt;p&gt;Y una línea de diseño para construir agentes es &lt;strong&gt;dejá que los agentes hagan lo que requiere criterio, y que el código haga lo determinista.&lt;/strong&gt; La prosa, la revisión de continuidad, el juicio sobre el ritmo: eso es trabajo de agentes, y es donde no se puede resolver vía código (simulando ser personas de un pequeño taller literario escribiendo y corrigiendo). El id de la sesión, la ruta del archivo, el contador de capítulos: eso es trabajo del código, determinista y predecible. Mezclarlos es justo donde aparecen las alucinaciones y bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3JgWF0T6PorfoRq7EdgkwO3F96P/stop-starting-from-scratch-build-agents-with-strands-harness" rel="noopener noreferrer"&gt;Stop Starting From Scratch: Build Agents with Strands harness (AWS Builder)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://strandsagents.com/blog/introducing-strands-harness/" rel="noopener noreferrer"&gt;Introducing Strands harness: frontier performance with 28% lower token cost (blog oficial)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Documentación oficial de Strands Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>strandsharness</category>
      <category>aws</category>
      <category>python</category>
    </item>
    <item>
      <title>Camino a AWS Certified Developer — Associate (DVA-C03): qué cambia y por qué</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Mon, 07 Sep 2026 22:57:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/camino-a-aws-certified-developer-associate-dva-c03-que-cambia-y-por-que-5575</link>
      <guid>https://dev.to/aws-builders/camino-a-aws-certified-developer-associate-dva-c03-que-cambia-y-por-que-5575</guid>
      <description>&lt;p&gt;El 1 de septiembre de 2026, AWS anunció oficialmente cambios de versión en varios de sus exámenes. Entre ellos, AWS Certified Developer — Associate. En este artículo te comparto mi experiencia con la versión actual que rendí y los cambios de lo que se viene.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 Hasta el 30 de noviembre de 2026 se puede rendir la versión actual DVA-C02. El registro para DVA-C03 abre el 27 de octubre de 2026, con disponibilidad general (GA) a partir del 1 de diciembre de 2026.*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hace unos meses escribí sobre mi experiencia rindiendo el examen &lt;a href="https://medium.com/@reinalau/camino-a-lograr-aws-certified-developer-associate-dva-c02-9a59112df16f" rel="noopener noreferrer"&gt;&lt;strong&gt;DVA-C02&lt;/strong&gt;&lt;/a&gt;, con los temas clave que profundicé y algunos tips de estudio. AWS &lt;a href="https://aws.amazon.com/es/blogs/training-and-certification/september-2026-new-offerings/" rel="noopener noreferrer"&gt;acaba de anunciar&lt;/a&gt; la actualización a &lt;strong&gt;DVA-C03&lt;/strong&gt;, y me pareció importante escribir esta continuación: no para reemplazar ese artículo, sino para sumarle lo nuevo. Si estás por empezar a estudiar hoy, mi recomendación es leer ambos: la base técnica de servicios (Lambda, DynamoDB, API Gateway, Cognito, etc.) sigue totalmente vigente, y acá vas a encontrar el "qué se agrega" y el "por qué" de esta actualización.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El calendario a tener en cuenta según el anuncio:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5d5uxs5sfztbef7abxlg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5d5uxs5sfztbef7abxlg.jpg" alt="Calendario certificacion" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué actualizan el examen
&lt;/h2&gt;

&lt;p&gt;El &lt;a href="https://aws.amazon.com/es/blogs/training-and-certification/september-2026-new-offerings/" rel="noopener noreferrer"&gt;blog oficial de AWS Training and Certification&lt;/a&gt; plantea que el rol de developer ya no es solamente escribir código de aplicación. Hoy un developer usa herramientas de &lt;em&gt;AI-assisted development&lt;/em&gt; para generar, revisar y optimizar código; integra servicios de AI administrados dentro de sus aplicaciones; y tiene que asegurar las interacciones de agentes de AI. Es decir, &lt;em&gt;AI-assisted development&lt;/em&gt; pasa a ser una competencia central del rol, no un "extra".&lt;/p&gt;

&lt;p&gt;Esto no significa que el examen se convierta en una certificación de AI. De hecho, el anuncio oficial aclara qué queda &lt;strong&gt;fuera de alcance&lt;/strong&gt;: prompt engineering, diseño de RAG, selección de modelos de AI, Amazon SageMaker AI y diseño de frameworks de gobernanza de AI. Es decir, no van a pedirte que diseñes soluciones de AI desde cero, sino que sepas &lt;strong&gt;cómo un developer integra y asegura&lt;/strong&gt; estas capacidades dentro del ciclo de vida de una aplicación en AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Datos generales de la nueva versión de examen
&lt;/h2&gt;

&lt;p&gt;Según el anuncio, son los mismos parámetros generales que ya conocemos de DVA-C02:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duración: 130 minutos&lt;/li&gt;
&lt;li&gt;Preguntas: 65 (50 puntuadas, 15 no puntuadas)&lt;/li&gt;
&lt;li&gt;Puntaje de aprobación: 720 sobre 1000&lt;/li&gt;
&lt;li&gt;Precio: 150 USD&lt;/li&gt;
&lt;li&gt;Idiomas: todos los actualmente soportados, disponibles desde el lanzamiento&lt;/li&gt;
&lt;li&gt;Modalidad: Pearson VUE (centro de examen u online proctoring)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lo estructural: los 4 dominios se mantienen
&lt;/h2&gt;

&lt;p&gt;Buena noticia para quien ya venía estudiando: los dominios y sus porcentajes &lt;strong&gt;no cambian&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development with AWS Services — 30%&lt;/li&gt;
&lt;li&gt;Security — 26%&lt;/li&gt;
&lt;li&gt;Testing and Deployment — 22%&lt;/li&gt;
&lt;li&gt;Troubleshooting and Optimization — 22%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que cambia es el contenido dentro de cada dominio: se consolidan skills que estaban demasiado fragmentadas, y se agregan skills nuevas relacionadas a AI y a Containers.&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;Tip:&lt;/strong&gt; si ya armaste tu planilla de "brechas de conocimiento" como sugería en mi artículo anterior, no la tires. Vas a poder reusar casi todo el contenido de Lambda, DynamoDB, API Gateway, Cognito, SQS/SNS, EventBridge, Step Functions, CI/CD, CloudFormation/CDK, seguridad de datos y STS. Lo que necesitás es agregar una columna nueva para lo que sigue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incorporación 1: una Task completa de seguridad de AI (Task 2.3)
&lt;/h2&gt;

&lt;p&gt;Esta es, para mí, la adición más relevante de todo el cambio. Dentro del dominio de Security se agrega la &lt;strong&gt;Task 2.3 — AI security&lt;/strong&gt;, con 5 skills nuevas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gestión de accesos para servicios de AI.&lt;/strong&gt; Aplica el mismo principio de mínimo privilegio que ya se usa para Lambda o DynamoDB, pero ahora sobre quién puede invocar un modelo, un agente o una Knowledge Base. Por ejemplo, se puede restringir con una política de recursos qué rol de IAM puede ejecutar &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; sobre un modelo específico.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Controles de privacidad de datos&lt;/strong&gt;, principalmente vía &lt;strong&gt;VPC endpoints&lt;/strong&gt; (AWS PrivateLink) y la garantía de que los inputs/outputs no se usen para entrenar modelos. Este punto no es solo teórico: el servicio nunca comparte tus datos con los proveedores de modelos ni los usa para entrenar foundation models, y todas las invocaciones se mantienen dentro de la red de AWS. Para reforzar el aislamiento de red, la &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html" rel="noopener noreferrer"&gt;guía de VPC interface endpoints de Bedrock&lt;/a&gt; explica que se puede crear un endpoint de interfaz (AWS PrivateLink) usando nombres de servicio como &lt;code&gt;com.amazonaws.region.bedrock-runtime&lt;/code&gt; o &lt;code&gt;com.amazonaws.region.bedrock-agent-runtime&lt;/code&gt;, evitando así exponer el tráfico a internet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content filtering y protección contra prompt injection.&lt;/strong&gt; Acá entra Amazon Bedrock Guardrails como mecanismo central. Los content filters detectan categorías de contenido dañino (odio, insultos, contenido inapropiado, violencia, mala conducta) y, dentro de esa misma categoría, un filtro específico de &lt;strong&gt;prompt attacks&lt;/strong&gt; detecta jailbreaks, prompt injections y prompt leakages. Hay que entender la diferencia entre prompt injection directo (el usuario intenta manipular el prompt) e indirecto (la instrucción maliciosa viene escondida en un dato externo, como un documento o una página web, que el agente procesa sin que el usuario lo note).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seguridad en interacciones de agentes de AI&lt;/strong&gt;: autorización de uso de tools, aislamiento de sesión (&lt;em&gt;session isolation&lt;/em&gt;) y flujos de aprobación humana (&lt;em&gt;human-in-the-loop&lt;/em&gt;). Esto conecta directo con Amazon Bedrock AgentCore Identity: este servicio provee autenticación, autorización y gestión de credenciales diseñada específicamente para agentes de AI y workloads automatizados, con identidades de workload que tienen atributos especializados para habilitar capacidades específicas de agentes, manteniendo compatibilidad con patrones estándar de identidad de workload de la industria.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protección de contenido sensible en logs de monitoreo.&lt;/strong&gt; Amazon Bedrock permite habilitar &lt;em&gt;model invocation logging&lt;/em&gt; hacia CloudWatch Logs o S3, pero por defecto esos logs pueden incluir el input/output completo de cada invocación. El mecanismo para proteger datos sensibles ahí es Amazon Bedrock Guardrails (vía la API &lt;code&gt;ApplyGuardrail&lt;/code&gt;), que permite enmascarar esa información antes de que quede registrada — mismo espíritu que ya aplicabas con &lt;code&gt;StreamViewType: KEYS_ONLY&lt;/code&gt; en DynamoDB Streams para no exponer información sensible.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmigjocxorfzs50iyd5j5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmigjocxorfzs50iyd5j5.png" alt="skills de Task 2.3" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;Tip de estudio:&lt;/strong&gt; si en DVA-C02 profundizaste STS, IAM trust relationships y KMS, ya tenés el 70% del razonamiento necesario para esta Task nueva. Lo que hay que sumar es el vocabulario y los mecanismos específicos de AI (Guardrails, AgentCore Identity, PrivateLink hacia Bedrock).&lt;/p&gt;

&lt;h2&gt;
  
  
  Incorporación 2: AI-assisted development transversal a los 4 dominios
&lt;/h2&gt;

&lt;p&gt;Esto se distribuye en los cuatro dominios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain 1 (Development):&lt;/strong&gt; generación de código y revisión automatizada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain 3 (Testing and Deployment):&lt;/strong&gt; automatización de pruebas y pruebas de regresión, y soporte para workflows de CI/CD incluyendo aprobaciones automatizadas de despliegue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain 4 (Troubleshooting and Optimization):&lt;/strong&gt; análisis de errores, sugerencias de troubleshooting y recomendaciones de optimización de performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un dato importante para no confundirse en el estudio: según la &lt;a href="https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-developer-ide-end-of-support.html" rel="noopener noreferrer"&gt;documentación oficial sobre el fin de soporte de los plugins IDE de Amazon Q Developer&lt;/a&gt;, AWS discontinuará el soporte de estos plugins el 30 de abril de 2027, y como alternativa para capacidades similares (coding agéntico, chat, soporte MCP) recomienda explorar &lt;strong&gt;Kiro&lt;/strong&gt;. &lt;br&gt;
Kiro es el IDE agéntico que propone &lt;em&gt;spec-driven development&lt;/em&gt;: convierte un prompt en requerimientos, diseño de sistema y lista de tareas antes de generar código, en lugar de saltar directo a la implementación. Como developer, puede que el examen busca que entiendas el concepto de &lt;em&gt;spec-driven development&lt;/em&gt; como enfoque de AI-assisted development.&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;Tip:&lt;/strong&gt; una lectura valida es entender en qué punto del ciclo de vida de desarrollo entra cada práctica de AI-assisted development, que es como está formulada la skill en cada dominio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incorporación 3: Gestión de contenedores
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Construcción y gestión de imágenes de contenedor con &lt;strong&gt;Amazon ECR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Despliegue de aplicaciones containerizadas con &lt;strong&gt;Amazon ECS&lt;/strong&gt;, &lt;strong&gt;Amazon EKS&lt;/strong&gt; y &lt;strong&gt;AWS Fargate&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En mi artículo anterior ya mencionaba ECS/EKS dentro de "Contenedores ECS, EKS" enfocado en monitoreo y observabilidad (CloudWatch, X-Ray) ya que no se me presentaron otro tipo de preguntas. Pero la diferencia es que en la nueva versión del examen amplía el foco: no solo observar un contenedor en ejecución, sino todo el ciclo — construir la imagen, subirla a ECR, y decidir entre ECS, EKS o Fargate según el caso de uso (gestión de clúster propia vs. serverless de contenedores).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpe9744wfi8v76ranh5u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpe9744wfi8v76ranh5u.jpg" alt="AWS Containers" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;Tip de laboratorio:&lt;/strong&gt; armá una imagen simple, subila a ECR, y desplegarla primero en ECS con Fargate y después evaluá qué cambiaría si migraras esa misma imagen a EKS. La diferencia conceptual entre "quién administra el plano de control y los nodos" es justamente lo que suelen evaluar en preguntas de este estilo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incorporación 4: Nuevos servicios que entran en el alcance del examen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock&lt;/strong&gt; → el servicio administrado para trabajar con modelos fundacionales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock AgentCore&lt;/strong&gt; → la plataforma para construir, desplegar y operar agentes de AI de forma segura y a escala, sin gestionar infraestructura. Se compone de Runtime (entorno serverless para correr agentes), Memory (memoria de corto y largo plazo), Gateway, Identity, herramientas built-in (ejecución de código en sandbox, browser tool) y Observability (monitoreo vía CloudWatch). Para un developer, dos piezas son especialmente relevantes:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gateway&lt;/strong&gt;, porque permite convertir APIs existentes (OpenAPI, Smithy) y funciones &lt;strong&gt;AWS Lambda&lt;/strong&gt; en tools compatibles con el protocolo &lt;strong&gt;MCP&lt;/strong&gt; (Model Context Protocol), sin tener que gestionar esa infraestructura — reutiliza directo el conocimiento de Lambda/API Gateway que ya tenés de DVA-C02.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt;, porque conecta directo con la Task 2.3 de seguridad que nombré más arriba.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Q&lt;/strong&gt; → el asistente conversacional generativo para entender, construir, extender y operar aplicaciones y recursos de AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro&lt;/strong&gt; → como comenté arriba, el IDE agéntico de &lt;em&gt;spec-driven development&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Data Firehose&lt;/strong&gt; → para entrega de streams de datos a destinos como S3, Redshift o servicios de terceros; relevante si tu arquitectura de eventos (la que ya veías con EventBridge y Kinesis Data Streams) necesita alimentar pipelines de AI o analítica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS PrivateLink&lt;/strong&gt; → ya mencionado en la Task de AI security, pero también entra como servicio in-scope de forma general, para conexiones privadas VPC-a-servicio sin pasar por internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12bcx435xqy7632tgnyi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12bcx435xqy7632tgnyi.jpg" alt="AWS AgentCore" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que se consolida (menos memorizar features aislados, más criterio)
&lt;/h2&gt;

&lt;p&gt;Se aclara que varias skills fragmentadas se agrupan en unidades más grandes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Área&lt;/th&gt;
&lt;th&gt;Antes (DVA-C02)&lt;/th&gt;
&lt;th&gt;Ahora (DVA-C03)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;4 skills separadas&lt;/td&gt;
&lt;td&gt;1 skill integral&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seguridad (cifrado y datos sensibles)&lt;/td&gt;
&lt;td&gt;13 skills en 2 tasks&lt;/td&gt;
&lt;td&gt;7 skills en 1 task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;11 skills en 2 tasks&lt;/td&gt;
&lt;td&gt;5 skills en 1 task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;8 skills&lt;/td&gt;
&lt;td&gt;5 skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimization&lt;/td&gt;
&lt;td&gt;9 skills&lt;/td&gt;
&lt;td&gt;6 skills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En la práctica, significa que el examen deja de preguntar detalles muy puntuales y aislados, y empieza a evaluar más el &lt;strong&gt;razonamiento integrado&lt;/strong&gt; sobre un tema (por ejemplo, DynamoDB completo como una sola competencia, en vez de partir claves, índices, capacidad y TTL en preguntas separadas). Si con la guía actual DVA-C02 ya estudiaste estos temas a fondo como yo, no vas a tener que aprender contenido nuevo acá. El cambio es más de "cómo te lo preguntan".&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo armaría el plan de estudio hoy hasta esperar el detalle de la nueva guía de examen
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Base técnica:&lt;/strong&gt; seguí usando mi &lt;a href="https://medium.com/@reinalau/camino-a-lograr-aws-certified-developer-associate-dva-c02-9a59112df16f" rel="noopener noreferrer"&gt;artículo de DVA-C02&lt;/a&gt; como guía de los servicios core (Lambda, DynamoDB, API Gateway, Cognito, SQS/SNS, EventBridge, Step Functions, S3, CI/CD, CDK/CloudFormation, CloudWatch/X-Ray, seguridad de datos y STS). Ahí no cambió la profundidad requerida.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sumá una capa de AI aplicada al developer:&lt;/strong&gt; entendé conceptualmente Amazon Bedrock, AgentCore (sobre todo Gateway e Identity), Amazon Q Developer y Kiro — no como usuario experto, sino como developer que decide cuándo y cómo integrarlos, y qué controles de seguridad aplicar (Guardrails, PrivateLink, aislamiento de sesión, human-in-the-loop).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profundizá contenedores end-to-end:&lt;/strong&gt; ECR para las imágenes, y el criterio de decisión entre ECS, EKS y Fargate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repasá la guía de examen oficial completa&lt;/strong&gt; apenas se publique (disponible cuando abra el registro, el 27 de octubre de 2026), porque ahí van a estar los task statements detallados de cada skill.&lt;/li&gt;
&lt;li&gt;Mantené la costumbre que ya recomendaba antes: hacé laboratorios chicos de todo lo anterior y practicar muchas preguntas de examen, más que memorizar teoría aislada.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;⭐ Y el mismo disclaimer que hice en el artículo anterior sigue más vigente que nunca: &lt;strong&gt;cuidado con usar AI para el estudio profundo de servicios sin chequear contra documentación oficial&lt;/strong&gt; — pueden aparecer alucinaciones si se utiliza el conocimiento del modelo que uses. Contrastá contra la documentación oficial via los MCP oficiales de AWS:&lt;br&gt;
"awslabs.aws-documentation-mcp-server" y "aws-knowledge-mcp-server" &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Conclusión:&lt;/strong&gt; Si estás arrancando de cero con esta certificación, te recomiendo leer primero &lt;a href="https://medium.com/@reinalau/camino-a-lograr-aws-certified-developer-associate-dva-c02-9a59112df16f" rel="noopener noreferrer"&gt;mi artículo sobre DVA-C02&lt;/a&gt; para la base de servicios, y usar este como complemento de lo nuevo que trae DVA-C03.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fuentes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/es/blogs/training-and-certification/september-2026-new-offerings/" rel="noopener noreferrer"&gt;Certification updates from AWS Training and Certification: September 2026 — AWS Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/bedrock/security-privacy-responsible-ai/" rel="noopener noreferrer"&gt;Security, privacy, and responsible AI — Amazon Bedrock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/vpc-interface-endpoints.html" rel="noopener noreferrer"&gt;Use interface VPC endpoints (AWS PrivateLink) to create a private connection to Amazon Bedrock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-components.html" rel="noopener noreferrer"&gt;Create your guardrail — Amazon Bedrock Guardrails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/identity-overview.html" rel="noopener noreferrer"&gt;Overview of Amazon Bedrock AgentCore Identity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/introducing-amazon-bedrock-agentcore-gateway-transforming-enterprise-ai-agent-tool-development/" rel="noopener noreferrer"&gt;Introducing Amazon Bedrock AgentCore Gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-developer-ide-end-of-support.html" rel="noopener noreferrer"&gt;Amazon Q Developer IDE plugins end of support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>certification</category>
      <category>developerassociate</category>
    </item>
    <item>
      <title>A2A en Strands: agentes remotos con routing determinístico y discovery dinámico</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:20:05 +0000</pubDate>
      <link>https://dev.to/aws-builders/a2a-en-strands-agentes-remotos-con-routing-deterministico-y-discovery-dinamico-36gn</link>
      <guid>https://dev.to/aws-builders/a2a-en-strands-agentes-remotos-con-routing-deterministico-y-discovery-dinamico-36gn</guid>
      <description>&lt;p&gt;Culminando la serie de artículos que trata la orquestación de multiagentes en Strands Agents, voy a explicar cómo implementar agentes remotos con A2A y algunas de sus variantes, utilizando routing determinístico y discovery dinámico de agentes. Lo ejemplifiqué con la construcción de dos casos de uso: &lt;code&gt;privacy-aware-routing&lt;/code&gt; y &lt;code&gt;multi-agent-trip-planner&lt;/code&gt;. Para probarlos utilicé Gemma4 + Ollama (local) y la API de Gemini, capa gratuita.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;La idea principal es experimentar con ejemplos sencillos algunas casuísticas del protocolo de comunicación A2A en tu notebook, sin necesidad de una cuenta de AWS ni de servicios como Bedrock. Strands hace de wrapper sobre el protocolo, permitiendo una comunicación fluida con agentes de IA remotos, más allá del framework o lenguaje con el que estén implementados.&lt;/p&gt;

&lt;p&gt;🗒 &lt;strong&gt;Los casos de usos&lt;/strong&gt; que elegí &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy aware routing&lt;/strong&gt;: Un asistente financiero-bancario de atención al cliente (orquestador, Gemini vía API key) necesita resolver consultas que a veces incluyen datos sensibles (ej. número de cuenta, DNI, monto de la cuenta). En lugar de resolver el agente general, el orquestador detecta cuándo la consulta toca datos sensibles y delega esa parte a un &lt;strong&gt;agente A2A local&lt;/strong&gt; (servidor separado, Gemma vía Ollama, corriendo en &lt;code&gt;localhost&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi agent trip planner&lt;/strong&gt;: Un asistente de viajes de atención al cliente donde el orquestador (Gemini vía API key) no conoce de antemano a qué agente delegar cada parte de la consulta. En su lugar, &lt;strong&gt;descubre en runtime&lt;/strong&gt; tres agentes especialistas: vuelos, hoteles e itinerario. Cada uno expuesto como servidor A2A local (Gemma vía Ollama), y decide dinámicamente a cuál invocar según lo que necesita resolver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En las secciones siguientes cubro los conceptos clave del protocolo A2A en Strands y el diseño e implementación de los casos de uso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conceptos Clave
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ➡ Protocolo A2A en Strands
&lt;/h3&gt;

&lt;p&gt;A2A (Agent-to-Agent) es un protocolo abierto —no una invención de Strands ni de AWS— que define cómo agentes que corren en procesos, máquinas o incluso organizaciones distintas se &lt;strong&gt;descubren&lt;/strong&gt; y &lt;strong&gt;comunican&lt;/strong&gt; entre sí por HTTP/JSON-RPC.&lt;/p&gt;

&lt;p&gt;Strands no reinventa el protocolo: lo envuelve (&lt;em&gt;wraps&lt;/em&gt;) con una interfaz familiar, como si invocáramos a un agente normal de Strands. El desarrollador no ve protocolo: visualiza un agente. Un ejemplo sencillo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.agent.a2a_agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A2AAgent&lt;/span&gt;

&lt;span class="c1"&gt;# Create an A2AAgent pointing to a remote A2A server
&lt;/span&gt;&lt;span class="n"&gt;privacy_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;A2AAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Invoked exactly like a local Agent — Strands' wrapper keeps
# the A2A protocol completely hidden from the caller
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;privacy_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I need the balance for account 1234-5678-9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'role': 'assistant', 'content': [{'text': 'The balance for account 1234-5678-9 is $85,400'}]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El constructor de &lt;code&gt;A2AAgent&lt;/code&gt; acepta varios parámetros:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Base URL of the remote A2A agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Agent name (auto-populated from agent card if not provided)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Agent description (auto-populated from agent card if not provided)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;Timeout for HTTP operations in seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a2a_client_factory&lt;/td&gt;
&lt;td&gt;ClientFactory&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Optional pre-configured A2A client factory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dos piezas hacen posible el wrap en Strands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;A2AServer&lt;/code&gt;&lt;/strong&gt;: expone cualquier &lt;code&gt;Agent&lt;/code&gt; de Strands como servicio A2A, publicando automáticamente un &lt;em&gt;agent card&lt;/em&gt; en &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt; → es la carta de presentación del agente (nombre, descripción, skills que ofrece).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;A2AAgent&lt;/code&gt;&lt;/strong&gt;: el lado cliente. Envuelve el endpoint de un agente remoto y lo invocás con la misma interfaz que un agente local.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Al igual que un &lt;code&gt;Agent&lt;/code&gt; local, &lt;code&gt;A2AAgent&lt;/code&gt; soporta invocación asíncrona (&lt;code&gt;invoke_async&lt;/code&gt;) y streaming de respuestas (&lt;code&gt;stream_async&lt;/code&gt;). En los ejemplos de este artículo, el orquestador transmite su respuesta en streaming, mientras que cada llamada A2A a un agente remoto es sincrónica (espera la respuesta completa del especialista antes de seguir).&lt;/p&gt;

&lt;p&gt;La metadata del agente remoto es la &lt;em&gt;agent card&lt;/em&gt; y se puede consultar explícitamente con &lt;code&gt;get_agent_card()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;privacy_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_agent_card&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skills: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada &lt;em&gt;skill&lt;/em&gt; dentro del agent card describe una capacidad puntual del agente: un identificador, una descripción en lenguaje natural, y opcionalmente ejemplos de uso. Es lo que le permite al orquestador razonar "este agente sabe buscar vuelos" sin necesidad de que el desarrollador lo codifique explícitamente. La decisión de a quién delegar se basa en hacer &lt;em&gt;match&lt;/em&gt; entre la consulta del usuario y las skills descubiertas.&lt;/p&gt;

&lt;p&gt;El método anterior el que después va a cobrar protagonismo real en el ejemplo &lt;code&gt;multi-agent-trip-planner&lt;/code&gt;: ahí el orquestador no invoca un endpoint conocido de antemano, sino que consulta las agent cards de varios agentes remotos para decidir dinámicamente a cuál delegar cada tarea.&lt;/p&gt;

&lt;h3&gt;
  
  
  ➡ A2AAgent dentro de otros patrones de orquestación en Strands
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;A2AAgent&lt;/code&gt; se puede combinar dentro de otros patrones de orquestación multiagente en Strands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Como tool de un agente orquestador&lt;/strong&gt;: es exactamente el enfoque que desarrollamos en la POC &lt;code&gt;privacy-aware-routing&lt;/code&gt; — el &lt;code&gt;A2AAgent&lt;/code&gt; se envuelve en un &lt;code&gt;@tool&lt;/code&gt; y el agente padre lo invoca como una función más, como si fuera un subagente. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Como nodo dentro de un Graph&lt;/strong&gt;: un &lt;code&gt;A2AAgent&lt;/code&gt; puede integrarse como nodo remoto en un flujo de Graph, mezclando agentes locales y remotos dentro del mismo grafo de dependencias. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;En Swarm&lt;/strong&gt;: al día de hoy NO está soportado, ya que ese patrón depende de handoffs basados en tools que el protocolo A2A todavía no contempla.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Habiendo visto el lado CLIENTE, veamos ahora cómo se expone un agente para que otros puedan consumirlo vía A2A.&lt;/p&gt;

&lt;h3&gt;
  
  
  ➡ Creando un A2A Server
&lt;/h3&gt;

&lt;p&gt;Del lado servidor, exponer un agente de Strands como servicio A2A es tan simple como envolverlo con &lt;code&gt;A2AServer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.multiagent.a2a&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A2AServer&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Privacy Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resolves queries containing sensitive customer data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;a2a_server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;A2AServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;a2a_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El servidor publica automáticamente el &lt;em&gt;agent card&lt;/em&gt; en &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt; y queda escuchando requests A2A en el puerto configurado (por default, &lt;code&gt;9000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Un detalle de diseño importante: se recomienda pasar un &lt;strong&gt;&lt;code&gt;agent_factory&lt;/code&gt;&lt;/strong&gt; (una función que crea un agente nuevo) en lugar de una instancia única de &lt;code&gt;Agent&lt;/code&gt;. La razón es el &lt;strong&gt;aislamiento por conversación&lt;/strong&gt;: A2A identifica cada conversación con un &lt;code&gt;context_id&lt;/code&gt;, y el servidor crea y reutiliza un agente dedicado por cada &lt;code&gt;context_id&lt;/code&gt;, para que distintas conversaciones nunca mezclen su historial entre sí. Pasar un único &lt;code&gt;Agent&lt;/code&gt; compartido está deprecado!&lt;/p&gt;

&lt;p&gt;Para los casos de uso de este artículo, ese &lt;strong&gt;aislamiento&lt;/strong&gt; no es el foco central: cada servidor atiende un tipo de tarea puntual, sin mantener contexto conversacional extenso.&lt;br&gt;
Pero es un concepto clave a tener presente si tu agente remoto necesita recordar el hilo de una conversación a lo largo de varios turnos.&lt;/p&gt;
&lt;h3&gt;
  
  
  ➡ Strands A2A Tool: descubrimiento dinámico
&lt;/h3&gt;

&lt;p&gt;Cuando hay &lt;strong&gt;varios&lt;/strong&gt; agentes remotos disponibles y no querés hardcodear el mapeo "qué tarea va a qué agente", Strands ofrece &lt;code&gt;A2AClientToolProvider&lt;/code&gt; (parte del paquete &lt;code&gt;strands-agents-tools&lt;/code&gt;). Le da al agente orquestador un conjunto de tools para &lt;strong&gt;descubrir&lt;/strong&gt; agentes A2A y comunicarse con ellos usando lenguaje natural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands_tools.a2a_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;A2AClientToolProvider&lt;/span&gt;

&lt;span class="c1"&gt;# known_agent_urls es opcional: podés pasar endpoints conocidos
# o dejar que el provider los descubra en runtime
&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;A2AClientToolProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;known_agent_urls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Flights Agent
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Hotels Agent
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9003&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Itinerary Agent
&lt;/span&gt;    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;orchestrator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;orchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quiero ir a Barcelona en marzo, 5 días&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El provider expone tres capacidades:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Discovery&lt;/strong&gt;: descubre automáticamente los agentes A2A disponibles y sus capacidades (leyendo sus &lt;em&gt;agent cards&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Communication&lt;/strong&gt;: envía mensajes usando el protocolo A2A estandarizado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural Language Interface&lt;/strong&gt;: el orquestador interactúa con los agentes remotos en lenguaje natural, sin que el desarrollador tenga que codear explícitamente "si la consulta es sobre vuelos, llamá a este endpoint".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esta es la pieza central de la POC de este articulo &lt;code&gt;multi-agent-trip-planner&lt;/code&gt;: acá el &lt;em&gt;agent card&lt;/em&gt; deja de ser un detalle de implementación y pasa a ser el mecanismo que le permite al orquestador razonar sobre a quién delegar cada subtarea, sin que ese mapeo esté escrito en el código.&lt;/p&gt;

&lt;h3&gt;
  
  
  ➡ Evaluaciones (Evals)
&lt;/h3&gt;

&lt;p&gt;Strands cuenta con un &lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/quickstart/" rel="noopener noreferrer"&gt;marco oficial&lt;/a&gt; para realizar evaluaciones, desde resultados simples hasta el análisis de interacciones multiagente complejas.&lt;/p&gt;

&lt;p&gt;Para evaluar los workflows usé &lt;code&gt;strands-agents-evals&lt;/code&gt; con el mismo provider configurado en &lt;code&gt;.env&lt;/code&gt; (Gemini) como juez via LiteLLM para mantener el proyecto sobre una sola credencial y sin cuenta de AWS. En producción la práctica estándar es usar un modelo más capaz e independiente como judge.&lt;/p&gt;

&lt;p&gt;En ambos proyectos combiné dos tipos de evaluación, para mostrar las dos familias que ofrece el SDK:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Determinística&lt;/strong&gt; (código puro, sin LLM): rápida y barata, ideal para CI (Continuous Integration). Verifica la &lt;em&gt;decisión&lt;/em&gt; del orquestador inspeccionando qué herramientas invocó (su trayectoria). Es el corazón de cada patrón A2A.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-a-judge&lt;/strong&gt;: un modelo puntúa, con una rúbrica, cualidades más subjetivas de la respuesta final: &lt;em&gt;Correctness&lt;/em&gt; (¿el dato coincide con la referencia?) y &lt;em&gt;Faithfulness&lt;/em&gt; (¿se apoya en los datos de las tools y no inventa ni evade?).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La diferencia está en &lt;em&gt;qué&lt;/em&gt; mide la capa determinística en cada patrón, porque cada uno decide algo distinto:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;privacy-aware-routing&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;multi-agent-trip-planner&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qué decide el orquestador&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Si delega o responde directo&lt;/td&gt;
&lt;td&gt;A quién descubre y a quién delega&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eval determinística&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;La tool &lt;code&gt;handle_sensitive_query&lt;/code&gt; se llama solo ante datos sensibles, y NO ante consultas genéricas&lt;/td&gt;
&lt;td&gt;El orquestador primero descubre (&lt;code&gt;a2a_list_discovered_agents&lt;/code&gt;) y luego delega (&lt;code&gt;a2a_send_message&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evaluadores usados&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ToolCalled&lt;/code&gt; + &lt;code&gt;ToolNotCalled&lt;/code&gt; (custom)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ToolCalled&lt;/code&gt; (discovery y delegación)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eval LLM-judge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correctness + Faithfulness sobre la respuesta del agente local&lt;/td&gt;
&lt;td&gt;Correctness + Faithfulness sobre la propuesta de viaje integrada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Problema que detecta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;El modelo local inventa un saldo en lugar de usar la tool&lt;/td&gt;
&lt;td&gt;Un especialista inventa vuelos/hoteles/atracciones en lugar de usar su tool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Descripción de la solución
&lt;/h2&gt;

&lt;p&gt;📦 &lt;strong&gt;Un único repositorio de GitHub con los dos proyectos:&lt;/strong&gt;  &lt;a href="https://github.com/reinalau/strands-a2a-patterns" rel="noopener noreferrer"&gt;github.com/reinalau/strands-a2a-patterns&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwv14r1fm36ssc7rqpbg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwv14r1fm36ssc7rqpbg.jpg" alt="Privacy Aware routing" width="686" height="714"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffn4q481vtkpnxhj9jj93.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffn4q481vtkpnxhj9jj93.jpg" alt="Multi-Agent travel trip planner" width="688" height="708"&gt;&lt;/a&gt;&lt;br&gt;
El repositorio reúne &lt;strong&gt;dos proyectos independientes&lt;/strong&gt;, cada uno es un caso de uso de A2A:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;privacy-aware-routing&lt;/code&gt;&lt;/strong&gt; - ruteo determinístico hacia un único agente remoto: el orquestador decide, según el contenido de la consulta, si responde él mismo o delega la parte sensible a un agente que corre 100% local.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;multi-agent-trip-planner&lt;/code&gt;&lt;/strong&gt; - descubrimiento dinámico de agentes: el orquestador no conoce de antemano a los especialistas, sino que los descubre en runtime leyendo sus &lt;em&gt;agent cards&lt;/em&gt; y delega cada subtarea al que corresponda. A su vez cada agente tiene tools disponibles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ambos comparten la misma base técnica (Strands, con Gemini como orquestador y Gemma local como agente remoto) y la misma forma de ejecutarse. El siguiente setup aplica a los dos por igual. Se usan los dos modelos, Ollama local y Gemini via Apikey:&lt;/p&gt;

&lt;p&gt;a. Ollama + el pequeño modelo &lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt; (que pesa poco más de 4gb) ejecutando en Docker. Para esto necesitás tener &lt;strong&gt;Docker Desktop&lt;/strong&gt; instalado y corriendo; una vez levantado, los comandos de abajo crean el contenedor de Ollama y descargan el modelo.&lt;br&gt;
En el caso de &lt;code&gt;multi-agent-trip-planner&lt;/code&gt; hay tres agentes remotos que pueden recibir requests casi al mismo tiempo; si querés que esas llamadas se ejecuten en paralelo, hay que indicárselo a Ollama, cuyo comportamiento por default procesa una inferencia por vez:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the Ollama server with a persistent volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="nt"&gt;-p&lt;/span&gt; 11434:11434 &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama ollama/ollama

&lt;span class="c"&gt;# Start the Ollama server with real parallelism (3 simultaneous requests)&lt;/span&gt;
&lt;span class="c"&gt;# docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama -e OLLAMA_NUM_PARALLEL=3  ollama/ollama&lt;/span&gt;

&lt;span class="c"&gt;# Download the model&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull gemma4:e2b-it-qat

&lt;span class="c"&gt;# Test that the model responds&lt;/span&gt;
&lt;span class="c"&gt;# If the container has already been created and &lt;/span&gt;
&lt;span class="c"&gt;# is currently stopped, simply use: docker start ollama&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama run gemma4:e2b-it-qat

&lt;span class="c"&gt;# Verify that the model is running&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b. API de Gemini. La API key se puede generar de manera gratuita desde &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;aquí&lt;/a&gt; y utilizar al menos estos modelos (experimentá con los que te permita tu cuenta):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini-3.6-flash
gemini-3.5-flash-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;El código de los 2 proyectos está en Python&lt;/strong&gt; y la estructura se diseñó de manera que su lógica sea más explicativa; todo el detalle lo encontrás en el &lt;code&gt;README.md&lt;/code&gt; de cada proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-a2a-patterns/
├── README.md                          # Overview of both use cases
│
├── privacy-aware-routing/             # Case 1: deterministic routing, single remote agent
│   ├── README.md
│   ├── requirements.txt
│   ├── .env.example
│   ├── common/                        # shared: prompts, config, logging
│   ├── remote_agent/
│   │   ├── server.py                  # A2AServer — Gemma via Ollama
│   │   └── bank_tools.py              # mock bank tools with fixed data
│   ├── orchestrator/
│   │   └── main.py                    # Gemini — A2AAgent as tool
│   ├── tests/
│   │   ├── test_routing_logic.py      # does the orchestrator delegate when it should?
│   │   └── test_a2a_integration.py    # real integration test against the running local server
│   ├── evals/
│   │   ├── eval_routing.py            # deterministic: routing decision
│   │   └── eval_response_quality.py   # LLM-judge: correctness + faithfulness
│   └── logs/
│       └── .gitkeep                    # run logs land here (gitignored)
│
└── multi-agent-trip-planner/          # Case 2: dynamic discovery, 3 remote agents
    ├── README.md
    ├── requirements.txt
    ├── .env.example
    ├── common/                        # shared: prompts, config, logging
    ├── remote_agents/
    │   ├── travel_tools.py            # mock travel tools with fixed data
    │   ├── flights_server.py          # A2AServer — Gemma via Ollama (port 9001)
    │   ├── hotels_server.py           # A2AServer — Gemma via Ollama (port 9002)
    │   └── itinerary_server.py        # A2AServer — Gemma via Ollama (port 9003)
    ├── orchestrator/
    │   └── main.py                    # Gemini — A2AClientToolProvider
    ├── tests/
    │   ├── test_discovery.py          # does the orchestrator discover before delegating?
    │   └── test_a2a_integration.py    # real integration test against the 3 running servers
    ├── evals/
    │   ├── eval_discovery.py          # deterministic: discovery + delegation decision
    │   └── eval_response_quality.py   # LLM-judge: correctness + faithfulness
    └── logs/
        └── .gitkeep                    # run logs land here (gitignored)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Ejecución Local
&lt;/h3&gt;

&lt;p&gt;Una vez que tenemos el repositorio clonado y el Docker de Ollama con el modelo gemma 4 (o la API key de Gemini generada), pasamos a armar el ambiente. Los pasos son los mismos en ambos proyectos; cada uno tiene su propio &lt;code&gt;requirements.txt&lt;/code&gt; y su &lt;code&gt;.env&lt;/code&gt;, así que hay que pararse dentro de la carpeta del proyecto que quieras correr.&lt;/p&gt;

&lt;p&gt;En &lt;code&gt;requirements.txt&lt;/code&gt; (con comentarios que explican para qué se necesita cada dependencia) encontramos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-agents[a2a,ollama,litellm]&amp;gt;=1.0.0
strands-agents-tools&amp;gt;=0.1.0
python-dotenv&amp;gt;=1.0.0
pytest&amp;gt;=8.0.0
pytest-asyncio&amp;gt;=0.23.0
strands-agents-evals&amp;gt;=1.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conviene instalar las dependencias en un entorno virtual, para aislarlas del resto del sistema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="c"&gt;# Activar: source .venv/bin/activate (Linux/macOS)&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En &lt;code&gt;.env&lt;/code&gt; se configuran, entre otros, la API key y el modelo del orquestador (Gemini), el modelo local (Gemma vía Ollama) y el nivel de log. Alcanza con copiar el ejemplo y completar la key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;En &lt;code&gt;common/config.py&lt;/code&gt;&lt;/strong&gt; está la carga y validación de esas variables (falla temprano con un mensaje si falta la API key), y &lt;strong&gt;en &lt;code&gt;common/prompts.py&lt;/code&gt;&lt;/strong&gt; los system prompts del orquestador y de cada agente remoto.&lt;/p&gt;

&lt;p&gt;📝 &lt;strong&gt;Sobre cambiar de modelo:&lt;/strong&gt; el orquestador usa &lt;code&gt;LiteLLMModel&lt;/code&gt;, así que no está atado a Gemini: LiteLLM soporta muchos proveedores (Anthropic, OpenAI, Bedrock, etc.). Alcanza con cambiar &lt;code&gt;ORCHESTRATOR_MODEL_ID&lt;/code&gt; en el &lt;code&gt;.env&lt;/code&gt; por el identificador del proveedor que quieras (con su prefijo, p. ej. &lt;code&gt;anthropic/…&lt;/code&gt;, &lt;code&gt;openai/…&lt;/code&gt;) y setear su API key. La lista completa está en la &lt;a href="https://strandsagents.com/docs/user-guide/concepts/model-providers/litellm/" rel="noopener noreferrer"&gt;documentación de model providers de Strands&lt;/a&gt;. Del mismo modo, el agente remoto no tiene por qué correr en Ollama: podés reemplazar &lt;code&gt;OllamaModel&lt;/code&gt; por otro provider en el código del servidor (&lt;code&gt;server.py&lt;/code&gt; / los &lt;code&gt;*_server.py&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Las pruebas&lt;/strong&gt; están en &lt;code&gt;tests/&lt;/code&gt; y se pueden correr una a una  (recomendado! ya que puede ser algo lento localmente) o todas juntas: unitarias rápidas (deciden si el orquestador delega/descubre cuando corresponde) e integración real contra los servidores A2A (arrancan solos como subprocesos; requieren Ollama corriendo).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Procedemos a ejecutar el caso real.&lt;/strong&gt; Acá está la diferencia práctica entre los dos proyectos: cada uno corre como &lt;strong&gt;varios procesos independientes&lt;/strong&gt;, uno por terminal. Primero se levantan los agentes remotos (cada &lt;code&gt;A2AServer&lt;/code&gt; queda escuchando en su puerto) y, con todos activos, se lanza el orquestador que los descubre y les habla por la red. El orden de arranque de los servidores no importa, pero deben estar todos activos antes de correr el orquestador.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;privacy-aware-routing&lt;/code&gt; — 2 procesos (2 terminales):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terminal - Comando&lt;/th&gt;
&lt;th&gt;Rol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 1&lt;/strong&gt;: &lt;code&gt;python -m remote_agent.server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Agente remoto local (Gemma), expone su agent card en el puerto 9000. Cuenta con tools para responder sobre datos sensibles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 2&lt;/strong&gt;: &lt;code&gt;python -m orchestrator.main "Quiero consultar el saldo de mi cuenta 1234-5678-9"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Orquestador (Gemini): decide si delega y arma la respuesta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;multi-agent-trip-planner&lt;/code&gt; — 4 procesos (4 terminales):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terminal - Comando&lt;/th&gt;
&lt;th&gt;Rol&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 1&lt;/strong&gt;: &lt;code&gt;python -m remote_agents.flights_server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Especialista de vuelos (Gemma), puerto 9001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 2&lt;/strong&gt;: &lt;code&gt;python -m remote_agents.hotels_server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Especialista de hoteles (Gemma), puerto 9002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 3&lt;/strong&gt;: &lt;code&gt;python -m remote_agents.itinerary_server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Especialista de itinerario (Gemma), puerto 9003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Terminal 4&lt;/strong&gt;: &lt;code&gt;python -m orchestrator.main "Quiero ir a Barcelona en marzo, 5 dias"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Orquestador (Gemini): descubre los 3 agentes, delega y arma la propuesta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El orquestador acepta la consulta como argumento o, si se lo llama sin argumentos, abre un chat interactivo. &lt;br&gt;
Para salir de los servidores remotos: &lt;code&gt;Ctrl+C&lt;/code&gt; en su terminal; para salir del chat interactivo: escribí &lt;code&gt;exit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Los logs&lt;/strong&gt; de las llamadas están en &lt;code&gt;logs/&lt;/code&gt;, separado por proceso. Contiene la data cruda que permite seguir el intercambio A2A: qué descubrió y delegó el orquestador, y qué tool ejecutó cada agente remoto (líneas &lt;code&gt;TOOL CALL: ...&lt;/code&gt;). Permite verificar que la respuesta salió de los datos y no de una &lt;strong&gt;alucinación&lt;/strong&gt; del modelo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El resultado final&lt;/strong&gt; se visualiza en pantalla, en streaming (token a token).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por último, hacemos una evaluación sobre el sistema multiagente real con el concepto de &lt;em&gt;"Evals".&lt;/em&gt;&lt;/strong&gt; Cada proyecto trae dos scripts: uno determinístico (verifica la decisión del orquestador) y uno LLM-as-judge (puntúa la calidad de la respuesta final). Ejecutan el flujo real contra el modelo configurado, muestran los resultados en consola y guardan el reporte completo en &lt;code&gt;evals/outputs/&amp;lt;nombre&amp;gt;_&amp;lt;timestamp&amp;gt;.json&lt;/code&gt;. (Leer más detalle arriba en Conceptos Clave)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# privacy-aware-routing&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.eval_routing
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.eval_response_quality

&lt;span class="c"&gt;# multi-agent-trip-planner&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.eval_discovery
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.eval_response_quality
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;Los dos proyectos implementan casos de uso distintos que se apoyan en la misma base exponer agentes con &lt;code&gt;A2AServer&lt;/code&gt;, pero difieren en &lt;strong&gt;cómo el orquestador consume esos agentes remotos&lt;/strong&gt;: uno con &lt;strong&gt;ruteo determinístico&lt;/strong&gt; hacia un agente conocido (&lt;code&gt;A2AAgent&lt;/code&gt;, endpoint fijo); y el otro, mediante un &lt;strong&gt;descubrimiento dinámico&lt;/strong&gt; de varios especialistas (&lt;code&gt;A2AClientToolProvider&lt;/code&gt;, que lee los &lt;em&gt;agent cards&lt;/em&gt; en runtime). &lt;/p&gt;

&lt;p&gt;⭐ En ambos casos de uso, la abstracción de Strands esconde la mecánica del protocolo (resolución del agent card, HTTP, JSON-RPC). Del lado del orquestador, delegar a un proceso remoto se ve casi igual que llamar a una función local.&lt;/p&gt;

&lt;p&gt;Algunas lecciones que dejaron las ejecuciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El &lt;em&gt;agent card&lt;/em&gt; es el contrato.&lt;/strong&gt; En el caso de discovery, la calidad de las &lt;code&gt;skills&lt;/code&gt; publicadas (nombre y descripción) es lo que le permite al orquestador rutear bien sin mapeos hardcodeados. Un agent card incompleta degrada la decisión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los modelos locales chicos usan tools de forma intermitente.&lt;/strong&gt; Con &lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;, a veces el agente responde sin llamar la tool y ahí puede inventar datos. Por eso importa (a) instruirlo en el prompt a usar siempre la tool, y (b) verificarlo: los logs &lt;code&gt;TOOL CALL: ...&lt;/code&gt; distinguen un dato real de una alucinación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluar los agentes exige leer el campo "reasons", no solo el score.&lt;/strong&gt; Un &lt;code&gt;0.0&lt;/code&gt; puede venir de la infraestructura (un &lt;code&gt;503&lt;/code&gt; del juez, un modelo dado de baja), no de una mala respuesta. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latencia y concurrencia son decisiones de arquitectura.&lt;/strong&gt; La inferencia local es lenta; con tres especialistas compartiendo un Ollama secuencial, las tareas se encolan. Habilitar paralelismo (&lt;code&gt;OLLAMA_NUM_PARALLEL&lt;/code&gt;) y ajustar los timeouts del cliente A2A cambia notablemente la experiencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los dos casos ejemplificados son solo algunas variantes de A2A.  Strands habilita capacidades que dejo anotadas para &lt;strong&gt;futuras pruebas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agentes remotos dentro de patrones multiagente&lt;/strong&gt;: un &lt;code&gt;A2AAgent&lt;/code&gt; puede ser un nodo más en un &lt;em&gt;Graph workflow&lt;/em&gt;, mezclando agentes locales y remotos en un mismo pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interrupciones (input_required)&lt;/strong&gt;: Similar a "human-in-the-loop", un agente remoto puede pausar una tarea y esperar una aprobación antes de continuar (por ejemplo, confirmar una reserva), y reanudar exactamente donde quedó.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistencia de conversaciones&lt;/strong&gt;: con un &lt;code&gt;SessionManager&lt;/code&gt; por &lt;code&gt;context_id&lt;/code&gt;, cada conversación puede sobrevivir al reinicio del proceso en lugar de vivir solo en memoria.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notificaciones push y despliegue distribuido&lt;/strong&gt;: para tareas largas y para exponer cada agente productivamente (por ejemplo, detrás de balanceadores, en AWS Fargate o vía AgentCore), escalando el sistema más allá de una notebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilizar MCP de manera complementaria&lt;/strong&gt;: Cada especialista puede usar MCP para sus tools y A2A para colaborar con otros. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Por último, aquí construyo los agentes en Python con Strands, pero como A2A es un estándar abierto (hoy bajo la Linux Foundation) y viaja sobre HTTP/JSON-RPC, la interoperabilidad ocurre a nivel del protocolo, no del framework. Un agente Strands puede comunicarse con agentes construidos en otros frameworks y lenguajes que también implementen A2A. El protocolo tiene SDKs oficiales en Python, JavaScript, Java, C#/.NET, Go y Rust, sin que ninguno conozca la lógica interna del otro.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recursos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Curso Fundamentos Strands&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://skillbuilder.aws/learn/CFBFZNS11J/building-ai-agent-harnesses-with-strands-agents/D6YG8S6N7M" rel="noopener noreferrer"&gt;Building AI Agent Harnesses with Strands Agents&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentación Strands Agents&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/evaluators/" rel="noopener noreferrer"&gt;Evals&lt;/a&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/multi-agent/agent-to-agent/" rel="noopener noreferrer"&gt;Agent-to-Agent (A2A) Protocol&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protocolo A2A (Agent2Agent)&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://a2a-protocol.org/latest/" rel="noopener noreferrer"&gt;Sitio oficial del protocolo A2A&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>strandsagents</category>
      <category>ai</category>
      <category>multiagents</category>
    </item>
    <item>
      <title>Workflow en Strands: un DAG de tareas ejecutado como herramienta única</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Wed, 19 Aug 2026 21:31:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/workflow-en-strands-un-dag-de-tareas-ejecutado-como-herramienta-unica-2a43</link>
      <guid>https://dev.to/aws-builders/workflow-en-strands-un-dag-de-tareas-ejecutado-como-herramienta-unica-2a43</guid>
      <description>&lt;p&gt;En estas notas te explico cómo orquestar agentes con la arquitectura Workflow de Strands Agents utilizando paralelismo y join points, ejemplificado con la construcción de un entregable para preparar y gestionar reuniones como Solutions Architect. Testado de dos maneras: con Gemma4 + Ollama (local) y con la API de Gemini.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;La idea principal es experimentar con el patrón de orquestación workflow sin gastar en suscripciones pagas de tokens, dejando el deploy en AWS para otra instancia. Durante el desarrollo y testing me encontré con algunos inconvenientes de features no implementadas y descubrí proveedores de modelos soportados al día de hoy. &lt;/p&gt;

&lt;p&gt;🗒 &lt;strong&gt;El caso de uso&lt;/strong&gt; que elegí para representar el patrón de orquestación &lt;em&gt;&lt;strong&gt;Workflow&lt;/strong&gt;&lt;/em&gt; es un copiloto para Solutions Architects que prepara y cierra discovery meetings con clientes: investiga la empresa, mapea a los stakeholders, genera preguntas de discovery fundamentadas en ese contexto, resume la reunión una vez ocurrida, y sintetiza todo en un entregable de seguimiento (email + próximos pasos + gaps a resolver).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;La idea del diseño se sacó del &lt;a href="https://partyrock.aws/u/jfescott/CkWsGSARh/Prepare-and-Manage-meeting-as-a-Solution-Architect" rel="noopener noreferrer"&gt;playground de PartyRock&lt;/a&gt; de Jeff Escott&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En las secciones siguientes cubro los conceptos claves del patrón, el diseño e implementación del caso de uso, los hallazgos reales de ejecución con ambos modelos, y las evaluaciones con &lt;code&gt;strands-agents-evals&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conceptos Claves
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ➡ Workflow de Agentes es un patrón de orquestación coordinado en una secuencia definida.
&lt;/h3&gt;

&lt;p&gt;Cada Agente realiza una serie de tareas definidas. El desarrollador puede descomponer tareas complejas en componentes manejables y distribuirlos entre agentes especializados. Cada workflow tiene un control explícito del &lt;strong&gt;orden de ejecución de las tareas&lt;/strong&gt;, &lt;strong&gt;las dependencias&lt;/strong&gt; y  &lt;strong&gt;el flujo de información entre tareas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Workflow es apropiado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuando se tiene un proceso complejo pero repetible que se desea encapsular en una herramienta única, confiable y reutilizable, un &lt;code&gt;Workflow&lt;/code&gt;es un grafo de tareas definido por el desarrollador que un agente puede ejecutar como una acción única y potente.&lt;/li&gt;
&lt;li&gt;Si tenemos complejidad en los procesos con múltiples pasos, resolución de tareas con distintas etapas secuenciales.&lt;/li&gt;
&lt;li&gt;Procesos que requieren subagentes especializados en cada etapa o un seguimiento detallado en cada paso&lt;/li&gt;
&lt;li&gt;Ejecutar tareas independientes en paralelo, mientras hay gestión de independencias. De esta manera optimizamos recursos.&lt;/li&gt;
&lt;li&gt;Si tenemos que &lt;strong&gt;reintentar pasos por error&lt;/strong&gt; tareas especificas sin reiniciar todo el flujo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para clarificar las diferencias entre patrones de orquestación, te muestro una comparativa entre workflow, graph y swarm. La mayor diferencia a considerar es &lt;strong&gt;cómo se determina la ruta de ejecución&lt;/strong&gt; :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Graph&lt;/th&gt;
&lt;th&gt;Swarm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Concepto fundamental&lt;/td&gt;
&lt;td&gt;Un grafo de tareas predefinido (DAG) ejecutado como una herramienta única y no conversacional.&lt;/td&gt;
&lt;td&gt;Un flowchart estructurado y definido por el desarrollador, donde un agente decide qué camino tomar.&lt;/td&gt;
&lt;td&gt;Un equipo dinámico y colaborativo de agentes que se pasan las tareas de forma autónoma.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estructura&lt;/td&gt;
&lt;td&gt;Un desarrollador define todas las tareas y sus dependencias en el código.&lt;/td&gt;
&lt;td&gt;Un desarrollador define todos los nodos (agentes) y aristas (transiciones) con antelación.&lt;/td&gt;
&lt;td&gt;Un desarrollador proporciona un pool de agentes. Los propios agentes deciden el camino.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flujo de ejecución&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Determinista y paralelo&lt;/strong&gt;. El flujo está determinado por el grafo de dependencias. Las tareas independientes se ejecutan en paralelo.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Controlado pero dinámico&lt;/strong&gt;. El flujo sigue los bordes del grafo, pero la decisión de un LLM en cada nodo determina el camino (path).&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Secuencial y autónomo&lt;/strong&gt;. Un agente ejecuta una tarea y luego cede el control a otro agente adecuado en el enjambre.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Permite ciclo?&lt;/td&gt;
&lt;td&gt;NO.&lt;/td&gt;
&lt;td&gt;SÍ.&lt;/td&gt;
&lt;td&gt;SÍ.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mecanismos de compartir estados&lt;/td&gt;
&lt;td&gt;La herramienta captura automáticamente los outputs de las tareas y los pasa como inputs a las tareas dependientes.&lt;/td&gt;
&lt;td&gt;Se pasa un objeto de estado compartido a todos los agentes, quienes pueden leerlo y modificarlo libremente.&lt;/td&gt;
&lt;td&gt;Todos los agentes disponen de un contexto compartido o memoria de trabajo, que contiene la solicitud original, el historial de tareas y el conocimiento de los agentes anteriores.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historial de conversaciones&lt;/td&gt;
&lt;td&gt;Contexto específico de la tarea. Una tarea recibe un resumen curado de los resultados relevantes de sus dependencias, no el historial completo.&lt;/td&gt;
&lt;td&gt;Transcripción completa. Todo el historial de diálogo forma parte del estado compartido, lo que proporciona a cada agente un contexto completo y abierto.&lt;/td&gt;
&lt;td&gt;Transcripción compartida. El contexto compartido proporciona un historial completo de los handoffs de agentes y el conocimiento aportado por agentes anteriores, disponible para el agente actual.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control del comportamiento&lt;/td&gt;
&lt;td&gt;La solicitud del usuario puede activar un workflow predefinido, pero no puede alterar su estructura interna.&lt;/td&gt;
&lt;td&gt;La información que introduce el usuario en cada paso puede influir directamente en la ruta que seguirá el grafo a continuación.&lt;/td&gt;
&lt;td&gt;La indicación inicial del usuario define el objetivo, pero a partir de ahí el Swarm corre de forma autónoma.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalabilidad&lt;/td&gt;
&lt;td&gt;Escala bien en operaciones repetibles y complejas.&lt;/td&gt;
&lt;td&gt;Escala bien en procesos complejos(muchas ramificaciones, condiciones).&lt;/td&gt;
&lt;td&gt;Escala en función del número de agentes especializados en el equipo y de la complejidad de la tarea colaborativa.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manejo de errores&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Sistémico&lt;/strong&gt;. Un fallo en una tarea detendrá todas las tareas dependientes posteriores. Es probable que todo el workflow entre en estado &lt;code&gt;failed&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Controlable&lt;/strong&gt;. Un desarrollador puede definir aristas de "error" explícitas para dirigir el flow a un nodo de error-handling si falla un paso.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Agent-driven&lt;/strong&gt;. Un agente puede decidir transferir la tarea a un especialista en manejo de errores. El sistema se basa en tiempos de espera y límites de transferencia para evitar bucles infinitos.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ➡ Conceptos genéricos de Workflow agénticos
&lt;/h3&gt;

&lt;p&gt;Para la implementación del caso de uso, solo usé la &lt;code&gt;workflow tool&lt;/code&gt;, una de las ~45 tools nativas disponibles en &lt;code&gt;strands-agents-tools&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Componentes claves en la arquitectura:&lt;/p&gt;

&lt;p&gt;⭐&lt;strong&gt;Definición y distribución de tareas.&lt;/strong&gt; Cada tarea necesita una descripción especifica de qué debe lograr el agente, una asignación que haga  matching con el agente adecuado según sus capacidades, y un nivel de prioridad que determine qué se ejecuta primero cuando hay margen de elección.&lt;/p&gt;

&lt;p&gt;⭐&lt;strong&gt;Gestión de dependencias.&lt;/strong&gt; Algunas tareas deben ejecutarse en un orden específico (dependencias secuenciales), mientras que las tareas independientes pueden correr en simultáneo (ejecución paralela). En la ejecución paralela tener en cuenta que el modelo de IA que se utilice debe permitirlo.&lt;br&gt;
Los &lt;em&gt;join points&lt;/em&gt; son los puntos donde varios caminos paralelos convergen antes de continuar. En el caso de uso elegido, los dos join points son &lt;code&gt;discovery_questions&lt;/code&gt; (espera a &lt;code&gt;company_research&lt;/code&gt; + &lt;code&gt;stakeholder_mapping&lt;/code&gt;) y &lt;code&gt;follow_up_actions&lt;/code&gt; (espera a &lt;code&gt;discovery_questions&lt;/code&gt; + &lt;code&gt;meeting_summary&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;⭐&lt;strong&gt;Flujo de información.&lt;/strong&gt; El mapeo de input/output conecta la salida de un agente con la entrada de otro, la preservación de contexto mantiene la información relevante a lo largo de todo el workflow, y la gestión de estado permite trackear el progreso general del workflow.&lt;/p&gt;

&lt;p&gt;Parámetros básicos del workflow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;action&lt;/code&gt; → create, start, status (indica si hubo error o no), list, delete.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;workflow_id&lt;/code&gt; → identificador único del workflow.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tasks&lt;/code&gt; → lista de tareas con sus correspondientes propiedades&lt;/p&gt;

&lt;p&gt;Algunas propiedades dentro de cada task:&lt;br&gt;
&lt;code&gt;task_id&lt;/code&gt; : Obligatorio. Identificador o nombre de la tarea.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;description&lt;/code&gt;: Obligatorio. se completa con un resumen de lo que hace la tarea, es un pequeño prompt.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;: Opcional. Encadenas una tarea con la otra para que no se ejecute si las dependencias aún no culminaron.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;system_prompt&lt;/code&gt;:  Obligatorio.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;priority&lt;/code&gt;: Propiedad numérica que indica prioridad en la ejecución por el modelo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tools&lt;/code&gt;: Opcional. Pero si no lo especificás puede que en la inferencia el modelo decida usar cualquier tool nativa y entre en un loop infinito. &lt;br&gt;
Si no querés que la tarea ejecute ninguna tool se puede especificar cómo &lt;code&gt;"tools": NO_TOOLS&lt;/code&gt;, esto no es un valor oficial del SDK, es un workaround verificado empíricamente: se pasa un nombre de tool que no existe en el registry, forzando al sub-agente a correr sin tools. Pasar &lt;code&gt;"tools": []&lt;/code&gt; no funciona porque es falsy en Python y el workflow lo ignora heredando todo el toolset del agente padre.&lt;/p&gt;
&lt;h3&gt;
  
  
  ➡ Topologías de Secuencias y Workflow
&lt;/h3&gt;

&lt;p&gt;En Strands Agents, "Workflow" puede referirse a dos cosas distintas: una arquitectura secuencial que se implementa manualmente encadenando agentes en código Python, o la &lt;em&gt;Workflow Tool&lt;/em&gt; (&lt;code&gt;strands-agents-tools&lt;/code&gt;), que gestiona dependencias, paralelismo y pasaje de contexto automáticamente. &lt;/p&gt;
&lt;h4&gt;
  
  
  Workflow Secuencial
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrsp016dpxtgsavpi0au.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrsp016dpxtgsavpi0au.jpg" alt="Workflow-secuencial" width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En la topología secuencial, cada agente es un objeto Python independiente y el desarrollador conecta las salidas manualmente. Es la forma más simple de encadenar agentes, pero no hay gestión automática de dependencias ni paralelismo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="c1"&gt;# Create specialized agents
&lt;/span&gt;&lt;span class="n"&gt;agent1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are Agent 1. Perform the first step.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are Agent 2. Perform the second step.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are Agent 3. Perform the final step.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Sequential workflow processing
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;step1_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;step2_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step1_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;final_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step2_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;final_result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Workflow Tool
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmzes5l1g4p3qcra6i1s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmzes5l1g4p3qcra6i1s.jpg" alt="Workflow-tool" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La &lt;em&gt;Workflow Tool&lt;/em&gt; gestiona dependencias, paralelismo y pasaje de contexto automáticamente. El desarrollador define la estructura (tasks + dependencies) y la tool se encarga del resto: qué corre en paralelo, qué espera, y qué contexto inyecta a cada tarea dependiente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands_tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;

&lt;span class="c1"&gt;# Create an agent with workflow capability
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Create a multi-agent workflow
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generic_workflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description of the first task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You perform the first task.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description of the second task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You perform the second task.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description of the third task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You perform the third task.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Execute workflow (parallel processing where possible)
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generic_workflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Check results
&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generic_workflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ➡ Evaluaciones (Evals)
&lt;/h3&gt;

&lt;p&gt;Strands cuenta con una &lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/quickstart/" rel="noopener noreferrer"&gt;marco oficial&lt;/a&gt; para realizar evaluaciones de resultados simples hasta el análisis de interacciones de multiagentes complejos. &lt;/p&gt;

&lt;p&gt;Para evaluar el workflow usé &lt;code&gt;strands-agents-evals&lt;/code&gt; con el mismo provider configurado en &lt;code&gt;.env&lt;/code&gt; (Gemini u Ollama) como juez. En producción la práctica estándar es usar un modelo más capaz como judge (ej. el workflow corre con &lt;code&gt;gemini-2.0-flash&lt;/code&gt; y el judge usa &lt;code&gt;gemini-2.5-pro&lt;/code&gt;), pero para este proyecto educativo se unifica por simplicidad.&lt;/p&gt;

&lt;p&gt;Combiné dos tipos de evaluación:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM-as-a-judge&lt;/strong&gt; — el modelo evalúa la calidad del output final:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluador&lt;/th&gt;
&lt;th&gt;¿Qué evalúa?&lt;/th&gt;
&lt;th&gt;Escala&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OutputEvaluator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifica que &lt;code&gt;follow_up_actions&lt;/code&gt; tenga las 3 secciones obligatorias (email, next steps, gaps) y que el contenido esté fundamentado en los datos reales de la reunión.&lt;/td&gt;
&lt;td&gt;0 / 0.5 / 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HelpfulnessEvaluator&lt;/code&gt;(1)&lt;/td&gt;
&lt;td&gt;Evalúa si el entregable final es útil para un Solutions Architect — no solo si tiene el formato correcto. (Es una instancia de &lt;em&gt;OutputEvaluator&lt;/em&gt; con rubric propio, no una clase separada del SDK!)&lt;/td&gt;
&lt;td&gt;0 / 0.5 / 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(1)&lt;code&gt;HelpfulnessEvaluator&lt;/code&gt; del SDK requiere trazas OpenTelemetry que la Workflow Tool no provee. Se reemplazó por un segundo &lt;code&gt;OutputEvaluator&lt;/code&gt; con rubric de helpfulness.&lt;br&gt;
Cuando usé &lt;code&gt;HelpfulnessEvaluator&lt;/code&gt; del SDK, el score devolvió 0.0 con el error: "Trace parsing requires actual_trajectory to be a Session object, got NoneType". El evaluador espera un objeto Session de Strands (que debe contener trazas OpenTelemetry de la ejecución del agente), no solo el texto del output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Métricas deterministas&lt;/strong&gt; — evalúan propiedades estructurales del patrón, sin llamadas a LLM:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;¿Qué mide?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;task_completion_rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fracción de tasks que completaron con &lt;code&gt;status="completed"&lt;/code&gt; en el JSON persistido.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;join_point_coverage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifica que ambos join points produjeron output no vacío, confirma que la inyección de contexto funcionó.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;required_sections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check determinista de las 3 secciones del entregable (email, next steps, gaps).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stakeholder_coverage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verifica que cada stakeholder del input aparece en &lt;code&gt;discovery_questions&lt;/code&gt;, confirma que el contexto de &lt;code&gt;stakeholder_mapping&lt;/code&gt; llegó al join point #1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyword_presence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Presencia de keywords de dominio (con soporte de sinónimos via `\&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Descripción de la solución
&lt;/h2&gt;

&lt;p&gt;📦 &lt;strong&gt;Repositorio GitHub:&lt;/strong&gt;  &lt;a href="https://github.com/reinalau/strands-workflow" rel="noopener noreferrer"&gt;github.com/reinalau/strands-workflow&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb9dsyquvbfspszs1fat.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb9dsyquvbfspszs1fat.jpg" alt="Workflow copiloto para Solutions Architects" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Para la prueba de concepto utilicé dos opciones locales de ejecución que permitió analizar sus resultados, pero se puede utilizar solo una:&lt;br&gt;
a. Ollama + el pequeño modelo &lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt; (que pesa poco mas de 4gb) ejecutando en Docker. &lt;br&gt;
En caso que quieras ejecutar realmente las task que son independientes en paralelo, hay que indicarselo a ollama (sino el comportamiento por default es secuencial) :&lt;br&gt;
{% raw %}&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the Ollama server with a persistent volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="nt"&gt;-p&lt;/span&gt; 11434:11434 &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama ollama/ollama

&lt;span class="c"&gt;# Start the Ollama server en parallel&lt;/span&gt;
&lt;span class="c"&gt;# docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama -e OLLAMA_NUM_PARALLEL=3  ollama/ollama&lt;/span&gt;

&lt;span class="c"&gt;# Download the model&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull gemma4:e2b-it-qat

&lt;span class="c"&gt;# Test that the model responds&lt;/span&gt;
&lt;span class="c"&gt;# If the container has already been created and &lt;/span&gt;
&lt;span class="c"&gt;# is currently stopped, simply use: docker start ollama&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama run gemma4:e2b-it-qat

&lt;span class="c"&gt;# Verify that the model is running&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;b. Api de gemini. La api key se puede generar de manera gratuita desde &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;aquí&lt;/a&gt; y utilizar algunos de estos modelos (experimentar con los que te permita):&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini-2.5-flash-lite
gemini-2.5-flash
gemini-3.5-flash
gemini-3.5-flash-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;El código está en Python&lt;/strong&gt; y la estructura del proyecto se diseñó de manera que sea más explicativo su lógica, todo el detalle lo encontrás en el &lt;code&gt;Readme.md&lt;/code&gt; del proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-workflow/
├── README.md
├── requirements.txt                  # strands-agents[litellm,ollama], strands-agents-tools, pydantic, pytest
├── .env.example                      # MODEL_PROVIDER, GEMINI_API_KEY, OLLAMA_MODEL_ID
├── .gitignore
├── pytest.ini                        # pythonpath=. so tests/ and evals/ resolve src./config imports
│
├── src/
│   ├── __init__.py
│   ├── config.py                     # get_model_config(): single global provider (Gemini or Ollama) via MODEL_PROVIDER env var
│   ├── main.py                       # Entry point: builds tasks, creates+starts the workflow, assembles the final .md
│   │
│   ├── agents/
│   │   ├── __init__.py
│   │   └── prompts.py                # System prompts per task_id (company_research, stakeholder_mapping, etc.)
│   │
│   ├── workflow/
│   │   ├── __init__.py
│   │   ├── builder.py                # build_tasks(): builds the task list — task_id, dependencies, priority
│   │   ├── deliverable.py            # Reads the persisted workflow JSON and assembles the final .md
│   │   └── instrumentation.py        # Timing hook: logs START/END + duration per task
│   │
│   ├── models/
│   │   └── schemas.py                # Pydantic: DiscoveryMeetingInput
│   │
│   └── utils/
│       ├── logging_config.py         # Tees stdout+stderr to logs/, optional DEBUG level
│       └── report_export.py          # Writes the final .md to outputs/
│
├── tests/
│   ├── __init__.py
│   ├── test_config.py                # get_model_config(): provider resolution + error if API key is missing
│   ├── test_workflow_structure.py    # Validates the workflow is well-formed: no cycles, existing dependencies, correct join points
│   ├── test_schemas.py               # Pydantic validation of DiscoveryMeetingInput
│   ├── test_report_export.py         # Generates the .md from a mocked workflow JSON
│   └── test_instrumentation.py       # Verifies install_task_timing_hooks() logs START/END with duration, without calling a real LLM
│
├── examples/
│   └── sample_input.json             # Sample input to run the workflow quickly
│
├── evals/
│   ├── __init__.py
│   ├── evaluate_workflow.py          # strands-evals runner: OutputEvaluator + HelpfulnessEvaluator + deterministic metrics
│   ├── metrics.py                    # Domain metrics: task completion rate, join point coverage, required sections, stakeholder coverage, keyword presence
│   └── test_cases/
│       └── workflow_eval_cases.json  # Input + expected properties per eval case
│
├── logs/
│   └── .gitkeep                      # logs/run_&amp;lt;timestamp&amp;gt;.log, gitignored otherwise
│
└── outputs/
    └── .gitkeep                      # .md generated per run, gitignored otherwise

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Ejecución Local
&lt;/h3&gt;

&lt;p&gt;Una vez que tengas el repositorio del código fuente clonado y el docker de ollama con el modelo gemma 4 o la api key de gemini generada, pasamos armar el ambiente.&lt;/p&gt;

&lt;p&gt;Los requerimientos que vas a encontrar en  &lt;code&gt;requirements.txt&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-agents[litellm,ollama]==1.52.0
strands-agents-tools==0.8.6
ollama==0.5.1
pydantic==2.13.4
pytest==8.3.4
strands-agents-evals&amp;gt;=1.0.0
python-dotenv==1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En &lt;code&gt;.env&lt;/code&gt; se necesitan estos valores. Notar que depende el modelo que elegimos podemos intercambiar el &lt;code&gt;MODEL_PROVIDER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Select ONE provider: "ollama" or "gemini"
MODEL_PROVIDER=ollama

# --- Gemini (only required if MODEL_PROVIDER=gemini) ---
# Free tier key: https://aistudio.google.com/apikey
GEMINI_API_KEY=YourApiKey
GEMINI_MODEL_ID=gemini-3.5-flash-lite

# --- Ollama (only used if MODEL_PROVIDER=ollama) ---
# Must have the Ollama daemon running locally: https://ollama.com
# Pull the model first: ollama pull gemma4:e2b-it-qat
OLLAMA_MODEL_ID=gemma4:e2b-it-qat
OLLAMA_HOST=http://localhost:11434

# --- Workflow persistence ---
# Redirects strands_tools' internal workflow state JSON into logs/ instead of
# the default ~/.strands/workflows/ — keeps everything from one run together.
STRANDS_WORKFLOW_DIR=logs/workflows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;En &lt;code&gt;src/config.py&lt;/code&gt;&lt;/strong&gt; está la configuración de ejecución, en este caso de uso utilicé parámetros como la &lt;code&gt;temperature&lt;/code&gt; y  &lt;code&gt;max_tokens&lt;/code&gt; (limita los tokens que pueden gastar los agentes). &lt;br&gt;
→ Si optas  por ejecutar con el provider &lt;em&gt;ollama&lt;/em&gt; hay que tener en cuenta el valor de la ventana de contexto: &lt;code&gt;num_ctx&lt;/code&gt;.&lt;br&gt;
→ Si optas por ejecutar con la apikey de Gemini, en el SDK de Strands, el dispatcher &lt;code&gt;create_model&lt;/code&gt; que usa &lt;code&gt;workflow&lt;/code&gt;internamente soporta estos modelos solo vía &lt;strong&gt;litellm&lt;/strong&gt; (&lt;code&gt;"model_provider": "litellm"&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Las pruebas&lt;/strong&gt; están organizadas en &lt;code&gt;tests/&lt;/code&gt; en &lt;strong&gt;un solo nivel: ejecución rápida y determinista&lt;/strong&gt;. Validan la estructura del workflow (sin ciclos, dependencias existentes, join points correctos), el schema del input con Pydantic, la exportación del &lt;code&gt;.md&lt;/code&gt; desde un JSON mockeado, y los hooks de timing (sin llamadas al LLM).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -m pytest
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ejecución del Workflow principal.&lt;/strong&gt; En &lt;code&gt;examples/sample_input.txt&lt;/code&gt;está el input de ejemplo en un formato json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"company_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"known_challenges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"stakeholders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"raw_meeting_notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se puede ejecutar contra el modelo ollama local o vía api de gemini comentado más arriba.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎯 &lt;strong&gt;Un bug real&lt;/strong&gt;: durante las pruebas descubrí que usando Gemini con paralelismo real puede hacer crashear el proceso de forma intermitente (y no es un problema de Windows, ocurre también en Linux). La causa es un bug conocido y abierto en LiteLLM: el cliente HTTP que usa para el streaming de Gemini es un singleton global que no distingue entre los distintos &lt;em&gt;event loops&lt;/em&gt; que crea cada tarea del workflow al correr en paralelo. &lt;br&gt;
&lt;strong&gt;Solución aplicada:&lt;/strong&gt; limitar la ejecución a un hilo por vez cuando el proveedor es Gemini, vía la env var &lt;code&gt;STRANDS_WORKFLOW_MAX_THREADS=1&lt;/code&gt; de Strands. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El log&lt;/strong&gt; de las llamadas del workflow está en &lt;code&gt;logs/&lt;/code&gt;. Contiene la data cruda que permite verificar si hubo paralelismo, cuanto tiempo tarda cada task, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El resultado final&lt;/strong&gt; &lt;br&gt;
Es un  documento markdown con diferentes secciones como:&lt;br&gt;
Company Research - Stakeholder Mapping - Meeting Summary - Discovery Questions - Follow-up Deliverable and Gap&lt;br&gt;
 Se guarda en &lt;code&gt;outputs/discovery_meeting_yyyymmdd_hhmmss.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por último,  hice una evaluación de pruebas sobre el workflow multiagente real con el concepto de &lt;em&gt;"Evals".&lt;/em&gt;&lt;/strong&gt; Ejecuta el workflow completo contra el modelo configurado, corre los evaluadores LLM-as-judge y las métricas deterministas, muestra los resultados en consola y guarda el reporte completo en &lt;code&gt;outputs/eval_report_&amp;lt;timestamp&amp;gt;.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python  &lt;span class="nt"&gt;-m&lt;/span&gt;  evals.evaluate_workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📝 &lt;strong&gt;Nota:&lt;/strong&gt; La explicación del código fuente y paso a paso de la ejecución se encuentra detallado en el &lt;code&gt;README.md&lt;/code&gt; en  &lt;a href="https://github.com/reinalau/strands-workflow" rel="noopener noreferrer"&gt;github&lt;/a&gt; .&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones y lecciones aprendidas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;El patrón Workflow en Strands resuelve en lineas generales lo que indica su documentación.&lt;/strong&gt; Dependency resolution, paralelismo real y context passing automático entre join points funcionaron correctamente y sin código extra de orquestación. El DAG se ejecuta en el orden correcto y los join points reciben el contexto inyectado de sus dependencias sin intervención manual.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La Workflow Tool de Strands está en maduración activa.&lt;/strong&gt; &lt;code&gt;pause&lt;/code&gt;/&lt;code&gt;resume&lt;/code&gt; están documentados como &lt;em&gt;Advanced Features&lt;/em&gt; pero no implementados — al ejecutarlos devuelven &lt;code&gt;"🚧 Action '...' is not yet implemented"&lt;/code&gt;, verificado contra el código fuente instalado. El workflow puede quedar colgado indefinidamente si una task termina en &lt;code&gt;error&lt;/code&gt; (el loop interno gira para siempre esperando dependientes que nunca se vuelven "ready"). El resultado de cada task no se expone via API... hay que leer el JSON persistido directamente. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Algunos comportamientos requieren workarounds no oficiales.&lt;/strong&gt; La herencia de tools en sub-agentes no está documentada. Por defecto cada sub-agente hereda todo el toolset del padre, incluida la &lt;code&gt;workflow&lt;/code&gt; tool, lo que con modelos agénticos lleva a llamadas recursivas reales. &lt;code&gt;"tools": []&lt;/code&gt; (lista vacía) no funciona como restricción porque es falsy en Python — la solución es &lt;code&gt;NO_TOOLS&lt;/code&gt; (&lt;code&gt;["__no_tools__"]&lt;/code&gt;), un placeholder que fuerza &lt;code&gt;filtered_tools&lt;/code&gt; a quedar vacío. No encontré hooks públicos para observar timing por task, se requiere monkeypatch sobre &lt;code&gt;WorkflowManager.execute_task&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Los hooks de Strands tienen un mecanismo &lt;code&gt;LimitToolCounts&lt;/code&gt;&lt;/strong&gt; (via &lt;code&gt;BeforeToolCallEvent&lt;/code&gt;) que permite cancelar una tool cuando se invoca más veces de lo permitido, útil como guardrail contra loops. Sin embargo, no aplica al bug de herencia de la Workflow Tool, ese mecanismo actúa sobre el agente padre, pero los sub-agentes creados internamente por el framework son instancias separadas sobre las que no hay control para registrar hooks desde afuera. Por eso &lt;code&gt;"tools": NO_TOOLS&lt;/code&gt; sigue siendo la solución correcta.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Concurrencia del workflow tool con Gemini.&lt;/strong&gt; Cuando hay tasks en paralelo + LiteLLM/aiohttp, se produce un &lt;code&gt;RuntimeError: Task &amp;lt;...&amp;gt; got Future &amp;lt;...&amp;gt; attached to a different loop&lt;/code&gt; intermitente. Cada task corre en su propio thread con su propio &lt;code&gt;asyncio.run()&lt;/code&gt;, y el cierre del connector de aiohttp cae en el loop equivocado. Se solucionó colocando una variables de entorno de Strands &lt;code&gt;STRANDS_WORKFLOW_MAX_THREADS=1&lt;/code&gt;, limitando la concurrencia. (esto no sucede con Ollama-Gemma4 via Docker).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Recursos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Curso Fundamentos (hacelos!)&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://skillbuilder.aws/learn/CFBFZNS11J/building-ai-agent-harnesses-with-strands-agents/D6YG8S6N7M" rel="noopener noreferrer"&gt;Building AI Agent Harnesses with Strands Agents&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentación Strands Agents&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/multi-agent/workflow/" rel="noopener noreferrer"&gt;Agent Workflows&lt;/a&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/evaluators/" rel="noopener noreferrer"&gt;Evals&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Otros&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3Ecodd9aFqsG1SAkQKmqZ2KxPiP/5-multi-agent-patterns-in-strands-agents-which-one-and-when" rel="noopener noreferrer"&gt;5 Multi-Agent Patterns in Strands&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>strandagents</category>
      <category>python</category>
      <category>agents</category>
    </item>
    <item>
      <title>Graph en Strands: Parallel + Aggregation y Feedback Loop para construir Knowledge Graphs</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:19:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/graph-en-strands-parallel-aggregation-y-feedback-loop-para-construir-knowledge-graphs-22n0</link>
      <guid>https://dev.to/aws-builders/graph-en-strands-parallel-aggregation-y-feedback-loop-para-construir-knowledge-graphs-22n0</guid>
      <description>&lt;p&gt;Siguiendo con la serie de artículos sobre la orquestación de multiagentes en Strands, le toca el turno a &lt;em&gt;Graph Multi-Agent Pattern&lt;/em&gt;, ejemplificado con la construcción de un knowledge graph en base a un input de texto (abstract), donde los nodos de un grafo determinista tienen las responsabilidades divididas para lograrlo. Se probó de dos maneras: con Gemma4 + Ollama y con la API de Gemini en su capa gratuita.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;La información del presente artículo la podés seguir teniendo alguna noción sobre agentes y grafos. Pero te propongo hacer un curso de fundamentos del framework &lt;em&gt;Strands Agents&lt;/em&gt;, para que en futuros proyectos puedas dar mayor complejidad a tus agentes. La idea principal es experimentar con el patrón sin gastar en suscripciones pagas de tokens, dejando la implementación en AWS para otra instancia. &lt;/p&gt;

&lt;p&gt;✳ &lt;strong&gt;El caso de uso&lt;/strong&gt; que elegí para representar el patrón &lt;strong&gt;&lt;em&gt;Graph&lt;/em&gt;&lt;/strong&gt; es &lt;strong&gt;la Extracción de entidades/relaciones para formar un &lt;em&gt;Knowledge Graph&lt;/em&gt; con validación&lt;/strong&gt;.  Se me ocurrió hacer algo útil para la comunidad académica, un mecanismo de extracción de conocimiento que sea capaz de tomar parte de un artículo científico e ingestar lo más importante en un documento json, que pueda ser legible por humanos y sistemas. La solución es un pipeline de construcción de &lt;em&gt;knowledge graph&lt;/em&gt; a partir de un texto (ej. un abstract), usando la topología &lt;strong&gt;Parallel Processing→ with Aggregation→ Feedback Loop&lt;/strong&gt;.&lt;br&gt;
El diseño es una versión reducida de arquitecturas multi-agente real para enriquecimiento de &lt;em&gt;knowledge graph&lt;/em&gt; descritos en:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.researchgate.net/publication/389031530_Revolutionizing_Knowledge_Graphs_with_Multi-Agent_Systems_AI-Powered_Construction_Enrichment_and_Applications" rel="noopener noreferrer"&gt;Revolutionizing Knowledge Graphs with Multi-Agent Systems: AI-Powered Construction, Enrichment, and Applications&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Conceptos Claves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;➡ Graph es un patrón de orquestación de agentes determinista&lt;/strong&gt; &lt;br&gt;
El desarrollador define explícitamente la topología de ejecución: qué nodos existen, en qué orden corren, y bajo qué condiciones se transita de uno a otro. Cada nodo es un agente y a diferencia de otros patrones como Swarm, donde el flujo surge de la negociación entre agentes, en Graph el flujo está fijado de antemano como un grafo dirigido (DAG o con ciclos controlados).&lt;/p&gt;

&lt;p&gt;Graph es apropiado cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La estructura de dependencias es conocida de antemano&lt;/strong&gt;: sabés qué pasos necesitan el resultado de otros antes de poder ejecutarse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hay paralelismo real que después converge&lt;/strong&gt;: varios agentes pueden trabajar al mismo tiempo sobre partes independientes del problema, y sus resultados se consolidan en un punto de agregación explícito.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Se necesita revisión con ciclos controlados&lt;/strong&gt;: un resultado puede requerir corrección y volver a un paso anterior (feedback loop), con límites de seguridad (&lt;code&gt;set_max_node_executions&lt;/code&gt;) que garantizan que el proceso termine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La trazabilidad y el control son prioritarios&lt;/strong&gt;: como cada nodo y cada transición son explícitos, es más fácil auditar, debuggear y predecir el comportamiento, es bueno en contextos con validación humana o compliance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;➡ Conceptos genéricos de Grafos Agenticos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El framework propone una interfaz sencilla para la construcción de grafos, con 3 componentes base: &lt;strong&gt;GraphNode&lt;/strong&gt;, &lt;strong&gt;GraphEdge&lt;/strong&gt;, &lt;strong&gt;GraphBuilder&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1.Los nodos representan agentes, nodos personalizados o sistemas multiagente. Ej:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.multiagent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GraphBuilder&lt;/span&gt;

&lt;span class="c1"&gt;# Build the graph
&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GraphBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Add nodes
&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[node_id_1]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[node_id_2]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2.Las aristas definen las dependencias y el flujo de información entre los nodos. Además se puede agregar lógica condicional (no lo utilicé en el caso de uso analizado):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add edges (dependencies)
&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[node_id_1]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[node_id_2]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3.La ejecución sigue la estructura del grafo, respeta dependencias, la salida de un nodo se convierte en la entrada de su dependiente. Y en el caso de que varios nodos tienen aristas hacia un nodo de destino, el comportamiento predeterminado para la ejecución del destino puede variar según el SDK (Python utiliza semánticas &lt;strong&gt;OR&lt;/strong&gt;: el nodo destino se dispara en cuanto &lt;strong&gt;cualquiera&lt;/strong&gt; de sus dependencias completa).&lt;br&gt;
4.Compatibilidad con patrones anidados, un grafo como nodo dentro de otro grafo.&lt;br&gt;
5.&lt;strong&gt;Remote Agents vía A2A&lt;/strong&gt;: Graph soporta agentes remotos como nodos a través de &lt;code&gt;A2AAgent&lt;/code&gt;, para arquitecturas distribuidas donde la orquestación corre localmente pero tareas especializadas se ejecutan en servicios remotos.&lt;br&gt;
6.&lt;strong&gt;Cyclic graph&lt;/strong&gt; permite que un nodo sea revisitado dentro del mismo flujo, con límites de ejecución y gestión de estado.&lt;br&gt;
7.&lt;strong&gt;Custom Node Types&lt;/strong&gt;: Graph permite extender &lt;code&gt;MultiAgentBase&lt;/code&gt; para crear nodos que ejecutan funciones Python deterministas en lugar de llamar a un LLM, útil para lógica de negocio, validaciones y pipelines de procesamiento de datos dentro del mismo grafo.&lt;br&gt;
8.&lt;strong&gt;Graphs as a Tool&lt;/strong&gt;: mediante el paquete &lt;code&gt;strands_tools&lt;/code&gt;, un agente puede recibir el &lt;code&gt;graph&lt;/code&gt; como tool y construir/ejecutar su propia topología de nodos en tiempo real, dejando que el LLM decida dinámicamente la orquestación en lugar de definirla de antemano con &lt;code&gt;GraphBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Topologías comunes&lt;/strong&gt;&lt;br&gt;
Strands documenta 4 topologías base que cubren la mayoría de casos de uso, y se pueden combinar según la complejidad del problema. &lt;strong&gt;Sequential Pipeline&lt;/strong&gt; sirve cuando cada paso depende estrictamente del anterior. &lt;strong&gt;Parallel + Aggregation&lt;/strong&gt; es ideal cuando hay trabajo independiente que puede correr simultáneamente y luego converger en un solo resultado. &lt;strong&gt;Branching Logic&lt;/strong&gt; permite que el flujo tome caminos distintos según el contenido generado en un paso previo. Y &lt;strong&gt;Feedback Loop&lt;/strong&gt; habilita ciclos de revisión con vuelta atrás, para casos donde un resultado necesita validación antes de darse por definitivo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmteh9ei94hn319omma04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmteh9ei94hn319omma04.png" alt="graph_common_topologies" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Evaluaciones (Evals)&lt;/strong&gt;  Strands cuenta con una &lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/quickstart/" rel="noopener noreferrer"&gt;marco oficial&lt;/a&gt; para realizar evaluaciones de resultados simples hasta el análisis de interacciones de multiagentes complejos. &lt;br&gt;
En el caso de uso para la creación de &lt;em&gt;Knowledge Graph&lt;/em&gt;  se realizaron 6 evaluaciones (3 sin utilizar el framework). &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluación / Métrica&lt;/th&gt;
&lt;th&gt;Tipo / Origen&lt;/th&gt;
&lt;th&gt;¿Qué evalúa?&lt;/th&gt;
&lt;th&gt;Escala&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OutputEvaluator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LLM-as-a-Judge (&lt;code&gt;strands-agents-evals&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Verifica que la salida del nodo &lt;em&gt;Publisher&lt;/em&gt; sea un JSON válido (&lt;code&gt;entities&lt;/code&gt; y &lt;code&gt;relations&lt;/code&gt;) y que las relaciones estén fundamentadas fácticamente en el texto original.&lt;/td&gt;
&lt;td&gt;0 / 0.5 / 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SemanticCorrectnessEvaluator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LLM-as-a-Judge (&lt;code&gt;strands-agents-evals&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Compara semánticamente el &lt;em&gt;knowledge graph&lt;/em&gt; generado contra las relaciones esperadas (&lt;em&gt;ground truth&lt;/em&gt;), dando crédito parcial a triples con distinto wording pero igual significado. Implementado como un segundo &lt;code&gt;OutputEvaluator&lt;/code&gt; con rúbrica de referencia, ya que el &lt;code&gt;CorrectnessEvaluator&lt;/code&gt; oficial requiere un objeto &lt;code&gt;Session&lt;/code&gt; de Strands (incompatible con grafos multi-agente).&lt;/td&gt;
&lt;td&gt;0 / 0.5 / 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exact-Match (P / R / F1)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Métrica Determinista (&lt;code&gt;metrics.py&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Mide la coincidencia exacta (normalizada a minúsculas/espacios) entre las triples extraídas y las triples de referencia del caso de prueba.&lt;/td&gt;
&lt;td&gt;0.0 – 1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Loop Iterations Count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Métrica de Topología (&lt;code&gt;metrics.py&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Cuenta cuántas iteraciones de revisión ocurrieron en el &lt;em&gt;feedback loop&lt;/em&gt; entre el &lt;strong&gt;Validator&lt;/strong&gt; y el &lt;strong&gt;Aggregator&lt;/strong&gt; antes de la aprobación final.&lt;/td&gt;
&lt;td&gt;entero ≥ 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bias-Flag Rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Métrica Determinista (&lt;code&gt;metrics.py&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Detecta si la respuesta del nodo &lt;strong&gt;Validator&lt;/strong&gt; levantó alguna objeción o alerta de sesgo/desbalance en las relaciones o entidades analizadas.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compound Entity Check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Métrica Determinista (&lt;code&gt;metrics.py&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Chequeo de regresión: detecta entidades compuestas (ej. "entities and relations") que deberían haberse dividido en dos nodos separados, según la regla en &lt;code&gt;extractor_agent.py&lt;/code&gt;/&lt;code&gt;aggregator_agent.py&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;lista de entidades detectadas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Para clarificar: una  &lt;strong&gt;triple&lt;/strong&gt;  es la unidad básica de información que compone un  &lt;em&gt;&lt;em&gt;Knowledge Graph&lt;/em&gt;&lt;/em&gt;. Representa una afirmación o hecho estructurado combinando tres elementos:&lt;/p&gt;

&lt;p&gt;$$\text{(Sujeto)} \xrightarrow{\quad\text{Relación / Predicado}\quad} \text{(Objeto)}$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Sujeto (&lt;em&gt;source&lt;/em&gt;)&lt;/strong&gt;: La entidad de origen (ej.  &lt;code&gt;"Laura Bolaños"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Relación (&lt;em&gt;relation&lt;/em&gt;)&lt;/strong&gt;: El tipo de vínculo o conector (ej.  &lt;code&gt;"born_in"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Objeto (&lt;em&gt;target&lt;/em&gt;)&lt;/strong&gt;: La entidad de destino (ej.  &lt;code&gt;"Argentina"&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Descripción de la solución
&lt;/h2&gt;

&lt;p&gt;📦 &lt;strong&gt;Repositorio GitHub:&lt;/strong&gt;  &lt;a href="https://github.com/reinalau/strands-graph" rel="noopener noreferrer"&gt;github.com/reinalau/strands-graph&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;⭐ El caso de uso combina dos topologías: &lt;strong&gt;paralelismo con agregación, seguido de un ciclo de feedback antes de publicar el resultado final&lt;/strong&gt;. Notar en la representación que Extractor es un agente que se instancia 3 veces, una única definición de agente, instanciada 3 veces (una por &lt;code&gt;chunk_number&lt;/code&gt;) dentro del loop en &lt;code&gt;builder.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F488r9eq02an0u0emeams.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F488r9eq02an0u0emeams.png" alt="Use Case Graph MultiAgent" width="800" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Para la prueba de concepto utilicé dos opciones locales de ejecución que me permitió analizar sus resultados, pero podes utilizar solo una:&lt;br&gt;
a. Ollama + el pequeño modelo &lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt; (que pesa poco mas de 4gb) ejecutando en Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the Ollama server with a persistent volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="nt"&gt;-p&lt;/span&gt; 11434:11434 &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama ollama/ollama

&lt;span class="c"&gt;# Download the model&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull gemma4:e2b-it-qat

&lt;span class="c"&gt;# Test that the model responds&lt;/span&gt;
&lt;span class="c"&gt;# If the container has already been created and &lt;/span&gt;
&lt;span class="c"&gt;# is currently stopped, simply use: docker start ollama&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama run gemma4:e2b-it-qat

&lt;span class="c"&gt;# Verify that the model is running&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b. Api de gemini. La api key se puede generar de manera gratuita desde &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;aquí&lt;/a&gt; y utilizar al menos estos modelos (experimentar con los que te permita):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini-2.5-flash-lite
gemini-2.5-flash
gemini-3.5-flash
gemini-3.5-flash-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;El código del multiagente Graph está en Python&lt;/strong&gt; y la estructura del proyecto se diseñó de manera que sea más explicativo su lógica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-graph/
├── README.md                        # Project overview, setup, and usage instructions
├── requirements.txt                  # Python dependencies (strands-agents[gemini,ollama], strands-agents-evals, pydantic, etc.)
├── .env.example                      # Template for API keys (GEMINI_API_KEY) and Ollama config
├── .gitignore
│
├── src/
│   ├── __init__.py
│   ├── config.py                     # Model configs: role -&amp;gt; provider mapping (Gemini/Ollama), get_model()
│   ├── main.py                       # Entry point: builds graph, runs on input, prints/exports result, saves run log
│   │
│   ├── agents/
│   │   ├── __init__.py               # Re-exports all create_*_agent() factories
│   │   ├── coordinator_agent.py      # Entry point: splits input text into chunks for parallel extraction
│   │   ├── extractor_agent.py        # Shared extractor logic, instantiated 3x (one per chunk); no compound entities
│   │   ├── aggregator_agent.py       # Conflict resolution: merges, deduplicates, splits any compound entities
│   │   ├── validator_agent.py        # structured_output_model: checks format, consistency, completeness, bias
│   │   └── publisher_agent.py        # Formats and exports the final validated KG
│   │
│   ├── graph/
│   │   ├── __init__.py
│   │   ├── builder.py                # GraphBuilder setup: 7 nodes, edges (incl. aggregator-&amp;gt;publisher), entry point
│   │   └── conditions.py             # Edge conditions: needs_revision, is_approved, all_dependencies_complete
│   │
│   ├── models/
│   │   └── schemas.py                # Pydantic schemas: Relation, KnowledgeGraph, ValidatorOutput
│   │
│   └── utils/
│       ├── logging_config.py         # Debug logging + tee_console_to_file (saves full run output to logs/)
│       └── kg_export.py              # Parses and exports final KG to JSON
│
├── tests/
│   ├── __init__.py
│   ├── test_conditions.py            # Unit tests for all_dependencies_complete, needs_revision, is_approved
│   ├── test_kg_export.py             # Unit tests for parse_publisher_output (valid/invalid JSON, schema mismatch)
│   ├── test_metrics.py               # Unit tests for evals/metrics.py, incl. find_compound_entities regression check
│   ├── test_schemas.py               # Unit tests for Relation/KnowledgeGraph/ValidatorOutput validation
│   └── test_settings.py              # Unit tests for get_model role/provider resolution and missing API key error
│
├── examples/
│   └── sample_input.txt              # Short input text used as the default demo run
│
├── evals/
│   ├── __init__.py
│   ├── test_cases/
│   │   └── kg_extraction_cases.json  # Input texts + expected relations for evaluation
│   ├── evaluators.py                 # Custom Strands Evals SDK evaluators (precision/recall, feedback loop)
│   ├── evaluate_graph.py             # Strands Evals SDK runner: Case/Experiment against test cases
│   └── metrics.py                    # Domain metrics: precision/recall, loop iterations, bias-flag rate, compound entities
│
├── logs/
│   └── .gitkeep                      # Full console output per run (logs/run_&amp;lt;timestamp&amp;gt;.log), gitignored otherwise
│
└── outputs/
    └── .gitkeep                      # Generated KG JSON + eval reports land here, gitignored otherwise

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Ejecución Local
&lt;/h3&gt;

&lt;p&gt;Una vez que tenemos el repositorio del código fuente clonado y el docker de ollama con el modelo gemma 4 o la api key de gemini generada, pasamos armar el ambiente.&lt;/p&gt;

&lt;p&gt;Los requerimientos que vamos a encontrar en  &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
strands-agents[gemini,ollama]&amp;gt;=1.0.0&lt;br&gt;
strands-agents-tools&amp;gt;=0.1.0&lt;br&gt;
strands-agents-evals&lt;br&gt;
python-dotenv&amp;gt;=1.0.0&lt;br&gt;
pydantic&amp;gt;=2.0.0&lt;br&gt;
pytest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En &lt;code&gt;.env&lt;/code&gt; se necesitan estos valores. Notar que depende el modelo que elegimos podemos intercambiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#Gemini (required if any role uses provider="gemini") ---
GEMINI_API_KEY=your_gemini_api_key_here
GEMINI_MODEL_ID=gemini-2.5-flash
#Ollama (required if any role uses provider="ollama") ---
OLLAMA_HOST=http://localhost:11434
OLLAMA_MODEL_ID=gemma4:e2b-it-qat
#Per-role provider overrides (optional) ---
#Values: "gemini" or "ollama". Defaults are set in src/config.py.
MODEL_PROVIDER_COORDINATOR=ollama
MODEL_PROVIDER_EXTRACTOR=ollama
MODEL_PROVIDER_AGGREGATOR=ollama
MODEL_PROVIDER_VALIDATOR=ollama
MODEL_PROVIDER_PUBLISHER=ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;En &lt;code&gt;src/config.py&lt;/code&gt;&lt;/strong&gt; está la configuración de despliegue, en este caso de ejecución local se encuentran parámetros como la &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p,&lt;/code&gt; &lt;code&gt;max_output_tokens&lt;/code&gt; (muy importante porque limita los tokens que pueden gastar los agentes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Las constantes estructurales del grafo a nivel módulo&lt;/strong&gt;, se encuentran en &lt;strong&gt;&lt;code&gt;src/graph/builder.py&lt;/code&gt;&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NUM_EXTRACTORS&lt;/code&gt;&lt;/strong&gt; → cantidad de nodos extractores paralelos (y de chunks que pide el Coordinator). Cambiarlo requiere que coincida con el &lt;code&gt;num_chunks&lt;/code&gt; pasado a &lt;code&gt;create_coordinator_agent()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MAX_NODE_EXECUTIONS&lt;/code&gt;&lt;/strong&gt; → límite global de ejecuciones de nodos en todo el grafo, la salvaguarda contra el feedback loop infinito Validator↔Aggregator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXECUTION_TIMEOUT_SECONDS&lt;/code&gt;&lt;/strong&gt; → timeout global del grafo completo.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;NUM_EXTRACTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;MAX_NODE_EXECUTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="n"&gt;EXECUTION_TIMEOUT_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt; &lt;span class="c1"&gt;# (For local execution, it's advisable to have a high value)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Las pruebas&lt;/strong&gt; están organizadas en &lt;code&gt;tests/&lt;/code&gt; en &lt;strong&gt;un solo nivel: ejecución rápida y determinista&lt;/strong&gt;. Validan la lógica de las funciones de condición de Graph (&lt;code&gt;needs_revision&lt;/code&gt;, &lt;code&gt;is_approved&lt;/code&gt;, &lt;code&gt;all_dependencies_complete&lt;/code&gt;), el parsing/validación de schemas (&lt;code&gt;Relation&lt;/code&gt;, &lt;code&gt;KnowledgeGraph&lt;/code&gt;, &lt;code&gt;ValidatorOutput&lt;/code&gt;).&lt;br&gt;
Se testea, además de los posibles errores del runtime,  la resolución de provider/modelo por rol de &lt;code&gt;src/config.py&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;_resolve_provider()&lt;/code&gt;&lt;/strong&gt; — que cada rol (&lt;code&gt;coordinator&lt;/code&gt;, &lt;code&gt;extractor&lt;/code&gt;, &lt;code&gt;aggregator&lt;/code&gt;, &lt;code&gt;validator&lt;/code&gt;, &lt;code&gt;publisher&lt;/code&gt;) resuelva al provider correcto: el default (&lt;code&gt;ROLE_PROVIDER_DEFAULTS&lt;/code&gt;) cuando no hay override, y la variable de entorno (&lt;code&gt;MODEL_PROVIDER_VALIDATOR=ollama&lt;/code&gt;, etc.) cuando sí la hay.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;get_model()&lt;/code&gt;&lt;/strong&gt; — que devuelva una instancia de &lt;code&gt;OllamaModel&lt;/code&gt; cuando el provider es &lt;code&gt;"ollama"&lt;/code&gt;, y &lt;code&gt;GeminiModel&lt;/code&gt; cuando es &lt;code&gt;"gemini"&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Procedemos a ejecutar el Graph principal.&lt;/strong&gt; En &lt;code&gt;examples/sample_input.txt&lt;/code&gt;está el input de ejemplo.&lt;br&gt;
Se puede ejecutar contra el modelo ollama local o vía api de gemini comentado más arriba.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;El log&lt;/strong&gt; de las llamadas entre el pipeline de agentes se visualiza en la carpeta &lt;code&gt;logs/&lt;/code&gt;. Contiene la data cruda que permite comparar comportamiento del modelo (cuántos turnos internos hace cada uno, cuánto tarda, si usa tool calls, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El resultado final&lt;/strong&gt; &lt;br&gt;
Es un  Knowledge graph y  se guarda en un &lt;code&gt;.json&lt;/code&gt; en&lt;code&gt;outputs/&lt;/code&gt;.&lt;br&gt;
Se visualiza la &lt;strong&gt;representación estructurada&lt;/strong&gt; del texto de entrada, describiendo: qué conceptos existen (&lt;code&gt;entities&lt;/code&gt;) y cómo se relacionan entre sí (&lt;code&gt;relations&lt;/code&gt;), en formato sujeto-predicado-objeto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por último, hacemos una evaluación de pruebas sobre el grafo multiagente real con el concepto de &lt;em&gt;"Evals".&lt;/em&gt;&lt;/strong&gt; Esto es importante para todos los agentes futuros que construyas, tomar la documentación de Strands y descubrir cómo y para qué tenemos que evaluar nuestros agentes. El resultado se guarda en &lt;code&gt;outputs/eval_report.json&lt;/code&gt; (el detalle de las evaluaciones están en "&lt;em&gt;Conceptos Claves&lt;/em&gt;"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python  &lt;span class="nt"&gt;-m&lt;/span&gt;  evals.evaluate_graph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📝 &lt;strong&gt;Nota:&lt;/strong&gt; La explicación del código fuente y paso a paso de la ejecución se encuentra detallado en el &lt;code&gt;README.md&lt;/code&gt; en el repositorio de &lt;a href="https://github.com/reinalau/strands-graph" rel="noopener noreferrer"&gt;github&lt;/a&gt; .&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones y lecciones aprendidas
&lt;/h2&gt;

&lt;p&gt;Construir este pipeline con Graph me dejó varias lecciones que van más allá de la forma de crearlo según su documentación:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Las constantes estructurales del grafo no son de uso de común para todos los modelos.&lt;/strong&gt; &lt;code&gt;NUM_EXTRACTORS&lt;/code&gt;, &lt;code&gt;MAX_NODE_EXECUTIONS&lt;/code&gt; y &lt;code&gt;EXECUTION_TIMEOUT_SECONDS&lt;/code&gt; en &lt;code&gt;builder.py&lt;/code&gt; parecen valores de infraestructura, pero en la práctica están acoplados al comportamiento del modelo elegido. Con Gemma4 local, el cuello de botella fue el tiempo (&lt;code&gt;EXECUTION_TIMEOUT_SECONDS&lt;/code&gt; tuvo que subir a 1800s). Con Gemini, el cuello de botella fue la convergencia del feedback loop: con &lt;code&gt;temperature=0.3&lt;/code&gt; y &lt;code&gt;top_p=0.9&lt;/code&gt; el Aggregator regeneraba las triples con variación de wording en cada ronda, y el Validator nunca encontraba dos corridas iguales para aprobar, agotando &lt;code&gt;MAX_NODE_EXECUTIONS&lt;/code&gt; sin converger. Solución: bajar a &lt;code&gt;temperature=0&lt;/code&gt; y &lt;code&gt;top_p=0.5&lt;/code&gt; .&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Análisis del input propagation de Graph&lt;/strong&gt; Un nodo sólo recibe el resultado de sus dependencias directas + el input original con el que comienza el grafo. El Publisher, dependiendo solo del Validator, se quedó sin las triples reales porque el Validator fue instruido a responder con una confirmación corta, no a repetir los datos. La solución fue agregar &lt;code&gt;aggregator → publisher&lt;/code&gt; como dependencia adicional. Hay que diseñar edges, pensando qué datos necesita cada nodo, no solo qué controla el flujo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Las semánticas OR de Python son una fuente de bugs silenciosos.&lt;/strong&gt; Sin &lt;code&gt;all_dependencies_complete()&lt;/code&gt;, el Aggregator se hubiera ejecutado 3 veces (una por extractor) en lugar de una vez con los 3 resultados completos. Cualquier nodo con múltiples edges de entrada en Python necesita esta verificación explícita si queremos esperar a todas las dependencias.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La granularidad de extracción es responsabilidad del prompt, no del framework.&lt;/strong&gt; Ver entidades compuestas como "entities and relations" en lugar de dos nodos separados es variabilidad normal de un LLM con temperatura &amp;gt; 0. Hay que reforzar reglas en el prompt del Extractor y del Aggregator.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;structured_output_model&lt;/code&gt; requiere que el modelo soporte tool calling&lt;/strong&gt; (este parámetro se pasa en la creación del Agent. Hay que verificarlo (&lt;code&gt;ollama show &amp;lt;model&amp;gt;&lt;/code&gt; → &lt;code&gt;Capabilities: tools&lt;/code&gt;) antes de asumir que cualquier modelo local lo soporta.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exact-match y LLM-as-judge miden cosas distintas.&lt;/strong&gt; En una corrida, el mismo caso obtuvo F1 exact-match = 0.0 pero SemanticCorrectnessEvaluator = 1.0, el pipeline había extraído el 100% del conocimiento correcto, solo con sinónimos distintos ("Aggregator node" vs "Aggregator"). Confiar solo en exact-match hubiera hecho pensar que el pipeline falló por completo.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Recursos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Curso Fundamentos&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://skillbuilder.aws/learn/CFBFZNS11J/building-ai-agent-harnesses-with-strands-agents/D6YG8S6N7M" rel="noopener noreferrer"&gt;Building AI Agent Harnesses with Strands Agents&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentación Strands Agents&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/multi-agent/graph/" rel="noopener noreferrer"&gt;Graph&lt;/a&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/evaluators/" rel="noopener noreferrer"&gt;Evals&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Otros&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/35nhh8YGNRwWHFhHL6zXPrOYzY3/multi-agent-systems-with-graph-pattern" rel="noopener noreferrer"&gt;Multi-Agent Systems with Graph Pattern&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/aws-espanol/como-detener-la-inyeccion-de-prompts-en-agentes-de-ia-que-leen-contenido-no-confiable-10h2"&gt;Inyección de prompts en agentes de IA&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3Ecodd9aFqsG1SAkQKmqZ2KxPiP/5-multi-agent-patterns-in-strands-agents-which-one-and-when" rel="noopener noreferrer"&gt;5 Multi-Agent Patterns in Strands&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>strandsagents</category>
      <category>python</category>
    </item>
    <item>
      <title>Swarm en Strands: orquestación autónoma mediante handoffs emergentes</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:55:38 +0000</pubDate>
      <link>https://dev.to/aws-builders/swarm-en-strands-orquestacion-autonoma-mediante-handoffs-emergentes-11a9</link>
      <guid>https://dev.to/aws-builders/swarm-en-strands-orquestacion-autonoma-mediante-handoffs-emergentes-11a9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Siguiendo con la serie de artículos sobre la orquestación de multiagentes en Strands, en estas notas le toca turno a Swarm o Enjambre explicado con un ejemplo gamificado de cinco agentes especializados, donde cada uno es responsable de  diseñar una dimensión distinta de un videojuego. Testeado de dos maneras con Gemma4 + Ollama  y con la api de Gemini.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Para desarrollar y comprender agentes con el framework&amp;nbsp;Strands Agents&amp;nbsp;te propongo hacer un curso de fundamentos o leer su documentación (que es extensa). Los fundamentos son importantes! Es primordial conocer cómo dar complejidad a nuestros agentes, no esperar delegar toda la responsabilidad del diseño a la IA (recursos al pie de estas notas).&lt;br&gt;
Para la prueba de concepto utilicé dos opciones locales de ejecución que me permitió analizar sus resultados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ollama + el pequeño modelo&amp;nbsp;&lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt;&amp;nbsp;(que pesa poco mas de 4gb) ejecutando en Docker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Con la Api de gemini. La api key se puede generar de manera gratuita y utilizar el modelo &lt;strong&gt;"Gemini 2.5 Flash"&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎮 &lt;strong&gt;El caso de uso que tomé para desarrollar el patrón&amp;nbsp;Swarm&lt;/strong&gt; es la creación de un Game Design Document (GDD) a partir de una premisa: "roguelike de cocina en un castillo maldito..."&lt;br&gt;
Participan cinco agentes especializados, cada uno responsable de una dimensión diferente del diseño del videojuego: mecánicas, narrativa, niveles, lore (historias, mitos, reglas, datos del pasado) y experiencia de juego. Todos colaboran para transformar una premisa simple en un &lt;strong&gt;Game Design Document (GDD)&lt;/strong&gt; coherente. &lt;/p&gt;

&lt;p&gt;A diferencia de un pipeline lineal, los agentes pueden detectar fricciones entre sus decisiones (una mecánica que contradice la narrativa, una regla de lore que rompe el balance) y reabrir la conversación con el agente correspondiente hasta llegar a un resultado consensuado.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conceptos Clave
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;➡ Swarm es un patrón de orquestación de agentes colaborativo&lt;/strong&gt;, donde múltiples agentes trabajan juntos como un equipo para resolver tareas complejas. A diferencia de los sistemas multiagente tradicionales, ya sean secuenciales o jerárquicos, un enjambre permite la coordinación autónoma entre agentes con &lt;strong&gt;contexto y memoria de trabajo compartidos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Swarm es apropiado cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La secuencia de pasos no se puede predefinir:&lt;/strong&gt; de antemano no sabés qué agente necesitará intervenir, porque depende del contenido que se vaya generando.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Puede haber correcciones o retrocesos:&lt;/strong&gt; el problema requiere ciclos de ida y vuelta entre especialistas (ejemplo: una decisión de un agente invalida el trabajo de otro).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hay múltiples dimensiones de expertise en tensión:&lt;/strong&gt; cada agente defiende una perspectiva distinta y el valor está justamente en que negocien entre sí, no en que trabajen aislados uno de otro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Se necesita razonamiento colectivo emergente:&lt;/strong&gt; la solución final surge de la interacción entre agentes, no de que un solo agente (o un humano) coordine todo desde arriba.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❗ No es el patrón ideal cuando el flujo de trabajo es fijo y predecible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Memoria / Contexto&lt;/strong&gt; &lt;br&gt;
Poseen memoria de trabajo compartida y contexto completo. Cuando el enjambre se ejecuta, el historial completo de mensajes (propuestas, objeciones y razonamientos de de los agentes anteriores) se transmite al nuevo agente que toma el control (&lt;em&gt;handoff&lt;/em&gt;). &lt;br&gt;
&lt;em&gt;Payload del Handoff&lt;/em&gt;: además del historial acumulado, cuando un agente ejecuta la herramienta de handoff, puede adjuntar un mensaje o nota explícita (&lt;code&gt;message&amp;nbsp;&lt;/code&gt;/&amp;nbsp;&lt;code&gt;context&lt;/code&gt;) explicando&amp;nbsp;por qué&amp;nbsp;le transfiere la tarea y qué espera que haga.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Nodos vs. Agentes&lt;/strong&gt;&lt;br&gt;
En Swarm, cada agente se registra como un "nodo" (&lt;code&gt;SwarmNode&lt;/code&gt;) que envuelve al &lt;code&gt;Agent&lt;/code&gt;. Esta distinción importa porque el swarm no razona en términos de "quién es quién" sino de "qué nodo ejecuta a continuación". El resultado final expone esto como &lt;code&gt;node_history&lt;/code&gt;, una lista de nodos ejecutados, no una conversación: es lo que permite reconstruir después la topología real que se ejecutó, sin haberla tenido que diseñar de antemano.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ El mecanismo de Handoff es tool-calling, no un cambio de contexto mágico&lt;/strong&gt; &lt;br&gt;
Vale la pena aclarar explícitamente que &lt;code&gt;handoff_to_agent&lt;/code&gt;no es una función especial del framework fuera del paradigma de LLMs: es una tool más, inyectada automáticamente por Strands en cada agente del swarm, con el mismo mecanismo de function-calling que cualquier otra tool. La consecuente es &lt;strong&gt;la fiabilidad del patrón Swarm depende enteramente de qué tan bien el modelo subyacente hace tool-calling&lt;/strong&gt;. No es una limitación de Strands, es una dependencia estructural del patrón. Por eso, acompañando este artículo el caso de uso se testeo de dos maneras: con el pequeño modelo &lt;em&gt;Gemma4&lt;/em&gt; y con el modelo &lt;em&gt;Gemini 2.5 Flash&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Topología emergente vs. predefinida (el argumento central de "por qué Swarm")&lt;/strong&gt;&lt;br&gt;
En Swarm, la topología es un &lt;strong&gt;resultado&lt;/strong&gt;, no una entrada. Se descubre recién en la ejecución, y puede variar entre corridas con la misma premisa (ejemplo: tengo 5 agentes y sólo intervienen 4). Esto es una ventaja de adaptabilidad y un riesgo no-determinista al mismo tiempo.&lt;br&gt;
Se necesita indicar donde comienza el enjambre y cada punto de decisión lo toma el propio agente en base al contexto acumulado. Aquí un topología de ejemplo del caso de uso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mechanic_designer
      ↓
level_architect ←──────────┐
      ↓                    │
playtest_simulator ─────────┤ (iteration 1: objection → adjustment)
      ↓                    │
level_architect ──────────→┘
      ↓
playtest_simulator ─────────┐ (iteration 2: objection → adjustment)
      ↓                    │
level_architect ←───────────┘
      ↓
narrative_weaver
      ↓
lore_keeper
      ↓
playtest_simulator ─────────┐ (iteration 1: objection → adjustment)
      ↓                    │
level_architect ←───────────┘
      ↓
playtest_simulator
      ↓
[VERDICT: NO FRICTION DETECTED] → END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;➡ Guardrails como parte del diseño&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;max_handoffs&lt;/code&gt;: Máximo de transferencias entre agentes permitidos en todo el swarm antes de forzar el corte. Es el límite global de "pasos" de la ejecución completa.&lt;br&gt;
&lt;code&gt;max_iterations&lt;/code&gt;: Máximo de iteraciones del swarm (ejecuciones de nodo) permitidas. En la práctica actúa junto con &lt;code&gt;max_handoffs&lt;/code&gt; como segunda red de seguridad contra ejecuciones descontroladas.&lt;br&gt;
&lt;code&gt;execution_timeout&lt;/code&gt;: Tiempo total en segundos que puede durar la ejecución completa del swarm, sumando todos los agentes y handoffs. Si se supera, el swarm corta con Status.FAILED aunque esté en medio de un turno.&lt;br&gt;
&lt;code&gt;node_timeout&lt;/code&gt;: Tiempo máximo (en segundos) que un agente individual puede tardar en un solo turno (una llamada al modelo + su respuesta). Protege contra que un nodo puntual se cuelgue sin afectar el límite global.&lt;br&gt;
&lt;code&gt;repetitive_handoff_detection_window&lt;/code&gt;: Tamaño de la "ventana" (cantidad de handoffs recientes) que Strands analiza para detectar patrones repetitivos. Con 6, mira los últimos 6 handoffs para decidir si hay un loop.&lt;br&gt;
&lt;code&gt;min_unique_agents&lt;/code&gt;: Dentro de esa ventana, cantidad mínima de agentes distintos que deben haber participado para considerar el flujo "sano". Si en los últimos 6 handoffs participaron menos de 3 agentes únicos (ej. solo 2 rebotando entre sí), Strands lo detecta como ping-pong y corta el swarm.&lt;br&gt;
Todos los guardrails anteriores no son opcionales para un swarm en producción, son necesarios para limitar la autonomía de los agentes y que el costo de tokens, tiempo o dinero en APIs pagas no se descontrole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ La importancia del diseño de prompts&lt;/strong&gt;&lt;br&gt;
Strands provee el mecanismo de handoff (la tool, el hook, los guardrails), pero no decide cuándo usarlo, eso está en el &lt;em&gt;system prompt&lt;/em&gt; de cada agente. En este caso de uso, cada uno de los 5 prompts define explícitamente tres cosas: el rol y criterios de evaluación del agente, a quién y bajo qué condición debe transferir el control, y reglas de convergencia para evitar que vuelva a objetar lo mismo una vez resuelto ("Convergence rule").&lt;/p&gt;

&lt;p&gt;Esta última fue necesaria escribirla ya que en las primeras ejecuciones algunos agentes re-abrían el mismo desacuerdo varias veces con distintas palabras, generando ciclos improductivos que ni los guardrails ni el hook podían distinguir de una negociación legítima. La solución fue de prompt, instruir explícitamente a cada agente a aceptar una corrección después de la primera vuelta, salvo que aparezca un motivo nuevo. Los guardrails protegen contra loops estructurales; esto resuelve los loops semánticos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Hooks como punto de extensión para mitigar limitaciones del modelo elegido&lt;/strong&gt;&lt;br&gt;
Strands permite engancharse al ciclo de ejecución del agente mediante &lt;em&gt;hooks&lt;/em&gt;, interceptando eventos como una llamada a herramienta exitosa. En el caso de uso se implementó &lt;code&gt;StopAfterHandoffHook&lt;/code&gt;, que fuerza al agente a detener su turno inmediatamente después de un handoff exitoso.&lt;/p&gt;

&lt;p&gt;Este mecanismos sirve para  cuando el prompt no alcanza para garantizar un comportamiento, evita que el modelo llame a la herramienta de handoff más de una vez por turno. Es una intervención determinista a nivel de código, sin depender del prompt engineering. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Evaluaciones (Evals)&lt;/strong&gt;&lt;br&gt;
Existe el package &lt;code&gt;strands-agents-evals&lt;/code&gt; que provee evaluadores reutilizables (por salida, por trayectoria de tool-calls, por trazas de ejecución, etc). En este caso de uso se implementaron dos evaluadores:&lt;br&gt;
&lt;strong&gt;- Evaluación por Trayectoria (Trajectory Evaluation)&lt;/strong&gt;. En vez de exigir una secuencia exacta de handoffs, valida propiedades estructurales de la ejecución. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;que el punto de entrada haya sido el correcto. &lt;/li&gt;
&lt;li&gt;que haya participado una cantidad mínima de agentes únicos, y que los agentes indispensables (como &lt;code&gt;mechanic_designer&lt;/code&gt;) hayan intervenido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;- Evaluación por Salida (Output Evaluation)&lt;/strong&gt;. Verifica que el GDD final contenga las palabras clave esperadas del dominio del juego (ej. "curse", "ingredient", "castle"), como chequeo básico de que el contenido generado es relevante a la premisa de origen.&lt;/p&gt;

&lt;p&gt;⭐ Este enfoque refleja una diferencia clave frente a patrones más deterministas como agents-as-tools donde se puede evaluar contra un resultado esperado preciso (si se delegó al agente correcto); en cambio en Swarm hay que evaluar la &lt;strong&gt;forma&lt;/strong&gt; del comportamiento, no una secuencia fija.&lt;/p&gt;
&lt;h2&gt;
  
  
  Descripción de la solución
&lt;/h2&gt;

&lt;p&gt;📦&amp;nbsp;Repositorio GitHub:&amp;nbsp;&lt;a href="https://github.com/reinalau/strands-swarm" rel="noopener noreferrer"&gt;github.com/reinalau/strands-swarm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyb3nt953zbb9q9gzxd8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyb3nt953zbb9q9gzxd8.jpg" alt=" " width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Como comenté más arriba, utilicé 2 opciones de modelos para la ejecución y analizar el comportamiento, pero podes usar solo una de ellas:&lt;/p&gt;

&lt;p&gt;a. Docker Desktop, luego descargué la imagen de Ollama e instalé&amp;nbsp;gemma4:e2b-it-qat&amp;nbsp;(que pesa poco mas de 4GB). Todos los modelos gemma los podés encontrar&amp;nbsp;&lt;a href="https://ollama.com/library/gemma4/tags" rel="noopener noreferrer"&gt;aquí&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the Ollama server with a persistent volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="nt"&gt;-p&lt;/span&gt; 11434:11434 &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama ollama/ollama

&lt;span class="c"&gt;# Download the model&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull gemma4:e2b-it-qat

&lt;span class="c"&gt;# Test that the model responds&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama run gemma4:e2b-it-qat

&lt;span class="c"&gt;# Verify that the model is running&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b. Generé una api key de gemini de capa gratuita, desde &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;aquí&lt;/a&gt;. Te permite usar el modelo &lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El código del multiagente está en Python&lt;/strong&gt; y la estructura del proyecto se diseñó de manera que sea más explicativo su lógica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-swarm/
├── README.md
├── requirements.txt
├── .env.example
├── .gitignore
│
├── src/
│   ├── __init__.py
│   ├── main.py                  # entry point, builds and runs the swarm
│   ├── config.py                # model provider config, timeouts, swarm limits
│   │
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── _common.py           # shared model factory + prompt loader
│   │   ├── _hooks.py            # StopAfterHandoffHook
│   │   ├── mechanic_designer.py
│   │   ├── narrative_weaver.py
│   │   ├── level_architect.py
│   │   ├── lore_keeper.py
│   │   └── playtest_simulator.py
│   │
│   ├── prompts/
│   │   ├── mechanic_designer.md
│   │   ├── narrative_weaver.md
│   │   ├── level_architect.md
│   │   ├── lore_keeper.md
│   │   └── playtest_simulator.md
│   │
│   ├── swarm/
│   │   ├── __init__.py
│   │   └── build_swarm.py       # instantiates agents + configures Swarm (max_handoffs, etc.)
│   │
│   └── output/
│       ├── __init__.py
│       └── gdd_builder.py       # consolidates node_history/results into the final GDD
│
├── evals/
│   ├── __init__.py
│   ├── eval_cases.py            # EvalCase definitions (premise, expected agents, keywords)
│   └── run_evals.py             # trajectory + output evaluators over the swarm
│
├── examples/
│   └── example_premise.txt      # "cooking roguelike in a cursed castle"
│
├── outputs/
│   └── .gitkeep                 # generated GDDs land here after each run
│
├── logs/
│   └── .gitkeep                 # execution logs / handoff events
│
└── tests/
    ├── __init__.py
    └── test_swarm_flow.py

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Ejecución Local
&lt;/h3&gt;

&lt;p&gt;Una vez que tenemos el repositorio del código fuente clonado y el docker de ollama con el modelo gemma 4 o la api key de gemini generada, pasamos armar el ambiente.&lt;br&gt;
Los requerimientos que vamos a encontrar en &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
strands-agents[ollama]&lt;br&gt;
strands-agents[gemini]&lt;br&gt;
strands-agents-evals&lt;br&gt;
python-dotenv&lt;br&gt;
pytest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En &lt;code&gt;.env&lt;/code&gt; necesitamos:&lt;br&gt;
OLLAMA_HOST=&lt;a href="http://localhost:11434" rel="noopener noreferrer"&gt;http://localhost:11434&lt;/a&gt;&lt;br&gt;
MODEL_NAME=gemma4:e2b-it-qat&lt;br&gt;
MODEL_PROVIDER=gemini   # ollama&lt;br&gt;
GEMINI_API_KEY=tuapikeyOpcional&lt;br&gt;
GEMINI_MODEL_NAME=gemini-2.5-flash&lt;br&gt;
LOG_LEVEL=INFO&lt;br&gt;
MODEL_TEMPERATURE=0.5&lt;br&gt;
MODEL_MAX_TOKENS=3500&lt;br&gt;
MODEL_NUM_CTX=4096&lt;br&gt;
MAX_HANDOFFS=12&lt;br&gt;
MAX_ITERATIONS=12&lt;br&gt;
EXECUTION_TIMEOUT=5000&lt;br&gt;
NODE_TIMEOUT=1500&lt;br&gt;
REPETITIVE_HANDOFF_DETECTION_WINDOW=6&lt;br&gt;
REPETITIVE_HANDOFF_MIN_UNIQUE_AGENTS=3&lt;br&gt;
ENTRY_POINT_AGENT=mechanic_designer&lt;br&gt;
LOG_DIR=logs&lt;br&gt;
OUTPUT_DIR=outputs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Las pruebas&lt;/strong&gt; están organizadas en un enfoque de 2 niveles (&lt;code&gt;tests/&lt;/code&gt;):&lt;br&gt;
&lt;strong&gt;Nivel 1 (Rápidas y deterministas):&lt;/strong&gt; Valida la estructura del swarm, la configuración de nodos/límites, la extracción de texto y la consolidación del GDD sin realizar llamadas reales al modelo. Usa cadenas simuladas como "Cooking roguelike in a cursed castle." o "Test Premise".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nivel 2 (Test de integración end-to-end):&lt;/strong&gt; Ejecuta el swarm completo contra el modelo real (Gemma4 en Ollama o Gemini). Está deshabilitado por default, pytest lo salta automáticamente, y se activa así en esta ejecución:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nv"&gt;RUN_INTEGRATION_TESTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-m&lt;/span&gt; integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📝 &lt;strong&gt;Nota:&lt;/strong&gt; el comportamiento de activación/desactivación explícita para tests que dependen de un LLM real es una práctica común cuando se integran estos flujos a pipelines de CI/CD, donde ejecutar llamadas reales a un modelo en cada commit sería muy lento.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Procedemos a ejecutar el Swarm principal&lt;/strong&gt;. Se puede ejecutar contra el modelo ollama local o vía api de gemini. Se configura en &lt;code&gt;.env&lt;/code&gt; , tener en cuenta de cambiar &lt;code&gt;MODEL_PROVIDER&lt;/code&gt; a "gemini" u "ollama". También recomiendo revisar valores default de &lt;code&gt;src/config.py&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;El log&lt;/strong&gt; de las llamadas entre agentes se visualiza en la carpeta &lt;code&gt;logs/&lt;/code&gt;. &lt;br&gt;
&lt;strong&gt;El resultado final&lt;/strong&gt; del intercambio de mensajes entre los agentes del enjambre es el Game Design Document y se guarda en un documento markdown en la carpeta &lt;code&gt;outputs/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;⭐ Te invito analizar la topología de handoffs después de las ejecuciones para entender si los agentes necesitan ajustes ya sea de prompts, hooks o parámetros de configuración.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por último, hacemos una evaluación de pruebas  sobre el agente real con el concepto de&amp;nbsp;"Evals"&lt;/strong&gt;.&amp;nbsp;Esto es importante para todos los agentes futuros que construyas, tomar la documentación de Strands y descubrir cómo y para qué tenemos que evaluar nuestros agentes.&lt;br&gt;
Se desactiva el recolector de telemetría&amp;nbsp;OpenTelemetry: &lt;code&gt;OTEL_SDK_DISABLED=true&lt;/code&gt;&amp;nbsp;para evitar cuelgues en un entorno local con el modelo en ollama (si internamente no alcanza un endpoint se cuelga indefinidamente).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_SDK_DISABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.run_evals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📝&amp;nbsp;Nota-2:&amp;nbsp;La explicación del código y paso a paso de la ejecución se encuentra detallado en el README.md en el repositorio de &lt;a href="https://github.com/reinalau/strands-swarm" rel="noopener noreferrer"&gt;github&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;El patrón *&lt;em&gt;Swarm *&lt;/em&gt; cede el control del flujo al propio modelo. Una corrida puede resolverse en 4 pasos o en 11, en un minuto o en dos horas, según decisiones que toman los agentes que intervienen. &lt;br&gt;
Con un modelo chico experimenté loops de handoff, ping-pong entre agentes, transferencias narradas en texto en lugar de ejecutadas y para resolverlo tuve que agregar controles explícitos: límites de iteración, detección de repetición y un hook que corta el turno apenas el handoff se confirma.&lt;br&gt;
La autonomía de un swarm no reemplaza los guardarraíles de ingeniería, los vuelve indispensables.&lt;/p&gt;

&lt;p&gt;⭐ Y como trabajo futuro hay que pensar cómo implementar un proyecto multiagente Swarm productivo. El presente proyecto es educativo y sirve para entender el funcionamiento del patrón, pero es insuficiente para producción. AWS ya tiene un camino natural para eso: &lt;strong&gt;Amazon Bedrock AgentCore&lt;/strong&gt;, un runtime serverless que ejecuta agentes Strands con aislamiento por sesión, observabilidad, memoria administrada y escalado automático. Queda para un próximo artículo explorar esa migración.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recursos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Curso Fundamentos&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://skillbuilder.aws/learn/CFBFZNS11J/building-ai-agent-harnesses-with-strands-agents/D6YG8S6N7M" rel="noopener noreferrer"&gt;Building AI Agent Harnesses with Strands Agents&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentación Strands Agents&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/multi-agent/swarm/" rel="noopener noreferrer"&gt;Swarm&lt;/a&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/evaluators/" rel="noopener noreferrer"&gt;Evals&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Otros&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GJxKulvQXV120QgfCSaq4QzG4I/blog-writer-agent-using-swarm-pattern/" rel="noopener noreferrer"&gt;Blog Writer Agent using Swarm Pattern&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/aws-espanol/detener-alucinaciones-de-agentes-de-ia-validar-antes-de-que-el-agente-escriba-en-memoria-2ad3/"&gt;Detener alucinaciones de agentes de IA&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>strandsagents</category>
      <category>python</category>
    </item>
    <item>
      <title>Agents as Tools en Strands: orquestación jerárquica en local con Gemma 4 y Ollama</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:33:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/agents-as-tools-en-strands-orquestacion-jerarquica-en-local-con-gemma-4-y-ollama-db4</link>
      <guid>https://dev.to/aws-builders/agents-as-tools-en-strands-orquestacion-jerarquica-en-local-con-gemma-4-y-ollama-db4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hace tiempo vengo consumiendo cursos de orquestación de agentes de distintos frameworks y me propuse navegar por los diferentes patrones de Strands Agents de complejidad media, ejecutados localmente (con la idea de encarar más adelante el deploy en AWS). Este artículo está dedicado a Agents-as-Tools, corriendo con Ollama en una laptop con solo 16GB de RAM 💻.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Para desarrollar y comprender agentes con el framework Strands Agents  te propongo hacer un curso de fundamentos o leer su documentación (que es extensa). Los fundamentos son importantes! Es primordial conocer cómo dar complejidad a nuestros agentes, no esperar delegar toda la responsabilidad del diseño a la IA (recursos al pie de estas notas).&lt;/p&gt;

&lt;p&gt;Para que la prueba de concepto se pueda ejecutar localmente, utilicé Ollama +  el pequeño modelo &lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt; (que pesa poco mas de 4gb) ejecutando en Docker. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El caso de uso que tomé para desarrollar el patrón&lt;/strong&gt; es la automatización del soporte básico de TI:&lt;br&gt;
🛠 Se recibe un ticket de soporte y se determina si es un ticket de facturación, técnico o seguridad. Luego se envía al agente especializado para que lo resuelva.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conceptos claves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;➡ Agents As Tools es un patrón de orquestación multiagente&lt;/strong&gt; que como se describe en la documentación es jerárquico (hub and spooke), donde un orquestador delega tareas a agentes especializados que tienen diferentes tools para resolver. El orquestador y cada agente poseen contexto aislado, no se contaminan entre sí. &lt;/p&gt;

&lt;p&gt;Para clarificar el funcionamiento: al delegar, &lt;strong&gt;hay dos agents loops anidados, no uno compartido:&lt;/strong&gt; el orquestador no "pausa" su razonamiento en espera del especialista, simplemente visualiza una tool call que devuelve texto, por detrás, esa tool call disparó un loop completo e independiente que ya terminó antes de volver.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Orchestrator loop
   └─ calls billing_agent&lt;span class="o"&gt;(&lt;/span&gt;query&lt;span class="o"&gt;)&lt;/span&gt; → triggers a new, isolated loop
         └─ Billing loop: check_invoice_status → process_refund → final response
   ← receives only the final string &lt;span class="o"&gt;(&lt;/span&gt;doesn&lt;span class="s1"&gt;'t see the internal tool calls)
   └─ continues its own loop: delegates to another specialist or replies to the user
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;➡&lt;/strong&gt; &lt;code&gt;@tool&lt;/code&gt; no distingue entre &lt;strong&gt;operación simple o integración vía APis y agente completo&lt;/strong&gt; , el patrón Agents-as-Tools explota que un agente &lt;strong&gt;cumpla la misma interfaz&lt;/strong&gt; que una tool (recibe input, devuelve output), para que se pueda componer recursivamente. Es lo que permite anidar agentes dentro de agentes sin que el nivel superior necesite saber la complejidad interna del nivel inferior.&lt;/p&gt;

&lt;p&gt;⭐ La descripción (docstring) de cada función &lt;code&gt;@tool&lt;/code&gt; no es un comentario decorativo: es el input que el LLM orquestador lee en tiempo de ejecución para decidir a qué especialista delegar. Distinto del parámetro &lt;code&gt;description=&lt;/code&gt; del propio &lt;code&gt;Agent&lt;/code&gt;, que es metadata informativa y no interviene en el routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Memoria del orquestador (ConversationManager)&lt;/strong&gt;. Utilicé el concepto de compresión al hacer que el agente recuerde los últimos 10 mensajes o tickets de la sesión para mantener contexto entre interacciones consecutivas con &lt;code&gt;SlidingWindowConversationManager(window_size=10)&lt;/code&gt; &lt;br&gt;
Mantiene los mensajes en una lista dentro del objeto Python en RAM durante la ejecución del proceso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;➡ Hook de dominio (SteeringHandler)&lt;/strong&gt;. Implementa el "&lt;em&gt;HookProvider&lt;/em&gt;" de Strands, en este caso de uso fuerza el orden de pasos del protocolo de respuesta ante incidentes de Seguridad. Si el LLM intenta llamar por ejemplo a &lt;code&gt;revoke_access_token&lt;/code&gt; sin haber auditado primero la IP, el hook lo bloquea con &lt;code&gt;event.cancel_tool&lt;/code&gt; antes de que la función se ejecute. El framework impone la lógica de negocio determinista, sin depender del criterio del modelo. Describe con precisión los tres eventos &lt;code&gt;BeforeInvocationEvent&lt;/code&gt;,&amp;nbsp;&lt;code&gt;AfterToolCallEvent&lt;/code&gt;,&amp;nbsp;&lt;code&gt;BeforeToolCallEvent&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Los hooks sirven en general para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevenir loops infinitos o excesivos de tool calls (control de costos/tokens)&lt;/li&gt;
&lt;li&gt;Guardrails de seguridad/negocio (ej. no permitir más de n transacciones, n consultas a una API con rate limit, etc.)&lt;/li&gt;
&lt;li&gt;Es el mecanismo estándar de Strands para inyectar lógica de control &lt;strong&gt;sin modificar el core del agent loop.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;➡ Evaluaciones (Evals):&lt;/strong&gt; evaluación de comportamiento del agente (con &lt;code&gt;Contains&lt;/code&gt;, no asserts tradicionales). &lt;/p&gt;

&lt;p&gt;Algunas consideraciones que tomé para poder ejecutar los evals sin que se cuelguen en un entorno local con el modelo en ollama: Desactivé OTLP de Opentelemetry,  este si internamente no alcanza un endpoint (&lt;code&gt;localhost:4318&lt;/code&gt;) se cuelga indefinidamente. &lt;/p&gt;

&lt;p&gt;En el caso de uso del repositorio se hacen evaluaciones deterministas ya que se utiliza solo un LLM, no se abarcan todas las evaluaciones pero sí dos importantes descritas debajo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output Evaluation&lt;/strong&gt;. Evalúa el &lt;strong&gt;resultado final&lt;/strong&gt; (el texto que recibe el usuario), sin importar el camino interno que tomó el agente para llegar ahí. Es un chequeo determinista de &lt;strong&gt;keyword matching&lt;/strong&gt;: la respuesta debe contener ciertas palabras clave esperadas (&lt;code&gt;INV-1002&lt;/code&gt;, &lt;code&gt;USR-884&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trajectory Evaluation&lt;/strong&gt;. No evalúa qué respondió el agente, sino cómo llegó ahí, &lt;strong&gt;qué tools/sub-agentes invocó durante su razonamiento&lt;/strong&gt;. En el código, &lt;code&gt;extract_called_tools()&lt;/code&gt; inspecciona &lt;code&gt;agent.messages&lt;/code&gt; buscando bloques &lt;code&gt;toolUse&lt;/code&gt;, y &lt;code&gt;evaluate_trajectory()&lt;/code&gt; verifica que el sub-agente esperado (ej. &lt;code&gt;BillingAgent&lt;/code&gt;) haya sido efectivamente el delegado. Para la ejecución en entorno local con Ollama, la función &lt;code&gt;extract_called_tools&lt;/code&gt; extrae directamente los nombres de las herramientas en agent.messages y comprueba en milisegundos si se invocó a &lt;code&gt;case.expected_agent&lt;/code&gt;, sin necesidad de instanciar un modelo juez adicional ni hacer peticiones extra a Bedrock/Ollama (sin cuelgues asíncronos).&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;PASS / FAIL a nivel dimensión, en el output:&lt;/strong&gt; cada &lt;code&gt;EvalCase&lt;/code&gt; se marca como aprobado (&lt;code&gt;case_passed&lt;/code&gt;) solo si ambas dimensiones pasan (&lt;code&gt;traj_eval.test_pass&lt;/code&gt; and &lt;code&gt;out_eval.test_pass&lt;/code&gt;).  Un agente puede delegar al especialista correcto (trayectoria OK) pero responder sin las keywords esperadas (output FAIL), y el caso se marca como fallido igual. &lt;strong&gt;Una sola dimensión de evaluación no alcanza&lt;/strong&gt;: el agente puede "hacer lo correcto" pero "comunicarlo mal", o viceversa.&lt;/p&gt;
&lt;h2&gt;
  
  
  Descripción de la solución
&lt;/h2&gt;

&lt;p&gt;📦 Repositorio GitHub: &lt;a href="https://github.com/reinalau/strands-agents-as-tools" rel="noopener noreferrer"&gt;github.com/reinalau/strands-agents-as-tools&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquhnii87fqcf4urzghj4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquhnii87fqcf4urzghj4.png" alt="Soporte TI - Agents-as-Tools" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Para alojar el LLM utilicé Docker Desktop, luego descargué la imagen de Ollama e instalé &lt;strong&gt;gemma4:e2b-it-qat&lt;/strong&gt; (que pesa poco mas de 4GB). Todos los modelos gemma los podés encontrar &lt;a href="https://ollama.com/library/gemma4/tags" rel="noopener noreferrer"&gt;aquí&lt;/a&gt; pero este en particular funciona OK en una laptop con 16GB de RAM. &lt;/p&gt;

&lt;p&gt;Como condición necesaria asegurate de dar memoria suficiente a Docker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the Ollama server with a persistent volume&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="nt"&gt;-p&lt;/span&gt; 11434:11434 &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama ollama/ollama
&lt;span class="c"&gt;# Download the model&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama pull gemma4:e2b-it-qat
&lt;span class="c"&gt;# Test that the model responds&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama run gemma4:e2b-it-qat
&lt;span class="c"&gt;# Verify that the model is running&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ollama ollama ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Importante!&lt;/strong&gt; Testear el modelo gemma 4 en la terminal haciendo alguna pregunta para corroborar que responda.&lt;/p&gt;

&lt;p&gt;El código del multiagente está en Python y la estructura del proyecto se diseñó de manera que sea más explicativo su lógica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strands-agents-as-tools
├── README.md
├── requirements.txt
├── .env.example
├── .gitignore
│
├── src/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   │
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── orchestrator.py
│   │   ├── billing_agent.py
│   │   ├── technical_agent.py
│   │   └── security_agent.py
│   │
│   ├── tools/
│   │   ├── __init__.py
│   │   ├── billing_tools.py
│   │   ├── technical_tools.py
│   │   └── security_tools.py
│   │
│   └── hooks/
│       ├── __init__.py
│       └── security_verification_hook.py   # Domain SteeringHandler
│
├── examples/
│   └── sample_tickets.py
│
├── evals/
│   ├── __init__.py
│   ├── eval_cases.py                 # Cases with inputs, expected agent and keywords
│   └── run_evals.py                  # Dual evaluator: Trajectory (Tool Call) + Output (Keywords)
│
└── tests/
    └── test_agents.py                # Unit tests for tools (no LLM)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Ejecución local
&lt;/h3&gt;

&lt;p&gt;Una vez que tenemos el repositorio del código fuente clonado y el docker de ollama con el modelo gemma 4 ejecutando, pasamos armar el ambiente.&lt;/p&gt;

&lt;p&gt;Los requerimientos que vamos a encontrar en &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
strands-agents&lt;br&gt;
strands-agents-evals&lt;br&gt;
ollama&lt;br&gt;
python-dotenv&lt;br&gt;
pytest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el .env vamos a necesitar:&lt;br&gt;
OLLAMA_HOST=&lt;a href="http://localhost:11434" rel="noopener noreferrer"&gt;http://localhost:11434&lt;/a&gt;&lt;br&gt;
MODEL_NAME=gemma4:e2b-it-qat&lt;br&gt;
LOG_LEVEL=INFO&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los test son &lt;strong&gt;pruebas unitarias deterministas&lt;/strong&gt; (&lt;code&gt;tests/&lt;/code&gt;).&amp;nbsp;Permite validar rápidamente y de forma aislada la lógica de código puro de las herramientas (tools) de negocio (facturación, soporte técnico y seguridad), sin requerir inferencia de LLM. Garantizan que las funciones auxiliares retornen los estados y formatos esperados antes de ser expuestas como capacidades&amp;nbsp;&lt;code&gt;@tool&lt;/code&gt;&amp;nbsp;al sistema multiagente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Procedemos a ejecutar el flujo principal demostrativo donde se ve al orquestador delegar tickets&lt;/strong&gt; a los especialistas procesados por Ollama. Puede ser algo lento debido al modelo de ollama elegido pero finalmente termina el flujo con los ejemplos de tickets. Se utilizan ejemplos de soportes dentro de main con &lt;code&gt;from examples.sample_tickets import SAMPLE_TICKETS&lt;/code&gt; ...te invito a cambiarlos!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Por último, hacemos una evaluación de pruebas de calidad y precisión ejecutadas sobre el agente real con el concepto de &lt;em&gt;"Evals"&lt;/em&gt;. Esto es importante para todos los agentes futuros que construyas: hay que tomar la documentación de Strands y descubrir cómo y para qué tenemos que evaluar nuestros agentes.&lt;/p&gt;

&lt;p&gt;También recomiendo establecer &lt;code&gt;OTEL_SDK_DISABLED=true&lt;/code&gt; para desactivar el recolector de telemetría OpenTelemetry (explicado en Conceptos Claves)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_SDK_DISABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.run_evals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este último script ejecuta dos metodologías de evaluación según las especificaciones de &lt;strong&gt;Strands Agents: Output / Keyword Evaluation y Tool-Call Trajectory Evaluation&lt;/strong&gt; (explicado en Conceptos Claves) &lt;/p&gt;

&lt;p&gt;📝 &lt;strong&gt;Nota:&lt;/strong&gt; Todo la explicación del código del repositorio y paso a paso de la ejecución se encuentra en el &lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;Implementar Agents-as-Tools con un modelo local vía Ollama fue una buena forma de entender el patrón de orquestación sin gastar, pero también descubrí que modelos chicos como &lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt; pueden ser inconsistentes en su razonamiento (llamadas duplicadas a una misma tool, dudas antes de delegar) a diferencia de un modelo más grande (Bedrock/Claude/GPT-4) que probablemente sea más preciso. Esto no invalida el patrón, todo lo contrario: reafirma por qué los guardrails deterministas (el hook de seguridad) y las evaluaciones automáticas es la forma de detectar el comportamiento errático antes de que llegue a producción.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitaciones del ejemplo
&lt;/h3&gt;

&lt;p&gt;Este repositorio es educativo, y tiene varias simplificaciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools hardcodeadas con datos mock:&lt;/strong&gt; &lt;code&gt;check_invoice_status&lt;/code&gt;, &lt;code&gt;check_system_status&lt;/code&gt;, &lt;code&gt;audit_ip_address&lt;/code&gt;, etc. devuelven datos fijos en el código, no consultan ningún sistema real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un solo LLM local:&lt;/strong&gt; no se probó el comportamiento con otros modelos para comparar consistencia de routing/tool-calling entre modelos de distinto tamaño.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluación:&lt;/strong&gt; solo se cubrieron Output y Trajectory de forma determinista; no se probó chaos testing, LLM-as-judge, ni simulación multi-turno (mirar la documentación oficial). &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pensando en este mismo Multiagente productivo 🤯
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conectar tools a APIs/sistemas reales:&lt;/strong&gt; reemplazar los mocks por llamadas a una API de facturación real, un endpoint de status de infraestructura (ej. AWS CloudWatch, Datadog), y un servicio real de gestión de identidad (ej. IAM, Okta) para revocación de tokens. Varios de estos ya existen como servidores MCP mantenidos por la comunidad/AWS (conectarlos vía &lt;code&gt;MCPClient&lt;/code&gt; sería más rápido que escribir cada tool a mano).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persistencia real de sesión:&lt;/strong&gt; en este ejemplo cada ticket es stateless entre ejecuciones de &lt;code&gt;main.py&lt;/code&gt;; sumar un &lt;code&gt;SessionManager&lt;/code&gt;(&lt;code&gt;FileSessionManager&lt;/code&gt;o &lt;code&gt;DynamoDBSessionManager&lt;/code&gt;) para simular conversaciones multi-turno reales de un mismo usuario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Comparar modelos:&lt;/strong&gt; correr el mismo test suite contra un modelo de Bedrock o la API de Anthropic, y comparar tasa de errores de routing/tool-calling contra el modelo local, cuantificar el trade-off costo/latencia vs. confiabilidad.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/chaos_testing/" rel="noopener noreferrer"&gt;Chaos testing&lt;/a&gt;:&lt;/strong&gt; simular caídas de las APIs reales (una vez conectadas) con &lt;code&gt;ChaosPlugin&lt;/code&gt;, para validar que el agente degrada con gracia en vez de alucinar una respuesta. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ &lt;strong&gt;Migrar a &lt;a href="https://strandsagents.com/docs/user-guide/deploy/deploy_to_bedrock_agentcore/" rel="noopener noreferrer"&gt;Bedrock AgentCore&lt;/a&gt;&lt;/strong&gt;: envolver el orquestador con &lt;code&gt;BedrockAgentCoreApp&lt;/code&gt;para exponerlo vía el runtime administrado de AWS, adicionando &lt;code&gt;AgentCoreMemorySessionManager&lt;/code&gt;para memoria semántica de usuario (facts/preferences) en vez de solo &lt;code&gt;SlidingWindowConversationManager&lt;/code&gt; &lt;/p&gt;




&lt;h2&gt;
  
  
  Recursos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cursos Fundamentos&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://skillbuilder.aws/learn/CFBFZNS11J/building-ai-agent-harnesses-with-strands-agents/D6YG8S6N7M" rel="noopener noreferrer"&gt;Building AI Agent Harnesses with Strands Agents&lt;/a&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3GYPrAplMhl2IAl7jJW0yeKzXrR/building-ai-agent-harnesses-with-strands-agents-a-free-14-video-course/" rel="noopener noreferrer"&gt;Building AI Agent Harnesses – Video Course&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentación Strands Agents&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/multi-agent/agents-as-tools/" rel="noopener noreferrer"&gt;Agents as Tools&lt;/a&gt;&lt;br&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/evals-sdk/evaluators/trajectory_evaluator/" rel="noopener noreferrer"&gt;Trajectory Evaluator&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Artículos de Builders y AWS Developer Advocate&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://builder.aws.com/content/3AUxm8HafsZjZiOb3qrPDIH2MlD/aws-strands-agents-simplifica-la-creacion-de-agentes-de-ia" rel="noopener noreferrer"&gt;AWS Strands Agents: simplifica la creación de agentes de IA&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/aws-espanol/detener-alucinaciones-de-agentes-de-ia-validar-antes-de-que-el-agente-escriba-en-memoria-2ad3/"&gt;Detener alucinaciones de agentes de IA&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>strandsagents</category>
      <category>python</category>
    </item>
    <item>
      <title>Apuntes para la certificación AWS CloudOps Engineer Associate (SOA-C03) - Parte 2</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:41:05 +0000</pubDate>
      <link>https://dev.to/aws-builders/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-2-2e04</link>
      <guid>https://dev.to/aws-builders/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-2-2e04</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Segunda parte de las notas de examen. La idea es que identifiques los principales temas para el examen de certificación y conocer algunos tips de estudio para alcanzar con éxito tu meta 🎓&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;⬅&lt;/strong&gt; &lt;a href="https://dev.to/reinalau/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-1-2dji"&gt;&lt;strong&gt;&lt;em&gt;Apuntes para la certificación AWS CloudOps Engineer Associate (SOA-C03) - Parte 1&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Temas que son claves conocer para el examen - Segunda Parte&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWS Config
&lt;/h3&gt;

&lt;p&gt;Es prioritario revisar todas sus casos de uso y su integración con acciones de remediación con SSM y sus runbook. La remediación puede ser manual o automática.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La integración importante es con EventBridge para realizar una acción. Algo a saber es que los avisos a un equipo de soporte se hacen a través de SNS.
&lt;/li&gt;
&lt;li&gt;Data Aggregation for AWS Config (Multi-Account - Multi-Region) ➡  recopila datos de configuración y cumplimiento de varias cuentas y varias regiones de AWS. Una organización en AWS Organizations y todas las cuentas de esa organización que tengan AWS Config habilitado.
&lt;/li&gt;
&lt;li&gt;Reglas de configuración comunes en AWS Config:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Qué chequea&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudtrail-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CloudTrail habilitado en la cuenta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mfa-enabled-for-iam-console-access&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Los usuarios de IAM con acceso a la consola tienen MFA habilitado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s3-bucket-public-read-prohibited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Los buckets de S3 no permiten acceso de lectura pública&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encrypted-volumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Los volúmenes de EBS están encriptados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rds-storage-encrypted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Las instancias RDS tienen encriptación del storage habilitada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iam-password-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;La política de contraseñas cumple con los requisitos especificados&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;restricted-ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Los Security Groups no permiten acceso SSH sin restricciones (puerto 22)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root-account-mfa-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;La cuenta root tiene MFA habilitado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vpc-flow-logs-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VPC Flow Logs está activado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloud-trail-encryption-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Los logs de CloudTrail están encriptados con KMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s3-bucket-ssl-requests-only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;S3 solo acepta requests HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ec2-instances-in-vpc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Las instancias EC2 están dentro de una VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;access-keys-rotated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Las access keys de IAM se rotan dentro de N días&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Control Tower
&lt;/h3&gt;

&lt;p&gt;Relacionado con casos de uso de Organization ➡ Proporciona una landing zone para múltiples cuentas. Automatiza la configuración de AWS Organizations, la venta de cuentas, las medidas de seguridad (controles preventivos y de detección) y un registro de auditoría centralizado.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Hub - WAF - Detective - Inspector -  GuardDuty
&lt;/h3&gt;

&lt;p&gt;Es útil realizar cuadro comparativo con los servicios Detective, Inspector y GuardDuty para visualizar sus diferencias y en qué casos de uso son más adecuados. La mayoría se utilizan para controles de detección.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security Hub:&lt;/strong&gt; gestiona la postura o posición de seguridad de una Organización. Automatiza las comprobaciones de mejores prácticas y agrega alertas de Seguridad.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAF:&lt;/strong&gt; los casos de uso referidos son sencillos de identificar, detección de ataques sql injection - bloqueo de ip de países-rate limiting por IP, etc. Se gestiona mediante &lt;strong&gt;Web ACLs&lt;/strong&gt; con reglas propias o &lt;strong&gt;AWS Managed Rules&lt;/strong&gt; (reglas preconfiguradas por AWS)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspector:&lt;/strong&gt; Evalúa automáticamente vulnerabilidades en EC2, ECR y funciones Lambda ➡ prevención y detección proactiva.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detective:&lt;/strong&gt; simplifica el proceso de &lt;strong&gt;analizar, investigar e identificar la causa raíz&lt;/strong&gt; de los hallazgos de seguridad o actividades sospechosas en las cuentas de AWS. Recopila registros de CloudTrail (frase clave es log de APIs), VPC Flows, GuardDuty, entre otros  ➡ Actúa en el post-incidente.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GuardDuty:&lt;/strong&gt; Detección de amenazas continua mediante análisis de &lt;strong&gt;CloudTrail, VPC Flow Logs y DNS logs&lt;/strong&gt;. Usa ML para identificar comportamiento anómalo (ej: instancia EC2 haciendo crypto mining, credenciales filtradas usadas desde IP sospechosa). Es el &lt;strong&gt;detector en tiempo real,&lt;/strong&gt; genera findings que pueden disparar acciones en EventBridge → Lambda/SNS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conceptos básicos de Networking
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NACL&lt;/strong&gt;: Actúan a nivel de subnet y no tienen estado. Hay que agregar reglas de entrada y de salida. Las reglas se evalúan de manera ascendente. Es el primer mecanismo de defensa a nivel subred, útil para &lt;strong&gt;bloquear IPs específicas.&lt;/strong&gt; Ejemplo de resolución de problema: una instancia EC2 (en una private subnet) no tiene salida a internet? ➡ Las reglas de entrada y salida deben permitir el tráfico (incluidos los puertos de retorno efímeros 1024–65535).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security groups&lt;/strong&gt;: Actúan a nivel de instancia o servicio. Tienen estado, todo lo que se permite entrar- se permite salir (salvo que indiques lo contrario en las reglas de salida).

&lt;ul&gt;
&lt;li&gt;Resolución de conexiones vía SSH o RDP a instancias EC2. Qué revisar? En principio las reglas de entrada del Security Group a los puertos correspondientes.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Nat Gateway:&lt;/strong&gt; es escalable mejor que &lt;em&gt;Instance Nat Gateway.&lt;/em&gt; Asociada a una subnet pública. Solo para IPV4. Asegurar que la &lt;em&gt;Private subnet route table&lt;/em&gt; debe tener
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Egress-Only Internet Gateway (EIGW):&lt;/strong&gt;  permite que los recursos en una subred privada se comuniquen con el exterior a través de &lt;strong&gt;IPv6&lt;/strong&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  VPC FlowLogs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VPC &amp;gt; Your VPCs &amp;gt; Flow logs &amp;gt; Create Flow Logs (se necesita un rol para escribir en CloudWatch). Monitorea todo el trafico de la VPC determinada aparece en &lt;strong&gt;&lt;em&gt;CloudWatch &amp;gt; Logs &amp;gt; Log Management&lt;/em&gt;&lt;/strong&gt; .
&lt;/li&gt;
&lt;li&gt;Otros destinos de logs son S3 y Kinesis Data Firehose. Se puede realizar consultas sql mediante &lt;strong&gt;Athena&lt;/strong&gt; tomando como fuente S3.
&lt;/li&gt;
&lt;li&gt;Conocer el formato de los registros Default VPC Flow Logs y Custom VPC Flow Log Format. Cómo configurar y qué soportes permite resolver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ejemplo cuando falta una regla en Nacl o Security Group para permitir una conexión:&lt;/p&gt;

&lt;p&gt;2 123456789012 eni-abc123 10.0.1.4 10.0.2.5 43234 443 6 12 1400 1624387215 1624387275 ACCEPT OK&lt;/p&gt;

&lt;p&gt;2 123456789012 eni-abc123 172.31.16.139 172.31.16.21 80 42333 6 24 2456 1624387215 1624387275 REJECT OK&lt;/p&gt;

&lt;h3&gt;
  
  
  Route 53
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hosted zone ➡ pública y privada. Comprender el concepto de casos de uso: El cliente externo consulta la &lt;strong&gt;zona pública&lt;/strong&gt; de Route 53 y llega a la &lt;strong&gt;versión pública&lt;/strong&gt; de la app y el cliente interno dentro de la VPC consulta la &lt;strong&gt;zona privada&lt;/strong&gt; de Route 53 y llega a la &lt;strong&gt;versión interna&lt;/strong&gt; de la app ➡ ambos pueden tener el mismo nombre laub.com pero devuelven destinos diferentes de acuerdo de donde se lo consulte.
&lt;/li&gt;
&lt;li&gt;Es primordial conocer los tipos de registros y casos de usos:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tipo&lt;/th&gt;
&lt;th&gt;Qué contiene&lt;/th&gt;
&lt;th&gt;Uso&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IPv4&lt;/td&gt;
&lt;td&gt;
&lt;a href="http://www.laub.com" rel="noopener noreferrer"&gt;www.laub.com&lt;/a&gt; → 52.10.1.100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AAAA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IPv6&lt;/td&gt;
&lt;td&gt;
&lt;a href="http://www.laub.com" rel="noopener noreferrer"&gt;www.laub.com&lt;/a&gt; → 2001:db8::1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Otro hostname&lt;/td&gt;
&lt;td&gt;www → otro.dominio.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Alias&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recurso AWS&lt;/td&gt;
&lt;td&gt;laub.com → mi-alb.us-east-1.elb.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Name servers de la zona&lt;/td&gt;
&lt;td&gt;Delegación de subdominios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SOA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metadatos de la zona&lt;/td&gt;
&lt;td&gt;Se crea automático&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Servidor de correo&lt;/td&gt;
&lt;td&gt;laub.com → mail.empresa.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Texto libre&lt;/td&gt;
&lt;td&gt;Verificación de dominio, SPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SRV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Servicio + puerto&lt;/td&gt;
&lt;td&gt;Usado por algunos protocolos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PTR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DNS inverso&lt;/td&gt;
&lt;td&gt;IP → hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route 53 Resolver:&lt;/strong&gt; define cómo y hacia dónde se hacen las consultas DNS.

&lt;ul&gt;
&lt;li&gt;En esta feature se da la relación de entornos híbridos, DNS de on-premises resuelto por Route 53 (Outbound endpoint) o un DNS en AWS (en VPC) que lo resuelve on-premises (Inbound endpoint).
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Route 53 routing policies&lt;/strong&gt;, hay preguntas de examen sobre estas features relacionados con estrategias de deployment o failover:&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Política&lt;/th&gt;
&lt;th&gt;Descripción&lt;/th&gt;
&lt;th&gt;Health Checks&lt;/th&gt;
&lt;th&gt;Caso de Uso&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Un único servidor. Sin lógica ni failover.&lt;/td&gt;
&lt;td&gt;No soporta&lt;/td&gt;
&lt;td&gt;Un solo recurso sin redundancia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weighted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distribución de tráfico por pesos.&lt;/td&gt;
&lt;td&gt;Soportado — si falla un destino su peso es ignorado&lt;/td&gt;
&lt;td&gt;Deployment canary, A/B testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dirige el tráfico al recurso con menor latencia.&lt;/td&gt;
&lt;td&gt;Soportado&lt;/td&gt;
&lt;td&gt;Usuarios globales que necesitan baja latencia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geolocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enruta según la ubicación geográfica del origen de la consulta DNS (continente, país o estado).&lt;/td&gt;
&lt;td&gt;Soportado&lt;/td&gt;
&lt;td&gt;Contenido regionalizado, cumplimiento normativo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geoproximity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enruta según ubicación geográfica de los recursos con valor de sesgo opcional. Requiere &lt;strong&gt;Route 53 Traffic Flow&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;Soportado&lt;/td&gt;
&lt;td&gt;Balanceo geográfico con control fino de fronteras&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enruta a recurso principal; si falla va al secundario. Recursos en VPC privada requieren CloudWatch Alarm como intermediario.&lt;/td&gt;
&lt;td&gt;Requerido&lt;/td&gt;
&lt;td&gt;Alta disponibilidad activo-pasivo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP-Based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enruta según la IP del remitente de la consulta DNS.&lt;/td&gt;
&lt;td&gt;Soportado&lt;/td&gt;
&lt;td&gt;Segmentación por ISP o rango de IPs corporativas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multivalue Answer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Devuelve hasta 8 registros saludables al azar.&lt;/td&gt;
&lt;td&gt;Soportado&lt;/td&gt;
&lt;td&gt;Distribución simple sin necesidad de ELB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross Account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;La zona padre delega un subdominio creando registros NS que apuntan a la zona hija en otra cuenta.&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Gestión de DNS entre múltiples cuentas AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query Logging:&lt;/strong&gt; Registra todas las consultas DNS está disponible para zonas públicas., con esto podemos resolver problemas.  Auditar consultas DNS de una zona privada ➡ no es Route 53 query logging, es &lt;em&gt;Route 53 Resolver Query Logs&lt;/em&gt; (servicio diferente).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tipos de Health checks:&lt;/strong&gt; Es un concepto básico a retener ➡Endpoint health check, Calculated health check y CloudWatch alarm health check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cálculo de CIDR para un Subnet
&lt;/h3&gt;

&lt;p&gt;En una VPC no se puede superponer el bloque de direcciones cidr de sus subnet ➡ ejemplo encontrar el tamaño mínimo de la subred que tenga 30 direcciones IP disponibles.&lt;/p&gt;

&lt;p&gt;Total de direcciones IP disponibles = 2^(32-X) – 5 (Reservadas por AWS)&lt;/p&gt;

&lt;p&gt;Donde X es la notación CIDR.&lt;/p&gt;

&lt;p&gt;Para el bloque CIDR 192.168.1.0/&lt;strong&gt;26&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Total de direcciones IP disponibles = 2^(32-26) – 5 = 59 direcciones IP.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Global Accelerator
&lt;/h3&gt;

&lt;p&gt;Si enfrentas alguna pregunta que necesitas tiempos de milisegundos con direcciones IP anycast estáticas ➡ la respuesta seguramente es Global Accelerator.&lt;/p&gt;

&lt;p&gt;Frases claves para este servicio: aceleración  del tráfico TCP/UDP.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC Peering - Transit Gateway
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC peering:&lt;/strong&gt;  Conocer las limitaciones de  ➡ hasta 10 interconexiones de VPC es potable. No hay transitividad en las conexiones. Es conveniente realizar laboratorio para entender cómo se configuran las Route Tables. Tener presente que se puede realizar conexiones entre diferentes regiones y cuentas.

&lt;ul&gt;
&lt;li&gt;Solucionar conexion de VPC Peering entre dos instancias de EC2 en dos VPC diferentes. Se configuró correctamente el VPC peering entre ambas. Una instancia A puede hacer ping a instancia B. Pero la instancia B no puede hacer ping a la instancia A ➡ Posiblemente para solucionar este problema es necesario actualizar el grupo de seguridad de la instancia A para permitir el tráfico ICMP entrante.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Transit Gateway:&lt;/strong&gt; actúa como una hub de redes eliminando la necesidad de complejas redes de interconexión. A diferencia de VPC peering tiene ruteo transitivo. Importante! conocer el concepto de adjuntar una VPC (Attach a VPC).

&lt;ul&gt;
&lt;li&gt;Soporta &lt;strong&gt;multicast&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Se puede usar con &lt;strong&gt;VPN y Direct Connect&lt;/strong&gt; como hub central, simplificando arquitecturas híbridas.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TGW Route Tables&lt;/strong&gt; permiten segmentar el tráfico entre VPCs conectadas.Ejemplo ➡ aislar producción de desarrollo.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cloudfront
&lt;/h3&gt;

&lt;p&gt;Comprender cómo configurar los diferentes &lt;strong&gt;orígenes.&lt;/strong&gt; El más sencillo y natural que conocemos, si has rendido alguna certificación, es S3. Pero vas a tener que preparar ELB y el caso especial de EC2:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Origin Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Clave para el examen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contenido estático&lt;/td&gt;
&lt;td&gt;OAC reemplaza OAI (legacy). Sin OAC, el bucket puede quedar público&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application Load Balancer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contenido dinámico&lt;/td&gt;
&lt;td&gt;El ELB debe ser &lt;strong&gt;público&lt;/strong&gt;. CloudFront no puede llegar a un ALB interno. Usá &lt;strong&gt;security group del ALB restringido a los IPs de CloudFront&lt;/strong&gt; para que el tráfico solo entre por CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 instance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servidores web custom&lt;/td&gt;
&lt;td&gt;La instancia debe ser &lt;strong&gt;pública&lt;/strong&gt; y con security group que permita IPs de CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST o HTTP APIs&lt;/td&gt;
&lt;td&gt;Podés deshabilitar el endpoint público de APIGW y forzar acceso solo vía CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom HTTP origin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servidores on-premise u otros&lt;/td&gt;
&lt;td&gt;Requiere HTTPS con certificado válido si usás HTTPS entre CloudFront y el origen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Qué pasa cuando hay pocos cache hit (&lt;strong&gt;Cache Hit bajo&lt;/strong&gt;)?

&lt;ul&gt;
&lt;li&gt;Revisar el &lt;strong&gt;TTL&lt;/strong&gt; , si es muy bajo, los objetos expiran rápido y van al origen frecuentemente.
&lt;/li&gt;
&lt;li&gt;Revisar la &lt;strong&gt;Cache Key&lt;/strong&gt; , si incluye headers, cookies o query strings innecesarios, fragmenta el caché y baja el hit rate. La solución es usar &lt;strong&gt;Cache Policies&lt;/strong&gt; para controlar qué forma parte de la cache key.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Qué hacer cuando lo cacheado quedó antiguo, cómo resolver inmediatamente sin esperar que expire el TTL?

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invalidación de caché&lt;/strong&gt; (&lt;code&gt;CreateInvalidation&lt;/code&gt;) hace que CloudFront descarte el objeto antes del TTL.
&lt;/li&gt;
&lt;li&gt;Alternativa más eficiente para el examen: &lt;strong&gt;versionado de archivos&lt;/strong&gt; en el nombre (&lt;code&gt;app.v2.js&lt;/code&gt;),  evita invalidaciones y es la práctica recomendada por AWS.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Behavior (comportamiento): analizar las variantes. Es útil realizar laboratorios.

&lt;ul&gt;
&lt;li&gt;Permiten definir reglas distintas por &lt;strong&gt;path pattern&lt;/strong&gt; (&lt;code&gt;/api/*&lt;/code&gt;, &lt;code&gt;/images/*&lt;/code&gt;, &lt;code&gt;/static/*&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;Cada behavior puede tener su propio origen, TTL, política de caché y restricciones de acceso.
&lt;/li&gt;
&lt;li&gt;Caso de uso común: &lt;code&gt;/api/*&lt;/code&gt; apunta a ALB (dinámico, sin caché) y &lt;code&gt;/*&lt;/code&gt; apunta a S3 (estático, con caché larga).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  VPC Endpoints
&lt;/h3&gt;

&lt;p&gt;Es clave conocer los tipos de punto final para aplicar seguridad a la conexión de servicios aws (sin pasar por internet).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gateway Endpoint :&lt;/strong&gt; Es sin costo, solo para conectar con S3, DynamoDB. Importante para sortear preguntas, solo tiene utilidad dentro de una VPC, no tiene la capacidad de acceso externo. También admite políticas de punto final para controlar el acceso.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface Endpoint (PrivateLink):&lt;/strong&gt; Tiene costo. Crea una ENI con IP privada en tu VPC para conectarse a servicios de AWS (y servicios de terceros). Permite el acceso desde on-premise si existe conectividad previa (VPN/Direct Connect). Admite políticas de punto final para controlar qué entidades principales pueden usarlo.

&lt;ul&gt;
&lt;li&gt;Al crearlo AWS genera DNS privados para el servicio, si &lt;strong&gt;Enable Private DNS&lt;/strong&gt; está activado, las llamadas al endpoint público se resuelven automáticamente a la ENI privada. Resolución de problemas: si el tráfico sigue saliendo a internet a pesar de tener un Interface Endpoint → verificar que &lt;strong&gt;Enable Private DNS&lt;/strong&gt; esté activado.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;⬅&lt;/strong&gt; &lt;a href="https://dev.to/reinalau/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-1-2dji"&gt;&lt;strong&gt;&lt;em&gt;Apuntes para la certificación AWS CloudOps Engineer Associate (SOA-C03) - Parte 1&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sysops</category>
      <category>aws</category>
      <category>certification</category>
    </item>
    <item>
      <title>Apuntes para la certificación AWS CloudOps Engineer Associate (SOA-C03) - Parte 1</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Thu, 04 Jun 2026 03:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-1-2dji</link>
      <guid>https://dev.to/aws-builders/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-1-2dji</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Primera parte de los apuntes del examen. La idea es que identifiques los principales temas para el examen de certificación y conocer algunos tips de estudio para alcanzar con éxito tu meta 🎓&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Características generales&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;La complejidad es de &lt;strong&gt;nivel medio&lt;/strong&gt;. A mi criterio lo más complejo son las diferentes formas de soporte vía SSM.&lt;/p&gt;

&lt;p&gt;Consta de &lt;a href="https://docs.aws.amazon.com/es_es/aws-certification/latest/sysops-administrator-associate-03/sysops-administrator-associate-03.html" rel="noopener noreferrer"&gt;5 dominios&lt;/a&gt; y toda la información detallada la podemos encontrar &lt;a href="https://aws.amazon.com/certification/certified-cloudops-engineer-associate/" rel="noopener noreferrer"&gt;aquí&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Solo se puede rendir en inglés, con lo cual es prioritario estudiar con apuntes, entrenamiento de preguntas y laboratorios en este idioma. Se aprueba con 720 puntos.&lt;/p&gt;

&lt;p&gt;Es una certificación que te invita a conocer en profundidad las arquitecturas mas tradicionales de AWS , la resolución de problemas en producción y el mantenimiento evolutivo al momento de actualizar infraestructura para la continuidad del negocio.&lt;/p&gt;

&lt;p&gt;⭐ Se superponen varios temas con Solutions Architect Associate y Developer Associate. Si ya rendiste alguna, te va a resultar familiar un % del contenido.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Método de estudio&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Estudiar para una certificación no tiene fórmulas mágicas, se necesita apartar tiempo de nuestro dia y constancia. Tener en claro que nos permite profundizar en conceptos o prácticas que tal vez no vemos en el trabajo diario...esa será nuestras motivación.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tu punto de partida debe ser la &lt;a href="https://docs.aws.amazon.com/es_es/aws-certification/latest/sysops-administrator-associate-03/sysops-administrator-associate-03.html" rel="noopener noreferrer"&gt;guia de examen&lt;/a&gt; siempre.
&lt;/li&gt;
&lt;li&gt;Conviene hacer la &lt;a href="https://skillbuilder.aws/learning-plan/S427S3T7B5/exam-prep-plan-aws-certified-cloudops-engineer--associate-soac03--english/" rel="noopener noreferrer"&gt;preparación de examen de SkillBuilder&lt;/a&gt;, te da un paneo general de temas y asi podes identificar las brechas de conocimientos en las que debes profundizar.
&lt;/li&gt;
&lt;li&gt;Mi forma favorita de estudiar es hacer una especie de RAG con &lt;em&gt;NotebookLM&lt;/em&gt; con la documentación oficial de AWS pertinente (aws permite  bajar los pdf).
&lt;/li&gt;
&lt;li&gt;La novedad es que hay mcp de AWS disponibles referidos a la documentación. Por ejemplo, utilizando los temas de la guia de examen como contexto, se puede generar apuntes. Utilice &lt;a href="https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server" rel="noopener noreferrer"&gt;este mcp&lt;/a&gt; , pero tambien hay uno que no he probado que es mucho más amplio y contiene temas de arquitectura &lt;a href="https://awslabs.github.io/mcp/servers/aws-knowledge-mcp-server" rel="noopener noreferrer"&gt;aquí&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;⭐ &lt;a href="https://github.com/reinalau/AWS-Certified/tree/main/SOA-C03" rel="noopener noreferrer"&gt;Resumen de los Dominios&lt;/a&gt; de examen generado por kiro con el mcp de la documentación de AWS.&lt;/p&gt;

&lt;p&gt;⭐ Utilicé &lt;em&gt;Tutorials Dojo&lt;/em&gt; como banco de preguntas para practicar, pero mi percepción es que las preguntas en el examen eran más difíciles.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Arquitecturas importantes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Te invito a pensar en sus variantes para lograr alta disponibilidad, escalabilidad y resiliencia. Los temas de networking son importantes en el examen: configuración de VPC, subnet,  tablas de ruteo, Security Groups entre otros, especialmente troubleshooting de conectividad.&lt;/p&gt;

&lt;h3&gt;
  
  
  High Available (Route53)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pxvkecvcbk261xjtr9x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pxvkecvcbk261xjtr9x.jpg" alt="High Available (Route53)" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  High Available and Scalable Web App (Cloudfront - ALB)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07tv409rfqmmofmwby85.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07tv409rfqmmofmwby85.jpg" alt="High Available and Scalable Web App (Cloudfront - ALB)" width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Worker Fleet - SQS
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw929qpaixg5uymehebtd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw929qpaixg5uymehebtd.jpg" alt="Worker Fleet and SQS" width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC Peering
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foytcdcx1ziccwewrus2o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foytcdcx1ziccwewrus2o.jpg" alt="VPC Peering" width="800" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Transit Gateway
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6yc5hz6wi6yylky0wd9n.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6yc5hz6wi6yylky0wd9n.jpg" alt="Transit Gateway" width="800" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudfront - OAC
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vpon37thqefrpuhu9f9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vpon37thqefrpuhu9f9.jpg" alt="Cloudfront - OAC" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Config
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhwaqua5spp8bl15y6ld.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhwaqua5spp8bl15y6ld.jpg" alt="Config" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Temas que son claves conocer para el examen&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Las preguntas de examen se centran en la resolución de problemas con menos carga operativa  y  la elección de arquitecturas más lógica.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudWatch
&lt;/h3&gt;

&lt;p&gt;Familiarizarse con las features de la consola de CloudWatch porque vas a enfrentarte a varias preguntas sobre la administración de logs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cómo visualizó los logs comunes? &lt;em&gt;**CloudWatch &amp;gt; Logs &amp;gt; Log Management&lt;/em&gt;&lt;em&gt;. Aquí se encuentran todos los "Logs Groups" como aws/lambda/..., aws/apigateway/...,ecs/..., ec2..&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Cómo creo un Log Group? Desde la CLI de AWS. Cómo inserto Log en CloudWatch? Desde la Api con Create
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch metrics&lt;/strong&gt;, tener en claro las diferentes tipos y circunstancias a utilizar. Cada métrica tiene un nombre, namespace y dimensiones (opcional).

&lt;ul&gt;
&lt;li&gt;Basic Monitoring (5 min, default).
&lt;/li&gt;
&lt;li&gt;Detailed Monitoring (1 min, se habilita manualmente)
&lt;/li&gt;
&lt;li&gt;High Resolution para métricas custom (hasta 1 segundo).
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Metric Filters&lt;/strong&gt; ➡ Definís un patrón de búsqueda sobre un Log Group y CloudWatch genera una métrica custom a partir de las coincidencias.

&lt;ul&gt;
&lt;li&gt;El caso de uso típico a resolver: detectar cantidad de errores 500 en logs de una aplicación. Metric Filter sobre el Log Group (métrica custom) → alarma de CloudWatch sobre esa métrica y acción de remediación automática.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;CloudWatch Agent&lt;/strong&gt; ➡ es uno de las features más importantes que nos va a permitir enviar Logs a CloudWatch desde las instancias de EC2, server onprem, EKS cluster (recopilar métricas y registros a nivel de nodo de los contenedores).

&lt;ul&gt;
&lt;li&gt;Comprender cómo se configura y se modifica el archivo de configuración del agente (sin reiniciar las instancias de EC2): &lt;strong&gt;amazon-cloudwatch-agent-ctl command with the fetch-config&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;CloudWatch Logs Insights:&lt;/strong&gt; sintaxis y comandos para el análisis de logs ➡ &lt;code&gt;fields&lt;/code&gt; , &lt;code&gt;display&lt;/code&gt; , &lt;code&gt;filter&lt;/code&gt; , &lt;code&gt;stats&lt;/code&gt; , &lt;code&gt;sort&lt;/code&gt; , &lt;code&gt;limit&lt;/code&gt; ...&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Escalabilidad y Elasticidad
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Administración de Fleet de EC2:  Estrategias para continuar procesamiento de trabajos minimizando las interrupciones de form costo-efectiva ➡ &lt;strong&gt;capacity-optimized&lt;/strong&gt; para minimizar interrupciones o &lt;strong&gt;price-capacity-optimized&lt;/strong&gt; para balance entre costo y disponibilidad. (Combinando instancias Spot y On-Demand con el parámetro  &lt;code&gt;OnDemandBaseCapacity&lt;/code&gt; se especifica cuántas instancias base On-Demand y el resto se cubre con Spot).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2 Auto Scaling Group:&lt;/strong&gt; puede aparecer pregunta puntual sobre administrar ASG, pero por lo general no aparece como concepto aislado sino asociado a un balanceador de carga (&lt;strong&gt;ALB/NLB&lt;/strong&gt;) y en arquitecturas multi-región con &lt;strong&gt;Route 53&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;Capacidad mínima, máxima y deseada.
&lt;/li&gt;
&lt;li&gt;Definición de Launch template, cómo aplicar una nueva configuración a todas las instancias del ASG sin &lt;em&gt;downtime&lt;/em&gt;? ➡ Cuando se actualiza un Launch Template, las instancias existentes &lt;strong&gt;no se reemplazan automáticamente,&lt;/strong&gt; siguen ejecutando la versión anterior. Las nuevas instancias que escalen usarán la versión nueva. Para forzar el reemplazo se usa &lt;strong&gt;Instance Refresh&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Distribución en AZ.
&lt;/li&gt;
&lt;li&gt;Health Checks de EC2 o si health checks desde un ELB  determina que las instancias que no están en buen estado se reemplazan automáticamente. &lt;strong&gt;Profundizar en este punto&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Es clave entender las siguientes políticas de escalamiento para responder sobre mejora de performance. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tipo de Política&lt;/th&gt;
&lt;th&gt;Cómo Funciona&lt;/th&gt;
&lt;th&gt;Caso de Uso&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target Tracking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mantiene una métrica en un valor objetivo (ej: CPU al 50%). AWS gestiona las alarmas de CloudWatch automáticamente.&lt;/td&gt;
&lt;td&gt;Aplicaciones web generales donde querés simplicidad sin configuración compleja.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ajusta la capacidad en pasos según cuánto se desvía una métrica del umbral. Permite diferentes tamaños de paso según la magnitud.&lt;/td&gt;
&lt;td&gt;Workloads con picos abruptos donde necesitás agregar más instancias cuanto mayor es el desvío.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agrega o quita una cantidad fija de instancias cuando se dispara una alarma. Incluye período de cooldown antes de la siguiente acción.&lt;/td&gt;
&lt;td&gt;Legacy, no recomendada. Target Tracking la reemplaza en casi todos los casos.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scheduled Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Escala la capacidad en un horario específico usando expresiones cron.&lt;/td&gt;
&lt;td&gt;Cargas predecibles: horario comercial, cierre de mes, campañas programadas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Predictive Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usa ML para predecir la carga futura y ajusta la capacidad de forma proactiva.&lt;/td&gt;
&lt;td&gt;E-commerce con patrones cíclicos (ej: picos semanales o estacionales recurrentes).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LifeCycle Hooks:&lt;/strong&gt; En Auto Scaling Group para analizar errores de las instancias antes de que se finalicen para ser reemplazadas se puede pausar la finalización de la instancia. También, este mecanismo, al inicio permite acciones como instalación de software. La instancia permanece en estado Pendiente
&lt;/li&gt;
&lt;li&gt;Conocer el término &lt;strong&gt;Warmup,&lt;/strong&gt; define un tiempo de calentamiento de la instancia. Es el período posterior al lanzamiento de una nueva instancia antes de que sus métricas contribuyan a la métrica de escalado. Evita el escalado por falsas métricas hasta que la instancia se estabilice.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application AutoScaling (no confundir con ASG):&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;ECS, cantidad de task.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;. Entender la diferencia entre Provisioned Concurrency y Reserved Concurrency. La primera es la que permite "autoescalar" ➡ muy probable que te planteen una situación con Lambda para resolver temas de concurrencia.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora réplicas&lt;/strong&gt;. Permite liberar la instancia del cluster, teniendo réplicas de lectura (no de escrituras).
&lt;/li&gt;
&lt;li&gt;DynamoDB, puede crear una política de AutoScaling. Luego DynamoDB pública métricas de capacidad consumida CloudWatch ➡ La alarma de CloudWatch invoca a AutoScaling de aplicaciones para evaluar la política de escalado, emite una solicitud &lt;code&gt;UpdateTable&lt;/code&gt; para ajustar el rendimiento aprovisionado de la tabla.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  ELB - High availability
&lt;/h3&gt;

&lt;p&gt;Con el siguiente cuadro podés responder varios casos de uso:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Load Balancer&lt;/th&gt;
&lt;th&gt;Capa&lt;/th&gt;
&lt;th&gt;Protocolos&lt;/th&gt;
&lt;th&gt;Características Clave&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 7 (HTTP/HTTPS)&lt;/td&gt;
&lt;td&gt;HTTP, HTTPS, gRPC, WebSocket&lt;/td&gt;
&lt;td&gt;Enrutamiento basado en contenido (path, host, headers, query strings), targets Lambda, integración con WAF, sticky sessions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network Load Balancer (NLB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 4 (TCP/UDP)&lt;/td&gt;
&lt;td&gt;TCP, UDP, TLS&lt;/td&gt;
&lt;td&gt;Ultra-baja latencia, IP estática por AZ, soporte Elastic IP, preserva la IP de origen, maneja millones de requests/segundo.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gateway Load Balancer (GWLB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 3 (IP)&lt;/td&gt;
&lt;td&gt;IP&lt;/td&gt;
&lt;td&gt;Despliega, escala y gestiona appliances virtuales de terceros (firewalls, IDS/IPS). Usa protocolo GENEVE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Classic Load Balancer (CLB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 4 y 7&lt;/td&gt;
&lt;td&gt;HTTP, HTTPS, TCP, SSL&lt;/td&gt;
&lt;td&gt;Legacy; no recomendado para nuevos despliegues.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Dónde encontrar información detallada como la &lt;strong&gt;dirección IP del cliente, latencias, rutas de solicitud y respuestas del servidor? ➡&lt;/strong&gt; La respuesta es habilitar los &lt;strong&gt;ELB Access Logs&lt;/strong&gt; y almacenarlos en un bucket de S3.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problemas de conectividad.&lt;/strong&gt; Una web inalcanzable con grupos de seguridad correctos ➡ La solución es &lt;strong&gt;permitir puertos efímeros (1024-65535)&lt;/strong&gt; en la &lt;strong&gt;Network ACL (NACL)&lt;/strong&gt;, porque ELB utiliza este rango para comunicarse con los clientes.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky Sessions:&lt;/strong&gt; Caso de uso donde una sola instancia EC2 tiene un uso de CPU del 95% mientras las otras están al 10% ➡ ajustar la expiración de las &lt;strong&gt;cookies de la aplicación&lt;/strong&gt; para rebalancear la carga.
&lt;/li&gt;
&lt;li&gt;Conocer &lt;strong&gt;Cross-Zone Load Balancing p&lt;/strong&gt;ara asegurar una distribución uniforme del tráfico en todas las zonas de disponibilidad habilitadas.
&lt;/li&gt;
&lt;li&gt;Integración de ALB con WAF pero no así con NLB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Placement Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster:&lt;/strong&gt; Agrupa las instancias muy cerca unas de otras en &lt;strong&gt;una única zona de disponibilidad&lt;/strong&gt;. Proporciona una red de baja latencia y alto ancho de banda (hasta 100 Gbps con EFA). &lt;strong&gt;Casos de uso ➡ HPC, Machine Learning,&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition:&lt;/strong&gt; Divide las instancias en particiones lógicas, cada una en racks separados con alimentación y red independientes. Hasta 7 particiones por zona de disponibilidad. Caso de uso ➡ base de datos Distribuidas: Cassandra,HDFS, Kafka&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spread:&lt;/strong&gt; Cada instancia se ubica en un hardware distinto (racks separados). Máximo 7 instancias por zona de disponibilidad por grupo. Caso de Uso ➡ grupos de instancias  que deben aislarse entre sí en hardware separado.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estrategias de Disaster Recovery
&lt;/h3&gt;

&lt;p&gt;Conceptos principales RTO (Recovery Time Objective), RPO (Recovery Point Objective) ➡ mientras mas pequeños mas costosos. &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Este enlace&lt;/strong&gt;&lt;/a&gt; tiene información detallada que vale la pena revisar de las siguientes estrategias:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Estrategia&lt;/th&gt;
&lt;th&gt;RTO&lt;/th&gt;
&lt;th&gt;RPO&lt;/th&gt;
&lt;th&gt;Costo&lt;/th&gt;
&lt;th&gt;Caso de Uso&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backup and Restore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Horas&lt;/td&gt;
&lt;td&gt;Horas&lt;/td&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;Dev/test, no-crítico&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pilot Light&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Varios minutos&lt;/td&gt;
&lt;td&gt;Minutos&lt;/td&gt;
&lt;td&gt;$$&lt;/td&gt;
&lt;td&gt;Apps internas, criticidad moderada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warm Standby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutos&lt;/td&gt;
&lt;td&gt;Segundos–minutos&lt;/td&gt;
&lt;td&gt;$$$&lt;/td&gt;
&lt;td&gt;Apps de negocio críticas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Site Active/Active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Segundos&lt;/td&gt;
&lt;td&gt;Segundos&lt;/td&gt;
&lt;td&gt;$$$$&lt;/td&gt;
&lt;td&gt;Crítico, zero-downtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Clases de S3 -  Versiones - Eventos
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hacer un pequeño cuadro comparativo de versiones de S3 y que casos de uso se utiliza para cada uno. Ejemplo ➡ cuando los patrones de acceso son &lt;strong&gt;impredecibles o cambiantes&lt;/strong&gt; y no querés gestionar manualmente las políticas de ciclo de vida: S3 Intelligent-Tiering
&lt;/li&gt;
&lt;li&gt;S3 Object Lock ➡ cuando se usa Governance mode o Compliance mode.
&lt;/li&gt;
&lt;li&gt;Activación de versionado. Relación con CRR (&lt;em&gt;Cross Region Replication&lt;/em&gt;) y SRR (replicación en misma región pero diferentes cuentas).

&lt;ul&gt;
&lt;li&gt;Resolución de problemas de &lt;strong&gt;latencia o errores&lt;/strong&gt; cuando el S3 es origen de CloudFront: Si hay millones de versiones y las operaciones de listado o ciclo de vida se vuelven pesadas ➡ Configurar &lt;strong&gt;S3 Lifecycle Policies&lt;/strong&gt; para expirar versiones antiguas automáticamente, usando &lt;code&gt;NoncurrentVersionExpiration&lt;/code&gt; para eliminar versiones no actuales después de cierta cantidad de días.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;em&gt;Event notifications&lt;/em&gt; para creación, eliminación, update de objetos. Integración con Lambda, SNS y Eventbridge. &lt;strong&gt;EventBridge&lt;/strong&gt; permite filtrar eventos por prefijo, sufijo, tamaño, etc. y enrutarlos a más de 20 destinos distintos.
&lt;/li&gt;

&lt;li&gt;Conocer las políticas de bucket para responder preguntas sobre que habilita o deniega una policy. Su alcance. Entender que un &lt;strong&gt;Deny explícito siempre gana&lt;/strong&gt;, incluso si hay un Allow en otra política.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  ElastiCache
&lt;/h3&gt;

&lt;p&gt;Conocer las llamadas básicas de la API es básico ➡ saber como crear un cluster &lt;em&gt;CreateCacheCluster&lt;/em&gt;, parámetros importantes como &lt;em&gt;CacheNodeType&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ElastiCache - Memcached&lt;/strong&gt;:  caso de uso que recibe alertas relacionadas con un alto uso de CPU en un clúster de Amazon ElastiCache Memcached, cómo solucionarlo? ➡ Agregar nodos adicionales proporcionará mayor capacidad al clúster o bien según la versión de Memcached se puede escalar verticalmente cambiando el tipo de nodo sin necesidad de crear un nuevo cluster. &lt;strong&gt;No tiene replicación, ni persistencia.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElastiCache - Redis:&lt;/strong&gt; El concepto más importante de esta opción es permitir &lt;strong&gt;Alta Disponibilidad&lt;/strong&gt; a través de sus réplicas: &lt;strong&gt;Multi-AZ con failover automático&lt;/strong&gt;. División en sharding. Compatible con PUB/SUB. Adecuado para conservar sesiones. El soporte más común a resolver por performance es cambiar el tipo de nodo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SSM (Simple Service Manager)
&lt;/h3&gt;

&lt;p&gt;Si no trabajas todos los días con este servicio, es necesario que no solo estudies teoría en cuanto a sus features, sino que también hagas laboratorios.&lt;/p&gt;

&lt;p&gt;Las preguntas que aparecen en el examen son muy variadas en cuanto a la administración de EC2, aplicación de parches, auto aprobaciones de releases y algunas que involucran webhooks con github.&lt;/p&gt;

&lt;p&gt;Recomiendo mirar la sintaxis de los documentos en JSON o Yaml que definen acciones y flujos de trabajo automatizados ➡ hay preguntas sencillas sobre este punto en el examen.&lt;/p&gt;

&lt;p&gt;Enumero algunas situaciones o casos de uso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un concepto sencillo útil: todas las ejecuciones se puede trackear su estatus y log de salidas desde la consola de &lt;em&gt;System Manager&lt;/em&gt;.
&lt;/li&gt;
&lt;li&gt;Se quiere automatizar la creación de Amazon Machine Images (AMIs) personalizadas. Se detecta errores timeout sobre el pipeline de EC2 Image Builder ➡ El agente de AWS Systems Manager (agente SSM) en la instancia de compilación no se está ejecutando o no está accesible.
&lt;/li&gt;
&lt;li&gt;¿Cómo dividimos los parches entre servidores de diferentes cuentas/entornos y SO? ➡ La solución combina &lt;strong&gt;Patch Baselines&lt;/strong&gt; (definen qué parches aplicar, con aprobaciones automáticas y listas de rechazo, por SO) + &lt;strong&gt;Patch Groups&lt;/strong&gt; (agrupan nodos mediante tags y los asocian a una Baseline específica).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Runbook&lt;/strong&gt;: Tipo específico de documento de Systems Manager (SSM) que define las acciones exactas que realiza el servicio Automation. Define yaml o json para orquestar un workflow de ejecución.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Por lo general, se usa para la remediación asociado con &lt;em&gt;AWS Config.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Usando AWS Organizations se necesita automatizar el proceso de stopping de instancias EC2 no productivas fuera del horario de negocio de todos las cuentas miembro ➡ Crear roles de IAM en cada cuenta con los permisos adecuados, utilizar Systems Manager Automation con una cuenta de administrador delegado para ejecutar un runbook que dirija los recursos de toda la organización según los tags.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch Policies:&lt;/strong&gt; se utiliza para Organizations. Tener en claro que centraliza el control para definir patching schedules y baselines para múltiples cuentas y regiones
&lt;/li&gt;
&lt;li&gt;Revisar los documentos de ejecuciones más importantes en SSM.
&lt;/li&gt;
&lt;li&gt;Integraciones por eventos externos, tener claro cómo trabaja en los diferentes casos webhooks (con avisos de software de terceros). Que flujos podemos tener:

&lt;ul&gt;
&lt;li&gt;Webhook → API GW → Lambda → SSM
&lt;/li&gt;
&lt;li&gt;GitHub Actions → SSM (puedes filtrar exactamente por eventos RELEASED o PRERELEASED).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EventBridge Partner Events → GitHub&lt;/strong&gt; como fuente nativa de eventos → SSM Automation o Lambda como destino.
&lt;/li&gt;
&lt;li&gt;EventBridge Scheduler → Lambda consulta GitHub API → escribe versión en SSM Parameter Store (como fuente de verdad para actualizaciones)
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;AWS RunRemoteScript: Documento SSM que permite ejecutar scripts alojados en S3 o GitHub directamente en los nodos, sin necesidad de copiarlos previamente.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Servicios y estrategias de Deployment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Casos de uso de AWS Code Pipeline, y los otros servicios que involucra ➡ Code Commit o GitHUb, Code Build, Code Deploy o CloudFormation. Entender donde se integran los test y tener en cuenta que se puede agregar una &lt;strong&gt;aprobación&lt;/strong&gt; vía sns antes de realizar deploy.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodePipeline&lt;/strong&gt; puede implementar CloudFormation StackSets, lo que permite implementaciones en múltiples cuentas y regiones como parte de una canalización de CI/CD.
&lt;/li&gt;
&lt;li&gt;Estrategias de deployment:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-place (All-at-once):&lt;/strong&gt; Implementa en todas las instancias simultáneamente. Downtime Posible. Es Lento (redeploy versión antigua). Se usa en entornos de desarrollo, cuando el downtime es aceptable
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling:&lt;/strong&gt; Reemplazar instancias en lotes. Downtime Mínimo. Se usa cuando querés ahorrar costos sin aprovisionar nueva infraestructura.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling with additional batch:&lt;/strong&gt; Agrega nuevas instancias antes de eliminar las antiguas. Downtime mejor que el anterior. Se usa cuando necesitas mantener capacidad completa durante el deploy
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutable:&lt;/strong&gt; Inicia instancias completamente nuevas; intercambia cuando estén en buen estado. Se usa en producción donde el rollback rápido es crítico.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue/Green:&lt;/strong&gt; Ejecuta dos entornos idénticos; cambia el tráfico. Downtime mínimo.  Se usa cuando necesitás rollback instantáneo y podés asumir el costo de doble infraestructura.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary:&lt;/strong&gt; Desvía primero un pequeño % del tráfico; lo va ampliando si todo está OK. Es rápido. Se usa para validar un release con un subconjunto de usuarios antes de rollout completo.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cloudformation
&lt;/h3&gt;

&lt;p&gt;Es un servicio fundamental en el examen. Conocer las secciones principales de la plantilla (JSON o YAmL) : Metadata, Parameters, Mappings (caso de uso de parametrización de Amazon Machine Images (AMIs) ), Conditions, Transform (SAM), Resources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una buena práctica para familiarizarse con la sintaxis de los templates es construir arquitecturas clásicas que utilicen EC2, ASG, ALB, RDS con Secret Manager, entre otras.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comandos CLI importantes:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aws cloudformation create-stack&lt;/code&gt; ➡ crea un stack nuevo
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws cloudformation deploy&lt;/code&gt; ➡ crea o actualiza un stack usando un template empaquetado (más usado en CI/CD)
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws cloudformation detect-drift&lt;/code&gt; ➡ inicia la detección de drift en un stack
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws cloudformation describe-stack-drift-detection-status&lt;/code&gt; ➡ consulta el resultado de la detección
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws cloudformation cancel-update-stack&lt;/code&gt; ➡ cancela una actualización en curso
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;StackSet&lt;/strong&gt; ➡ caso de uso importante de cuentas asociadas en Organizations. Gestiona de manera  centralizada para implementar cambios en las stack en varias cuentas. Se puede usar StackSets para crear, actualizar o eliminar stacks en varias cuentas y regiones de AWS con una sola operación.
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Intrinsic functions:&lt;/strong&gt; comandos que se utilizan dentro de las plantillas para asignar valores a propiedades que no están disponibles hasta el momento de la implementación (en tiempo de ejecución) ➡ revisar implementaciones de las mas importantes: &lt;code&gt;Ref&lt;/code&gt; &lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;Fn::GetAtt&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;Fn::ImportValue&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;Fn::FindInMap&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;Fn::ForEach&lt;/code&gt;&lt;strong&gt;,&lt;/strong&gt; &lt;code&gt;Fn::Transform ...&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;NestedStack:&lt;/strong&gt; &lt;code&gt;AWS::CloudFormation::Stack&lt;/code&gt; &lt;strong&gt;➡ mecanismo&lt;/strong&gt; para crear infraestructura &lt;strong&gt;modular, reutilizable y gestionable&lt;/strong&gt; mediante la integración de una pila dentro de otra. En lugar de crear una plantilla única y masiva que resulta difícil de mantener, se puede dividir la arquitectura en componentes lógicos (por ejemplo, VPC, base de datos, capa de aplicación) y referenciarlos como pilas separadas dentro de una plantilla "raíz" o "padre".
&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Mecanismos de protección en CloudFormation:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack Policy:&lt;/strong&gt; documento JSON que restringe qué recursos pueden ser actualizados. Ideal para proteger RDS de producción de cambios accidentales.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Termination Protection:&lt;/strong&gt; protección a nivel stack contra eliminación accidental. Se hereda a los nested stacks.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeletionPolicy:&lt;/strong&gt; &lt;code&gt;Retain&lt;/code&gt; conserva el recurso, &lt;code&gt;Snapshot&lt;/code&gt; genera snapshot antes de eliminar (soportado en RDS, EBS). La situación típica que se plantea, como conservar una RDS si se elimina el stack de cloudformation?
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UpdateReplacePolicy:&lt;/strong&gt; igual que DeletionPolicy pero aplica cuando un update requiere reemplazar el recurso físico.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift Detection:&lt;/strong&gt; detecta recursos cuya configuración real difiere del template, útil para identificar cambios manuales que bypasearon el pipeline.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Profundizar en los Script Auxiliares más importantes, mecanismos para sincronizar la creación o actualización de recursos, para que el stack espere hasta que la configuración esté lista antes de continuar.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cfn-init&lt;/code&gt; → lee y ejecuta la sección &lt;code&gt;AWS::CloudFormation::Init&lt;/code&gt; del template (instala paquetes, archivos, servicios). Se ejecuta durante el User Data.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cfn-signal&lt;/code&gt; ➡ Generalmente se implementa al final de un script de &lt;em&gt;User Data&lt;/em&gt;. Un patrón común es ejecutarlo después de &lt;code&gt;cfn-init&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tener en cuenta los atributos que requieren señales como &lt;strong&gt;CreationPolicy&lt;/strong&gt; (puede estar asociados con &lt;code&gt;AWS::EC2::Instance&lt;/code&gt; o &lt;code&gt;AWS::AutoScaling::AutoScalingGroup&lt;/code&gt; ) y los tipos de &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-updatepolicy.html" rel="noopener noreferrer"&gt;&lt;strong&gt;UpdatePolicy&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;(con WaitOnResourceSignals).&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;CloudFormation Traffic Shifting&lt;/strong&gt;  ➡ se implementa definiendo &lt;code&gt;DeploymentPreference&lt;/code&gt; en el template. CodeDeploy ejecuta el shifting según la estrategia elegida (Canary, Linear, AllAtOnce).&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;p&gt;Evaluar y seleccionar que tipo de Storage es adecuado a un caso de uso. Si estudiaste para la certificación de SAA son preguntas similares.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storage en entornos híbridos ➡ &lt;strong&gt;Storage Gateway&lt;/strong&gt; y todas sus variantes

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 File Gateway:&lt;/strong&gt; Acceso NFS/SMB a objetos en S3. Los archivos se almacenan como objetos nativos. Caso de uso: migración de archivos on-premise a S3.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume Gateway:&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Cached mode:&lt;/em&gt; datos principales en S3, caché local de los más accedidos.
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Stored mode:&lt;/em&gt; datos principales on-premise, backup asíncrono a S3 como EBS snapshots.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tape Gateway:&lt;/strong&gt; Reemplaza cintas físicas. Importante es que se expande automáticamente, esta última característica puede ser un caso de uso que aparezca en el examen.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FSx File Gateway:&lt;/strong&gt; Caché local de FSx for Windows File Server para acceso de baja latencia desde on-premise.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;FSx for Windows FileServer y su otras variantes, cuando usarlo?

&lt;ul&gt;
&lt;li&gt;Es MultiAZ, es una feature importante.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow Copies:&lt;/strong&gt; los usuarios finales puedan ver y restaurar por sí mismos versiones anteriores de archivos o carpetas individuales sin depender de otro mecanismo.
&lt;/li&gt;
&lt;li&gt;Usado con Windows SMB, Active Directory.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;FSx for Lustre ➡ HPC, ML, high throughput
&lt;/li&gt;

&lt;li&gt;EFS (Elastic File System) comprender el caso de uso que puede compartir storage entre varias EC2, Linux NFS.

&lt;ul&gt;
&lt;li&gt;Multi-AZ
&lt;/li&gt;
&lt;li&gt;Performance modes: general Purpose y MaxIO
&lt;/li&gt;
&lt;li&gt;Throughput Modes: Elastic (recomendado), Provisioned, Bursting.
&lt;/li&gt;
&lt;li&gt;Conocer qué EFS tiene Storage Class y  Lifecycle Policies ➡ aunque suele no aparecer en el examen.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;EBS&lt;/strong&gt; ➡ siempre en el contexto de EC2 y la elección del disco para optimizar las cargas de trabajo: &lt;strong&gt;gp3&lt;/strong&gt; sobre gp2 sin incrementar los costos, &lt;strong&gt;gp3&lt;/strong&gt; permite configurar IOPS y throughput &lt;strong&gt;independientemente&lt;/strong&gt; del tamaño del volumen. Para transacciones, persistencia y durabilidad es &lt;strong&gt;io2&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Data Lifecycle Manager (DLM)&lt;/strong&gt;: Automatiza la creación, retención y eliminación de instantáneas de EBS y AMI respaldadas por EBS mediante políticas de ciclo de vida.
&lt;/li&gt;
&lt;li&gt;EBS Snapshot: Una copia de seguridad puntual de un volumen EBS almacenado en S3 (gestionado por AWS). Los snapshot son incrementales.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  RDS y Aurora
&lt;/h3&gt;

&lt;p&gt;Los accesos se deben guardar en Secrets Manager y luego las aplicaciones acceden a eso. Característica importante &lt;em&gt;Cross-Region backup replication.&lt;/em&gt; Tanto RDS como Aurora tienen PITR (Point-in-Time Restore).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RDS Multi-AZ&lt;/strong&gt; ➡ standby síncrono en otra AZ, failover automático pero más lento &lt;em&gt;que Aurora.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Se puede asociar con el servicio &lt;strong&gt;RDS Proxy&lt;/strong&gt; para el manejo de conexiones.
&lt;/li&gt;
&lt;li&gt;Aurora ➡  Almacenamiento distribuido que se replica automáticamente en 3 AZs, hasta 15 réplicas de lectura, failover automático en segundos.
&lt;/li&gt;
&lt;li&gt;Aurora Serverless v2 ➡ la característica importante es que escala a "cero" y no genera costo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  DynamoDB
&lt;/h3&gt;

&lt;p&gt;Las preguntas se centran en resolver problemas de performance; cómo volver a un recovery point. Ejemplo:  La tabla experimenta picos de tráfico incrementales durante el horario laboral, con patrones predecibles, cuando se producen millones de lecturas y escrituras. Hay que configurar una solución para aumentar la capacidad de procesamiento ➡ Habilitar el escalado automático en la tabla de DynamoDB. Configurar una política de escalado automático para ajustar el tamaño en función de las unidades de capacidad de lectura y escritura.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Point-in-Time Recovery (PITR) ➡&lt;/strong&gt; backup continuo, permite restaurar a cualquier &lt;strong&gt;segundo&lt;/strong&gt; de los últimos 35 días.
&lt;/li&gt;
&lt;li&gt;Para esto debemos conocer los tipos de Lectura &lt;em&gt;Eventually Consistent Read&lt;/em&gt; (por defecto) y &lt;em&gt;Strongly Consistent Read&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Los diferentes modos de capacidad &lt;em&gt;On-Demand y Provisioned&lt;/em&gt; &lt;strong&gt;(profundizar en estos dos conceptos).&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB Global Tables&lt;/strong&gt; &lt;em&gt;➡&lt;/em&gt; replicación multirregional y multiactiva. Cada región puede aceptar lecturas y escrituras, y los cambios se replican de forma asíncrona. Esto permite la escalabilidad global y la recuperación ante desastres.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Encriptación en reposo, tránsito y uso de KMS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Encriptación con S3 de KMS: &lt;strong&gt;SSE-S3&lt;/strong&gt; (clave gestionada por AWS), &lt;strong&gt;SSE-KMS&lt;/strong&gt; (usás una KMS key propia, permite auditoría via CloudTrail y control de acceso granular) y &lt;strong&gt;SSE-C&lt;/strong&gt; (el cliente provee la clave, AWS no la almacena). Si la pregunta menciona &lt;strong&gt;auditoría de uso de claves o control granular&lt;/strong&gt; ➡ SSE-KMS.
&lt;/li&gt;
&lt;li&gt;Clave simetrica y asimetrica son básicos. Aunque en el examen no hay preguntas profundas sobre este punto.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Policies:&lt;/strong&gt; Cada clave KMS tiene una política de claves. Una política basada en recursos que controla quién puede usar y administrar la clave. A diferencia de las políticas de IAM, la política de claves es el principal mecanismo de control de acceso para las claves KMS.
&lt;/li&gt;
&lt;li&gt;Rotación de Key, hay 3 variantes:

&lt;ul&gt;
&lt;li&gt;automática, para customer managed keys puede habilitar la rotación automática
&lt;/li&gt;
&lt;li&gt;manual u On-demand por medio de trigger.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Importación de material de clave de un cliente en KMS, pero no rota automáticamente.
&lt;/li&gt;

&lt;li&gt;Replicación AWS KMS Multi-Region Keys: Una clave KMS que existe en una región diferente, pero comparte el mismo ID de clave y material de clave que su clave principal.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  AWS Certificate Manager (ACM)
&lt;/h3&gt;

&lt;p&gt;Las preguntas son sencillas, si se habla que servicio proporciona  certificados SSL/TLS la respuesta es  ACM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revisar los casos de uso de como integrar ACM con ELB, CloudFront, ApiGateway, Elastic Beanstalk, Cognito.
&lt;/li&gt;
&lt;li&gt;Tipos de certificados ➡ públicos, privados e importación de terceros. Identificar los casos de uso para poder resolver situaciones.
&lt;/li&gt;
&lt;li&gt;Mecanismo de renovación e importación de certificados de terceros.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seguridad IAM
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pensar que todo servicio para ejecutar algo debe tener un rol de IAM asociado. En ciertas ocasiones para permitir que usuarios utilicen servicios o listen ciertas features de manera temporal podemos agregar inline policies y luego eliminarlas.
&lt;/li&gt;
&lt;li&gt;Casos de uso de &lt;code&gt;iam:PassRole&lt;/code&gt;. Entender por qué es una capa de seguridad importante para proteger el entorno de AWS de actividades no deseadas y de la escalada de privilegios.
&lt;/li&gt;
&lt;li&gt;Caso de uso de credenciales temporales&lt;code&gt;iam:AssumeRole.&lt;/code&gt; Ejemplo ➡ la entidad en la Cuenta A utiliza para ejecutar servicios en la Cuenta B como si fuera un usuario local en esa cuenta.
&lt;/li&gt;
&lt;li&gt;Federación de Identidades: Permite a los usuarios autenticados por un proveedor de identidad externo (IdP) acceder a AWS sin crear usuarios de IAM individuales. Conocer los casos de uso:

&lt;ul&gt;
&lt;li&gt;SAML 2.0: se integra con Microsoft Active Directory Federation Services (ADFS) u Okta. Los usuarios se autentican con sus credenciales corporativas y reciben credenciales temporales de AWS.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;OpenID Connect (OIDC): &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;para aplicaciones web y móviles&lt;/strong&gt;. Admite proveedores de identidad como Google, Facebook o cualquier proveedor compatible con OIDC. Solución de error "Not authorized to perform sts:AssumeRoleWithWebIdentity" ➡ agregar el ARN del proveedor de identidad en el elemento principal de la política de confianza (trust policy).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AWS Organizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OU (Organizational Units):&lt;/strong&gt; las cuentas se agrupan en OUs y los SCPs se aplican a nivel OU o cuenta individual, heredando hacia abajo en la jerarquía.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegated Administrator:&lt;/strong&gt; permite que una cuenta miembro (no la cuenta de administración) gestione servicios específicos como Security Hub, GuardDuty o Config para toda la organización.
&lt;/li&gt;
&lt;li&gt;Relaciona el concepto de administrar muchas cuentas con &lt;strong&gt;Service control policies&lt;/strong&gt; (SCPs). Recordar que &lt;strong&gt;&lt;em&gt;afectan únicamente a los usuarios y roles de IAM&lt;/em&gt;&lt;/strong&gt; gestionados por las cuentas que forman parte de la organización. SCPs no afectan directamente a las políticas basadas en recursos.
&lt;/li&gt;
&lt;li&gt;Políticas de tagging. Se puede asegurar que los usuarios no puedan crear recursos sin una etiqueta específica.
&lt;/li&gt;
&lt;li&gt;Integración con &lt;strong&gt;&lt;em&gt;CloudFormation StackSet.&lt;/em&gt;&lt;/strong&gt; Definir un StackSet que implementa una plantilla base (almacenada en S3) en toda una Unidad Organizativa (OU) en varias regiones utilizando &lt;em&gt;Service-Managed permission model&lt;/em&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consolidated Billing:&lt;/strong&gt; todas las cuentas miembro consolidan el pago en la cuenta de administración, permite aprovechar descuentos por volumen y Reserved Instances compartidas entre cuentas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;AWS Artifact&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Su caso de uso es acceder &lt;strong&gt;a informes de auditoría.&lt;/strong&gt; Se puede descargar bajo demanda certificaciones de cumplimiento normativo (como SOC 1/2/3, ISO/IEC, y PCI DSS).&lt;/p&gt;




&lt;p&gt;Este apunte continua en &lt;strong&gt;➡&lt;/strong&gt; &lt;a href="https://dev.to/reinalau/apuntes-para-la-certificacion-aws-cloudops-engineer-associate-soa-c03-parte-2-2e04"&gt;&lt;strong&gt;&lt;em&gt;Apuntes para la certificación AWS CloudOps Engineer Associate (SOA-C03) - Parte 2&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sysops</category>
      <category>aws</category>
      <category>certification</category>
    </item>
    <item>
      <title>Patrones de acceso seguro a S3 🌐</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:09:23 +0000</pubDate>
      <link>https://dev.to/aws-builders/patrones-de-acceso-seguro-a-s3-l7c</link>
      <guid>https://dev.to/aws-builders/patrones-de-acceso-seguro-a-s3-l7c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Este análisis se propone clarificar algunos casos de uso donde se requiere servir contenido de S3 de manera segura, identificando las brechas de seguridad a las que debemos enfrentarnos y explorar posibles mejoras. El desafío real no está en analizar cada servicio por separado, sino en combinarlos correctamente.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. CloudFront + OAC con presigned url
&lt;/h2&gt;

&lt;p&gt;Caso de uso frecuente: Un sitio web que muestra imágenes/vídeos de una temática determinada. Una parte de los objetos multimedia puede restringirse a que solo lo vean usuarios que iniciaron sesión vía Cognito. Aquí la solución más sencilla:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload de Multimedia (escritura)&lt;/strong&gt;&lt;br&gt;
El frontend solicita una Presigned URL a un endpoint protegido por Cognito (una api). La api se integra con una función Lambda que genera una URL temporal (válida por x minutos) firmada con permisos de "s3:PutObject" para el bucket privado de imágenes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visualización de Multimedia (lectura)&lt;/strong&gt;&lt;br&gt;
El bucket de S3 tiene el bloqueo de acceso público activado.&lt;br&gt;
Los elementos multimedia se sirven a través de CloudFront, utilizando OAC (Origin Access Control) para firmar internamente la petición (usando el protocolo SigV4) y solicitar el objeto a S3 de forma segura.&lt;br&gt;
La política (Bucket Policy) del bucket S3 solo permite la acción "s3:GetObject" si la petición proviene específicamente del ARN de la distribución de CloudFront ("Condition: AWS:SourceArn").&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0j2r3afdmhfgjhewpj5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0j2r3afdmhfgjhewpj5.jpg" alt="CloudFront + OAC con presigned url" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conceptualmente una &lt;strong&gt;Presigned URL&lt;/strong&gt; se puede utilizar tanto para cargar como para leer objetos, en el ejemplo anterior notar que solo se usa para cargar los objetos a S3*.* Una presigned URL es simplemente una URL con la firma embebida ➡ cualquiera que la tenga puede usarla hasta que expire, sin autenticación. Por eso debemos aplicar diferentes técnicas de seguridad.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La url puede tener una duración desde 1 minuto hasta 12 horas, si alguien no autorizado consigue la url y ya caduco ➡ NO la puede utilizar.&lt;/li&gt;
&lt;li&gt;Con el uso de Cognito, el token JWT tiene una duración y se puede aplicar este control con &lt;strong&gt;Cognito JWT Authorizer&lt;/strong&gt; (nativo de API Gateway). La otra posibilidad es tener Lambda Authorizer en API Gateway. Esta última tiene lógica de autorización custom.&lt;/li&gt;
&lt;li&gt;Cuando generamos una presigned url para upload, se puede controlar que tipos de objetos cargar y su peso máximo. Algunos ejemplos:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generate presigned URL for PUT
# ContentType specifies the types of objects and controls their size on the frontend
&lt;/span&gt;&lt;span class="n"&gt;upload_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_presigned_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;put_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;IMAGES_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;imagen_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ContentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected_content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ExpiresIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PRESIGNED_URL_TTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# the frontend makes a multipart POST request
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_presigned_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;IMAGES_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;imagen_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected_content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expected_content_type&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length-range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5242880&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# máx 5MB
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;ExpiresIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PRESIGNED_URL_TTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.2 Caso de uso complementario - Validación de acceso en el edge (Lambda@Edge)
&lt;/h3&gt;

&lt;p&gt;Podemos enfrentarnos a dos requerimientos no funcionales: minimizar la latencia y asegurar quién puede hacer request a Cloudfront.&lt;br&gt;
El caso anterior asegura que solo CloudFront puede acceder a S3, pero no controla quién puede hacer requests a CloudFront. Con Lambda@Edge es posible agregar lógica de autorización en los nodos de la CDN, antes de que el request llegue al origen. La secuencia es la siguiente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuando un usuario solicita un objeto de un bucket, CloudFront invoca una función Lambda@Edge en el nodo más cercano geográficamente.&lt;/li&gt;
&lt;li&gt;La función valida el JWT de Cognito (o cualquier token de sesión) sin necesidad de redirigir el request al backend.&lt;/li&gt;
&lt;li&gt;Si el token es válido, el request continúa hacia S3 a través de OAC normalmente.&lt;/li&gt;
&lt;li&gt;Si no es válido, Lambda@Edge retorna un &lt;code&gt;401&lt;/code&gt; o redirige al login desde el edge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ La ventaja es la &lt;strong&gt;baja latencia&lt;/strong&gt;, la validación ocurre en el edge node más cercano al usuario, sin necesidad de viajar hasta la región de AWS donde vive el backend. Cada request de contenido protegido se resuelve en milisegundos en lugar de sumar el tiempo de ida y vuelta a tu servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Descarga temporal de un archivo privado - S3 Presigned URLs
&lt;/h2&gt;

&lt;p&gt;Para esta opción el casos de uso típico: Un usuario necesita descargar un reporte generado, una factura o un documento legal guardado en bucket S3. No se utiliza Cloudfront porque no necesitamos cachear contenido de la petición.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fougq75ayilzbgr5wpfaq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fougq75ayilzbgr5wpfaq.jpg" alt="S3 Presigned URLs" width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Se produce la petición del usuario, el backend valida que ese usuario tiene permiso sobre ese archivo específico.&lt;/li&gt;
&lt;li&gt;Lambda genera una &lt;strong&gt;presigned URL&lt;/strong&gt; con &lt;code&gt;GetObject&lt;/code&gt; que expire entre 5-15 min. La lógica de validación de lambda es idéntica del caso de uso 1 de Upload.&lt;/li&gt;
&lt;li&gt;El browser descarga directamente desde S3 usando esa URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔐 &lt;strong&gt;Brechas de seguridad detectadas&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Riesgos de seguridad&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mitigación posible&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;El usuario la copia y la comparte intencionalmente o no.&lt;/td&gt;
&lt;td&gt;TTL muy corto (1-2 minutos si es descarga inmediata)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queda en logs del browser, proxies o herramientas de red&lt;/td&gt;
&lt;td&gt;Generar la URL y redirigir directamente (&lt;code&gt;302&lt;/code&gt;) en lugar de devolverla al frontend (nunca queda expuesta en el browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Se filtra por un &lt;code&gt;Referer&lt;/code&gt; header en un redirect&lt;/td&gt;
&lt;td&gt;Incluir el &lt;code&gt;userId&lt;/code&gt; en la key del objeto (&lt;code&gt;uploads/{userId}/{filename}&lt;/code&gt;) para que aunque alguien obtenga la URL, solo pueda acceder al archivo de ese usuario específico.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Distribución segura con cookies por sesión - CloudFront Signed Cookies
&lt;/h2&gt;

&lt;p&gt;Caso de uso típico: Una plataforma web con contenido que se accede por suscripción, los usuarios se loguean pero cada sesión debe tener acceso acotado en el tiempo. En lugar de firmar URL por URL, el backend emite una &lt;strong&gt;Signed Cookie de CloudFront&lt;/strong&gt; al momento del login, que el browser envía automáticamente en cada request de objetos multimedia.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4ofyfbqg1jfvgil5q39.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4ofyfbqg1jfvgil5q39.jpg" alt="CloudFront Signed Cookies" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El backend verifica la sesión y envía tres headers &lt;code&gt;Set-Cookie&lt;/code&gt; al browser (CloudFront requiere exactamente tres pares clave-valor para una Signed Cookie). Estos headers deben enviarse &lt;strong&gt;antes&lt;/strong&gt; de que el usuario solicite el contenido privado.&lt;/li&gt;
&lt;li&gt;Si se configura un tiempo de expiración corto, el backend puede reenviar tres nuevos headers &lt;code&gt;Set-Cookie&lt;/code&gt; en requests posteriores para mantener el acceso activo.&lt;/li&gt;
&lt;li&gt;CloudFront valida las cookies en cada request antes de servir los objetos multimedia. La distribución debe tener al menos dos comportamientos de &lt;strong&gt;cache&lt;/strong&gt;: uno público y uno que requiere autenticación. La sección privada redirige al login si no hay cookie válida.&lt;/li&gt;
&lt;li&gt;Al cerrar sesión o expirar las cookies, el acceso se revoca sin cambiar las URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Esta opción es Ideal cuando un usuario accede a múltiples objetos multimedia de una misma sección sin generar una firma por cada archivo.&lt;br&gt;
⭐ Si CloudFront se configura para cachear basándose en cookies, NO generará entradas de caché separadas por los atributos de las &lt;em&gt;Signed Cookies&lt;/em&gt;. Es decir, todos los usuarios autorizados compartirán la misma entrada de caché para un objeto determinado— CloudFront valida la cookie para permitir o denegar el acceso, pero la ignora al construir la cache key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Aspectos generales a tener en cuenta
&lt;/h2&gt;

&lt;p&gt;➡ &lt;strong&gt;Diseño&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Considerar Patrón BFF (Backend for Frontend)&lt;/strong&gt;. Para el caso de descarga privada, se puede interponer una capa BFF entre el frontend y API Gateway. Esta capa maneja la sesión del usuario y puede ejecutar un redirect &lt;code&gt;302&lt;/code&gt; directo hacia S3, evitando que la presigned URL quede expuesta en el browser.&lt;/p&gt;

&lt;p&gt;➡ &lt;strong&gt;Performance/Costo&lt;/strong&gt;&lt;br&gt;
Invalidaciones de CloudFront (incurre costo después de 1000/mes). Además del costo analizar tiempo de cacheo de datos.&lt;/p&gt;

&lt;p&gt;S3 request rates (3500 PUT/5500 GET por segundo por prefix, si el volumen es alto distribuir con prefijos).&lt;/p&gt;

&lt;p&gt;Costo de Lambda por invocación para generar presigned URLs a escala.&lt;/p&gt;

&lt;p&gt;➡ &lt;strong&gt;Seguridad&lt;/strong&gt;&lt;br&gt;
Rotación de claves KMS si el bucket usa SSE-KMS.&lt;/p&gt;

&lt;p&gt;Versionado de objetos en S3 para evitar sobreescritura accidental.&lt;/p&gt;

&lt;p&gt;➡ &lt;strong&gt;Operacional&lt;/strong&gt;&lt;br&gt;
TTL de presigned URLs vs tiempo de expiración del JWT — si el JWT expira antes que la URL, tenemos un problema!&lt;/p&gt;

&lt;p&gt;Estrategia de nombres de objetos (&lt;code&gt;userId/filename&lt;/code&gt;) para evitar colisiones y facilitar políticas IAM por prefijo.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>cloudfront</category>
      <category>s3</category>
    </item>
    <item>
      <title>Pensar en Serverless para crear bellos sueños 🦋</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Fri, 10 Apr 2026 12:56:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/pensar-en-serverless-para-crear-bellos-suenos-390</link>
      <guid>https://dev.to/aws-builders/pensar-en-serverless-para-crear-bellos-suenos-390</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkt4xky3sc2hei44br55.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkt4xky3sc2hei44br55.jpg" alt="Mariposa Monarca" width="688" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;En estas líneas te explico una sencilla arquitectura para desplegar un sitio web íntegramente utilizando servicios Serverless que podría servir, como en mi caso, para mostrarle al mundo mi interés por la ecología (con repositorio github incluido)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En un mundo digital donde se crean productos financieros, web que muestran dashboard de eficiencias, p&amp;amp;L y todo tipo de temáticas asociada al dinero ...y que está muy bien! olvidamos que nuestro hábitat natural necesita ayuda.&lt;br&gt;
Hace unos años me percate de esto y comencé a leer/hablar de árboles, arbustos y plantas mal llamadas malezas - nativas de mi región que son fuente de alimento de mariposas e insectos que son fundamentales para la biodiversidad - que están desapareciendo con el avance de las especies exóticas, los agroquímicos y la destrucción de humedales.&lt;br&gt;
La mayoría de las personas cuando comentas la problemática te dice "OK estoy de acuerdo contigo" pero no activan en tener un pequeño jardín de nativas o a la hora de elegir un árbol para su vereda priorizan especies exóticas solo por estetica, sin saber que muchas nativas son igual de hermosas y además sostienen la fauna local (hablo especialmente de la Provincia de Buenos Aires, Argentina).&lt;/p&gt;

&lt;p&gt;....Y qué pasaría si cambio mi discurso en mi afán de convencer y digo: &lt;strong&gt;"si no ayudamos plantando nativas en nuestros hogares las mariposas pueden desaparecer"&lt;/strong&gt; ¿Suena drástico? pero en gran parte es cierto. Estudios afirman que la población ha disminuido muy rápido, especialmente la mariposa &lt;strong&gt;Bandera Argentina&lt;/strong&gt; debido a que no encuentra su planta nutricia (Coronillo) donde desovar sus huevos 💔 Cabe aclarar que la disminución de mariposas afecta directamente a la polinización y con esto atenta contra la seguridad alimentaria.&lt;br&gt;
Por todo lo anterior y para bajar un poco el dramatismo decidí hacer algo: crear un proyecto web que muestre mariposas e indique la planta nativa nutricia que alimenta sus orugas! Y que los usuarios registrados puedan cargar avistamientos de mariposas en la región. Para acompañar esta idea pensé en una arquitectura Serverless, que reduce la huella de carbono y acompaña mi pensamiento ecológico 🌱.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comencemos analizando la arquitectura
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksxqm8z5i19gaiscwxpi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksxqm8z5i19gaiscwxpi.png" alt="Diagrama de Arquitectura" width="800" height="509"&gt;&lt;/a&gt;&lt;br&gt;
Como podés observar el laboratorio es de un nivel entre 100 y 200 (si necesitas ayuda me comentas).&lt;br&gt;
Implementa una arquitectura Serverless de &lt;strong&gt;3 capas&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Web Tier:&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;S3-Sitio Web + CloudFront&lt;/strong&gt; ➡ contiene todo el sitio web creado con react. El código está preparado para integrar con los servicios &lt;strong&gt;Apigateway y Cognito&lt;/strong&gt;. Cloudfront servicio para servir el sitio web de S3. Este s3 no es público, solo se accede a través de Cloudfront.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;App Tier:&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Cognito + API Gateway + Lambda&lt;/strong&gt; ➡ Cognito realiza la autenticación del usuario que se inicia sesión. Se aclara que los usuarios se deben crear manualmente por la Consola de AWS o por cli (explicado en el Readme de repositorio). Las Lambdas de crear imagen, get imagen, presigned_URL y delete imagen(funcionalidad no desarrollada aún desde el frontend) se invocan via Apigateway. Solo los usuarios autorizados por Cognito pueden acceder a estas funcionalidades.&lt;br&gt;
Las lambdas están codificadas en Python.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data Tier:&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;DynamoDB + S3 - imágenes&lt;/strong&gt; ➡ Toda la información de la fichas de las imágenes de mariposas se guarda en DynamoDB incluyendo la presigned-URL de S3-imagenes. En este último bucket se guardarán las imágenes de mariposas subidas por el usuario.&lt;br&gt;
Aunque &lt;strong&gt;Route53&lt;/strong&gt; aparece en el diagrama, en el repo no lo vas a ver codificado en la IaC. Por qué? porque para el entorno educativo del laboratorio no es necesario, con la url de cloudfront el sitio web queda totalmente funcional.&lt;/p&gt;

&lt;p&gt;⭐ El objetivo es construir una aplicación real con autenticación, subida de archivos y CRUD completo utilizando solo servicios serverless de AWS, con la opción de deployar en CloudFormation o Terraform. Porque también es muy importante aprender las diferentes formas de desplegar sin tocar el Console Home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como utilizar el repositorio de código
&lt;/h2&gt;

&lt;p&gt;✅ Ten en cuenta que como requisito para comenzar con este ejercicio necesitas instalado Python, Terraform, AWS Cli (todo explicadisimo en el repo 🤓)&lt;/p&gt;

&lt;p&gt;El código se encuentra en una url de un repositorio padre &lt;strong&gt;labs-aws-llb&lt;/strong&gt; donde hay otros laboratorios, pero el que nos interesa es: &lt;a href="https://github.com/reinalau/labs-aws-llb/tree/main/webAppServerless" rel="noopener noreferrer"&gt;webAppServerles&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Comenzar leyendo Arquitectura.md que explica un poco la secuencia de las acciones en los componentes y las decisiones de Arquitectura, como por ejemplo por qué no pasar las imagenes por el payload de Lambda.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leer el &lt;strong&gt;Readme.md&lt;/strong&gt; principal: contiene el paso a paso que debes hacer y si decides hacer el deploy con terraform o cloudformation te indicará ir a su correspondiente directorio. Cada directorio también contiene su Readme.md.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leer &lt;strong&gt;Integration.md&lt;/strong&gt; : te vas a encontrar que una vez deployada la IaC en AWS, solo necesitarás tomar los outputs de terraform o el descripción del stack de cloudformation y configurar el archivo &lt;strong&gt;aws-config.ts&lt;/strong&gt; . Luego volverás al &lt;strong&gt;Readme principal&lt;/strong&gt; para seguir el paso a paso ➡ volver a subir la web buildeada a S3 y crear una invalidación de CloudFront para actualizar la web integrada con el backend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;El laboratorio tiene un costo mínimo (si estás en la capa gratuita cero) Como siempre recomiendo limpiar todo lo desplegado. Si estas pensando en crear una web funcional estable ten en cuenta los costos. Información detallada en Readme principal.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Funcionalidades
&lt;/h2&gt;

&lt;p&gt;Una vez deployado el sitio web siguiendo las indicaciones, tenemos las siguientes funcionalidades.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Usuario anónimo&lt;/th&gt;
&lt;th&gt;Usuario logueado&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ver catálogo de mariposas precargadas&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ver galería de avistamientos de usuarios&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subir foto + metadata de avistamiento&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtrar por ecorregión&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ver ficha detallada de mariposa&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;td&gt;🦋&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Después de todo el trabajo... con la url de Cloudfront descubrirás esta hermosa web funcional de mariposas bonaerenses 💗 sustentada íntegramente con servicios serverless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmh01boamvwd9gosnvxog.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmh01boamvwd9gosnvxog.jpg" alt="Imagen sitio web" width="800" height="953"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En el título hablo de sueños porque realmente lo es, sueño que mis vecinos tengan aunque sea solo un ejemplar nativo. No hay estadística clara sobre la vegetación nativa, pero sabemos que las especies van retrocediendo y con esto también las mariposas.&lt;br&gt;
&lt;strong&gt;La foto principal es una Monarca del Sur recién nacida en mi jardín, ya que generé las condiciones para que eso pasara.&lt;/strong&gt;&lt;br&gt;
Este laboratorio sirve para mostrar que casi sin costo podemos difundir información sobre temas que nos favorecen a todos y a todas. Y en tu región, ¿qué mariposas habitan?&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
