<?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: AWS Heroes</title>
    <description>The latest articles on DEV Community by AWS Heroes (aws-heroes).</description>
    <link>https://dev.to/aws-heroes</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%2Forganization%2Fprofile_image%2F2491%2Ff0c1a659-c959-42cd-bb12-cd25909dd9db.png</url>
      <title>DEV Community: AWS Heroes</title>
      <link>https://dev.to/aws-heroes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aws-heroes"/>
    <language>en</language>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 1 Introduction to the series and services</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to the series
&lt;/h2&gt;

&lt;p&gt;Throughout this article series, we'll develop a simple application to show how to build multi-step applications and orchestrate workflows using AWS Lambda durable functions. For it, we'll explore the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt;. This simple application provides the functionality to extract the content for the provided user. This content involves searching for YouTube videos of the authors and their upcoming talks. We'll store the result on Amazon S3 Files in JSON format. As this operation may potentially take time, we'll additionally provide access to the author content through the Amazon API Gateway:&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%2F0l2gsqbvyfq2dh2gjpzn.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%2F0l2gsqbvyfq2dh2gjpzn.png" alt=" " width="510" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We'll start with the simple application to search for YouTube videos and upcoming talks of the author synchronously. First, we'll use durable steps and then later add waits and callbacks. We won't implement any persistent layer and store the static content in memory. &lt;/li&gt;
&lt;li&gt;We'll then improve our application by adding asynchronous communication where it makes sense. For example, searching for YouTube videos and upcoming talks can be done in parallel.&lt;/li&gt;
&lt;li&gt;We'll then further improve our application by converting the AWS durable function to a pure orchestrator. This means we'll move the business logic for searching for YouTube videos and upcoming talks into separate Lambda functions. With that, our Lambda durable function will invoke those separate Lambda functions in parallel and combine the results&lt;/li&gt;
&lt;li&gt;Finally, we'll further extend our application to use &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Web Search Tool&lt;/a&gt; to search for the author content on the web. With that, our Lambda durable function becomes an orchestrator of the agentic AI workflows. As we'll use Java in this series, we'll explore how to run this business logic responsible for the Amazon Bedrock AgentCore Web Search Tool. As this tool is exposed as an MCP tool by the &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Gateway&lt;/a&gt;, we need a framework capable of providing AI functionality and supporting MCP. That's why we'll use Spring AI. As 2 operational environments for our agentic AI workflows, we'll explore an AWS Lambda function and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the code examples in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-java-sdk&lt;/a&gt; repository. We'll go through all the examples in more detail in the subsequent articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the services used throughout the series
&lt;/h2&gt;

&lt;p&gt;Throughout this series, we'll use the following AWS services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions&lt;/a&gt;. With Lambda durable functions, you can build resilient multi-step applications and AI workflows. These can execute for up to one year while maintaining reliable progress despite interruptions. When a durable function runs, this complete lifecycle is called a durable execution, which uses checkpoints to track progress and automatically recover from failures through replay, re-executing from the beginning while skipping completed work. Within each function, you use durable operations as fundamental building blocks. Steps execute business logic with built-in retries and progress tracking. Waits suspend execution without incurring compute charges, making them ideal for long-running processes. These processes include human-in-the-loop workflows or polling external dependencies. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html#s3-files-what-is" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;. S3 Files is a shared file system that connects any AWS compute resource directly with your data in Amazon S3. It provides fast, direct access to all of your S3 data as files with full file system semantics and low-latency performance, without your data ever leaving S3. Every file-based application, agent, and team can access and work with your S3 data as a file system using the tools they already depend on. Built using Amazon EFS, S3 Files gives you the performance and simplicity of a file system with the scalability, durability, and cost-effectiveness of S3. You can read, write, and organize data using file and directory operations. At the same time, S3 Files manages the synchronization of changes between your bucket and file system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We'll also introduce in the later parts of this series the following Amazon Bedrock AgentCore services or features: &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Gateway&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Web Search Tool&lt;/a&gt;, and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Runtime&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 3 Develop Agents as web application with Embabel</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In this article, we'll slightly adjust our application to convert it into a web application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement AI agents as a web application with Embabel
&lt;/h2&gt;

&lt;p&gt;You can look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. We'll completely reuse the implementation of both agents, the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; and the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; that we implemented in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those agents are capable of responding to the following prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." &lt;em&gt;SearchForTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;li&gt;"Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created." &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, let's check that we declared the following dependencies in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-bedrock&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure not to declare the &lt;em&gt;embabel-agent-starter-shell&lt;/em&gt; dependency, as we don't use Embabel shell in this article.&lt;/p&gt;

&lt;p&gt;All we need to do now is to additionally implement the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/controller/EmbabelAgentController.java" rel="noopener noreferrer"&gt;EmbabelAgentController&lt;/a&gt; controller.&lt;/p&gt;

&lt;p&gt;Let's first implement a generic endpoint that will accept both prompts. Let's first autowire and create some objects coming from Embabel that we'll use for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbabelAgentController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProcessOptions&lt;/span&gt; &lt;span class="n"&gt;processOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProcessOptions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withVerbosity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Verbosity&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPrompts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowLlmResponses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPlanning&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDebug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;


&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmbabelAgentController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we autowire the Embabel's &lt;em&gt;AgentPlatform&lt;/em&gt; object and use it to create the &lt;em&gt;AgentInvocation&lt;/em&gt; object. We additionally pass the &lt;em&gt;ProcessOption&lt;/em&gt; object that is configured to show planning, LLM responses, and prompts. We also pass the type of the response - the domain object returned after the agents achieve their goals; in our case, the  &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.ConferenceApplications&lt;/a&gt; class:&lt;/p&gt;

&lt;p&gt;Now, let's implement the endpoint itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/generic"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;genericPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we wrap the prompt into Embabel's &lt;em&gt;UserInput&lt;/em&gt; object that we put into the map with all inputs required to run the agent. Then we use the &lt;em&gt;AgentInvocation&lt;/em&gt; object to run the agent with the specified input synchronously. We can also invoke &lt;em&gt;runAsync&lt;/em&gt; to run the agent asynchronously. Finally, we retrieve the result of the agent invocation with the specified type.&lt;/p&gt;

&lt;p&gt;Let's build the application with &lt;code&gt;mvn clean package&lt;/code&gt; and run it locally with &lt;code&gt;mvn spring-boot:run&lt;/code&gt;. We'll send the prompt number 1 to this endpoint. What we'll see is that Embabel picked the wrong agent, &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt;, to run. Why? As we don't use the Embabel shell, Embabel doesn't analyze the prompt and compute the score to determine what exact agent to run. It currently only looks into the return type of all agents and picks the first one that matches the type defined in the &lt;em&gt;AgentInvocation&lt;/em&gt; object. But both our agents have the same return type &lt;em&gt;Domain.ConferenceApplications&lt;/em&gt;. Their goals, in the end, are the same: to apply for the conferences. With that, for our 2 agents, there is a 50% probability that Embabel will pick the wrong agent to run.&lt;/p&gt;

&lt;p&gt;How can we fix that? For example, we can define the endpoint per agent. Let's do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithExistingTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithExistingTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
       &lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithNewTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithNewTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CreateTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I additionally wrote 2 helper methods, &lt;em&gt;invokeAgent&lt;/em&gt; and &lt;em&gt;getAgentByName&lt;/em&gt;, to simplify the agent search by name and its subsequent invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;        
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgentProcess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="nf"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

     &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agent with the name "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" not found"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain what happens in the &lt;em&gt;invokeAgent&lt;/em&gt; method. We first invoke the &lt;em&gt;getAgentByName&lt;/em&gt; method. It searches in the collection of agents that the agent platform knows for one that has a specified name. All classes annotated with Embabel's &lt;em&gt;Agent&lt;/em&gt; annotation will be registered in the agent platform. Then we use the &lt;em&gt;AgentPlatform&lt;/em&gt; object to create the &lt;em&gt;AgentProcess&lt;/em&gt; and pass the found agent, the already described process options, and inputs to it. Then we run the &lt;em&gt;AgentProcess&lt;/em&gt; and return its last output of type &lt;em&gt;Domain.ConferenceApplications.class&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now we send prompt number 1 (see above) to the &lt;em&gt;applyToConferencesWithExistingTalks&lt;/em&gt; endpoint and prompt number 2 to the &lt;em&gt;applyToConferencesWithNewTalks&lt;/em&gt; endpoint. We'll see something similar as a result, as we saw in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, here I use &lt;a href="https://httpie.io/" rel="noopener noreferrer"&gt;httpie&lt;/a&gt; to send prompt number 1: &lt;br&gt;
&lt;code&gt;http GET http://localhost:8080/applyToConferencesWithExistingTalks?prompt="Please provide me with the list of conferences including their IDs with Java topic happening in 2026 and 2027 with call for papers open today. Also provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." Content-Type:text/plain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the response of the agent:&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%2Fxq4w3wz6pde5nonc5twv.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%2Fxq4w3wz6pde5nonc5twv.png" alt=" " width="799" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having one endpoint per Embabel agent has its pros and cons. The biggest disadvantage is that a service like &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-1n11"&gt;AgentCore Runtime&lt;/a&gt; exposes only one HTTP POST &lt;em&gt;/invocations&lt;/em&gt; endpoint to send the prompt to the web application. This means that we'll need to think about how to design, implement, and deploy each Embabel agent on a separate AgentCore Runtime. Each agent belonging to the same web application will have a lot of shared logic with other agents. This is not very convenient. The biggest advantage is that you can scale each Embabel agent individually. AgentCore Runtime will do it for you.&lt;/p&gt;

&lt;p&gt;It's worth checking for the upcoming version of Embabel whether there are other options available &lt;a href="https://docs.embabel.com/embabel-agent/guide/1.0.0-SNAPSHOT/#reference.invoking" rel="noopener noreferrer"&gt;to invoke the agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we covered how to develop a web application with Embabel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>EventBridge Scheduler DLQ'd My Successful Invokes</title>
      <dc:creator>Danielle Heberling</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/eventbridge-scheduler-dlqd-my-successful-invokes-4fhh</link>
      <guid>https://dev.to/aws-heroes/eventbridge-scheduler-dlqd-my-successful-invokes-4fhh</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://dev.to/aws-heroes/let-an-ai-agent-do-your-job-searching-558a"&gt;letting an AI agent do your job searching&lt;/a&gt;. That post ended with a quirk I couldn't explain. EventBridge Scheduler would invoke my agent, the agent would run fine, I'd get the email, and the invocation would land in the dead letter queue anyway. Every single time.&lt;/p&gt;

&lt;p&gt;I figured I was either doing something wrong or hitting an AgentCore bug. Turns out it was neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Problem
&lt;/h2&gt;

&lt;p&gt;EventBridge Scheduler universal targets (the feature that lets Scheduler call almost any AWS API action directly, no Lambda in between) are synchronous. Scheduler makes the API call and waits for a response before deciding whether the invocation succeeded. The docs describe this as &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;at-least-once delivery&lt;/a&gt;, where at least one delivery succeeds &lt;em&gt;with a response from the target&lt;/em&gt;. I glossed over that last part.&lt;/p&gt;

&lt;p&gt;My schedule was calling the Bedrock AgentCore &lt;code&gt;invokeAgentRuntime&lt;/code&gt; action, which blocks until the agent finishes. A search takes 30 to 75 seconds. Scheduler gives up around the 30 second mark and marks the invocation failed. The agent, which has no idea any of this happened, keeps going, finishes the search, and publishes to SNS. I get my email. The "failed" invocation goes to the DLQ. Here's what one of those messages looks like:&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%2Fy6olsz1jsl6yc8xajknj.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%2Fy6olsz1jsl6yc8xajknj.png" alt="EventBridge Scheduler DLQ message showing an ErrorCode and ErrorMessage after a successful AgentCore invocation." width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything downstream worked. The only broken part was Scheduler's opinion of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  About That 30 Seconds
&lt;/h2&gt;

&lt;p&gt;I went looking for this in the docs and could not find it. The &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/scheduler-quotas.html" rel="noopener noreferrer"&gt;EventBridge Scheduler quotas page&lt;/a&gt; covers number of schedules, API request rates, and invocation throughput. Nothing about how long Scheduler waits for a target to respond.&lt;/p&gt;

&lt;p&gt;The one timeout you &lt;em&gt;can&lt;/em&gt; configure is &lt;code&gt;MaximumEventAgeInSeconds&lt;/code&gt; in the &lt;a href="https://docs.aws.amazon.com/scheduler/latest/APIReference/API\_Target.html" rel="noopener noreferrer"&gt;retry policy&lt;/a&gt;. That's the maximum age of an event across retries, not a per-call limit. Different thing.&lt;/p&gt;

&lt;p&gt;So treat ~30 seconds as an observed number, not a contract. In my opinion the safer bet is designing against "Scheduler will not wait long" rather than a specific value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Instead of making the search faster, I made the response faster. The entrypoint now kicks the search off as a background task and returns immediately, so Scheduler gets its response well inside the window and the agent runs for as long as it needs to (up to AgentCore's 8 hour session cap, which my searches are nowhere near).&lt;/p&gt;

&lt;p&gt;AgentCore has first-class support for this. You register the background work with &lt;code&gt;add_async_task&lt;/code&gt;, the runtime reports &lt;code&gt;HealthyBusy&lt;/code&gt; on &lt;code&gt;/ping&lt;/code&gt; while it's in flight, and the session stays alive instead of getting reaped. That last part matters because AgentCore terminates sessions after 15 minutes of idle, and without task tracking your background work looks exactly like idle. The &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-long-run.html" rel="noopener noreferrer"&gt;async processing docs&lt;/a&gt; cover the pattern.&lt;/p&gt;

&lt;p&gt;Here's the relevant part of the entrypoint:&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;# strong refs: asyncio only weakly references tasks
&lt;/span&gt;&lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_tracked_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&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="n"&gt;title&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="n"&gt;location&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run the job search as a tracked async task so ping reports HealthyBusy.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_async_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete_async_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Main entrypoint for the agent invocation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# ...payload validation...
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Respond before EventBridge Scheduler's ~30s call timeout DLQs the invocation
&lt;/span&gt;    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_tracked_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&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;search_criteria&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;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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;Scheduler gets &lt;code&gt;{"status": "accepted"}&lt;/code&gt; in well under a second. The search runs in the background. No more DLQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Things to Watch For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Strands call has to be awaited.&lt;/strong&gt; I was calling &lt;code&gt;agent(prompt)&lt;/code&gt; inside an &lt;code&gt;async def&lt;/code&gt; entrypoint, which blocks the event loop. Survivable when the entrypoint blocked anyway. But once the runtime needs to answer health checks mid-search, a blocking call starves everything. Switching to &lt;code&gt;await agent.invoke_async(prompt)&lt;/code&gt; was required, not a style preference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hold a reference to the task.&lt;/strong&gt; &lt;code&gt;asyncio&lt;/code&gt; only weakly references tasks from &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.create\_task" rel="noopener noreferrer"&gt;&lt;code&gt;create_task&lt;/code&gt;&lt;/a&gt;, so an unreferenced task can get garbage collected mid-run. That's the &lt;code&gt;_background_tasks&lt;/code&gt; set above, lifted straight from the pattern in the Python docs. It usually works fine right up until it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a synchronous path.&lt;/strong&gt; Fire and forget is great for the scheduler and terrible for local development. A &lt;code&gt;"sync": true&lt;/code&gt; flag in the payload runs the search inline and returns the full result. That's what I use for curl and the AgentCore playground.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing About Retries
&lt;/h2&gt;

&lt;p&gt;In the original post I disabled retries, because Scheduler retrying a "failed" invocation meant duplicate agent runs and duplicate emails. The CDK target default is 185 retries with exponential backoff over up to 24 hours, so every scheduled search was set up to keep re-running (and re-billing) until I caught it.&lt;/p&gt;

&lt;p&gt;With the async path, I turned retries back on (&lt;code&gt;retryAttempts: 0&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;). Now Scheduler gets an ack right away, so it won’t keep retrying while the search is still running. There’s still a small chance of a duplicate if the ack gets lost, but that’s a trade-off I’m comfortable with here.&lt;/p&gt;

&lt;p&gt;There's a tradeoff here. Scheduler now only confirms your agent started. It says nothing about whether the search actually worked. If the search blows up 40 seconds in, Scheduler is happy and your DLQ stays empty. You need logs, metrics, or the notification itself to know the work finished. For me a missing email is a loud signal. If your silence is more ambiguous, plan for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You're Hitting This
&lt;/h2&gt;

&lt;p&gt;If you're invoking AgentCore Runtime from an EventBridge schedule and seeing DLQ messages for runs that clearly worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check how long your agent actually takes. Anything over ~30 seconds is suspect.&lt;/li&gt;
&lt;li&gt;Return an ack from your entrypoint instead of the result.&lt;/li&gt;
&lt;li&gt;Wrap the real work in &lt;code&gt;add_async_task&lt;/code&gt; / &lt;code&gt;complete_async_task&lt;/code&gt; so the runtime knows to keep the session alive.&lt;/li&gt;
&lt;li&gt;Make sure nothing in that background path blocks the event loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code is in the &lt;a href="https://github.com/deeheber/job-search-agent" rel="noopener noreferrer"&gt;job-search-agent repo&lt;/a&gt; and the change is &lt;a href="https://github.com/deeheber/job-search-agent/pull/51" rel="noopener noreferrer"&gt;PR #51&lt;/a&gt; if you want to see the whole diff.&lt;/p&gt;

&lt;p&gt;None of this is AgentCore specific. Any long-running universal target invoked from EventBridge Scheduler has the same problem. Agents just happen to be really good at being slow. 😅&lt;/p&gt;

&lt;p&gt;If you've hit this with a different long-running target, or if you've found the actual timeout documented somewhere I missed, I'd love to hear about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
    <item>
      <title># Blog #9: I Gave My Minecraft Mod a Serverless Brain — And It Decides How Many Allies to Summon Based on How Screwed You Are</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:13:30 +0000</pubDate>
      <link>https://dev.to/aws-heroes/-blog-9-i-gave-my-minecraft-mod-a-serverless-brain-and-it-decides-how-many-allies-to-summon-5bid</link>
      <guid>https://dev.to/aws-heroes/-blog-9-i-gave-my-minecraft-mod-a-serverless-brain-and-it-decides-how-many-allies-to-summon-5bid</guid>
      <description>&lt;h2&gt;
  
  
  I Gave My Minecraft Mod a Serverless Brain — And It Decides How Many Allies to Summon Based on How Screwed You Are
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Cloud Swords Mod — Blog 9&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been building a Minecraft mod that teaches cloud computing through gameplay. Swords named after AWS services. Energy systems that work like network infrastructure. Villagers that trade cloud certifications.&lt;/p&gt;

&lt;p&gt;But version 0.3.8 is where things got weird. I connected the mod to an actual AI agent running in the cloud. Now when you swing the Sword of Lambda, instead of spawning a fixed number of minions — an AI evaluates your combat situation and decides how many allies you need.&lt;/p&gt;

&lt;p&gt;The game never freezes. The AI never breaks balance. And the whole thing runs on a serverless backend that costs essentially nothing.&lt;/p&gt;

&lt;p&gt;Here's how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Items That Make It Work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tome of API Key
&lt;/h3&gt;

&lt;p&gt;Every cloud connection starts with authentication. In the mod, that's the &lt;strong&gt;Tome of API Key&lt;/strong&gt; — a rare artifact found in dungeon chests (8% drop rate in bastions, end cities, nether fortresses).&lt;/p&gt;

&lt;p&gt;When you first pick it up, it generates a unique API key (&lt;code&gt;csk-XXXX&lt;/code&gt; format) and shows 100 charges. It glows with enchant shimmer. It's your passport to the cloud.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On first inventory tick, generate unique key&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api_key"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"csk-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;putString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api_key"&lt;/span&gt;&lt;span class="o"&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;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;putInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"charges"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tooltip shows your key preview, remaining charges, and connection status. When charges hit zero, the cloud refuses your invocations — just like hitting a rate limit on a real API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud API Requester (Block)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Cloud API Requester&lt;/strong&gt; is the bridge between Minecraft and AWS. It's a crafted block (Iron + Circuit Board + Redstone + Ender Pearl + Data Core) with two slots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your Tome of API Key&lt;/li&gt;
&lt;li&gt;The weapon you want to cloud-enable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop both in, wait ~8 seconds (the "deployment timer"), and your weapon gets marked with &lt;code&gt;cloud_enabled=true&lt;/code&gt; in its NBT data. The tooltip now shows "☁ Cloud Enabled" in cyan.&lt;/p&gt;

&lt;p&gt;This is the CI/CD pipeline of the mod. You're deploying your weapon to the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud-Enabled Weapons
&lt;/h3&gt;

&lt;p&gt;Once cloud-enabled, the Sword of Lambda changes behavior completely. Instead of spawning a hardcoded number of minions, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sends your combat context to the backend (health, nearby mobs, biome, dimension)&lt;/li&gt;
&lt;li&gt;Shows "☁ Cloud invocation sent..." in the actionbar&lt;/li&gt;
&lt;li&gt;Waits for the AI's decision&lt;/li&gt;
&lt;li&gt;Applies it: "☁ Cloud decided: 5 minions (Critical threat — max invocations)"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're NOT cloud-enabled, the sword works exactly as before. Backward compatible. No cloud required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                     MINECRAFT CLIENT                         │
│                                                             │
│  Player swings Sword of Lambda (cloud-enabled)              │
│       │                                                     │
│       ▼                                                     │
│  CloudCombatHandler builds context:                         │
│  {player, weapon, health, biome, dimension, nearby_mobs}   │
│       │                                                     │
│       ▼                                                     │
│  CloudApiClient.sendEvent(context)  ← async, non-blocking  │
└───────┬─────────────────────────────────────────────────────┘
        │ POST /event
        ▼
┌─────────────────────────────────────────────────────────────┐
│                    BACKEND (CDK Stack)                        │
│                                                             │
│  API Gateway (rate limited: 100/day, 10/sec)                │
│       │                                                     │
│       ▼                                                     │
│  Lambda: IngestEvent                                        │
│  - Receives context                                         │
│  - Calls Strands Agent (or rule-based fallback)             │
│  - Stores decision in DynamoDB (TTL: 60s)                   │
│  - Returns {request_id}                                     │
│                                                             │
│  DynamoDB: CloudSwordsResults                               │
│  - Partition key: request_id                                │
│  - TTL auto-cleanup                                         │
│                                                             │
│  Lambda: PollResult                                         │
│  - GET /result/{request_id}                                 │
│  - Returns decision when ready                              │
└───────┬─────────────────────────────────────────────────────┘
        │ Poll every 2s
        ▼
┌─────────────────────────────────────────────────────────────┐
│  CloudApiClient receives decision                           │
│       │                                                     │
│       ▼                                                     │
│  CloudCombatHandler.applyDecision()                         │
│  - Summon "☁ Cloud Invocation" minions (named, cyan)        │
│  - Or apply buff (regeneration, resistance, strength)       │
│  - Or do nothing ("No action needed")                       │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Async Pattern: Fire-and-Forget + Polling
&lt;/h2&gt;

&lt;p&gt;The game can NEVER freeze waiting for a network response. So I used the same pattern you'd use in any real-time application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fire&lt;/strong&gt;: Send the event, get back a &lt;code&gt;request_id&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forget&lt;/strong&gt;: Continue gameplay, don't block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt;: Every 2 seconds (via &lt;code&gt;ServerTickEvents&lt;/code&gt;), check if the result is ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt;: When ready, queue the callback for the server thread
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CloudApiClient — non-blocking&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonObject&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseRequestId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;startPolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Polling on server tick (every 40 ticks = 2 seconds)&lt;/span&gt;
&lt;span class="nc"&gt;ServerTickEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;END_SERVER_TICK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Execute queued callbacks on server thread&lt;/span&gt;
    &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendingCallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;poll&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threading fix was critical. Java's &lt;code&gt;HttpClient&lt;/code&gt; runs callbacks on its own async thread — but Minecraft entity spawning MUST happen on the server thread. Without the &lt;code&gt;ConcurrentLinkedQueue&lt;/code&gt; bridge, buffs wouldn't apply and entities would crash the game.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI Agent: Strands + Ollama Cloud
&lt;/h2&gt;

&lt;p&gt;The brain is a Strands Agent running &lt;code&gt;gpt-oss:20b&lt;/code&gt; via Ollama Cloud (free tier — no Bedrock cost for development):&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.ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaModel&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 a combat advisor for a Minecraft RPG mod.
Given player context, decide ONE action.

Available actions (respond ONLY with valid JSON):
- {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 1-5, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 5-15, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brief&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
- {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regeneration|resistance|strength|speed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 3-10, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brief&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
- {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brief&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}

Rules:
- If health &amp;lt; 8 and many mobs: summon max allies or buff resistance
- If health &amp;gt; 15 and few mobs: action none or summon 1
- Scale response to threat level
- ONLY output the JSON object, nothing else&lt;/span&gt;&lt;span class="sh"&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;OllamaModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ollama.com&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_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;gpt-oss:20b&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="nc"&gt;Agent&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="n"&gt;model&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="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent receives context like:&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;"player"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carlos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weapon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sword_of_lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"health"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nearby_mobs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"biome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nether_wastes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minecraft:the_nether"&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;And responds:&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="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Critical threat"&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;The system prompt enforces bounded actions — the AI can't break game balance because it can only choose from a fixed set of actions with capped values.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CDK Stack
&lt;/h2&gt;

&lt;p&gt;The production backend is a single CDK stack:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CloudSwordsStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# DynamoDB for async results
&lt;/span&gt;        &lt;span class="n"&gt;results_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DecisionResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;partition_key&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;name&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;request_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;type&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;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;time_to_live_attribute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;billing_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Lambda: receive event, decide, store
&lt;/span&gt;        &lt;span class="n"&gt;ingest_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IngestEvent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handler.lambda_handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda/ingest&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="c1"&gt;# Lambda: poll result
&lt;/span&gt;        &lt;span class="n"&gt;poll_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PollResult&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handler.lambda_handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda/poll&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="c1"&gt;# API Gateway with usage plan (rate limiting = gameplay mechanic)
&lt;/span&gt;        &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CloudSwordsApi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_usage_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;throttle&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;rate_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;burst_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;quota&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;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DAY&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;The usage plan isn't just infrastructure — it's a gameplay mechanic. 100 requests per day means you can't spam the cloud. You have to be strategic about when you invoke it. Just like real API budgets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local Development: The Mock Server
&lt;/h2&gt;

&lt;p&gt;For development, I run a mock server that integrates the Strands Agent directly:&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;# mock_server.py — localhost:8777
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;combat_advisor&lt;/span&gt; &lt;span class="kn"&gt;import&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;decide&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_agent&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ Strands Agent loaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠ Agent unavailable, using rule-based fallback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&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="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Fallback: simple rules
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nearby_mobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;summon&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;count&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;Critical&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;summon&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;count&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;Standard&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;The mock server logs every interaction to &lt;code&gt;interactions.json&lt;/code&gt; — which becomes the eval dataset for Blog 10.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Concepts Taught Through Gameplay
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game Mechanic&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tome of API Key&lt;/td&gt;
&lt;td&gt;API keys + authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 charges&lt;/td&gt;
&lt;td&gt;Rate limiting / usage plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API Requester (8s timer)&lt;/td&gt;
&lt;td&gt;CI/CD deployment pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cloud_enabled=true&lt;/code&gt; NBT&lt;/td&gt;
&lt;td&gt;Feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST /event → request_id&lt;/td&gt;
&lt;td&gt;Async event processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll every 2s&lt;/td&gt;
&lt;td&gt;Eventual consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI decides action&lt;/td&gt;
&lt;td&gt;Serverless compute (Lambda)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bounded actions (1-5 minions)&lt;/td&gt;
&lt;td&gt;Guardrails / IAM boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDK stack&lt;/td&gt;
&lt;td&gt;Infrastructure as Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock server&lt;/td&gt;
&lt;td&gt;Local development environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Threading in Minecraft is brutal&lt;/strong&gt; — anything touching entities must be on the server thread. Async HTTP + game logic = careful queue management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limiting as gameplay works&lt;/strong&gt; — players naturally strategize around limited cloud invocations. It teaches resource management without a lecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI guardrails through system prompts&lt;/strong&gt; — bounding the action space (1-5 minions, specific buffs) means the AI literally cannot break the game. The worst it can do is summon 5 minions when 3 would suffice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallback patterns matter&lt;/strong&gt; — if the cloud is down, the sword still works with local logic. Graceful degradation, taught through a video game.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The AI makes decisions. But are they good decisions? In Blog 10, I build an evaluation framework that judges the agent's combat advice — using another AI as the judge. Meta? Absolutely. Useful? Surprisingly yes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mod: &lt;a href="https://github.com/ccortezb/cloud-swords-mod" rel="noopener noreferrer"&gt;cloud-swords-mod-1.20.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backend: &lt;a href="https://github.com/ccortezb/cloud-swords-backend-cdk" rel="noopener noreferrer"&gt;cloud-swords-backend-cdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent: &lt;a href="https://github.com/ccortezb/cloud-swords-strands-agent" rel="noopener noreferrer"&gt;cloud-swords-strands-agent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm Carlos Cortez — cloud engineer, gamer, and apparently someone who connects Minecraft to AI agents for fun. This is Breaking the Cloud.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>I Built a Cloud Caravan Merchants Progression System as a cloud Marketplace</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:10:01 +0000</pubDate>
      <link>https://dev.to/aws-heroes/i-built-a-cloud-caravan-merchants-progression-system-as-a-cloud-marketplace-k1k</link>
      <guid>https://dev.to/aws-heroes/i-built-a-cloud-caravan-merchants-progression-system-as-a-cloud-marketplace-k1k</guid>
      <description>&lt;p&gt;Every cloud platform has a marketplace. AWS has the AWS Marketplace, Azure has its own, GCP too. They're places where you find specialized vendors offering services you can't build yourself — and the more you buy, the more they offer you.&lt;/p&gt;

&lt;p&gt;We built that in Minecraft: &lt;strong&gt;Cloud Caravan Merchants&lt;/strong&gt;. Three specialized traders that travel with donkeys carrying loot chests, offer 5 base trades each, and evolve into Elite merchants when you complete their missions — unlocking premium trades including Elytra, Tridents, and Totems of Undying.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="Cloud Caravan with 3 merchants and donkeys" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: The Elite progression system mirrors how cloud marketplace vendors work — start with basic offerings, prove you're a serious customer (complete trades), and unlock enterprise-tier products. The 3 merchant types map to cloud specializations: infrastructure (Packet Runner), platform (Protocol Broker), and application layer (Quantum Courier).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3 Merchants — 3 Cloud Layers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Merchant&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Specialty&lt;/th&gt;
&lt;th&gt;Cloud Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet Runner&lt;/td&gt;
&lt;td&gt;Cyan&lt;/td&gt;
&lt;td&gt;Raw materials &amp;amp; resources&lt;/td&gt;
&lt;td&gt;Infrastructure (IaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol Broker&lt;/td&gt;
&lt;td&gt;Orange&lt;/td&gt;
&lt;td&gt;Equipment &amp;amp; upgrades&lt;/td&gt;
&lt;td&gt;Platform (PaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Courier&lt;/td&gt;
&lt;td&gt;Purple&lt;/td&gt;
&lt;td&gt;Runes, tomes &amp;amp; magic&lt;/td&gt;
&lt;td&gt;Application (SaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each spawns with a donkey carrying a loot chest with starter items. It's a welcome package — like free tier credits when you sign up for a cloud platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spawn Egg — Full Caravan Unit
&lt;/h2&gt;

&lt;p&gt;The custom spawn egg doesn't just spawn a merchant. It spawns the entire unit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Merchant entity (custom type, wandering trader model)&lt;/li&gt;
&lt;li&gt;Donkey (tamed, with chest)&lt;/li&gt;
&lt;li&gt;Loot in the chest (via loot table)&lt;/li&gt;
&lt;li&gt;Leash connecting donkey to merchant
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ActionResult&lt;/span&gt; &lt;span class="nf"&gt;useOnBlock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ItemUsageContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Spawn merchant&lt;/span&gt;
    &lt;span class="nc"&gt;CloudMerchantEntity&lt;/span&gt; &lt;span class="n"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merchantType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPosition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getZ&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Spawn donkey with chest&lt;/span&gt;
    &lt;span class="nc"&gt;DonkeyEntity&lt;/span&gt; &lt;span class="n"&gt;donkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EntityType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DONKEY&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHasChest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Fill chest via NBT (inventory must be properly sized)&lt;/span&gt;
    &lt;span class="nc"&gt;NbtCompound&lt;/span&gt; &lt;span class="n"&gt;donkeyNbt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NbtCompound&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeNbt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;putBoolean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ChestedHorse"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;NbtList&lt;/span&gt; &lt;span class="n"&gt;itemsList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NbtList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... add loot items to NBT&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readNbt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Leash&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachLeash&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Donkey Chest Bug — A War Story
&lt;/h3&gt;

&lt;p&gt;Fun debugging story: donkeys in 1.20.1 have a &lt;code&gt;SimpleInventory&lt;/code&gt; field called &lt;code&gt;items&lt;/code&gt;. When you call &lt;code&gt;setHasChest(true)&lt;/code&gt;, it's supposed to expand the inventory from 2 slots (saddle + armor) to 17 slots (+ 15 chest slots). But the expansion happens in &lt;code&gt;onChestedStatusChanged()&lt;/code&gt;, which recreates the inventory.&lt;/p&gt;

&lt;p&gt;If you try to put items in the chest BEFORE the inventory is properly sized, you get &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;. If you put them AFTER spawning, the inventory might not be synced to the client yet.&lt;/p&gt;

&lt;p&gt;The solution: write the chest contents via NBT. Set &lt;code&gt;ChestedHorse: true&lt;/code&gt; and &lt;code&gt;Items: [...]&lt;/code&gt; in the NBT compound, then call &lt;code&gt;readNbt()&lt;/code&gt;. This forces the entity to reconstruct its inventory with the correct size AND the items already in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elite Progression — The Loyalty Program
&lt;/h2&gt;

&lt;p&gt;Complete all 5 base trades (use them until sold out) and the merchant evolves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base (5 trades) → Elite ★ (7 trades) → Elite ★★ (9 trades) → Elite ★★★ (11 trades)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each promotion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adds 2 new trades with premium items&lt;/li&gt;
&lt;li&gt;Changes the merchant's name to gold with stars&lt;/li&gt;
&lt;li&gt;Triggers a Totem of Undying particle burst&lt;/li&gt;
&lt;li&gt;Adds ambient END_ROD particles
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkElitePromotion&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;requiredTrades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eliteLevel&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completedTrades&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;requiredTrades&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;eliteLevel&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;eliteLevel&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;applyEliteName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "★★ Protocol Broker ★★"&lt;/span&gt;
        &lt;span class="n"&gt;addEliteTrades&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOffers&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;eliteLevel&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Celebration particles&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnParticles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ParticleTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TOTEM_OF_UNDYING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Elite Trades — Premium Vanilla Items
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cycle&lt;/th&gt;
&lt;th&gt;Packet Runner&lt;/th&gt;
&lt;th&gt;Protocol Broker&lt;/th&gt;
&lt;th&gt;Quantum Courier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;Quantum Shards, Spyglass&lt;/td&gt;
&lt;td&gt;Netherlink Plating, Enchanted Golden Apple&lt;/td&gt;
&lt;td&gt;Deployed Runes, Ender Eyes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★&lt;/td&gt;
&lt;td&gt;Cloud Edge Pearls, Echo Shards&lt;/td&gt;
&lt;td&gt;Cloudsteel Plating, Trident&lt;/td&gt;
&lt;td&gt;Grimoire of Architect, XP Bottles ×16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;Nether Star, Totem of Undying&lt;/td&gt;
&lt;td&gt;Netherite ×2, Elytra&lt;/td&gt;
&lt;td&gt;Quantum Runes ×16, End Crystals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The endgame trades offer items that are normally very hard to obtain (Elytra, Trident) in exchange for Cloud Credits and rare materials. It's the cloud marketplace premium tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural Spawning — The Caravan Arrives
&lt;/h2&gt;

&lt;p&gt;Every ~1 hour of real time, there's a 15% chance a caravan spawns 48-96 blocks from a random player:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;spawnCaravan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ServerWorld&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BlockPos&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Random&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;caravanSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1, 2, or 3 merchants&lt;/span&gt;
    &lt;span class="c1"&gt;// Shuffle types to avoid duplicates&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MerchantType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MerchantType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;caravanSize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Spawn merchant + donkey at offset positions&lt;/span&gt;
        &lt;span class="n"&gt;spawnUnit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full caravan (3 merchants) gives you access to all specializations at once. It's like a cloud conference — all the vendors in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never Despawn — Persistent Infrastructure
&lt;/h2&gt;

&lt;p&gt;Unlike vanilla wandering traders that disappear after 40 minutes, Cloud Merchants are permanent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPersistent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDespawnDelay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_VALUE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you find (or spawn) a merchant, they stay forever. They're your infrastructure — you invested in them, they stick around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Textures — Visual Identity
&lt;/h2&gt;

&lt;p&gt;Each merchant uses a recolored wandering trader texture generated with Python PIL:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hue_shift&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Shift hue of non-skin pixels&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pixel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_skin_tone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rgb_to_hsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hue_shift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
            &lt;span class="n"&gt;pixel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hsv_to_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Packet Runner: cyan shift (networking)&lt;/li&gt;
&lt;li&gt;Protocol Broker: orange shift (fire/security)&lt;/li&gt;
&lt;li&gt;Quantum Courier: purple shift (quantum/magic)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rune Table Recipes — Mod Interop
&lt;/h2&gt;

&lt;p&gt;We also added recipes for the Rune Crafting Altar (from the Runes mod):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rune&lt;/th&gt;
&lt;th&gt;Altar Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet&lt;/td&gt;
&lt;td&gt;Cloud Shard + Redstone&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Lapis, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;Packet Rune + Blazing Shard&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Gold, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Protocol Rune + Quantum Shard&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Ender Pearl, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is mod interop — using another mod's crafting system to provide alternative recipes. Like using a third-party service that's cheaper than building your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Donkey chest inventory size&lt;/td&gt;
&lt;td&gt;Write items via NBT, not direct slot access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity renderer missing = crash&lt;/td&gt;
&lt;td&gt;Register renderer in ClientModInitializer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wandering trader despawn timer&lt;/td&gt;
&lt;td&gt;Override with MAX_VALUE despawnDelay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spawn egg model missing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;template_spawn_egg&lt;/code&gt; parent model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade completion tracking&lt;/td&gt;
&lt;td&gt;NBT persistence (CompletedTrades, EliteLevel)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Series Wrap-Up
&lt;/h2&gt;

&lt;p&gt;Over 8 posts, we went from "what if AWS services were Minecraft swords" to a full tech-magic mod with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 cloud-themed swords with unique abilities&lt;/li&gt;
&lt;li&gt;28 spells across 9 spellbooks&lt;/li&gt;
&lt;li&gt;4 armor sets with set bonuses&lt;/li&gt;
&lt;li&gt;Multiblock machines with BFS energy networks&lt;/li&gt;
&lt;li&gt;7 villager professions with a Cloud Credits economy&lt;/li&gt;
&lt;li&gt;3 Caravan Merchants with Elite progression&lt;/li&gt;
&lt;li&gt;Full compatibility with 650+ mod modpacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mod teaches cloud computing by making you &lt;em&gt;experience&lt;/em&gt; the concepts. You don't read about Auto Scaling — you feel your damage stack. You don't study fault tolerance — you dodge attacks with Quantum Armor. You don't memorize service tiers — you progress through them.&lt;/p&gt;

&lt;p&gt;That's the power of game-based learning. And we built it all with AI as a co-pilot.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The complete mod is on &lt;a href="https://github.com/ccortezb/breakingthecloud/tree/main/minecraft/minecraft/cloud-swords-mod-1.20.1" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone it, build it, play it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and this series proved that the best way to learn cloud is to play it. See you in the next adventure! 🎮☁️⚔️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>Designing an Armor Progression That Mirrors Cloud Certification Levels</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:09:22 +0000</pubDate>
      <link>https://dev.to/aws-heroes/designing-an-armor-progression-that-mirrors-cloud-certification-levels-4k6f</link>
      <guid>https://dev.to/aws-heroes/designing-an-armor-progression-that-mirrors-cloud-certification-levels-4k6f</guid>
      <description>&lt;p&gt;In cloud computing, you level up through certifications: Practitioner → Associate → Professional → Specialty. Each level unlocks more capabilities and proves deeper expertise. We built the same thing as armor sets in Minecraft.&lt;/p&gt;

&lt;p&gt;4 armor sets, each with a set bonus, a dodge mechanic, and a clear progression path that mirrors going from cloud beginner to architect.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="4 Cloud Armor sets displayed" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: Dataflow Armor gives Speed (network optimization). Netherlink gives Fire Resistance (firewall protection). Quantum gives damage resistance (fault tolerance). Cloudsteel gives flight (cloud-native freedom). The set bonuses ARE the cloud concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4 Armor Sets — 4 Cloud Maturity Levels
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Armor&lt;/th&gt;
&lt;th&gt;Toughness&lt;/th&gt;
&lt;th&gt;Cloud Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataflow&lt;/td&gt;
&lt;td&gt;Post-Iron&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;Practitioner — basic networking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlink&lt;/td&gt;
&lt;td&gt;Post-Diamond&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;Associate — security focus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Post-Netherite&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;Professional — fault tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudsteel&lt;/td&gt;
&lt;td&gt;Endgame&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;Specialty — full mastery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each set has a unique visual identity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dataflow&lt;/strong&gt;: Cyan/teal with circuit line patterns + bright nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netherlink&lt;/strong&gt;: Dark red/orange with ember glow edges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantum&lt;/strong&gt;: Purple with glitch pixels (digital distortion)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudsteel&lt;/strong&gt;: White/silver with cloud wisps + luminous edges&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set Bonuses — Cloud Capabilities
&lt;/h2&gt;

&lt;p&gt;When you wear all 4 pieces of a set, you get passive effects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;th&gt;Effects&lt;/th&gt;
&lt;th&gt;Dodge %&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataflow&lt;/td&gt;
&lt;td&gt;Speed I + Haste I&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Network optimization — faster throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlink&lt;/td&gt;
&lt;td&gt;Fire Resistance + Strength I (Nether)&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;Firewall — immune to fire attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Resistance I&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Fault tolerance — reduced impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudsteel&lt;/td&gt;
&lt;td&gt;Slow Falling + Jump Boost II&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Cloud-native — freedom of movement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Dodge System
&lt;/h3&gt;

&lt;p&gt;Every set has a chance to &lt;strong&gt;completely nullify incoming damage&lt;/strong&gt;. When triggered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portal particles burst around the player&lt;/li&gt;
&lt;li&gt;Enderman teleport sound plays&lt;/li&gt;
&lt;li&gt;Damage is set to zero
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuantumDodgeHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ALLOW_DAMAGE event — return false to cancel damage&lt;/span&gt;
        &lt;span class="nc"&gt;DamageEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALLOW&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dodgeChance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getSetDodgeChance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dodgeChance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRandom&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;nextFloat&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;dodgeChance&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Dodge! Cancel damage&lt;/span&gt;
                &lt;span class="n"&gt;spawnDodgeParticles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher-tier armor = higher dodge chance. It's probabilistic fault tolerance — like how multi-AZ deployments reduce (but don't eliminate) the chance of downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crafting Progression — The Certification Path
&lt;/h2&gt;

&lt;p&gt;You don't jump straight to Cloudsteel. The progression uses the Smithing Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iron Armor + Dataflow Plating + Cloud Shard → Dataflow Armor
Diamond Armor + Netherlink Plating + Blazing Shard → Netherlink Armor
Netherite Armor + Quantum Plating + Quantum Shard → Quantum Armor
Quantum Armor + Cloudsteel Plating + Cloud Edge Pearl → Cloudsteel Armor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tier requires the previous tier's armor as base. You can't skip levels — just like certifications build on each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platings (4 ingots each)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;III
I_I  →  1 Plating
___
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple shaped recipe. 4 platings per set (helmet, chest, legs, boots) × 4 ingots = 16 ingots per full set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessories Integration — Optional Dependencies
&lt;/h2&gt;

&lt;p&gt;The mod works with or without the Accessories mod installed. When present, the Cloud Engineer Lens can be equipped in the face trinket slot instead of taking the helmet slot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccessoriesCompat&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEquipped&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Runtime check — only call if mod is loaded&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AccessoriesCapability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEquipped&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detection priority: Accessories slot → Trinkets slot → Helmet → Offhand → Mainhand.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;optional dependency pattern&lt;/strong&gt; — your mod works standalone, but integrates with other mods when available. Like how a cloud service works alone but integrates with others via APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spell Book Slot — Cross-System Integration
&lt;/h2&gt;

&lt;p&gt;All 18 tomes (9 base + 9 empowered) implement a &lt;code&gt;SpellBookItem&lt;/code&gt; interface and register in the &lt;code&gt;trinkets:charm/spell_book&lt;/code&gt; tag. This means they can be equipped in the Spell Book accessory slot from other magic mods.&lt;/p&gt;

&lt;p&gt;Cross-mod compatibility isn't just nice to have — it's essential for modpack inclusion. Same as cloud services: if your service doesn't integrate with the ecosystem, nobody uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nether Star Duplication — Endgame Recipe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 Cloud Edge Pearls + 4 Data-Infused Quantum Shards + 1 Nether Star → 2 Nether Stars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An endgame recipe that avoids Wither farming. Expensive inputs, but saves time. It's like Reserved Instances — pay more upfront in materials, save time in the long run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set bonus detection every tick is expensive&lt;/td&gt;
&lt;td&gt;Check every 20 ticks (1 second), not every tick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dodge feels unfair in PvP&lt;/td&gt;
&lt;td&gt;Balance with low percentages (10-20%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessories as optional dep&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;compileOnly&lt;/code&gt; + runtime class check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Armor textures&lt;/td&gt;
&lt;td&gt;Recolor vanilla iron/netherite with unique effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progression too fast&lt;/td&gt;
&lt;td&gt;Require previous tier as base (sequential)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We have swords, spells, machines, and armor. But what about NPCs that sell you things? In the final post of this series, we'll build &lt;strong&gt;Cloud Caravan Merchants&lt;/strong&gt; — wandering traders with donkeys, loot chests, and an Elite progression system. Because every cloud platform needs a marketplace.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and today we armored up with cloud certifications. See you in the next one! 🛡️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>I Built a Magic System Where Every Spell is a Cloud Operation</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:13:45 +0000</pubDate>
      <link>https://dev.to/aws-heroes/i-built-a-magic-system-where-every-spell-is-a-cloud-operation-1jfd</link>
      <guid>https://dev.to/aws-heroes/i-built-a-magic-system-where-every-spell-is-a-cloud-operation-1jfd</guid>
      <description>&lt;p&gt;What does casting a spell have in common with calling an API? More than you'd think. Both consume resources (mana/runes vs. API credits), both have cooldowns (cast time vs. rate limits), both have tiers (spell levels vs. service tiers), and both produce effects at a distance.&lt;/p&gt;

&lt;p&gt;So we built &lt;strong&gt;Cloud Spellbooks&lt;/strong&gt; — 9 tomes, 28 spells, 5 rune types, and a full upgrade path. Every spell is named after a cloud operation, and every mechanic maps to how cloud services actually work.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="9 Cloud Tomes arranged by tier" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: When you cast "Auto Scale" and it does AoE damage that scales with targets, you're experiencing what Auto Scaling &lt;em&gt;does&lt;/em&gt;. When "Region Failover" fully heals you with a 5-minute cooldown, you understand why failover is a last resort with a long recovery time. The game mechanics ARE the cloud concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Architecture: Spell Engine Integration
&lt;/h2&gt;

&lt;p&gt;We didn't build a spell system from scratch. We integrated with &lt;strong&gt;Spell Engine&lt;/strong&gt; — a Fabric mod that provides data-driven spell registration, the Binding Table mechanic, and compatibility with other magic mods.&lt;/p&gt;

&lt;p&gt;Our spells are defined as 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;"school"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arcane"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cast"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"channel_ticks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"cost"&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;"item_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudswords:packet_rune"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;"cooldown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"impact"&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;span class="nl"&gt;"action"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spell_engine:damage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"damage"&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;"spell_power_coefficient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spell_engine:status_effect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status_effect"&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;"effect_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minecraft:glowing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&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;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;This is Infrastructure as Code for magic. Define your spell's behavior declaratively, and the engine handles execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  9 Tomes, 3 Tiers — Like Service Tiers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Tomes&lt;/th&gt;
&lt;th&gt;Rune Cost&lt;/th&gt;
&lt;th&gt;Spell Power&lt;/th&gt;
&lt;th&gt;Cloud Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;Hacker's Notebook, Codex of Uptime&lt;/td&gt;
&lt;td&gt;Packet Rune&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;td&gt;Free tier — basic operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;Pipeline, SRE Codex, Cloud Captain, DevOps&lt;/td&gt;
&lt;td&gt;Protocol Rune&lt;/td&gt;
&lt;td&gt;+4&lt;/td&gt;
&lt;td&gt;Standard tier — production workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;Serverless Bible, Grimoire of Architect, Zero Downtime&lt;/td&gt;
&lt;td&gt;Quantum Rune&lt;/td&gt;
&lt;td&gt;+7&lt;/td&gt;
&lt;td&gt;Enterprise tier — mission-critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each tier requires a more expensive rune to cast. Just like cloud services — basic operations are cheap, enterprise features cost more.&lt;/p&gt;

&lt;h2&gt;
  
  
  28 Spells — Cloud Operations as Game Mechanics
&lt;/h2&gt;

&lt;p&gt;Here are some highlights showing how cloud concepts become spells:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spell&lt;/th&gt;
&lt;th&gt;Tome&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ping&lt;/td&gt;
&lt;td&gt;Hacker's Notebook&lt;/td&gt;
&lt;td&gt;Damage + Glowing&lt;/td&gt;
&lt;td&gt;Network ping — reveals target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDoS Flood&lt;/td&gt;
&lt;td&gt;Hacker's Notebook&lt;/td&gt;
&lt;td&gt;Triple rapid hit&lt;/td&gt;
&lt;td&gt;Distributed denial — overwhelming force&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall Burst&lt;/td&gt;
&lt;td&gt;Codex of Uptime&lt;/td&gt;
&lt;td&gt;Resistance + Fire Res&lt;/td&gt;
&lt;td&gt;Firewall — blocks incoming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto Scale&lt;/td&gt;
&lt;td&gt;Tome of Pipeline&lt;/td&gt;
&lt;td&gt;AoE damage radius 6&lt;/td&gt;
&lt;td&gt;Auto Scaling — handles multiple targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident Response&lt;/td&gt;
&lt;td&gt;SRE Codex&lt;/td&gt;
&lt;td&gt;Regen III + Absorption&lt;/td&gt;
&lt;td&gt;Incident response — emergency healing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balance&lt;/td&gt;
&lt;td&gt;Cloud Captain&lt;/td&gt;
&lt;td&gt;Strength + Speed to allies&lt;/td&gt;
&lt;td&gt;Load balancer — distributes power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Sprint&lt;/td&gt;
&lt;td&gt;DevOps Manifesto&lt;/td&gt;
&lt;td&gt;Speed III + Haste II&lt;/td&gt;
&lt;td&gt;CI/CD — everything faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Nuke&lt;/td&gt;
&lt;td&gt;Serverless Bible&lt;/td&gt;
&lt;td&gt;Massive AoE&lt;/td&gt;
&lt;td&gt;Nuclear option — destroy everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Tunnel&lt;/td&gt;
&lt;td&gt;Grimoire of Architect&lt;/td&gt;
&lt;td&gt;Teleport 20 blocks&lt;/td&gt;
&lt;td&gt;Quantum networking — instant transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region Failover&lt;/td&gt;
&lt;td&gt;Zero Downtime&lt;/td&gt;
&lt;td&gt;Full heal + Res IV&lt;/td&gt;
&lt;td&gt;Failover — complete recovery, long CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cooldowns tell a story too: "Ping" is 3 seconds (quick diagnostic), "Region Failover" is 5 minutes (disaster recovery is expensive and slow).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rune System — API Credits
&lt;/h2&gt;

&lt;p&gt;Runes are consumed when you cast spells. They're your API credits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rune&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Crafting&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet Rune&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;Cloud Shard + Redstone + Lapis&lt;/td&gt;
&lt;td&gt;Basic API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol Rune&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;Packet Rune + Blazing Shard + Gold&lt;/td&gt;
&lt;td&gt;Standard API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Rune&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;Protocol Rune + Quantum Shard + Ender Pearl&lt;/td&gt;
&lt;td&gt;Enterprise API call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Higher-tier spells consume more expensive runes. You can't spam "Cloud Nuke" because Quantum Runes are expensive to craft — just like you can't spam expensive API calls without budget consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade Paths — Service Evolution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hacker's Notebook → Tome of the Pipeline → The Serverless Bible
Codex of Uptime → The SRE Codex → Grimoire of Zero Downtime
                → Tome of the Cloud Captain → Grimoire of the Architect
                → DevOps Manifesto (independent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upgrades use the Smithing Table: Tome + Compiled/Deployed Rune + vanilla item. The path from T1 to T3 mirrors how cloud services evolve — you start with basic tools and upgrade to enterprise solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empowered Tomes — Premium Tier
&lt;/h2&gt;

&lt;p&gt;Each tome has an "Empowered" variant (+2 extra spell power) crafted with rare materials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;T1 Empowered: + Amethyst Shard&lt;/li&gt;
&lt;li&gt;T2 Empowered: + Blaze Rod
&lt;/li&gt;
&lt;li&gt;T3 Empowered: + Nether Star&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's like going from standard to reserved instances — same service, better performance, higher upfront cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-Click Casting — The UX
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpellTome&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ItemStack&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Hand&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Check rune in inventory&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;hasRune&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getRequiredRune&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;literal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"§c✗ Missing "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;getRuneName&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Check cooldown&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isOnCooldown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. Consume rune&lt;/span&gt;
        &lt;span class="n"&gt;consumeRune&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getRequiredRune&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. Cast spell&lt;/span&gt;
        &lt;span class="n"&gt;castSpell&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// 5. Apply cooldown&lt;/span&gt;
        &lt;span class="n"&gt;applyCooldown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple flow: verify resources → consume → execute → cooldown. It's the same pattern as any API call: authenticate → charge → process → rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;28 spells is a lot of JSON&lt;/td&gt;
&lt;td&gt;Data-driven approach — one template, many configs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balancing cooldowns&lt;/td&gt;
&lt;td&gt;Map to real-world operation frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rune economy&lt;/td&gt;
&lt;td&gt;Make T1 cheap, T3 expensive — natural gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spell Engine compatibility&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;is_proxy: true&lt;/code&gt; for Binding Table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual identity per tome&lt;/td&gt;
&lt;td&gt;Isometric textures with unique colors + symbols&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Magic is great, but it needs infrastructure to support it. In the next post, we'll build a &lt;strong&gt;multiblock Data Melter&lt;/strong&gt; with BFS energy transfer through cables — because even in a fantasy world, you need power grids.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and today we cast cloud operations as spells. See you in the next one! ✨&lt;/p&gt;

</description>
      <category>programming</category>
      <category>minecraft</category>
      <category>gaming</category>
      <category>aws</category>
    </item>
    <item>
      <title>Monthly Amazon Location Service Updates - 2026.07</title>
      <dc:creator>Yasunori Kirimoto</dc:creator>
      <pubDate>Sat, 01 Aug 2026 02:10:06 +0000</pubDate>
      <link>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202607-45a3</link>
      <guid>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202607-45a3</guid>
      <description>&lt;h3&gt;
  
  
  Monthly Amazon Location Service Updates - 2026.07
&lt;/h3&gt;



&lt;p&gt;This is a summary of the July updates for Amazon Location Service.&lt;/p&gt;



&lt;h2&gt;
  
  
  2026.07 Updates
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-location-service-enhanced-address-search/" rel="noopener noreferrer"&gt;Amazon Location Service enhances Places APIs with new address and search options&lt;/a&gt;&lt;br&gt;
This release introduces the AddressNamesMode parameter to control how address components are returned, the AddressTranslations parameter to return place name translations in 50+ languages, and the TravelMode parameter to optimize search results for users in motion. It also adds the DriveThrough attribute indicating whether a place offers drive-through service, and a Parsing.AdditionalInfo response field in the Geocode API that details how the input address was interpreted.&lt;/p&gt;


&lt;h2&gt;
  
  
  Other Info
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://location.aws.com" rel="noopener noreferrer"&gt;Amazon Location Service Demo&lt;/a&gt;&lt;br&gt;
Official Amazon Location Service demo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/location/latest/developerguide" rel="noopener noreferrer"&gt;Amazon Location Service Developer Guide&lt;/a&gt;&lt;br&gt;
Official Amazon Location Service Documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aws-geospatial" rel="noopener noreferrer"&gt;AWS Geospatial&lt;/a&gt;&lt;br&gt;
Official AWS Geospatial samples.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mug-jp/maplibregljs-amazon-location-service-starter" rel="noopener noreferrer"&gt;maplibregljs-amazon-location-service-starter&lt;/a&gt;&lt;br&gt;
Build environment to get started with Amazon Location Service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/dayjournal"&gt;dev.to&lt;/a&gt;&lt;br&gt;
Articles on Amazon Location Service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://day-journal.com/memo/tags/Amazon-Location-Service" rel="noopener noreferrer"&gt;tags - Amazon Location Service&lt;/a&gt;&lt;br&gt;
&lt;a href="https://day-journal.com/memo/tags/Try" rel="noopener noreferrer"&gt;tags - Try&lt;/a&gt;&lt;br&gt;
Notes on Amazon Location Service. (Japanese)&lt;/p&gt;


&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" class="crayons-story__hidden-navigation-link"&gt;Monthly Amazon Location Service Updates - 2026.06&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/aws-heroes"&gt;
            &lt;img alt="AWS Heroes logo" 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%2Forganization%2Fprofile_image%2F2491%2Ff0c1a659-c959-42cd-bb12-cd25909dd9db.png" class="crayons-logo__image" width="504" height="504"&gt;
          &lt;/a&gt;

          &lt;a href="/dayjournal" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&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%2Fuser%2Fprofile_image%2F723587%2F6a4ad5ac-4836-4acc-8c61-0a0e3185429f.jpg" alt="dayjournal profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/dayjournal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yasunori Kirimoto
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yasunori Kirimoto
                
              
              &lt;div id="story-author-preview-content-4118158" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/dayjournal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F723587%2F6a4ad5ac-4836-4acc-8c61-0a0e3185429f.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yasunori Kirimoto&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/aws-heroes" class="crayons-story__secondary fw-medium"&gt;AWS Heroes&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" id="article-link-4118158"&gt;
          Monthly Amazon Location Service Updates - 2026.06
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/amazonlocationservice"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;amazonlocationservice&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/amazonlocationserviceupdates"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;amazonlocationserviceupdates&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




&lt;p&gt;&lt;a href="https://spotify.link/Hz9CHCuXAXb" rel="noopener noreferrer"&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%2Fm9qrnxbzxq95nih5fefo.png" width="650" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/@norivlog_ch" rel="noopener noreferrer"&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%2Fq87a67x9yapshd534duh.png" width="650" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>amazonlocationservice</category>
      <category>amazonlocationserviceupdates</category>
    </item>
    <item>
      <title>Send your events later</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Wed, 29 Jul 2026 20:26:23 +0000</pubDate>
      <link>https://dev.to/aws-heroes/send-your-events-later-25ha</link>
      <guid>https://dev.to/aws-heroes/send-your-events-later-25ha</guid>
      <description>&lt;p&gt;I build a lot of automated workflows. Across the suite of apps that powers Ready, Set, Cloud, I have a dozen or so mature, long-running automations that do some work, wait for something, then do more work. A great example is &lt;a href="https://readysetcloud.io/newsletter/sign-up" rel="noopener noreferrer"&gt;my newsletter&lt;/a&gt;. I often write it on Fridays and publish it on Mondays. After I open a PR for an issue, my backend service does some initial work, but it waits until Monday to send the emails and start analytics tracking.&lt;/p&gt;

&lt;p&gt;In the past, I've used the &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html" rel="noopener noreferrer"&gt;Step Functions &lt;code&gt;Wait&lt;/code&gt; state&lt;/a&gt; for this because it's the exact use case it was designed for. But as part of a big refactor I did to separate Ready, Set, Cloud logic from the generic newsletter logic, I needed to find a simpler way to schedule work - ideally as part of my CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;I thought using the AWS SDK in a script to create a one-time EventBridge schedule that published an event at a designated time was my best bet. But that brings in some implications with IAM and passing roles to the scheduler in order to invoke a &lt;code&gt;PutEvents&lt;/code&gt; call. It ended up being way too messy for a simple CI script.&lt;/p&gt;

&lt;p&gt;But I had an epiphany that immediately made me think, "&lt;em&gt;I should write about this&lt;/em&gt;" because it's very simple but not always obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprisingly-simple-yet-not-obvious solution
&lt;/h2&gt;

&lt;p&gt;I moved the EventBridge Scheduler event publish behind an EventBridge rule. Anything that can call &lt;code&gt;PutEvents&lt;/code&gt; can now ask for a deferred event.&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%2Fpurnrtw8zfdvw8euvaia.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%2Fpurnrtw8zfdvw8euvaia.png" alt="Flowchart showing how EventBridge creates a schedule to defer publishing an event" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's essentially a one-time loop with a customizable wait step in it. Here's an example payload of an event to get my static site to rebuild because future-dated content &lt;a href="https://www.readysetcloud.io/blog/allen.helton/serverless-post-scheduler-for-static-sites/" rel="noopener noreferrer"&gt;doesn't render automatically&lt;/a&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;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"readysetcloud.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schedule Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Detail"&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;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-03T09:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"America/Chicago"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"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;"newsletter-rebuild-227"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event"&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;"detailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trigger Site Rebuild"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"detail"&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;"issueNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;227&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;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;Publishing the above event will create a schedule that triggers at 9 a.m. CDT on August 3. The schedule will publish a &lt;code&gt;Trigger Site Rebuild&lt;/code&gt; event back onto the bus, which is wired up to a rule that kicks off a build in Amplify. Nifty, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  Just a little bit of logic
&lt;/h3&gt;

&lt;p&gt;While I normally try to skip Lambda in this type of integration, I needed it for a couple of reasons. One, you can't set the EventBridge Scheduler as a &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;target of a rule&lt;/a&gt; 🙃. And even if you could, I would have lost my ability to maintain schedules. If I needed to reschedule an event for a different time, I would have been out of luck.&lt;/p&gt;

&lt;p&gt;So I wrote a Lambda function as the target that will upsert the schedule, validate the event (&lt;em&gt;WHICH YOU SHOULD ALWAYS DO&lt;/em&gt;), and transform the provided time to the format the scheduler expects.&lt;/p&gt;

&lt;p&gt;As a bonus, the Lambda function also recognizes when the scheduled date is in the past, and will publish the event immediately or throw it away, depending on a parameter optionally provided in the event payload.&lt;/p&gt;

&lt;p&gt;So in this case, it made a lot of sense to add the function in the middle, which increases the complexity of the build, but the tradeoff is a richer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build your own platform
&lt;/h2&gt;

&lt;p&gt;A few years ago, I made what I called my &lt;a href="https://readysetcloud.io/blog/allen.helton/automate-your-life-and-save-time-with-serverless-technology" rel="noopener noreferrer"&gt;&lt;em&gt;serverless toolbox&lt;/em&gt;&lt;/a&gt;, with some tools I could use across all of my apps that ran in the same AWS account. What I didn't know then was that I was essentially building a platform for all of my applications to run.&lt;/p&gt;

&lt;p&gt;I've since abandoned the toolbox and have turned my attention fully toward building a platform my apps run on. It's the peak of DRY (Don't Repeat Yourself) and in this case, is incredibly useful. I've added this scheduling primitive to &lt;a href="https://github.com/readysetcloud/rsc-core" rel="noopener noreferrer"&gt;my platform repo&lt;/a&gt; so it can be used everywhere - my newsletter service, blog service, and all the other services I've built that power my website.&lt;/p&gt;

&lt;p&gt;I only need to solve this problem once. Time and time again I've reimplemented things that worked in the past only to find myself with 4 versions of the same thing and at least one of them stale. So I make an intentional effort to consolidate the shared work and make the entry point to it as simple as possible (what's simpler than calling &lt;code&gt;PutEvents&lt;/code&gt;?!).&lt;/p&gt;

&lt;p&gt;Anyway, this is intentionally short and sweet. I was excited by the idea of delaying an event with nothing more than &lt;code&gt;PutEvents&lt;/code&gt;, and wanted to share. For the full source code, you can check out the &lt;a href="https://github.com/readysetcloud/rsc-core/blob/main/functions/schedule-event.mjs" rel="noopener noreferrer"&gt;function here&lt;/a&gt; or view the &lt;a href="https://github.com/readysetcloud/rsc-core/pull/223" rel="noopener noreferrer"&gt;entire PR&lt;/a&gt; to my platform app.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 2 Develop Agents with Embabel shell</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:35:30 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-1-introduction-to-1ebl"&gt;part 1&lt;/a&gt;, we addressed some challenges when developing AI agents with Spring AI frameworks only. We start exploring how Embabel is capable of addressing and solving those challenges. Throughout this series, we'll start improving the agent developed in the &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt; article using the Embabel framework. First, we'll use the Embabel shell, then the web application. You can look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. I updated them to use the recently released Embabel version 1.0.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement AI agents with Embabel shell
&lt;/h2&gt;

&lt;p&gt;In this article, we'll implement 2 AI agents capable of responding to the following prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference."&lt;/li&gt;
&lt;li&gt;"Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As our sample application also supports only searching for the conference as an attendee, not as a speaker, you can similarly implement those agents.&lt;/p&gt;

&lt;p&gt;Before we start, we need to understand the following &lt;a href="https://github.com/embabel/embabel-agent#key-concepts" rel="noopener noreferrer"&gt;key concepts&lt;/a&gt; of the Embabel framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actions: Steps an agent takes&lt;/li&gt;
&lt;li&gt;Goals: What an agent is trying to achieve&lt;/li&gt;
&lt;li&gt;Conditions: Conditions to assess before executing an action or determining that a goal has been achieved. Conditions are reassessed after each action is executed.&lt;/li&gt;
&lt;li&gt;Domain model: Objects underpinning the flow and informing Actions, Goals and Conditions.&lt;/li&gt;
&lt;li&gt;Plan: A sequence of actions to achieve a goal. Plans are dynamically formulated by the system, not the programmer. The system replans after the completion of each action, allowing it to adapt to new information as well as observe the effects of the previous action. This is effectively an OODA loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's first define the &lt;em&gt;actions&lt;/em&gt; for our agent - steps an agent takes.&lt;br&gt;
We can split our first agent into the following actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request: "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today". LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "Provide me with the list of my talks with this topic in the title".  LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "For each conference and talk retrieved, apply individually for the conference". This request means that the LLM uses its responses from the first 2 requests as the request and responds to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And we can split our second agent into the following actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request: "Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service". LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "Provide me with the list of my talks with this topic in the title".  LLM responds to it.&lt;/li&gt;
&lt;li&gt;Request: "For each conference and talk, apply individually for the conference". This request means that the LLM uses its responses from the first 2 requests as the request and responds to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this particular case, steps 2 and 3 are mainly the same for our 2 agents. The difference lies in step 1, where we either retrieve an existing talk(s) or create a new one. Your case might be different, so the design of the agents and their actions might also be different.&lt;/p&gt;

&lt;p&gt;First, let's declare some important dependencies in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt; to use the Embabel framework (I use version 1.0.0) and its shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-bedrock&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-shell&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure not to declare the &lt;em&gt;spring-boot-starter-web&lt;/em&gt; dependency, as we'll implement a web application in the next article.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/EmbabelConferenceApplication.java" rel="noopener noreferrer"&gt;EmbabelConferenceApplication&lt;/a&gt; is the main entry point of our application. Here we also load Amazon Bedrock models, which we declared in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/models/additional-bedrock.yaml" rel="noopener noreferrer"&gt;additional-bedrock.yaml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdditionalBedrockModels&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
   &lt;span class="nc"&gt;BedrockModelLoader&lt;/span&gt; &lt;span class="nf"&gt;bedrockModels&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BedrockModelLoader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultResourceLoader&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"classpath:models/additional-bedrock.yaml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain&lt;/a&gt; class, we declared a domain object (Java record) for each request and response, which corresponds to the agent actions described above. For example, for the action to search for the talks, those domain objects look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Request to LLM to create the talk&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ConferenceSearchRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
  &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersOpenOnThistDate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// Response from LLM for the talk creation request&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;Conference&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;conferenceId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;homepage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;endDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersStartDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;callForPapersEndDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;linkToCallforPapers&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's design those 2 agents. We defined the common actions for both of them in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class. We defined individual agent-specific actions in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; classes, respectively.&lt;/p&gt;

&lt;p&gt;Let's explain the &lt;em&gt;talk search&lt;/em&gt; agent action from the &lt;em&gt;SearchForTalksAndApplyForConferencesAgent&lt;/em&gt; class in more detail. First of all, we annotate each agent with Embabel's &lt;em&gt;Agent&lt;/em&gt; annotation, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Agent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"search for the talk(s), search for the conference(s) by the given criteria (all, by the topic, by the date range, and by call for papers being open on some date), and apply for them with the found talks"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractConferenceAgent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
 &lt;span class="o"&gt;....&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we give the agent a name and description.&lt;/p&gt;

&lt;p&gt;The first question is how we can break down a big unstructured prompt into the 3 actions that we described above? Let's start with the action to search for the talk first. The answer is: we can ask LLMs to do this job. Even cheap models are already capable of doing this. Here is how this looks for the action: "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt; &lt;span class="nf"&gt;extractTalkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserInput&lt;/span&gt; &lt;span class="n"&gt;userInput&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OperationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withLlm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LlmOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"us.amazon.nova-pro-v1:0"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
Create a talk search request from this user input, extracting the optional parameters like talk title substring, conference start and end date, and whether the call for papers is still open on a certain date. 
Don't include any other information in the request.
Here is the user input:
     %s"""&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain step by step what's happening here. The &lt;em&gt;extractTalkSearchRequest&lt;/em&gt; method is an action, which the Agent executes.  That's why we annotated it with the Embabel &lt;em&gt;Action&lt;/em&gt; annotation. This method also takes 2 parameters, both coming from the Embabel framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;UserInput&lt;/em&gt; object, which represents the unstructured user prompt&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;OperationContext&lt;/em&gt;, which gives us access to the AI functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We first invoke the &lt;em&gt;ai()&lt;/em&gt; method of the &lt;em&gt;context&lt;/em&gt; object and then provide the LLM to do the job. We can either pass the concrete model AI or use other options that are available, like using the default LLM, LLM by role, and others. As a result, we get back the Embabel implementation of the &lt;em&gt;PromptRunnerOperations&lt;/em&gt;. Or, we can declare such LLMs in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;, for example :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;embabel.models.default-llm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.best&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.balanced&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-pro-v1:0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By invoking the &lt;em&gt;createObject()&lt;/em&gt; method on the &lt;em&gt;PromptRunnerOperations&lt;/em&gt; implementation, we pass the user prompt and instruction to the LLM on how to split this prompt into one subtask or action. It should only contain the information about the talk search criteria. Additionally, we pass the domain object, in this case, the object of type &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.TalkSearchRequest&lt;/a&gt;. Embabel will make the best effort to map the LLM response to this domain object, which additionally provides us with type safety. &lt;/p&gt;

&lt;p&gt;The next step is to implement the talk search itself. For this, we define another action in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt; &lt;span class="nf"&gt;talkSearch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TalkSearchRequest&lt;/span&gt; &lt;span class="n"&gt;talkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Ai&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;        
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;     
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptRunner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;            
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPromptContributors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;talkSearchRequest&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get-talks-by-"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Search for the talks with the given criteria"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note that the object of the &lt;em&gt;Domain.TalkSearchRequest&lt;/em&gt; type, which is the output of the previous action, is now the input of this action. Also, we inject the &lt;em&gt;Ai&lt;/em&gt; object coming from the Embabel framework this time directly.  Let's first ignore the &lt;em&gt;config.speaker&lt;/em&gt; invocation, which we'll explain later. Here we pass the prompt, which contains the properties required to search for the talk. When with &lt;em&gt;createObject()&lt;/em&gt; method invocation, we once again pass the prompt and the domain object, which should match the outcome, in this case, the object of type &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.Talks&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To respond to this prompt, the agent needs tools. Please review the articles &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-local-mcp-client-560a"&gt;Develop local MCP client for Conference application &lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-2odf"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt; to understand the whole setup. I implemented very similar code to initialize the MCP client connected to the Bedrock AgentCore Gateway in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/service/McpToolService.java" rel="noopener noreferrer"&gt;McpToolService&lt;/a&gt; class. MCP client is then exposed as a Spring bean in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/EmbabelConferenceApplication.java" rel="noopener noreferrer"&gt;EmbabelConferenceApplication&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt; 
&lt;span class="nd"&gt;@Primary&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;McpSyncClient&lt;/span&gt; &lt;span class="nf"&gt;geMcpClient&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcpToolService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By invoking the &lt;em&gt;withToolGroup(this.getMcpToolGroupByName("get-talks-by-"))&lt;/em&gt; method on the &lt;em&gt;PromptRunner&lt;/em&gt; (see above), we achieved another goal that we described in part 1. We not only split the prompt into individual actions, but are now also able to provide exact MCP tool(s) required to execute it.  Embabel provides the &lt;em&gt;ToolGroup&lt;/em&gt; abstraction for this. In this case, we give the LLM only the tools whose names consist of the &lt;em&gt;get-talks-by-&lt;/em&gt; substring. I implemented the MCP tool filtering in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;ToolGroup&lt;/span&gt; &lt;span class="nf"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt;  &lt;span class="n"&gt;toolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="nc"&gt;ToolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Companion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
         &lt;span class="s"&gt;"A collection of tools to interact with 
         the MCP conference search service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
         &lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
   &lt;span class="o"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;noOp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolCallContextMcpMetaConverter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Companion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;noOp&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;toolGroupDescription&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="s"&gt;"Vadym"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolGroupPermission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INTERNET_ACCESS&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpClient&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filterMcpTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;noOp&lt;/span&gt;
      &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;filterMcpTool&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCallback&lt;/span&gt; &lt;span class="n"&gt;toolCallback&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// not passing name means return true -&amp;gt; pass all tools&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;toolCallback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolDefinition&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downside is that if we were to rename the MCP tools, we'd need to adjust their names here also. The alternative is to use semantic search for the tools. The alternative is to use semantic search for the tools.&lt;/p&gt;

&lt;p&gt;Now let's come back to the &lt;em&gt;config.speaker()&lt;/em&gt; method invocation, which we saw in the &lt;em&gt;talkSearch&lt;/em&gt; action. Embabel gives the possibility to create different roles and attach different LLMs to them. In our case, we have 2 roles: conference attendee and conference speaker. We can define these roles either programmatically or in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;conference.attendee.llm.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conference attendee&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.goal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conduct a thorough conference search with the information (topics, conference dates) provided so I decide which to attend.&lt;/span&gt;
&lt;span class="py"&gt;conference.attendee.persona.backstory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;As a Conference Attendee Specialist, your mission is to find a suitable conference that matches the given request&lt;/span&gt;


&lt;span class="py"&gt;conference.speaker.llm.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;best&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conference speaker&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.goal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Conduct the search for the talks or create the talk with the given criteria with the conference application for those talks&lt;/span&gt;
&lt;span class="py"&gt;conference.speaker.persona.backstory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;As a Conference Speaker Specialist, your mission is to search for the talk or create the talk with the given criteria with the conference application for those talks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also link each role to the LLM model by referencing the previously defined LLM strategy (in our case, by the name &lt;em&gt;best&lt;/em&gt;). See the LLM definition once again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;embabel.models.default-llm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.best&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-6&lt;/span&gt;
&lt;span class="py"&gt;embabel.models.llms.balanced&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;us.amazon.nova-pro-v1:0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, the &lt;em&gt;best&lt;/em&gt; LLM strategy will use the &lt;em&gt;us.anthropic.claude-sonnet-4-6&lt;/em&gt; model.&lt;br&gt;
Next, we defined &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/config/ConferenceConfig.java" rel="noopener noreferrer"&gt;ConferenceConfig&lt;/a&gt; to have access to the roles and actors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Validated&lt;/span&gt;
&lt;span class="nd"&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"conference"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ConferenceConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoleGoalBackstory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;attendee&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   
    &lt;span class="nc"&gt;Actor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoleGoalBackstory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;conference&lt;/em&gt; prefix, together with &lt;em&gt;Actor&lt;/em&gt; definitions, determines the mapping to the roles. For example, &lt;em&gt;Actor speaker&lt;/em&gt; matched the &lt;em&gt;conference.speaker&lt;/em&gt; role definition in the application.properties.&lt;/p&gt;

&lt;p&gt;Finally, we injected the &lt;em&gt;ConferenceConfig&lt;/em&gt; object (as config) into the constructor of each agent. &lt;/p&gt;

&lt;p&gt;Similarly, we split the initial prompt into other individual steps or actions for searching for conferences. See the &lt;em&gt;extractConferenceSearchRequest&lt;/em&gt; and &lt;em&gt;conferenceSearch&lt;/em&gt; agents in the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/AbstractConferenceAgent.java" rel="noopener noreferrer"&gt;AbstractConferenceAgent&lt;/a&gt; class.  Also, the last step (applying for conferences with talks) looks similar. See the &lt;em&gt;applyForConference&lt;/em&gt; action in the same class. There are 2 things to notice here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We don't ask the LLM to split the prompt. This is because the input to this action is the output of 2 actions before: &lt;em&gt;talkSearch&lt;/em&gt; and &lt;em&gt;conferenceSearch&lt;/em&gt;, and 2 domain objects, &lt;em&gt;Domain.Conferences&lt;/em&gt; and &lt;em&gt;Domain.Talks&lt;/em&gt; respectively. Both become the input to the &lt;em&gt;PromptContributors&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;This agent is additionally annotated with the &lt;em&gt;AchievesGoal&lt;/em&gt; annotation to outline that this action execution is the final step that the agent should take in the workflow. See the complete code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Action&lt;/span&gt;
&lt;span class="nd"&gt;@AchievesGoal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"apply for the conferences with their IDs and talk IDs and provide status of the application"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyForConference&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Conferences&lt;/span&gt; &lt;span class="n"&gt;conferences&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Talks&lt;/span&gt; &lt;span class="n"&gt;talks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Ai&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speaker&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptRunner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPromptContributors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conferences&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;talks&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withToolGroup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMcpToolGroupByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"apply-to-conference"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Apply for the conference with the given criteria"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're completely done with the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; agent. &lt;/p&gt;

&lt;p&gt;The implementation of the agent &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; looks very similar. The biggest difference is the first step: instead of searching for the existing talks, the talk is created. See the &lt;em&gt;extractTalkCreationRequest&lt;/em&gt; and &lt;em&gt;createTalks&lt;/em&gt; actions.&lt;/p&gt;

&lt;p&gt;Now come the biggest questions: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How is Embabel capable of identifying which agent to execute?&lt;/li&gt;
&lt;li&gt;How is Embabel capable of identifying the order of actions to execute for the individual agent? We only defined the last action to finally achieve the goal properly by annotation. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build the application with &lt;code&gt;mvn clean package&lt;/code&gt; and run it locally with &lt;code&gt;mvn spring-boot:run&lt;/code&gt;. We see that Embabel starts the shell, and we can execute the prompt by running: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;x 'Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference.'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's see what happens next: &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%2F2ld5oyy2lwipllhrapd8.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%2F2ld5oyy2lwipllhrapd8.png" alt=" " width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Embabel identifies 2 agents, takes their description to compute the score, and chooses a concrete agent to execute based on the user input (prompt). I assume Embabel also uses an LLM for it. As a result, it computed: As a result, it computed :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SearchForExistingTalksAndApplyForConferencesAgent: 0,97&lt;/li&gt;
&lt;li&gt;CreateNewTalksAndApplyForConferencesAgent: 0,05&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the &lt;em&gt;SearchForExistingTalksAndApplyForConferencesAgent&lt;/em&gt; agent is clearly a winner.&lt;/p&gt;

&lt;p&gt;Next, we see that Embabel formulates a plan to achieve the goal:&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%2Fmmd3fkkej5t3t37hz99c.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%2Fmmd3fkkej5t3t37hz99c.png" alt=" " width="796" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sequence of agent actions to execute is a correct one :&lt;/p&gt;

&lt;p&gt;Embabel could also come up with the plan to first search for the conferences, then for the talks, and then to apply. Both plans are valid. How does Embabel create such a plan? The system, not the programmer, dynamically formulates plans. The system replans after the completion of each action, allowing it to adapt to new information as well as observe the effects of the previous action. This is effectively an OODA loop. Emabebl analyzes the input and output types of each agent that we defined. This also helps to formulate the plan. &lt;/p&gt;

&lt;p&gt;We see how Embabel executes those individual actions in the identified order. It logs all structured outputs of each action. The final output looks like this:&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%2Fo3vwb5zwfdps6vbnkctj.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%2Fo3vwb5zwfdps6vbnkctj.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It found 2 talks and 1 conference to apply those talks that matched the criteria that we defined in the prompt. Finally, Embabel provides the statistics of the LLM used, the number of input (prompt) and output (completion) tokens spent. We also see the total cost of the agent execution. Embabel maintains the LLM pricing database. I don't expect this information to be very accurate, but it provides some indication. We also see the MCP tool names that the LLM used during the agent execution, their response times, and the number of failures.&lt;/p&gt;

&lt;p&gt;If we execute the prompt &lt;code&gt;x 'Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created.'&lt;/code&gt;, we'll see that Embabel chooses to execute the &lt;em&gt;CreateNewTalksAndApplyForConferencesAgent&lt;/em&gt; agent :&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%2F8q4edk7uyuzhu22cbssy.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%2F8q4edk7uyuzhu22cbssy.png" alt=" " width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It then formulates the plan and executes the actions to achieve the goal:&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%2Fwheywxj1ywnypnqznm66.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%2Fwheywxj1ywnypnqznm66.png" alt=" " width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, it provides the same statistics, which we previously explained:&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%2Flszjugfl3ytuiubz5otd.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%2Flszjugfl3ytuiubz5otd.png" alt=" " width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are many more features that Embabel provides, like retries, which we'll explore in later articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In the next article, we'll slightly adjust our application to convert it into a web application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Beyond The Single-Agent Ceiling: Scale Out With MCP Agent Teams</title>
      <dc:creator>Guy</dc:creator>
      <pubDate>Fri, 24 Jul 2026 22:10:08 +0000</pubDate>
      <link>https://dev.to/aws-heroes/beyond-the-single-agent-ceiling-scale-out-with-mcp-agent-teams-80b</link>
      <guid>https://dev.to/aws-heroes/beyond-the-single-agent-ceiling-scale-out-with-mcp-agent-teams-80b</guid>
      <description>&lt;p&gt;Most people's experience with AI is a conversation with one assistant. ChatGPT, Claude, and similar products present one conversational partner. You ask it a question, it reasons, perhaps calls a few tools, and gives you an answer.&lt;/p&gt;

&lt;p&gt;That experience creates a natural architectural instinct: if one agent is useful, make that one agent more capable. Give it better instructions. Connect more tools. Add more context. Increase its token budget. Upgrade the model.&lt;/p&gt;

&lt;p&gt;That is the right place to start.&lt;/p&gt;

&lt;p&gt;It is not an infinitely scalable place to finish.&lt;/p&gt;

&lt;p&gt;At some point, the single agent is no longer doing one coherent job. Its instructions contain a job description, an organization chart, a workflow engine, a memory policy, a delegation policy, a review checklist, and a recovery procedure. Its context contains the request, the plan, intermediate evidence, failed attempts, tool results, and the agent's own conclusions about all of them. Its tool list spans several domains. It is asked to investigate, calculate, edit, criticize, validate, and finally approve its own work.&lt;/p&gt;

&lt;p&gt;The architecture still looks simple because there is only one box labeled "agent." The complexity did not disappear. It moved inside the box, where it became harder to see, test, and govern.&lt;/p&gt;

&lt;p&gt;This article is about what happens after that point.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dev.to/aws-heroes/build-mcp-servers-before-agents-the-enterprise-shift-ai-teams-need-2cjc"&gt;previous article&lt;/a&gt; reduced an agent to three parts: instructions, an LLM, and MCP tools. That remains the correct foundation. This article extends it with two arguments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A single agent has a capability ceiling.&lt;/strong&gt; For sufficiently complex, decomposable work, a team can scale beyond that ceiling through specialization, separate contexts, parallelism, and independent validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A team pays a coordination tax.&lt;/strong&gt; But that tax exists in a single-agent system too, hidden in an increasingly confusing prompt and volatile context. MCP lets us externalize it into explicit, reusable collaboration infrastructure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical pattern is to scale up one agent while simplicity remains an advantage, then scale out into a team when implicit coordination becomes the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MCP? (The 30-Second Version)
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP, &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25" rel="noopener noreferrer"&gt;spec 2025-11-25&lt;/a&gt;) is the interface layer between AI clients and external systems through tools, prompts, and resources. Throughout this series, we have treated MCP servers as the governed capability layer for enterprise AI: thin, remote, mostly stateless interfaces to internal systems and SaaS systems.&lt;/p&gt;

&lt;p&gt;An agent is an MCP client with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;instructions&lt;/strong&gt; describing its job and boundaries&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;LLM&lt;/strong&gt; providing reasoning and language capabilities&lt;/li&gt;
&lt;li&gt;selected &lt;strong&gt;MCP tools&lt;/strong&gt; connecting it to the outside world&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent team does not replace that model. It composes it recursively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a specialized agent can be exposed as an MCP tool&lt;/li&gt;
&lt;li&gt;another agent can discover and call that tool&lt;/li&gt;
&lt;li&gt;shared collaboration services can also be exposed through MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is not a new species of software. It is a scale-out architecture built from the same capability model.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Single Agent Is The Right Place To Start
&lt;/h2&gt;

&lt;p&gt;Teams introduce real cost. A coordinator has to decompose work, select collaborators, wait for results, resolve conflicts, and synthesize an answer. Multiple agents consume more tokens and create more failure boundaries. Shared state needs ownership, permissions, and lifecycle rules.&lt;/p&gt;

&lt;p&gt;For many tasks, none of that is justified.&lt;/p&gt;

&lt;p&gt;Use one agent when the work is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;short&lt;/li&gt;
&lt;li&gt;mostly sequential&lt;/li&gt;
&lt;li&gt;contained within one context&lt;/li&gt;
&lt;li&gt;served by a small, coherent toolset&lt;/li&gt;
&lt;li&gt;easy to validate directly&lt;/li&gt;
&lt;li&gt;not valuable enough to justify additional inference and infrastructure cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before adding agents, scale up the single agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;improve its instructions&lt;/li&gt;
&lt;li&gt;use a stronger model where the economics make sense&lt;/li&gt;
&lt;li&gt;reduce and clarify its tools&lt;/li&gt;
&lt;li&gt;move deterministic computation into MCP servers&lt;/li&gt;
&lt;li&gt;package repeatable workflows as prompts&lt;/li&gt;
&lt;li&gt;give long-running work an explicit task lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This follows the same principle used throughout the series: do not add probabilistic orchestration where a deterministic capability would be better. If server code can reliably perform a calculation or enforce a known workflow, use server code. A second agent is not a substitute for a well-designed tool.&lt;/p&gt;

&lt;p&gt;But scaling up eventually reaches diminishing returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Single-Agent Ceiling
&lt;/h2&gt;

&lt;p&gt;The ceiling is not a single model limit. Several pressures compound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context saturation&lt;/strong&gt;&lt;br&gt;
One agent must carry the plan, evidence, intermediate results, conversation history, and its own reasoning in one working context. A larger context window increases capacity, but it does not guarantee that every relevant detail receives the right attention at the right time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool overload&lt;/strong&gt;&lt;br&gt;
A generalist agent needs more tools. The &lt;a href="https://dev.to/aws-heroes/mcp-tool-design-why-your-ai-agent-is-failing-and-how-to-fix-it-40fc"&gt;tool-design article&lt;/a&gt; showed that tool performance can fall off a cliff as the choice set grows. Dividing work across specialists lets each agent see a smaller, more relevant capability surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role collision&lt;/strong&gt;&lt;br&gt;
Planning, execution, criticism, approval, and final communication require different behavior. Encoding all of them into one instruction set creates conflicting objectives: move quickly but verify everything, explore alternatives but remain concise, propose a solution but distrust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential throughput&lt;/strong&gt;&lt;br&gt;
One agent explores one trajectory at a time. When a problem contains several independent research paths or checks, a team can work on them concurrently using separate contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path dependence&lt;/strong&gt;&lt;br&gt;
An early mistaken assumption shapes later reasoning. The same agent reviewing its own work sees the conclusion through the context that produced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak self-verification&lt;/strong&gt;&lt;br&gt;
Telling an agent to "double-check your answer" is useful, but it does not create an independent reviewer, an authoritative state boundary, or a requirement that validation succeed before completion.&lt;/p&gt;

&lt;p&gt;These pressures explain why adding more instructions can make an agent worse. Each new rule may be sensible in isolation, but the complete prompt becomes a crowded control plane for responsibilities that should have explicit owners and interfaces.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Agent Scaling Dilemma
&lt;/h2&gt;

&lt;p&gt;Database architecture has long faced a similar decision: scale up one system while that remains economical, then scale out when the growing system crosses a complexity or capacity boundary. The same curve applies to agents.&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%2Fngcu4e3w2cbcg0mw214g.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%2Fngcu4e3w2cbcg0mw214g.png" alt="The Agent Scaling Dilemma: a single agent starts cheaply but its hidden coordination cost rises sharply, while an agent team requires more initial investment and then scales more gradually" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram is a conceptual architecture curve, not benchmark data.&lt;/p&gt;

&lt;p&gt;The single-agent line begins lower. One prompt, one loop, and one deployment are hard to beat for a prototype. During initial growth, better instructions, a better model, and a curated toolset extend its useful range.&lt;/p&gt;

&lt;p&gt;The team line begins higher because collaboration infrastructure must exist before the team can work well. Agents need contracts, shared artifacts, state ownership, memory boundaries, validation, status tracking, and governance.&lt;/p&gt;

&lt;p&gt;The crossover arrives when keeping those concerns implicit inside one agent becomes more expensive and less reliable than representing them explicitly. Beyond that point, the team architecture can add specialized capacity without continually expanding one agent's prompt, context, and tool surface.&lt;/p&gt;

&lt;p&gt;This is the important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A team is not cheaper because coordination disappears. A team becomes scalable because coordination is made explicit, reusable, and governable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Teams Scale Reasoning Out
&lt;/h2&gt;

&lt;p&gt;Scale-out is useful when a problem can be divided into bounded responsibilities.&lt;/p&gt;

&lt;p&gt;Different agents can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;different instructions&lt;/li&gt;
&lt;li&gt;different tools and permissions&lt;/li&gt;
&lt;li&gt;different working contexts&lt;/li&gt;
&lt;li&gt;different artifact ownership&lt;/li&gt;
&lt;li&gt;different validation responsibilities&lt;/li&gt;
&lt;li&gt;even different models chosen for the economics of their role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A research agent can explore one branch while another explores a second branch. A finance agent can receive only finance tools and policy. A reviewer can inspect an artifact without inheriting every assumption made by its author. A coordinator can work from concise findings instead of carrying every raw document in its own context.&lt;/p&gt;

&lt;p&gt;There is growing evidence that this helps for the right task shapes. Anthropic reported that its multi-agent research system outperformed its single-agent baseline by 90.2% on an internal research evaluation, especially for breadth-first queries with independent directions. The same report also says the multi-agent system used about 15 times as many tokens as ordinary chat and was a poor fit for work with tightly coupled dependencies. (&lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;Anthropic engineering&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;A controlled study across 180 agent configurations found a similar boundary: centralized coordination improved performance substantially on parallelizable tasks in its benchmarks, while every tested multi-agent topology degraded performance on sequential reasoning tasks. (&lt;a href="https://arxiv.org/abs/2512.08296" rel="noopener noreferrer"&gt;Towards a Science of Scaling Agent Systems&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The conclusion is not that more agents are always better. It is that teams raise the ceiling when the work benefits from partitioning, parallel exploration, specialization, or independent checking.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Coordination Tax Exists Either Way
&lt;/h2&gt;

&lt;p&gt;Multi-agent systems are often criticized for adding coordination overhead. That criticism is correct but incomplete.&lt;/p&gt;

&lt;p&gt;A complex single agent also coordinates. It decides which responsibility it is performing, tracks intermediate state, remembers failed approaches, checks its evidence, manages its tool results, and decides whether its own answer is valid. The difference is that all of this happens implicitly inside probabilistic reasoning.&lt;/p&gt;

&lt;p&gt;The choice is therefore not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;coordination&lt;/li&gt;
&lt;li&gt;no coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;implicit coordination inside one prompt and context&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;explicit coordination through contracts, artifacts, memory, tasks, and ownership&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;Concern&lt;/th&gt;
&lt;th&gt;Hidden Inside One Agent&lt;/th&gt;
&lt;th&gt;Externalized In A Team&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Responsibility&lt;/td&gt;
&lt;td&gt;Sections of one large instruction prompt&lt;/td&gt;
&lt;td&gt;Small role-specific instructions and agent contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegation&lt;/td&gt;
&lt;td&gt;Internal reasoning about what to do next&lt;/td&gt;
&lt;td&gt;Typed calls to selected collaborators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working state&lt;/td&gt;
&lt;td&gt;Conversation and model context&lt;/td&gt;
&lt;td&gt;Named, durable artifacts with explicit owners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prior attempts&lt;/td&gt;
&lt;td&gt;Whatever remains salient in context&lt;/td&gt;
&lt;td&gt;Append-only hypothesis or decision logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;The author checks its own conclusion&lt;/td&gt;
&lt;td&gt;An independently instructed reviewer or referee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutation&lt;/td&gt;
&lt;td&gt;Any reasoning step may silently revise the working answer&lt;/td&gt;
&lt;td&gt;A designated writer validates and records changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-running work&lt;/td&gt;
&lt;td&gt;The caller waits or invents hidden state&lt;/td&gt;
&lt;td&gt;Task identifiers, status, retry, cancellation, and results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning&lt;/td&gt;
&lt;td&gt;More instructions added to the prompt&lt;/td&gt;
&lt;td&gt;Scoped, searchable team memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure analysis&lt;/td&gt;
&lt;td&gt;Reconstruct the model's reasoning after the fact&lt;/td&gt;
&lt;td&gt;Inspect calls, files, logs, status, and validation results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Making coordination explicit does not make it free. It makes it addressable.&lt;/p&gt;

&lt;p&gt;The engineering checklist becomes concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How is work decomposed?&lt;/li&gt;
&lt;li&gt;Which agent owns each responsibility?&lt;/li&gt;
&lt;li&gt;What is the input and output contract?&lt;/li&gt;
&lt;li&gt;Where do intermediate artifacts live?&lt;/li&gt;
&lt;li&gt;Who may update authoritative state?&lt;/li&gt;
&lt;li&gt;How are duplicate work and conflicting changes prevented?&lt;/li&gt;
&lt;li&gt;How are progress, retries, timeouts, and cancellation represented?&lt;/li&gt;
&lt;li&gt;Who resolves contradictory findings?&lt;/li&gt;
&lt;li&gt;What information may be remembered, for how long, and for whom?&lt;/li&gt;
&lt;li&gt;How do identity, authorization, and audit context propagate?&lt;/li&gt;
&lt;li&gt;How are token, latency, and infrastructure budgets enforced?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are difficult to answer when their only implementation is prose buried in one system prompt. Once they are implemented as collaboration services, the investment can support many teams rather than one carefully tuned agent.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Three MCP Services An Agent Team Needs
&lt;/h2&gt;

&lt;p&gt;In an MCP-native platform, three shared services cover most of the collaboration substrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            ┌─────────────────────┐
                            │  coordinator agent  │
                            └──────────┬──────────┘
                                       │
                 ┌─────────────────────┼─────────────────────┐
                 │                     │                     │
                 ▼                     ▼                     ▼
          ┌─────────────┐       ┌─────────────┐       ┌─────────────────┐
          │  team-mcp   │       │   team-fs   │       │   team-memory   │
          │ delegation  │       │  artifacts  │       │ semantic recall │
          └──────┬──────┘       └──────┬──────┘       └────────┬────────┘
                 │                     │                       │
          specialized agents      shared case files      reusable lessons
                 │                     │                       │
                 └─────────────────────┼───────────────────────┘
                                       │
                                       ▼
                             domain MCP servers
                                       │
                                       ▼
                             systems of record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The names matter less than the separation of responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;team-mcp&lt;/code&gt;: The Coordination Plane
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;team-mcp&lt;/code&gt; exposes selected agents as MCP tools.&lt;/p&gt;

&lt;p&gt;A coordinator might see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;finance_checker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;policy_reviewer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;research_agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;independent_validator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each agent has the same things a good MCP tool has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear name and description&lt;/li&gt;
&lt;li&gt;a typed input contract&lt;/li&gt;
&lt;li&gt;a structured output contract&lt;/li&gt;
&lt;li&gt;explicit error behavior&lt;/li&gt;
&lt;li&gt;task or artifact references where appropriate&lt;/li&gt;
&lt;li&gt;scoped permissions&lt;/li&gt;
&lt;li&gt;observable invocation boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the caller's perspective, delegation remains ordinary tool use. The coordinator reads descriptions, selects the right capability, supplies the required context or artifact references, and reads the result.&lt;/p&gt;

&lt;p&gt;This does not mean every agent should see every other agent. That would recreate the tool-overload problem at the team layer. &lt;code&gt;team-mcp&lt;/code&gt; should expose a small, role-appropriate roster to each caller. A lead agent may see administrative specialists; an evidence agent may see domain specialists; a writer may see only validated artifacts and the validator.&lt;/p&gt;

&lt;p&gt;The team topology becomes a governed capability graph rather than an unrestricted group chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;team-fs&lt;/code&gt;: The Artifact Plane
&lt;/h3&gt;

&lt;p&gt;Agents often collaborate on durable work products and documents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plans&lt;/li&gt;
&lt;li&gt;evidence&lt;/li&gt;
&lt;li&gt;reports&lt;/li&gt;
&lt;li&gt;spreadsheets&lt;/li&gt;
&lt;li&gt;generated code&lt;/li&gt;
&lt;li&gt;structured state&lt;/li&gt;
&lt;li&gt;append-only decision logs&lt;/li&gt;
&lt;li&gt;validation results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passing all of that through tool-call payloads wastes context and creates brittle handoff chains. &lt;code&gt;team-fs&lt;/code&gt; gives the team a shared workspace. Agents exchange stable file or artifact references instead of repeatedly copying entire documents through the coordinator.&lt;/p&gt;

&lt;p&gt;The filesystem should not be an ungoverned scratch directory. Useful conventions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable file names or artifact URIs&lt;/li&gt;
&lt;li&gt;an explicit owner for authoritative files&lt;/li&gt;
&lt;li&gt;single-writer rules where consistency matters&lt;/li&gt;
&lt;li&gt;append-only logs for decisions and rejected changes&lt;/li&gt;
&lt;li&gt;versioning or optimistic concurrency for shared updates&lt;/li&gt;
&lt;li&gt;access control by team, task, and role&lt;/li&gt;
&lt;li&gt;cleanup and retention policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names of shared files effectively become a small business API for the team. &lt;code&gt;evidence.md&lt;/code&gt;, &lt;code&gt;current_plan.json&lt;/code&gt;, &lt;code&gt;contradictions.log&lt;/code&gt;, and &lt;code&gt;final_report.md&lt;/code&gt; communicate purpose more reliably than a collection of anonymous text blobs in model context.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;team-memory&lt;/code&gt;: The Semantic Context Plane
&lt;/h3&gt;

&lt;p&gt;Shared memory exists for information that should remain useful beyond one call or artifact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operating lessons&lt;/li&gt;
&lt;li&gt;recurring failure patterns&lt;/li&gt;
&lt;li&gt;durable team conventions&lt;/li&gt;
&lt;li&gt;previously validated approaches&lt;/li&gt;
&lt;li&gt;task history that is useful for later retrieval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semantic search lets an agent retrieve the small subset relevant to its present work rather than loading the team's entire history.&lt;/p&gt;

&lt;p&gt;But memory is not a system of record. It may be stale, incomplete, contaminated, or retrieved in the wrong context. A robust memory service needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scope: agent, user, task, or team&lt;/li&gt;
&lt;li&gt;provenance: who or what wrote the memory&lt;/li&gt;
&lt;li&gt;timestamps and retention&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;confidence or validation status&lt;/li&gt;
&lt;li&gt;deletion and correction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Current task facts should normally remain in task state, artifacts, or systems of record. Memory can recall that "board updates require supporting evidence." It should not be treated as authoritative evidence that "Alice selected the science-fiction movie."&lt;/p&gt;

&lt;p&gt;That separation prevents semantic memory from quietly becoming a shadow database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Systems Of Record And Tasks Still Matter
&lt;/h3&gt;

&lt;p&gt;The three collaboration services do not replace operational systems.&lt;/p&gt;

&lt;p&gt;Customer state belongs in the CRM. Work status may belong in a ticket or case system. Approvals belong in an approval system. Financial truth belongs in financial systems. Agents should access those sources through governed domain MCP servers rather than recreate them in team memory or loose files.&lt;/p&gt;

&lt;p&gt;Tasks cut across the architecture. When delegated work becomes long-running, retryable, approval-gated, or independently auditable, the caller should receive a task handle rather than block on a large hidden conversation. The experimental Tasks capability introduced in the MCP 2025-11-25 specification provides one protocol model for durable state, polling, deferred results, and cancellation. (&lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks" rel="noopener noreferrer"&gt;MCP Tasks&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The durable task says what is happening. &lt;code&gt;team-fs&lt;/code&gt; holds the work products. &lt;code&gt;team-memory&lt;/code&gt; holds reusable learning. Systems of record remain authoritative for the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Team Of Agents Can Look Like An MCP Server
&lt;/h2&gt;

&lt;p&gt;With that infrastructure in place, the recursive model from the previous article becomes practical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside one governed MCP-native platform, a team of agents can be presented as an MCP server that exposes selected specialized agents as tools.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The important word is &lt;em&gt;presented&lt;/em&gt;. The team is not literally only a server process. Behind that surface are agent runtimes, shared files, memory, tasks, domain systems, permissions, and observability. MCP gives all of them a consistent interface boundary.&lt;/p&gt;

&lt;p&gt;This is why a dedicated agent-to-agent protocol may be unnecessary inside one MCP-native platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;discovery already exists through tool descriptions&lt;/li&gt;
&lt;li&gt;invocation already exists through tool calls&lt;/li&gt;
&lt;li&gt;structured results and output schemas already exist&lt;/li&gt;
&lt;li&gt;tasks can represent deferred work&lt;/li&gt;
&lt;li&gt;resources and artifact links can carry context&lt;/li&gt;
&lt;li&gt;security, testing, and observability can remain on the MCP surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cross-organization or independently administered agent ecosystems may have broader interoperability requirements. But inside one governed platform, the burden of proof should be on adding a second protocol, not on reusing the capability layer already in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Puzzle That Exposes The Ceiling
&lt;/h2&gt;

&lt;p&gt;Logic-grid puzzles provide a useful demonstration because they have exact answers and can grow in controlled increments.&lt;/p&gt;

&lt;p&gt;Consider a movie-night puzzle. A small version has three people and three categories: which movie each person watched, which snack they ate, and where they sat. A harder version adds people, streaming platforms, arrival times, drinks, more clues, and longer chains of dependencies.&lt;/p&gt;

&lt;p&gt;The canonical test ladder grows from:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;th&gt;People&lt;/th&gt;
&lt;th&gt;Categories&lt;/th&gt;
&lt;th&gt;What Changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Very easy&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Basic smoke test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;More assignments and platform ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Ordered arrival constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;A fifth person and longer evidence chains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very hard&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Full stress case with cross-category dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The standalone agent receives one long instruction set covering intake, clue parsing, hypothesis testing, grid updates, validation, and final explanation. It can solve the smaller puzzles.&lt;/p&gt;

&lt;p&gt;As the puzzle grows, a more dangerous failure appears than merely returning "I don't know." The agent can announce that it solved the puzzle while its answer violates a clue, assigns a value twice, or leaves an unsupported conclusion. Asking the same agent to review its work does not reliably fix the problem because the proposed solution and the assumptions that produced it share the same context.&lt;/p&gt;

&lt;p&gt;The team uses the same underlying model capabilities, but distributes responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Lead Investigator&lt;/strong&gt; chooses the next bounded question and coordinates work&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Hypothesis Log&lt;/strong&gt; records proposed, accepted, rejected, and repeated ideas&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Evidence Clerk&lt;/strong&gt; checks deductions against the numbered clues&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Board Manager&lt;/strong&gt; is the only agent allowed to update the authoritative grid&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Referee&lt;/strong&gt; independently checks every clue, uniqueness, completeness, and evidentiary support&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;Final Report Writer&lt;/strong&gt; produces the answer only after the Referee validates it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The collaboration infrastructure is not incidental to the result.&lt;/p&gt;

&lt;p&gt;Through &lt;code&gt;team-mcp&lt;/code&gt;, the Lead Investigator calls these responsibilities as tools. Through &lt;code&gt;team-fs&lt;/code&gt;, the team shares explicit case artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clues.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constraints.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;puzzle_grid.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hypotheses.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update_requests.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contradictions.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final_solution.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final_proof.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Through &lt;code&gt;team-memory&lt;/code&gt;, the team can retain reusable operating lessons such as "every board update requires clue IDs or an evidence summary." It deliberately does &lt;strong&gt;not&lt;/strong&gt; store current grid assignments or temporary guesses as semantic memory. Current case facts belong in the case files.&lt;/p&gt;

&lt;p&gt;The single agent is asked to remember, reason, mutate state, and validate itself. The team turns those responsibilities into separate contracts with an inspectable chain of evidence.&lt;/p&gt;

&lt;p&gt;That is the capability crossover in concrete form. The individual agents are not magically more intelligent than the standalone agent. The system becomes more capable because it adds bounded contexts, explicit ownership, controlled mutation, durable state, and independent validation.&lt;/p&gt;

&lt;p&gt;A later article will cover the full experiment: the team design, prompts, puzzle ladder, failure traces, repeated evaluations, and the cost of achieving the higher completion rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure The Crossover, Do Not Assume It
&lt;/h2&gt;

&lt;p&gt;A multi-agent architecture should earn its overhead against a strong single-agent baseline.&lt;/p&gt;

&lt;p&gt;For the puzzle experiment, a run succeeds only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the final answer exactly matches the gold solution&lt;/li&gt;
&lt;li&gt;every clue is satisfied&lt;/li&gt;
&lt;li&gt;no category contains duplicate assignments&lt;/li&gt;
&lt;li&gt;no person has a missing assignment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most revealing measurement distinguishes claimed success from actual success:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;false_completion = declared_success &amp;amp;&amp;amp; !actual_success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That captures the case where an agent crosses its capability ceiling without recognizing that it crossed it.&lt;/p&gt;

&lt;p&gt;A serious comparison should also record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exact task-completion rate&lt;/li&gt;
&lt;li&gt;unsupported conclusions&lt;/li&gt;
&lt;li&gt;validation failures&lt;/li&gt;
&lt;li&gt;retries and rejected updates&lt;/li&gt;
&lt;li&gt;wall-clock latency&lt;/li&gt;
&lt;li&gt;token usage&lt;/li&gt;
&lt;li&gt;model and infrastructure cost&lt;/li&gt;
&lt;li&gt;tool calls and delegated agent calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run each puzzle multiple times because model behavior is probabilistic. Compare both quality and economics. A team that improves completion by one percentage point at fifteen times the cost may be a poor design for a low-value task. The same cost may be trivial for a high-value investigation that the single agent cannot complete reliably.&lt;/p&gt;

&lt;p&gt;The purpose of the crossover is to make the decision measurable, not ideological.&lt;/p&gt;

&lt;h2&gt;
  
  
  When To Scale Out
&lt;/h2&gt;

&lt;p&gt;An agent team is a strong candidate when the work has several of these properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;independent branches can run in parallel&lt;/li&gt;
&lt;li&gt;the evidence exceeds one useful working context&lt;/li&gt;
&lt;li&gt;specialists need different tools, policies, or permissions&lt;/li&gt;
&lt;li&gt;intermediate artifacts have clear owners&lt;/li&gt;
&lt;li&gt;independent validation materially improves correctness&lt;/li&gt;
&lt;li&gt;the work is long-running, retryable, or resumable&lt;/li&gt;
&lt;li&gt;different models can be used economically for different roles&lt;/li&gt;
&lt;li&gt;the value of a correct result justifies higher inference cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep one agent when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the task is short and sequential&lt;/li&gt;
&lt;li&gt;every step depends tightly on the previous step&lt;/li&gt;
&lt;li&gt;all participants would need the same complete context&lt;/li&gt;
&lt;li&gt;coordination would cost more than the work&lt;/li&gt;
&lt;li&gt;one agent already meets the target completion rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And use neither architecture for work that should be deterministic. If a known algorithm, database query, workflow engine, constraint solver, or ordinary service can produce the answer reliably, put that capability behind an MCP server and let agents call it.&lt;/p&gt;

&lt;p&gt;A practical decision sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can deterministic software do the work?&lt;/strong&gt; Build a tool or workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can one agent do it reliably with a bounded context and toolset?&lt;/strong&gt; Use one agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the remaining work benefit from specialization, parallelism, durable collaboration, or independent verification?&lt;/strong&gt; Build a team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to maximize agent count. It is to choose the smallest architecture that reliably completes the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security And Governance Multiply With The Team
&lt;/h2&gt;

&lt;p&gt;Exposing an agent as a tool does not weaken the security requirements from earlier articles. It adds new places to apply them.&lt;/p&gt;

&lt;p&gt;For every agent capability, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who may call it?&lt;/li&gt;
&lt;li&gt;which tools and files may it access?&lt;/li&gt;
&lt;li&gt;whose identity does it act on behalf of?&lt;/li&gt;
&lt;li&gt;which other agents may it delegate to?&lt;/li&gt;
&lt;li&gt;which outputs may be written to shared state?&lt;/li&gt;
&lt;li&gt;what prevents recursive delegation from running without a bound?&lt;/li&gt;
&lt;li&gt;how are untrusted instructions and artifacts contained?&lt;/li&gt;
&lt;li&gt;what is logged for reconstruction and audit?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared collaboration services need their own controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;team-mcp&lt;/code&gt; enforces the allowed collaboration graph and delegation budgets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;team-fs&lt;/code&gt; enforces role-based paths, ownership, and mutation rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;team-memory&lt;/code&gt; enforces scope, provenance, retention, and deletion&lt;/li&gt;
&lt;li&gt;domain MCP servers continue to enforce business authorization closest to the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing also moves up one level. Test each agent contract independently, then test the complete team on real scenarios. A specialist that performs well in isolation can still fail as a team dependency if its description is ambiguous, its output is difficult to consume, or the coordinator calls it at the wrong time.&lt;/p&gt;

&lt;p&gt;Multi-agent systems do not escape MCP discipline. They multiply the reasons to apply it well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supervisors, Sub-Agents, and Swarms
&lt;/h2&gt;

&lt;p&gt;Once the architecture is explicit, much of the market vocabulary becomes easier to place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supervisor/Router agent&lt;/strong&gt;&lt;br&gt;
An agent instructed to coordinate, review, or route work, with selected collaborators exposed as tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sub-agent&lt;/strong&gt;&lt;br&gt;
A specialized agent called by another agent through a bounded capability contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swarm&lt;/strong&gt;&lt;br&gt;
A label for a larger or more decentralized group of agents, often with overlapping capabilities and looser routing rules to execute in parallel.&lt;/p&gt;

&lt;p&gt;The patterns may be useful. The names do not remove the need to define responsibilities, state, validation, permissions, and failure behavior. A vague swarm with no ownership is not more scalable than an overloaded single agent. It has merely distributed the ambiguity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Teams Are Also Preparing For Human Teammates
&lt;/h2&gt;

&lt;p&gt;Tasks, artifacts, evidence, ownership, and validation make an agent team legible to humans as a useful side effect. A person can inspect a case file, approve a task, correct an artifact, or review a validation result without needing access to an agent's hidden prompt state.&lt;/p&gt;

&lt;p&gt;That does not make this a human-agent collaboration article. The next step requires its own treatment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which decisions should remain human&lt;/li&gt;
&lt;li&gt;where human domain expertise outperforms model reasoning&lt;/li&gt;
&lt;li&gt;how agents should escalate uncertainty&lt;/li&gt;
&lt;li&gt;how authority and accountability work in a mixed team&lt;/li&gt;
&lt;li&gt;how interfaces help people supervise without becoming coordination bottlenecks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A later article will extend this architecture into hybrid teams in which human experts and AI agents contribute different capabilities through the same tasks, artifacts, and governed interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Team Model In One View
&lt;/h2&gt;

&lt;p&gt;Carry these principles forward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with one agent.&lt;/strong&gt;&lt;br&gt;
It has the lowest coordination cost and is the right default for bounded work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not confuse a simple diagram with a simple system.&lt;/strong&gt;&lt;br&gt;
Coordination hidden inside one prompt and context is still coordination.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scale up before scaling out.&lt;/strong&gt;&lt;br&gt;
Improve instructions, models, tools, workflows, and deterministic services first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Teams raise the ceiling for the right task shapes.&lt;/strong&gt;&lt;br&gt;
Specialization, parallel contexts, explicit ownership, and independent validation enable harder work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The coordination tax exists either way.&lt;/strong&gt;&lt;br&gt;
Teams make it explicit so it can be tested, observed, governed, and reused.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;team-mcp&lt;/code&gt; for delegation.&lt;/strong&gt;&lt;br&gt;
Expose a small, role-appropriate set of specialized agents as typed capabilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;team-fs&lt;/code&gt; for working artifacts.&lt;/strong&gt;&lt;br&gt;
Give important state stable names, clear owners, mutation rules, and history.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;team-memory&lt;/code&gt; for reusable learning.&lt;/strong&gt;&lt;br&gt;
Keep semantic recall separate from authoritative task and business state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure the crossover.&lt;/strong&gt;&lt;br&gt;
Compare completion, false confidence, latency, tokens, and cost against a strong single-agent baseline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the smallest architecture that completes the task reliably.&lt;/strong&gt;&lt;br&gt;
Sometimes that is deterministic software, sometimes one agent, and sometimes a governed team.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scalable idea is not "more agents." It is the separation of responsibilities and state behind explicit interfaces.&lt;/p&gt;

&lt;p&gt;Scale up while simplicity wins. Scale out when coordination becomes the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue the Series
&lt;/h2&gt;

&lt;p&gt;This article extended the capability-first model from one agent to a governed team. The rest of the series provides the layers underneath it and the next steps beyond it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Need the single-agent foundation?&lt;/strong&gt; Read &lt;a href="https://dev.to/aws-heroes/build-mcp-servers-before-agents-the-enterprise-shift-ai-teams-need-2cjc"&gt;Build MCP Servers Before Agents&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need better capability boundaries?&lt;/strong&gt; Read &lt;a href="https://dev.to/aws-heroes/mcp-tool-design-why-your-ai-agent-is-failing-and-how-to-fix-it-40fc"&gt;Tool Design&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/mcp-prompts-and-resources-the-primitives-youre-not-using-3oo1"&gt;Prompts and Resources&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need release and security discipline?&lt;/strong&gt; Read &lt;a href="https://dev.to/aws-heroes/testing-mcp-servers-the-five-gates-between-demo-and-production-2inf"&gt;Testing MCP Servers&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/security-for-mcp-servers-governed-access-beats-uploading-spreadsheets-to-chatgpt-3ag7"&gt;Security for MCP Servers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need governed flexibility for the long tail requests?&lt;/strong&gt; Read &lt;a href="https://dev.to/aws-heroes/code-mode-for-mcp-the-long-tail-escape-hatch-not-the-front-door-40ga"&gt;Code Mode for MCP&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want the evidence behind the crossover?&lt;/strong&gt; A future article will present the complete grid-puzzle team, evaluation ladder, and single-agent comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What follows agent-only teams?&lt;/strong&gt; A later article will cover hybrid teams of human experts and AI agents.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Build MCP Servers Before Agents: The Enterprise Shift AI Teams Need</title>
      <dc:creator>Guy</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:27:54 +0000</pubDate>
      <link>https://dev.to/aws-heroes/build-mcp-servers-before-agents-the-enterprise-shift-ai-teams-need-2cjc</link>
      <guid>https://dev.to/aws-heroes/build-mcp-servers-before-agents-the-enterprise-shift-ai-teams-need-2cjc</guid>
      <description>&lt;p&gt;A team builds a support agent in an agent framework. Another team builds a finance agent. A third builds a sales operations agent. Each demo is impressive. Each agent has a prompt, a model, some memory, a few tools, and a nice workflow graph. Then the enterprise reality arrives.&lt;/p&gt;

&lt;p&gt;The support agent has its own private Salesforce connector. The finance agent has a different connector to the same customer data. The sales operations agent defines "North America" differently from both of them. Each team handles authentication a little differently. Each team logs different things. Each team wraps the same APIs again. Six months later, the company does not have an agent platform. It has a convoy of bespoke machines held together by prompts, glue code, and optimism.&lt;/p&gt;

&lt;p&gt;That is the wrong foundation.&lt;/p&gt;

&lt;p&gt;The current market narrative tells teams to start with the agent: pick a framework, write the instructions, choose a model, add tools, and ship. That works for demos. It is backward for enterprises.&lt;/p&gt;

&lt;p&gt;The reusable asset in enterprise AI is usually not the agent. It is the &lt;strong&gt;MCP server&lt;/strong&gt; (AKA connector).&lt;/p&gt;

&lt;p&gt;Agents should come later, as lightweight clients built from three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;instructions&lt;/strong&gt;: what the agent should do, what it should not do, and how it should behave — much like instructions you would give a human employee. These are defined by business people and updated frequently as the business learns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: the reasoning and language component, usually from a major vendor such as OpenAI or Anthropic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tools&lt;/strong&gt;: capabilities exposed through MCP servers, with all the design, security, testing, and governance work we covered in the earlier articles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the shift this article is about. And to make it concrete, this time we will not stop at the diagram — we will build a complete, running agent and show that the "hard part" was never the agent at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MCP? (The 30-Second Version)
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP, &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25" rel="noopener noreferrer"&gt;spec 2025-11-25&lt;/a&gt;) is the interface layer between AI clients and external systems through tools, prompts, and resources. In enterprise deployments, MCP servers should be thin, remote, mostly stateless interface layers over internal systems and SaaS platforms. They are the AI-facing equivalent of web servers and mobile backends.&lt;/p&gt;

&lt;p&gt;That makes the next architectural point straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP servers are the governed capability layer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;agents are MCP clients&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  An Agent Is Simpler Than Many Frameworks Make It Look
&lt;/h2&gt;

&lt;p&gt;Before talking about architecture, it helps to simplify the mental model.&lt;/p&gt;

&lt;p&gt;At its core, an enterprise agent usually needs only three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instructions&lt;/strong&gt; — the job description. What this agent is for, what success looks like, what boundaries it must respect, what tone to use, when to ask for clarification, when to stop, and when to escalate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LLM&lt;/strong&gt; — the reasoning and language engine. It interprets the request, decides what to do next, calls tools, reads results, and produces the answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt; — the actions and data-access surfaces. In a serious enterprise design, these come from MCP servers rather than as private helper functions bundled inside a single agent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That leads to a simple loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read instructions
read user request
decide whether to answer directly or call a tool
if needed, call a tool on an MCP server
read the tool result
repeat until the request is satisfied or the task fails safely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the basic ReAct loop. For many real enterprise cases, that is the right default model. The agent does not need a giant graph, a maze of private adapters, or a pile of framework-specific abstractions to be useful.&lt;/p&gt;

&lt;p&gt;The claim that "an agent is simple" is easy to make and easy to doubt. So let us prove it in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simplicity Is Real: An Agent in Forty Lines
&lt;/h2&gt;

&lt;p&gt;Here is a complete agent written in Rust with the &lt;a href="https://crates.io/crates/pmcp-agent" rel="noopener noreferrer"&gt;PMCP SDK&lt;/a&gt;. Don't worry if you're not fluent in Rust — the comments walk through every important line, and the shape is what matters. Watch how directly the three parts above map onto three pieces of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// -- Dependencies --&lt;/span&gt;
&lt;span class="c1"&gt;// pmcp-agent: the deploy-anywhere agent decision loop for the PMCP SDK.&lt;/span&gt;
&lt;span class="c1"&gt;// An agent here is a loop over THREE swappable "seams":&lt;/span&gt;
&lt;span class="c1"&gt;//   CompletionSource  -&amp;gt; the LLM (produce the next model turn)&lt;/span&gt;
&lt;span class="c1"&gt;//   ToolInvoker       -&amp;gt; the tools (run a tool call, return its result)&lt;/span&gt;
&lt;span class="c1"&gt;//   ConversationStore -&amp;gt; the memory (persist and replay the conversation)&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;pmcp_agent&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="n"&gt;AgentEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ResolvedAgentConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CompletionSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolInvoker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolCallResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RunOutcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;async_trait&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;async_trait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// -- Part 1: INSTRUCTIONS --&lt;/span&gt;
&lt;span class="c1"&gt;// The "job description." Plain text a business person can own and edit.&lt;/span&gt;
&lt;span class="c1"&gt;// The last two numbers are guardrails, not intelligence: a token budget&lt;/span&gt;
&lt;span class="c1"&gt;// and a hard cap on how many tool-calling turns the loop may take before&lt;/span&gt;
&lt;span class="c1"&gt;// it stops safely. That cap is why a thin agent cannot run away.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ResolvedAgentConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;ResolvedAgentConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"You are a concise support assistant. Look up orders when asked. &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;
         If a request is outside orders, say so and stop."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"claude-sonnet-4-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the model name; the LLM itself is the next seam&lt;/span&gt;
        &lt;span class="cm"&gt;/* max_tokens     */&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="cm"&gt;/* max_iterations */&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// -- Part 3: TOOLS --&lt;/span&gt;
&lt;span class="c1"&gt;// The tools seam. This is the ENTIRE surface where an agent touches the&lt;/span&gt;
&lt;span class="c1"&gt;// outside world. In production you would hand this seam a `ClientToolInvoker`&lt;/span&gt;
&lt;span class="c1"&gt;// that forwards each call to a governed MCP server (see the next section).&lt;/span&gt;
&lt;span class="c1"&gt;// Here we stub one tool inline so the example runs with no network:&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;OrdersTools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ToolInvoker&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;OrdersTools&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ToolCallResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// `call.name` is the tool the LLM chose; `call.arguments` is its JSON input.&lt;/span&gt;
        &lt;span class="c1"&gt;// A real invoker dispatches to an MCP server and returns its result verbatim.&lt;/span&gt;
        &lt;span class="nn"&gt;ToolCallResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="py"&gt;.id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"shipped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"eta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2 days"&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="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// -- Assemble the agent from its three parts + memory --&lt;/span&gt;
    &lt;span class="c1"&gt;// Part 2 (the LLM / CompletionSource) is passed in as `llm`. In a test, it is&lt;/span&gt;
    &lt;span class="c1"&gt;// a mock; in production it is one line — see "Swapping the Brain" below.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* a CompletionSource: mock, Anthropic, or any OpenAI-compatible endpoint */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AgentEngine&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;// Part 2: the brain&lt;/span&gt;
        &lt;span class="n"&gt;OrdersTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// Part 3: the tools (the MCP boundary)&lt;/span&gt;
        &lt;span class="nn"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// memory&lt;/span&gt;
        &lt;span class="nf"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;       &lt;span class="c1"&gt;// Part 1: the job description&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// -- Run it. This IS the ReAct loop. --&lt;/span&gt;
    &lt;span class="c1"&gt;// A "run" is a conversation identified by `run_id`; its first turn is the&lt;/span&gt;
    &lt;span class="c1"&gt;// user's request, seeded into the store. (In production the AgentServer&lt;/span&gt;
    &lt;span class="c1"&gt;// adapter seeds it for you from the incoming MCP tool call — see below.)&lt;/span&gt;
    &lt;span class="c1"&gt;// `run` then interprets that request, lets the model choose a tool, dispatches&lt;/span&gt;
    &lt;span class="c1"&gt;// it through the invoker, feeds the result back, and repeats until the model&lt;/span&gt;
    &lt;span class="c1"&gt;// ends the turn or a guardrail trips. You did not write that loop.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order-status-4815"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;RunOutcome&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Completed&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"done: {result:?}"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;RunOutcome&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LimitReached&lt;/span&gt;        &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hit the iteration guardrail — stopped safely"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;other&lt;/span&gt;                           &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stopped: {other:?}"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now look at what you did &lt;strong&gt;not&lt;/strong&gt; write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No ReAct loop. &lt;code&gt;AgentEngine::run&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the loop.&lt;/li&gt;
&lt;li&gt;No tool-dispatch plumbing, no argument parsing, no result threading.&lt;/li&gt;
&lt;li&gt;No retry, budget, or turn-limit bookkeeping — the config and the engine own that.&lt;/li&gt;
&lt;li&gt;No framework-specific graph, node, or edge abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What remains is exactly the three-part model: a paragraph of instructions, a model name, and a tools seam. The agent is thin on purpose. All of its leverage lives on the other side of that &lt;code&gt;ToolInvoker&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to scaffold this rather than type it, the CLI generates the whole thing and runs it offline against a scripted model, so you can see the loop before wiring a real LLM or any MCP servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo pmcp agent new support-agent   &lt;span class="c"&gt;# scaffold instructions + the three seams&lt;/span&gt;
cargo pmcp agent dev &lt;span class="nt"&gt;--source&lt;/span&gt; fixed   &lt;span class="c"&gt;# run the loop offline, no API keys, no network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where The Tools Come From
&lt;/h2&gt;

&lt;p&gt;The single most important line in that example is the one that looks least important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;OrdersTools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// Part 3: the tools (the MCP boundary)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ToolInvoker&lt;/code&gt; seam is the &lt;em&gt;entire&lt;/em&gt; boundary between the agent and the outside world. Whatever you plug in there decides where the agent's capabilities live. That is the whole architectural argument of this series, expressed as one type parameter. Once you understand that an &lt;strong&gt;AI agent is simply an MCP client&lt;/strong&gt;, everything else falls into place, and all the investment in building MCP server connectors returns with huge dividends of security, simplicity, and scale. &lt;/p&gt;

&lt;p&gt;Plug in a struct full of private helper functions, and you have rebuilt the Mad Max convoy: this agent's Salesforce logic, this agent's "North America," this agent's auth. Plug in an invoker that forwards each call to a &lt;strong&gt;governed MCP server&lt;/strong&gt;, and every capability comes from the shared platform layer — designed, secured, tested, and observed once.&lt;/p&gt;

&lt;p&gt;The PMCP SDK ships exactly that production invoker. Instead of &lt;code&gt;OrdersTools&lt;/code&gt;, a real agent uses &lt;code&gt;ClientToolInvoker&lt;/code&gt;, which dispatches each tool call to an MCP server and returns its result — including long-running work modeled as &lt;a href="//../02-prompts-resources/article.md"&gt;tasks&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Production: the tools seam forwards to a governed MCP server.&lt;/span&gt;
&lt;span class="c1"&gt;// `connector` is an MCP client pointed at your orders/CRM/warehouse server.&lt;/span&gt;
&lt;span class="c1"&gt;// The agent gains no private connector code; it borrows the platform's.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ClientToolInvoker&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* max_poll_secs */&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AgentEngine&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&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="nn"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else in the agent changes. The instructions are the same, the loop is the same, the model is the same. The only thing that changed is &lt;em&gt;where the capabilities come from&lt;/em&gt; — and that is the only thing that should decide whether your fleet of agents is governable.&lt;/p&gt;

&lt;p&gt;This is the capability-first model as a code diff: swapping a private-tools invoker for an MCP-backed one is a one-line change, and it is the difference between a rogue demo and a platform citizen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swapping The Brain Is One Line
&lt;/h2&gt;

&lt;p&gt;The other seam that proves how thin an agent should be is the LLM. Because the model sits behind &lt;code&gt;CompletionSource&lt;/code&gt;, changing it never touches the loop, the tools, or the instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;pmcp_agent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AnthropicSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OpenAiCompatSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecretString&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;SecretString&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LLM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A local model for development (any OpenAI-compatible endpoint, e.g. Ollama):&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenAiCompatSource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:11434/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llama3.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A frontier model for production — same engine, same tools, same instructions:&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AnthropicSource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.anthropic.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"claude-sonnet-4-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both satisfy the same seam, so both drop straight into &lt;code&gt;AgentEngine::new&lt;/code&gt;. You can develop offline against a scripted or local model and promote to a hosted one for production without rewriting the agent. When the model is this easy to swap, you stop entangling business logic with a particular vendor — and you keep the reusable work where it belongs, on the MCP servers behind the tools seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Capability-First Agent Model
&lt;/h2&gt;

&lt;p&gt;Step back from the code and the architecture reads as one principle: &lt;strong&gt;capabilities are shared infrastructure; agents are thin clients.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Capabilities live on MCP servers.&lt;/strong&gt; Tools, prompts, resources, and governed code-mode surfaces belong in the shared platform layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Agents stay thin.&lt;/strong&gt; An agent is mostly instructions, an LLM, and a selected set of those capabilities — the forty lines above.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specialized agents can themselves become capabilities.&lt;/strong&gt; If a later design exposes a specialist agent through MCP, another agent can call it like any other tool. (The PMCP &lt;code&gt;AgentServer&lt;/code&gt; adapter does exactly this — it exposes the same loop as a normal MCP server. That recursive step is the subject of the next article.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shared state stays explicit.&lt;/strong&gt; Tasks, artifacts, and systems of record are better homes for shared state than hidden prompt state or private connector logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This article is mainly about the first two points. The next article extends the same model to teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake: Treating Tools As A By-Product Of The Agent
&lt;/h2&gt;

&lt;p&gt;The common development pattern today looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choose an agent framework&lt;/li&gt;
&lt;li&gt;define the agent's instructions&lt;/li&gt;
&lt;li&gt;pick a model&lt;/li&gt;
&lt;li&gt;write some tools for that agent&lt;/li&gt;
&lt;li&gt;wire them together&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That feels natural because the agent is the visible thing. But architecturally it is upside down. When tools are created as part of one agent, the problems appear quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same backend gets wrapped multiple times by different teams.&lt;/li&gt;
&lt;li&gt;Business semantics drift between agents.&lt;/li&gt;
&lt;li&gt;Authentication and authorization logic get duplicated.&lt;/li&gt;
&lt;li&gt;Observability is fragmented across agent codebases.&lt;/li&gt;
&lt;li&gt;Testing quality varies by team.&lt;/li&gt;
&lt;li&gt;Security posture depends on whoever wrote the last connector.&lt;/li&gt;
&lt;li&gt;No one knows which tool implementation is the canonical one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how organizations end up with &lt;strong&gt;rogue agents&lt;/strong&gt;: individually clever, collectively ungovernable. The deeper issue is that the tool layer is being treated as disposable application code when it should be treated as shared platform infrastructure. In the code, that mistake is a single choice — what you pass to the &lt;code&gt;ToolInvoker&lt;/code&gt; seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build The Roads Before The Cars
&lt;/h2&gt;

&lt;p&gt;The cleanest analogy I know is transportation.&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%2Fw98y7u44mj049wvuvang.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%2Fw98y7u44mj049wvuvang.png" alt="Rogue agents vs. ordered ones" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent-first model looks like a Mad Max convoy. Every team builds its own vehicle from scratch. The wheels are different. The fuel systems are different. The controls are different. Each vehicle may be brilliant in isolation, but the system as a whole is chaotic and expensive.&lt;/p&gt;

&lt;p&gt;The MCP-first model looks like a city that built the roads first. The roads are paved, the lanes are marked, the traffic lights are standardized, the signs are shared, maintenance is organized, and safety rules are explicit. Once that infrastructure exists, many kinds of efficient vehicles that everybody can drive can use it.&lt;/p&gt;

&lt;p&gt;That is what MCP servers are for enterprise AI. They are the roads. They give the organization shared interfaces to data systems, shared business definitions, shared authentication and authorization boundaries, shared observability, shared testing discipline, and shared governance. Agents are then the vehicles that move on top of that infrastructure — some simple, some specialized, some aggressively optimized, but all benefiting from the same road system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Enterprise Shift
&lt;/h2&gt;

&lt;p&gt;The shift I want enterprise teams to make is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop asking "what tools should this agent have?" before you ask "what MCP servers should this organization have?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are not the same question. The first is local and short-term. The second is architectural. An enterprise-first sequence looks more like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the core data systems and business domains.&lt;/li&gt;
&lt;li&gt;Build MCP servers for those systems using the best practices from the earlier articles.&lt;/li&gt;
&lt;li&gt;Expose outcome-oriented tools, prompts, resources, and, where appropriate, code mode.&lt;/li&gt;
&lt;li&gt;Apply security, testing, and governance at the server layer.&lt;/li&gt;
&lt;li&gt;Only then define agents as instruction sets plus models plus selected MCP capabilities.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order matters because the MCP server layer is where reuse happens. It is not a call for a big-bang platform rewrite — start with one business domain, publish one or two well-governed MCP servers, and build a small number of agents on top. The important shift is not "wait until the platform is perfect." It is "put reusable capability work in the platform layer from the beginning."&lt;/p&gt;

&lt;p&gt;The agent-first model can look fine when there are three demos in one lab. It breaks when the organization starts talking about dozens, then hundreds, then thousands of agents. At that scale you are no longer solving a prompt problem; you are solving a platform problem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Agent-First Model&lt;/th&gt;
&lt;th&gt;MCP-First Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where do tools live?&lt;/td&gt;
&lt;td&gt;Inside each agent codebase&lt;/td&gt;
&lt;td&gt;In shared MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who defines business semantics?&lt;/td&gt;
&lt;td&gt;Each team, repeatedly&lt;/td&gt;
&lt;td&gt;Once per governed server surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How is auth handled?&lt;/td&gt;
&lt;td&gt;Per agent, inconsistently&lt;/td&gt;
&lt;td&gt;At the server layer, consistently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do agents share capabilities?&lt;/td&gt;
&lt;td&gt;Copy code or rebuild connectors&lt;/td&gt;
&lt;td&gt;Reuse the same server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you test the interface?&lt;/td&gt;
&lt;td&gt;Ad hoc by team&lt;/td&gt;
&lt;td&gt;Server-level test gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you observe usage?&lt;/td&gt;
&lt;td&gt;Fragmented logs across agents&lt;/td&gt;
&lt;td&gt;Centralized capability-layer telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you swap models or instructions?&lt;/td&gt;
&lt;td&gt;Often entangled with tools&lt;/td&gt;
&lt;td&gt;Independent from tools (one line)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you span systems?&lt;/td&gt;
&lt;td&gt;Custom orchestration in every agent&lt;/td&gt;
&lt;td&gt;Compose across MCP servers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is why the current market emphasis on "building agents" can be misleading. It draws attention to the system's visible tip — the forty lines — and away from the part that determines whether the whole thing can scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal MCP Servers First, Vendor MCP Servers Second, Agents Third
&lt;/h2&gt;

&lt;p&gt;Enterprise AI rarely lives in one system. A real company has internal databases, internal APIs, data warehouses, identity systems, document stores, ticketing systems, and SaaS platforms. At the same time, major vendors are all pushing their own agent stories — Salesforce, Azure, AWS, ServiceNow, Google — and each wants you to build agents inside its orbit.&lt;/p&gt;

&lt;p&gt;That is understandable from the vendor's perspective, but it is not the right control point for the enterprise. The better model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build MCP servers around your &lt;strong&gt;internal&lt;/strong&gt; systems.&lt;/li&gt;
&lt;li&gt;Adopt MCP servers from &lt;strong&gt;external SaaS providers&lt;/strong&gt; when they exist and are good enough.&lt;/li&gt;
&lt;li&gt;Build your agents as MCP clients that can call both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A customer-success agent might need internal customer master data, a SaaS CRM, a support platform, and internal revenue and usage data. No single vendor should own that whole orchestration surface by default. The agent should sit above those systems — behind that one &lt;code&gt;ToolInvoker&lt;/code&gt; seam — not be trapped inside one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Business Analyst Matters More, Not Less
&lt;/h2&gt;

&lt;p&gt;This shift does not remove the role of the business analyst. It makes that role more important. If agents are lightweight clients on top of MCP servers, then the quality of those servers matters even more. Someone still has to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which business outcomes deserve first-class tools&lt;/li&gt;
&lt;li&gt;Which workflows should be packaged as prompts&lt;/li&gt;
&lt;li&gt;Which resources should be injected as governed context&lt;/li&gt;
&lt;li&gt;Which definitions of quarter, region, customer status, and policy are canonical&lt;/li&gt;
&lt;li&gt;Which actions should be impossible rather than merely discouraged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every team embeds those choices inside its own agent prompt, the organization gets semantic drift. If those choices are encoded once in the MCP layer, the organization gets consistency. The &lt;a href="//../05-security/article.md"&gt;security article&lt;/a&gt; made exactly this point for access control and aggregation semantics; the same idea applies here at the full agent-platform level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ReAct Loop Is Simple. The Platform Under It Is Not.
&lt;/h2&gt;

&lt;p&gt;It is important not to confuse two statements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The runtime loop of a good agent can be simple — you saw it fit in forty lines.&lt;/li&gt;
&lt;li&gt;The infrastructure that makes simple agents safe and scalable is not simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why the earlier articles matter. The loop can stay small &lt;em&gt;precisely because&lt;/em&gt; the MCP layer carries the real engineering burden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tool design makes the actions understandable&lt;/li&gt;
&lt;li&gt;prompts and resources package repeatable workflows and governed context&lt;/li&gt;
&lt;li&gt;testing makes the capability surface stable&lt;/li&gt;
&lt;li&gt;code mode covers the long tail without exploding tool count&lt;/li&gt;
&lt;li&gt;security makes the whole thing enterprise-safe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that foundation is missing, teams compensate by stuffing more policy, more special cases, and more connector logic into the agent itself — until the forty lines become four thousand. The result is the illusion of a smart agent built atop a weak platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Message
&lt;/h2&gt;

&lt;p&gt;The market talks constantly about building agents. Agents are visible. They demo well. Vendors can package them. Frameworks can make them feel like the main event. But for enterprise AI, the real leverage is lower in the stack, and the agent itself — as the code showed — is genuinely small.&lt;/p&gt;

&lt;p&gt;Carry this model forward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An agent is not magic.&lt;/strong&gt; It is instructions, an LLM, and tools — three seams and a loop you don't have to write.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The right default loop is simple.&lt;/strong&gt; Read instructions, read the request, call the right tool, inspect the result, repeat until done or failed safely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tools should not be private by-products of each agent.&lt;/strong&gt; They belong on MCP servers, reached through the one seam that defines the agent's whole outside world.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MCP servers are the reusable foundation.&lt;/strong&gt; They carry semantics, security, testing, observability, and governance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The scalable model is cross-system.&lt;/strong&gt; Internal MCP servers plus external SaaS MCP servers give agents a governed way to work across the real enterprise data estate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The enterprise shift is architectural.&lt;/strong&gt; Stop starting with "What tools should this agent have?" Start with "What MCP servers should this organization have?"&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If an organization wants to run a handful of demos, it can build agent-first and survive the mess. If it wants to run AI seriously across the company, it should build MCP servers first, treat them as shared platform infrastructure, encode business semantics and governance there, and compose thin agents on top — incrementally. One good server in one domain plus one or two thin agents on top of it is already a better enterprise foundation than five siloed agents with five private connectors.&lt;/p&gt;

&lt;p&gt;Build the roads before the cars.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
