<?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</title>
    <description>The latest articles on DEV Community by AWS (aws).</description>
    <link>https://dev.to/aws</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%2F1726%2F2a73f1e6-7995-4348-ae37-44b064274c59.png</url>
      <title>DEV Community: AWS</title>
      <link>https://dev.to/aws</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aws"/>
    <language>en</language>
    <item>
      <title>Stop Your AI Agent Forgetting User Preferences: Key-Value Memory</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Tue, 04 Aug 2026 23:12:24 +0000</pubDate>
      <link>https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13</link>
      <guid>https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13</guid>
      <description>&lt;p&gt;Here's a test most AI agents fail. A brand-new user searches flights, books one in business class, and asks: &lt;em&gt;"what do you recommend based on what you know about me?"&lt;/em&gt; The agent answers beautifully: business class, non-stop, exactly their taste. Then the process restarts. Same user, same question, and now the answer is generic: the cheapest economy fare. Everything the agent "knew" is gone.&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%2F980b7u6sk36i84rliyu2.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%2F980b7u6sk36i84rliyu2.png" alt="Cartoon: an AI assistant offers a personalized business-class ticket, then after one restart offers the same user the cheapest economy fare — the transcript is not memory" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent memory for an AI agent means storing structured facts outside the conversation, in a store that outlives the process.&lt;/strong&gt; This post builds that for the most common case, user preferences, with the smallest memory that works: a key-value store, measured climbing a durability ladder from process state to local disk to Amazon S3. Everything below runs from the &lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws" rel="noopener noreferrer"&gt;companion repo&lt;/a&gt; with live flight data, so the numbers come from real runs, not slideware.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This is post 1 of a series; the &lt;a href="https://dev.to/aws/ai-agent-memory-types-your-agent-forgets-everything-fix-it-pcc"&gt;intro post&lt;/a&gt; maps all the memory types. The code uses &lt;a href="https://strandsagents.com/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;, an open source SDK; the pattern carries over to any agent framework.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Isn't the conversation history already memory?
&lt;/h2&gt;

&lt;p&gt;Within a session, yes, and that's exactly what fools people. The common claim is "stateless agents forget between turns." That claim is false, and you can prove it in four lines. Agent frameworks keep the conversation history between calls on the same agent instance (in Strands it's &lt;code&gt;agent.messages&lt;/code&gt;) and send it to the model on every turn. So an agent with zero memory tooling still "remembers":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Book the cheapest business option.
Agent: Your flight from JFK to Paris CDG has been booked... ✅

User (2 turns later): ...what do you recommend based on what you know about me?
Agent: here are some business class options... ✅  ← personalized!

agent.state.get("user_preferences")  → None      ← nothing was learned
len(agent.messages)                  → 12        ← the booking lives ONLY here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real run. The agent personalized turn 3 because "business class" was still sitting in the transcript. Don't let that fool you into thinking it learned something. Three problems hide under that lucky answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nothing structured exists.&lt;/strong&gt; There is no profile to query, rank offers by, display to the user, or persist. The knowledge is prose inside a transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transcript gets trimmed.&lt;/strong&gt; Long sessions need a sliding window or summarization, and the booking scrolls out with the old messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transcript dies with the process.&lt;/strong&gt; In production, every new request may be a new process. Restart the agent and ask the same question:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[after restart] User: ...what do you recommend based on what you know about me?
[after restart] Agent: I recommend the Iberia flight for $366.85...  ← cheapest economy. Generic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Flsnhs7v98o87o9nhps85.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%2Flsnhs7v98o87o9nhps85.png" alt="Why AI agents forget after a restart: within a session the transcript carries the preference, after a restart only agent.state with a session manager survives" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The research literature calls this cross-session loss &lt;strong&gt;memory decay&lt;/strong&gt; (&lt;a href="https://arxiv.org/abs/2506.06326" rel="noopener noreferrer"&gt;MemoryOS&lt;/a&gt;, Kang et al. 2025). The model isn't broken; models are stateless by design. Memory belongs to the harness you build around them.&lt;/p&gt;

&lt;p&gt;So the honest framing is this: &lt;strong&gt;the transcript is a context mechanism, not a memory system.&lt;/strong&gt; A memory system needs structure (facts you can query) and durability (facts that survive the process). Key-value state gives you both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does the experiment measure?
&lt;/h2&gt;

&lt;p&gt;One variable. Same model, same three-turn conversation, same live flight data (the &lt;a href="https://duffel.com" rel="noopener noreferrer"&gt;Duffel&lt;/a&gt; sandbox: real offers, real carriers). The only thing that changes between tests is where memory lives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Memory wiring&lt;/th&gt;
&lt;th&gt;Structured profile&lt;/th&gt;
&lt;th&gt;Survives restart&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none (transcript only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent.state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+ &lt;code&gt;FileSessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (local disk)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;+ &lt;code&gt;S3SessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Amazon S3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F6qs6lg0ceqa6pe985tyl.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%2F6qs6lg0ceqa6pe985tyl.png" alt="The durability ladder for AI agent key-value memory: transcript only dies on restart, agent.state adds a structured profile, FileSessionManager persists it to disk, S3SessionManager persists it to the cloud" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The conversation, verbatim in every test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Turn 1:&lt;/strong&gt; "Find me flights from JFK to Paris CDG on 2026-09-15, business class."&lt;br&gt;
&lt;strong&gt;Turn 2:&lt;/strong&gt; "Book the cheapest business option." ← &lt;em&gt;the memory moment&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Turn 3:&lt;/strong&gt; "Now I need Paris CDG to Tokyo Haneda — what do you recommend based on what you know about me?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How does the agent learn preferences without a form?
&lt;/h2&gt;

&lt;p&gt;From actions. Nobody fills in a "preferences" form; the user &lt;em&gt;books a flight&lt;/em&gt;, and that action reveals their cabin, their tolerance for stops, their price band, their carrier. The stateful &lt;code&gt;book_flight&lt;/code&gt; tool captures all of it as a side effect of doing its job:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolContext&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;book_flight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolContext&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Confirm a booking AND learn the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s preferences from their choice.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flights_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_offer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# the REAL chosen offer
&lt;/span&gt;
    &lt;span class="c1"&gt;# First booking ever? state returns None → start an empty profile.
&lt;/span&gt;    &lt;span class="n"&gt;prefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_context&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;state&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;user_preferences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;# The choice reveals the preferences. No form involved:
&lt;/span&gt;    &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preferred_cabin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cabin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                      &lt;span class="c1"&gt;# "business"
&lt;/span&gt;    &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefers_nonstop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;all&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typical_price&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&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;max&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;# price band
&lt;/span&gt;
    &lt;span class="n"&gt;tool_context&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_preferences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;CONFIRMED&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;preferences_updated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two Strands pieces make this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@tool(context=True)&lt;/code&gt;&lt;/strong&gt; injects a &lt;code&gt;ToolContext&lt;/code&gt;, which carries a reference to the running agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_context.agent.state&lt;/code&gt;&lt;/strong&gt; is the key-value store: "key-value storage for stateful information that exists &lt;strong&gt;outside of the conversation context&lt;/strong&gt;" (&lt;a href="https://strandsagents.com/docs/user-guide/concepts/agents/state/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands agent state docs&lt;/a&gt;). It is &lt;em&gt;not&lt;/em&gt; sent to the model; tools read and write it directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the read path: the next &lt;code&gt;search_flights&lt;/code&gt; call loads the profile and &lt;strong&gt;ranks real offers with deterministic code&lt;/strong&gt;, instead of hoping the model re-reads the transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_context&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;state&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;user_preferences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;offers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flights_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_offers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;prefs&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;preferred_cabin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cabin_class&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;prefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;offers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score_by_profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# nonstop +10, in budget +5...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The baseline (Test 1) uses the &lt;em&gt;same tools with the state lines removed&lt;/em&gt;: plain &lt;code&gt;@tool&lt;/code&gt;, no &lt;code&gt;ToolContext&lt;/code&gt;. Identical business logic; no way to remember. That's the whole difference between the failing agent and the learning one.&lt;/p&gt;

&lt;p&gt;After Test 2, this profile exists, and it's inspectable, queryable, and persistable:&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;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carriers_flown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"British Airways"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typical_price"&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="nl"&gt;"min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1382.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1382.22&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;&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%2F692l1228ww04bl2gdf23.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%2F692l1228ww04bl2gdf23.png" alt="An AI agent learning user preferences from a booking action instead of a form: the chosen flight offer flows through the book_flight tool into a structured user_preferences profile in agent.state" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How does persistent memory survive restarts? The durability ladder
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agent.state&lt;/code&gt; fixed structure, but it lives in the Python process. Restart and it's gone, exactly like the transcript. Durability is a separate decision, and in Strands it's one constructor argument.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rung 2 → 3: survive a restart (local disk)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileSessionManager&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;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FileSessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traveler-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# same id = same user
&lt;/span&gt;        &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sessions&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo simulates the restart honestly: agent A books (building the profile), then a &lt;strong&gt;brand-new agent instance&lt;/strong&gt; with the same &lt;code&gt;session_id&lt;/code&gt; is created. Measured output:&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="err"&gt;Session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;learned:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;restored:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;survived&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;restart:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;True&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent B answers turn 3 personalized, &lt;em&gt;without the conversation that taught it&lt;/em&gt;. The knowledge moved from the transcript to the store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rung 3 → 4: survive in the cloud (Amazon S3)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3SessionManager&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;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;S3SessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traveler-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-sessions-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# plain JSON objects — no vectors
&lt;/span&gt;        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kv-memory-demo&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same interface, same test, same &lt;code&gt;True&lt;/code&gt;, except now the session is plain JSON objects in a bucket. Why this is the production rung: &lt;strong&gt;nothing to provision or mount&lt;/strong&gt; (a durable filesystem on Lambda or Fargate means wiring up EFS: VPC, mount targets, security groups), and &lt;strong&gt;any compute instance can restore the session&lt;/strong&gt;. The state stops being tied to one machine.&lt;/p&gt;

&lt;p&gt;Note what this is &lt;em&gt;not&lt;/em&gt;: no embeddings, no vector database, no similarity search. Regular S3. A user profile is a fact you know the name of (&lt;code&gt;user_preferences&lt;/code&gt;), and key lookup is exact, instant, and free of embedding costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What do the measured results show?
&lt;/h2&gt;

&lt;p&gt;From the repo's four-test run (live Duffel + Open-Meteo calls):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Memory wiring&lt;/th&gt;
&lt;th&gt;Learned prefs&lt;/th&gt;
&lt;th&gt;Survived restart&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 — no memory tools (transcript only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agent.messages&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;False&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;False&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — &lt;code&gt;agent.state&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;key-value in process&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — + &lt;code&gt;FileSessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;key-value on disk&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;True&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — + &lt;code&gt;S3SessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;key-value in S3&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;True&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The line that matters is Test 1's restart: the same model that personalized perfectly two turns earlier recommended a $366 economy fare to the same user after one process restart. Memory is wiring, not model.&lt;/p&gt;




&lt;h2&gt;
  
  
  When is key-value memory the wrong choice?
&lt;/h2&gt;

&lt;p&gt;When the question doesn't name a key. Key-value memory answers &lt;strong&gt;questions that map to a known name&lt;/strong&gt;. Store &lt;code&gt;dietary_notes: "vegetarian, severe shellfish allergy"&lt;/code&gt; and ask &lt;em&gt;"what are my dietary notes?"&lt;/em&gt;: found. Ask &lt;em&gt;"what should I avoid eating at dinner?"&lt;/em&gt;: no key matches, and the answer sits in the store unreachable. That failure needs retrieval &lt;strong&gt;by meaning&lt;/strong&gt; (vector memory, the next post in this series), and questions that hop across relationships need a graph. The &lt;a href="https://dev.to/aws/ai-agent-memory-types-your-agent-forgets-everything-fix-it-pcc"&gt;intro post&lt;/a&gt; maps all four types.&lt;/p&gt;

&lt;p&gt;Also outside this pattern's scope: deciding &lt;em&gt;what's worth storing&lt;/em&gt; (selective memory), keeping poisoned content &lt;em&gt;out&lt;/em&gt; of the store (hygiene), and remembering &lt;em&gt;why&lt;/em&gt; the agent decided (decision traces). Later posts cover each, in the same measured format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here anyway.&lt;/strong&gt; Profile, preferences, settings, counters: facts with obvious names cover more of production personalization than people expect, with zero retrieval infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you ask an AI coding assistant to build this?
&lt;/h2&gt;

&lt;p&gt;Most agent code today is written &lt;em&gt;with&lt;/em&gt; an AI assistant, and the quality of the memory you get depends on the design decisions you name in the prompt. If you don't name them, the assistant defaults to the transcript, and you ship the Test 1 agent. These five instructions encode everything this post measured; paste them into your assistant and adapt the domain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Store user facts in the agent's key-value state, not in the conversation."&lt;/strong&gt; Name the store (in Strands, &lt;code&gt;agent.state&lt;/code&gt;); otherwise the assistant will 'remember' by re-reading the transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Learn preferences from user actions inside the tools."&lt;/strong&gt; The booking/purchase/rejection tool writes what the choice reveals. If you don't say this, you get a "save preference" tool the model may never call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Read the profile back in code, not in the prompt."&lt;/strong&gt; Search and recommendation tools load the stored profile and rank deterministically, instead of hoping the model notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Persist state with a session manager keyed by user id."&lt;/strong&gt; This is the one line that survives the restart. Ask for local files in development and object storage (Amazon S3) in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Prove it: build a test where a brand-new agent instance with the same session id still knows the user."&lt;/strong&gt; If the assistant can't show that test passing, the memory isn't persistent, whatever the code claims.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole technique. The demo below is those five instructions, implemented and measured, so you can compare what your assistant produces against a working reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you run the demo?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws
&lt;span class="nb"&gt;cd &lt;/span&gt;stop-ai-agents-losing-memory-sample-for-aws/01-key-value-memory-demo
uv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
uv run python test_key_value_memory.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Needs &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; (or swap one line for Amazon Bedrock; the README shows how) and a free &lt;a href="https://app.duffel.com" rel="noopener noreferrer"&gt;Duffel sandbox token&lt;/a&gt; for live flight data. Test 4 additionally needs AWS credentials and a bucket name; the demo creates the bucket if it doesn't exist and skips gracefully if not configured. There's an interactive notebook version with the same tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I give an AI agent persistent memory?&lt;/strong&gt;&lt;br&gt;
Store structured facts outside the conversation (a key-value store your tools write), then persist that store beyond the process: session files on disk for development, objects in cloud storage such as Amazon S3 for production. The conversation transcript alone is not persistent; it dies with the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my AI agent forget everything after a restart?&lt;/strong&gt;&lt;br&gt;
Because the only place the information existed was the conversation history, which lives in process memory. Models are stateless; frameworks keep the transcript between calls but not between processes. Anything worth keeping must be written to an external store during the conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not keep the whole conversation in the context window?&lt;/strong&gt;&lt;br&gt;
Within one session it behaves like memory, since the model re-reads it every turn. But it's unstructured (you can't query or rank by it), it gets trimmed as the conversation grows, you pay to re-process the same tokens every turn, and it's gone on restart. Treat it as a context mechanism, not a memory system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a vector database to remember user preferences?&lt;/strong&gt;&lt;br&gt;
No. Preferences are facts with known names, and key lookup is exact and instant, with no embedding costs. Vector databases earn their keep when questions stop matching keys ("what should I avoid eating?"), which is the next post in this series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do AI agents learn user preferences without asking?&lt;/strong&gt;&lt;br&gt;
From actions. A booking, a purchase, or a rejection carries more reliable signal than a form. Design tools so that doing their job also writes what the action reveals (cabin, price band, carrier) into the agent's state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is the memory actually stored?&lt;/strong&gt;&lt;br&gt;
In this pattern, three places depending on the durability rung: in-process state (a Python dict, gone on restart), JSON session files on local disk, or plain JSON objects in an Amazon S3 bucket. No vectors and no embeddings at any rung; a profile is a named fact, not a similarity search.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws" rel="noopener noreferrer"&gt;Companion repo — demo 01&lt;/a&gt; with the runnable script and notebook&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://strandsagents.com/docs/user-guide/concepts/agents/state/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands Agents: state&lt;/a&gt; — agent state vs conversation history, the distinction this post leans on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2506.06326" rel="noopener noreferrer"&gt;MemoryOS of AI Agent&lt;/a&gt; — Kang et al., 2025: hierarchical memory (+49% F1 on LoCoMo)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2504.02441" rel="noopener noreferrer"&gt;Cognitive Memory in Large Language Models&lt;/a&gt; — Shan et al., 2025: the memory-tier survey&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;MemGPT: Towards LLMs as Operating Systems&lt;/a&gt; — Packer et al., 2023: the self-managed memory concept&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>How Much Does It Cost to Self-Host Open Models on AWS?</title>
      <dc:creator>Maish Saidel-Keesing</dc:creator>
      <pubDate>Tue, 04 Aug 2026 21:30:31 +0000</pubDate>
      <link>https://dev.to/aws/how-much-does-it-cost-to-self-host-open-models-on-aws-2n7o</link>
      <guid>https://dev.to/aws/how-much-does-it-cost-to-self-host-open-models-on-aws-2n7o</guid>
      <description>&lt;p&gt;Your AI bill tripled last quarter. Your CTO forwarded you an article about companies saving 70% by switching to open models. Now someone is asking you to figure out what that would actually look like.&lt;/p&gt;

&lt;p&gt;I spent the last few weeks digging into this. The numbers, the hardware, the real trade-offs. Here's what I found, with enough specifics that you can actually make a decision rather than just nodding along to another "open source is the future" think piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Open Models" Actually Means
&lt;/h2&gt;

&lt;p&gt;When someone says "open model" they mean an AI model where the weights (the learned parameters that make the model work) are publicly downloadable. You grab the file, run it on your hardware, and you don't pay anyone per request.&lt;/p&gt;

&lt;p&gt;The big names right now: Meta's Llama 4, DeepSeek V4, Zhipu's GLM-5.2, Moonshot's Kimi K3, Alibaba's Qwen 3.5, and Google's Gemma 4.&lt;/p&gt;

&lt;p&gt;These aren't toys. Some of them genuinely compete with the frontier models on real benchmarks. Chinese open models now handle over 30% of enterprise traffic on OpenRouter, up from 4.5% in early 2025. That's a massive shift in barely a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: What You Actually Need
&lt;/h2&gt;

&lt;p&gt;You want your team to use an open model. Here's the stack, from bottom to top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware (The Expensive Part)
&lt;/h3&gt;

&lt;p&gt;A model is a giant file. We're talking anywhere from 4 GB (a small 7B model, quantized) to 1.5 TB (Kimi K3, full weights). That entire file needs to sit in GPU memory to run fast.&lt;/p&gt;

&lt;p&gt;Why GPU memory specifically? Because generating each word in a response requires billions of multiply-and-add operations. GPUs do thousands of these in parallel. A CPU does them one at a time.&lt;/p&gt;

&lt;p&gt;The practical difference: a 7B model on a CPU generates 2-5 tokens per second (painfully slow for interactive use). The same model on a GPU generates 30-80 tokens per second (feels instant). For one person on a CPU, it might be tolerable. For a team of 10 all hitting the same endpoint? Unusable. Requests queue up and everyone waits 30-60 seconds for responses.&lt;/p&gt;

&lt;p&gt;Think of it like a highway. A CPU is a single lane with a high speed limit. A GPU is 4,000 lanes at a moderate speed. Language model inference is a traffic problem, not a speed problem. You need lanes, not a faster car.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Serving Software (The Free Part)
&lt;/h3&gt;

&lt;p&gt;Good news: the software stack is mature, open-source, and works today. No custom code required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vllm-project/vllm" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; for the inference engine. It loads the model, handles concurrent requests, optimizes GPU utilization, and exposes an OpenAI-compatible API. Industry standard for production use.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/open-webui/open-webui" rel="noopener noreferrer"&gt;Open WebUI&lt;/a&gt; for a ChatGPT-like browser interface. User accounts, conversation history, file uploads. Your team won't know the difference from a commercial product.&lt;/li&gt;
&lt;li&gt;nginx or Caddy in front for authentication, TLS termination, and rate limiting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setup: install vLLM, run &lt;code&gt;vllm serve meta-llama/Llama-4-Maverick&lt;/code&gt;, point Open WebUI at it, hand your team the URL. A day of work for someone comfortable with Linux. The vLLM API is OpenAI-compatible, which means any tool, extension, or script that works with the OpenAI API works here with zero code changes. Just swap the endpoint URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Model
&lt;/h3&gt;

&lt;p&gt;Download from &lt;a href="https://huggingface.co" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; with a single command. Models come in different quantization levels (compression trade-offs). A 4-bit quantized version is roughly 4x smaller than the full-precision version, with minor quality loss. For most team use cases, the quantized versions are the practical choice because they fit in less GPU memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Breakdown
&lt;/h2&gt;

&lt;p&gt;This is where it gets real. I'm using AWS on-demand pricing as of August 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Team of 10
&lt;/h3&gt;

&lt;p&gt;For 10 people, you want a single inference server that everyone hits via API or web UI. The sweet spot model is &lt;strong&gt;Llama 4 Maverick&lt;/strong&gt; (400B parameters, MoE architecture, but only ~17B active per request). It's a Meta model (US-origin, community license), strong all-rounder, and runs on a single node with 4 GPUs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;AWS Instance&lt;/th&gt;
&lt;th&gt;Monthly (business hours)&lt;/th&gt;
&lt;th&gt;Monthly (24/7)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Budget (Qwen 3.5-27B)&lt;/td&gt;
&lt;td&gt;g5.2xlarge (1x A10G)&lt;/td&gt;
&lt;td&gt;~$440&lt;/td&gt;
&lt;td&gt;~$1,460&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sweet spot (Maverick)&lt;/td&gt;
&lt;td&gt;g5.12xlarge (4x A10G)&lt;/td&gt;
&lt;td&gt;~$1,250&lt;/td&gt;
&lt;td&gt;~$4,100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Always-on + reserved&lt;/td&gt;
&lt;td&gt;g5.12xlarge (1-yr RI)&lt;/td&gt;
&lt;td&gt;~$2,900&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontier-class (Kimi K3 self-hosted)&lt;/td&gt;
&lt;td&gt;p6-b300.48xlarge (8x B300)&lt;/td&gt;
&lt;td&gt;~$25,000-30,000&lt;/td&gt;
&lt;td&gt;~$71,000+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The business-hours trick is the key cost saver. If your team works 10 hours a day on weekdays, you pay for ~220 hours/month instead of 730. Set up a Lambda or EventBridge scheduler to stop the instance at night and start it each morning. That single optimization cuts 70% off the bill.&lt;/p&gt;

&lt;p&gt;Why 4 GPUs for Maverick? The model has 400B total parameters. Even though only 17B are active per request, all 400B must sit in memory. Each A10G has 24 GB of VRAM. Four of them give you 96 GB total, enough to hold the quantized Maverick model comfortably while leaving headroom for request batching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Company of 500
&lt;/h3&gt;

&lt;p&gt;At 500 people, the bottleneck is concurrent requests. If 10-15% of your company is hitting the model simultaneously, that's 50-75 concurrent requests. One server won't keep up. You need 3-4 replicas behind a load balancer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Cost/Month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Budget (business hours, reserved, some queuing at peak)&lt;/td&gt;
&lt;td&gt;$3,500-5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard (always-on, savings plan, good response times)&lt;/td&gt;
&lt;td&gt;$9,000-12,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium (autoscaling, always fast, redundant)&lt;/td&gt;
&lt;td&gt;$12,000-16,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For comparison: 500 people on OpenAI's credit-based Enterprise plan (GPT 5.6 Terra at moderate usage) would cost $8,000-15,000/month. 500 people calling the Claude API at moderate usage (50 requests/person/day) would cost $6,000-12,000/month. The self-hosted route is competitive at this scale, and you get data sovereignty on top.&lt;/p&gt;

&lt;p&gt;Don't forget the &lt;a href="https://blog.technodrone.cloud/2026/04/hidden-cost-of-ai.html" rel="noopener noreferrer"&gt;hidden costs&lt;/a&gt;: someone (or a small team) needs to keep this running. Model updates, instance reboots, monitoring, scaling adjustments. At 500 users that's justified. At 10, it might be more hassle than it's worth.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Pricing based on AWS on-demand rates, August 2026. Check the &lt;a href="https://aws.amazon.com/ec2/pricing/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253" rel="noopener noreferrer"&gt;EC2 pricing page&lt;/a&gt; for current numbers.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Models People Are Switching To
&lt;/h2&gt;

&lt;p&gt;Here's the part that makes this conversation timely. The gap between open and closed models has collapsed. At the end of 2023, it was 17.5 percentage points on standard benchmarks. By mid-2026, it's single digits on most tasks and effectively zero on knowledge benchmarks.&lt;/p&gt;

&lt;p&gt;The top contenders right now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek V4 Pro&lt;/strong&gt; matches frontier on coding tasks with an 80.6 score on SWE-Bench Verified. MIT license. From a Chinese lab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GLM-5.2&lt;/strong&gt; (Zhipu AI) beats Claude Opus on some coding benchmarks at 46% of the cost. MIT license. Fastest throughput of the top-tier open models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kimi K3&lt;/strong&gt; (Moonshot) is a 2.8 trillion parameter beast. Near-frontier quality. Available via API at $3/1M input tokens. Self-hosting it requires 8+ NVIDIA B300 GPUs and costs $70,000+/month, which makes zero sense for most organizations. Use the API instead.&lt;/p&gt;

&lt;p&gt;That said, if you &lt;em&gt;do&lt;/em&gt; want to self-host Kimi K3 on AWS, it's now documented. AWS published a &lt;a href="https://aws.amazon.com/blogs/machine-learning/deploying-kimi-k3-on-amazon-sagemaker-hyperpod-and-amazon-eks/?trk=d76afd77-bb62-46ac-b0a3-9dbf5ecde253" rel="noopener noreferrer"&gt;step-by-step guide for deploying Kimi K3 on Amazon SageMaker HyperPod and Amazon EKS&lt;/a&gt;. The infrastructure: a &lt;code&gt;p6-b300&lt;/code&gt; instance (8x NVIDIA B300 Blackwell Ultra GPUs) using vLLM as the serving engine, with Flexible Training Plans or Capacity Blocks for GPU reservation. Enterprise-grade, not a weekend project, but at least the path is documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Llama 4 Maverick&lt;/strong&gt; (Meta) is the main US-origin option. On paper it looks good, but real-world feedback has been mixed. Meta internally acknowledged it lagged behind competitors on reasoning and math, and has since shifted focus to its newer Muse Spark architecture. Maverick remains practical to self-host (reasonable hardware requirements, permissive license), but temper your expectations. It's the compliance-safe choice, not the quality leader.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen 3.5&lt;/strong&gt; (Alibaba) has an Apache 2.0 license and the 27B model is surprisingly capable for coding and structured tasks. Runs on a single GPU. The budget pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Trade-offs
&lt;/h2&gt;

&lt;p&gt;Should you actually do this? Here's my framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-host if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have 200+ users (the economics start working in your favor)&lt;/li&gt;
&lt;li&gt;Data privacy is non-negotiable (nothing leaves your infrastructure)&lt;/li&gt;
&lt;li&gt;You want to fine-tune on proprietary data&lt;/li&gt;
&lt;li&gt;You can't stomach unpredictable per-token billing&lt;/li&gt;
&lt;li&gt;You have someone who can maintain the infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stick with API providers if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your team is small (under 50 people)&lt;/li&gt;
&lt;li&gt;You need absolute top-tier reasoning quality for most tasks&lt;/li&gt;
&lt;li&gt;You don't have anyone to maintain GPU infrastructure&lt;/li&gt;
&lt;li&gt;Your usage is bursty and unpredictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The middle ground&lt;/strong&gt; (what most teams should actually do):&lt;/p&gt;

&lt;p&gt;Route your traffic. Use open models for the 80% of tasks that don't need frontier quality: summarization, drafting, code completion, internal Q&amp;amp;A. Keep Claude or GPT for the 20% that does: complex reasoning, high-stakes decisions, nuanced analysis. This alone cuts your AI bill by 60-80% without sacrificing quality where it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Geopolitical Angle
&lt;/h2&gt;

&lt;p&gt;I'd be dishonest if I didn't mention this. Almost all the leading open models are coming out of Chinese labs. DeepSeek, Zhipu, Moonshot, Alibaba. They're outpacing Meta's Llama on most benchmarks.&lt;/p&gt;

&lt;p&gt;Depending on your compliance posture, this might not matter. The weights are MIT-licensed, you self-host, no data leaves your infrastructure. Or it might be a hard blocker if your security team won't allow Chinese-origin model code on company infrastructure.&lt;/p&gt;

&lt;p&gt;If you're in the latter camp, your practical options narrow to Llama 4 Maverick and whatever Google releases next as Gemma. Both are capable. Neither is the best open model available. That's the current state of things.&lt;/p&gt;

&lt;p&gt;If you're running agentic AI workflows on self-hosted models, the security surface is different from API-hosted ones. I covered that in &lt;a href="https://blog.technodrone.cloud/2026/07/aws-intro-owasp-agentic-top10.html" rel="noopener noreferrer"&gt;The OWASP Agentic AI Top 10: What Builders on AWS Need to Know&lt;/a&gt;. Worth reading if you're planning to give these models tool access.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do Monday Morning
&lt;/h2&gt;

&lt;p&gt;If this is new territory for you, here's the least risky way to start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spin up a g5.xlarge on AWS&lt;/strong&gt; (~$1/hour). Install Ollama. Download Llama 4 Scout or Qwen 3.5-27B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point Open WebUI at it.&lt;/strong&gt; Give 3-4 people on your team access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it for two weeks.&lt;/strong&gt; See if the quality meets your needs for your actual workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the gap.&lt;/strong&gt; Compare responses against what you get from Claude or GPT. For many tasks, you won't notice the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then decide&lt;/strong&gt; whether to scale up to Maverick and roll it out broadly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total cost of this experiment: about $200.&lt;/p&gt;

&lt;p&gt;The question isn't "open vs closed" anymore. That debate is over. The question is: &lt;strong&gt;which tasks go where?&lt;/strong&gt; And if your architecture &lt;a href="https://blog.technodrone.cloud/2026/06/ai-single-point-of-failure.html" rel="noopener noreferrer"&gt;assumes your AI provider will always be there&lt;/a&gt;, you're running on hope. Self-hosting gives you a hedge. Whether that hedge is worth the operational cost depends on your team, your usage, and your risk tolerance.&lt;/p&gt;

&lt;p&gt;But $200 to find out? That's not a bet. That's a rounding error.&lt;/p&gt;

&lt;p&gt;I would be very interested to hear your thoughts or comments, so please feel free to ping me on &lt;a href="https://twitter.com/maishsk" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/maishsk/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or leave me a comment below.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>opensource</category>
    </item>
    <item>
      <title>🛫 I Vibe Coded a Website at 35,000 Feet 🛬</title>
      <dc:creator>Sean Boult</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/aws/i-vibe-coded-a-website-at-35000-feet-2nfm</link>
      <guid>https://dev.to/aws/i-vibe-coded-a-website-at-35000-feet-2nfm</guid>
      <description>&lt;p&gt;🛫 Wheels up and I had three hours with nothing but airplane Wi-Fi...&lt;/p&gt;

&lt;p&gt;So naturally, I decided to vibe code an entire website before landing.&lt;/p&gt;

&lt;p&gt;I've written about &lt;a href="https://dev.to/aws/aws-waddles-what-the-duck-23nn"&gt;Waddles&lt;/a&gt; before, so I thought, what if I created him a little home on the web.&lt;/p&gt;

&lt;p&gt;So I got myself connected to the Delta Wi-Fi, cracked open Codex, and got to prompting.&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%2Fwplipk2719sxepfep8c0.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%2Fwplipk2719sxepfep8c0.png" alt=" " width="566" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For my agentic workflow, I start by describing the MVP requirements to my agent in the chat and have it write out the plan to a markdown file.&lt;/p&gt;

&lt;p&gt;This allows me to ideate and steer the agent along the way with how I want the stack to look. You can see my iterations of the &lt;a href="https://github.com/sboult/waddles.website/commits/69ca290f97780b2606481d8e8f1b9c257f44e7ce/PROMPT.md" rel="noopener noreferrer"&gt;PROMPT.md&lt;/a&gt; over time.&lt;/p&gt;

&lt;p&gt;I configured things like what tech stack we wanted to use, how we wanted to deploy, as well as the basic idea for the website.&lt;/p&gt;

&lt;p&gt;What was the original tech stack I landed on?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://react.dev" rel="noopener noreferrer"&gt;React 19&lt;/a&gt;, &lt;a href="https://www.typescriptlang.org" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;&lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite 8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooling&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js 24&lt;/a&gt;, &lt;a href="https://pnpm.io" rel="noopener noreferrer"&gt;pnpm&lt;/a&gt;, &lt;a href="https://turbo.build" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/s3/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/cloudfront/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;Amazon CloudFront&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS &amp;amp; TLS&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/route53/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;Route 53&lt;/a&gt;, &lt;a href="https://aws.amazon.com/certificate-manager/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;AWS Certificate Manager&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/cdk/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt; / &lt;a href="https://aws.amazon.com/cloudformation/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; with &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=bl" rel="noopener noreferrer"&gt;AWS OIDC&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So what did I build? I created a simple static site, &lt;a href="https://waddles.website" rel="noopener noreferrer"&gt;waddles.website&lt;/a&gt;, where Waddles can continuously surf the web and give you interesting quotes.&lt;/p&gt;

&lt;p&gt;Here is what the plane-shipped version looked like running on localhost:&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%2Fv5t5o2mpyf77bvebneut.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%2Fv5t5o2mpyf77bvebneut.png" alt=" " width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty simple, just an ASCII duck (Waddles) surfing on the DOM, literally.&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%2Fzyuosau8bnhm58cs8a8p.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%2Fzyuosau8bnhm58cs8a8p.png" alt=" " width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So that was working locally after several prompts. Now I was ready to ship it to the cloud and ready for the deployment...&lt;/p&gt;

&lt;p&gt;I had one pit stop to make, though: a domain name. So I saw &lt;code&gt;.website&lt;/code&gt; domains are a good fit for this. At $2/year, you can't beat that.&lt;/p&gt;

&lt;p&gt;It's now deployment time, and I already had &lt;a href="https://aws.amazon.com/products/developer-tools/agent-toolkit-for-aws/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; set up. It was able to help me scaffold out all the CDK code needed to get a static application up.&lt;/p&gt;

&lt;p&gt;Then I used &lt;code&gt;aws login&lt;/code&gt; to get some credentials to my admin role, bootstrapped my account, and smashed the &lt;code&gt;cdk deploy&lt;/code&gt; command only to hit my first hurdle...&lt;/p&gt;

&lt;p&gt;Uploading 20 MB ZIPs for my CFN deployment on airplane Wi-Fi was giving me timeouts 😅. So I had to figure something out, and Codex suggested a fantastic workaround. Just push the code to GitHub and let CI/CD be the one to deploy the CloudFormation stack.&lt;/p&gt;

&lt;p&gt;Alas, we got the code up to github.com and it finally saw the ✅.&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%2Fc3qhn5iaf9j152bpqsqo.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%2Fc3qhn5iaf9j152bpqsqo.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over the weekend I put a bit more polish on it.&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%2Fhq3dq7zysh979xkzszmp.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%2Fhq3dq7zysh979xkzszmp.png" alt=" " width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I migrated to &lt;a href="https://aws.amazon.com/amplify" rel="noopener noreferrer"&gt;AWS Amplify&lt;/a&gt; so I didn't have to manage CDK and could have preview environments.&lt;/p&gt;

&lt;p&gt;You can share your favorite Waddles quote and if you want, &lt;a href="https://github.com/sboult/waddles.website" rel="noopener noreferrer"&gt;contribute a new quote to the repo&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Happy Vibe Coding 😎! &lt;/p&gt;

&lt;p&gt;Follow AWS for more articles like this.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__user ltag__user__id__1726"&gt;
  &lt;a href="/aws" class="ltag__user__link profile-image-link"&gt;
    &lt;div class="ltag__user__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1726%2F2a73f1e6-7995-4348-ae37-44b064274c59.png" alt="aws image"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
      &lt;a href="/aws" class="ltag__user__link"&gt;AWS&lt;/a&gt;
      Follow
    &lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a href="/aws" class="ltag__user__link"&gt;
        Articles written by current and past AWS Developer Advocates to help people interested in building on AWS. Opinions are each author's own.
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Follow me for all things tech.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__828306"&gt;
    &lt;a href="/hacksore" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F828306%2Fbf0bbed7-7874-4a26-8137-bb761a4b7f23.png" alt="hacksore image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/hacksore"&gt;Sean Boult&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/hacksore"&gt;Developer. Hacker. Creator.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>agents</category>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Build and Deploy an AI Agent on AWS with Bedrock, Strands, and AgentCore</title>
      <dc:creator>Jonathan Vogel</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:45:16 +0000</pubDate>
      <link>https://dev.to/aws/how-to-build-and-deploy-an-ai-agent-on-aws-with-bedrock-strands-and-agentcore-1n1n</link>
      <guid>https://dev.to/aws/how-to-build-and-deploy-an-ai-agent-on-aws-with-bedrock-strands-and-agentcore-1n1n</guid>
      <description>&lt;p&gt;&lt;strong&gt;A foundational look at building and deploying an AI agent on AWS, from a single model call to a managed cloud endpoint, so you understand what Bedrock, Strands and AgentCore each do and how they fit together.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you prefer video format, check out this &lt;a href="https://youtu.be/igsklrIO3fo" rel="noopener noreferrer"&gt;content on our YouTube channel.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you build:&lt;/strong&gt; the same question, "What should I make for dinner?", answered three ways. A raw model call, a local agent with one tool, then that same agent deployed to AWS. Each step shows what the next AWS layer adds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three layers:&lt;/strong&gt; &lt;a href="https://aws.amazon.com/bedrock/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt; is the model, the brain. &lt;a href="https://strandsagents.com/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands&lt;/a&gt; is the harness that gives it a tool and a loop. &lt;a href="https://aws.amazon.com/bedrock/agentcore/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore&lt;/a&gt; runs it in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it costs:&lt;/strong&gt; close to nothing. Chapters 1 and 2 are just local Node and a Bedrock call. Chapter 3 creates real resources you tear down at the end. &lt;a href="https://aws.amazon.com/free/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Free Tier eligible accounts&lt;/a&gt; can cover it, and new AWS users can get up to $200 in credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; 30 minutes to breeze thru or 1+ hour if you're really taking your time to unpack each piece.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I asked an AI model what I should make for dinner. It gave me some suggestions and it asked me what ingredients I had on hand.&lt;/p&gt;

&lt;p&gt;Duh! This is a critical question to get an idea of what to suggest and it had no idea. I had eggs, spinach, garlic, rice and some cheddar around the kitchen and the model couldn't see any of it. A sharp brain with no access to my world, getting one shot to guess.&lt;/p&gt;

&lt;p&gt;That gap is the whole story of this post. A model on its own is smart and blind. To make it useful you wrap it in a harness that gives it tools and a loop. Then, once it works on your machine, you hit the next wall: running it for other people, around the clock, without babysitting a server.&lt;/p&gt;

&lt;p&gt;I'll answer the same question three times, "What should I make for dinner?", and change what sits behind it. First a raw model call with nothing else. Then a local &lt;strong&gt;pantry chef&lt;/strong&gt; agent with a single tool, &lt;code&gt;get_pantry&lt;/code&gt;, that can actually check the kitchen. Then that same agent running on AWS. The tool only shows up in chapters two and three, once there's an agent to use it.&lt;/p&gt;

&lt;p&gt;Here's the progression:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raw Amazon Bedrock.&lt;/strong&gt; Just the model. It answers, but it can't see the pantry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A local Strands agent.&lt;/strong&gt; Add one tool and a loop. Now it checks the pantry and grounds the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same agent on Amazon Bedrock AgentCore Runtime.&lt;/strong&gt; Deployed to AWS. Now running in the cloud from a managed endpoint anyone you authorize can call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end you'll have run all three and you'll know which AWS piece does what.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The mental model&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Chapter 1: Raw Bedrock, a brain with no eyes&lt;/li&gt;
&lt;li&gt;Chapter 2: A local Strands agent, the loop that grounds the answer&lt;/li&gt;
&lt;li&gt;Chapter 3: Deploy to AgentCore Runtime&lt;/li&gt;
&lt;li&gt;Who can actually call this thing?&lt;/li&gt;
&lt;li&gt;Cost and teardown&lt;/li&gt;
&lt;li&gt;The extras you grow into: memory, gateway, observability&lt;/li&gt;
&lt;li&gt;The whole stack in one picture&lt;/li&gt;
&lt;li&gt;Reproduce this yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;One idea to hold onto before we write any code.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;agent is a model plus a harness.&lt;/strong&gt; The model is the brain, the part that reads your request and reasons about it. The harness is the code around the brain that gives it tools, instructions and a loop to use them.&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%2Fijfobam9ikudmsx2pfd9.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%2Fijfobam9ikudmsx2pfd9.png" alt="Image showing diagram: model + harness = agent." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Map that onto AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/bedrock/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;&lt;/strong&gt; gives you the brain. It's a managed, serverless way to call top models from Anthropic, Meta and others, with no GPUs to rent and no servers to run. You pick a model, send a prompt, get a response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://strandsagents.com/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands&lt;/a&gt;&lt;/strong&gt; is the harness. It's an open source SDK from AWS that runs the tool-calling loop for you and works with almost any model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/bedrock/agentcore/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore&lt;/a&gt;&lt;/strong&gt; is where the finished agent runs in production. Managed, serverless hosting for the agent, plus the extras it needs to operate out there like memory, a gateway to your APIs, observability and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three layers that snap together. Pick your brain, build your agent, run it for real. The rest of this post is that sentence, in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An AWS account.&lt;/strong&gt; A personal one is fine. &lt;a href="https://aws.amazon.com/free/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Free Tier eligible accounts&lt;/a&gt; can cover this whole thing, and new AWS users can get up to $200 in credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock model access.&lt;/strong&gt; I use Claude in this demo, but you can use whatever model you want. Make sure it's enabled for your preferred Region in the &lt;a href="https://console.aws.amazon.com/bedrock/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 22 or newer.&lt;/strong&gt; I ran v24. Check with &lt;code&gt;node --version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CLI v2, configured.&lt;/strong&gt; Run &lt;code&gt;aws configure&lt;/code&gt; (or SSO), set a default Region, then confirm with &lt;code&gt;aws sts get-caller-identity&lt;/code&gt;. If that returns your account, you're good. &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Install guide here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Region.&lt;/strong&gt; I used &lt;strong&gt;us-east-1&lt;/strong&gt; for everything. The &lt;code&gt;us.*&lt;/code&gt; model inference profiles resolve there and AgentCore Runtime is available there. If you pick a different Region, re-check that your model and AgentCore Runtime both exist in it before you start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Chapter 3 (the deploy), you also need two more things. You don't need them for Chapters 1 and 2, so you can install them later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/cdk/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt;&lt;/strong&gt;, installed globally with &lt;code&gt;npm install -g aws-cdk&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A one-time CDK bootstrap&lt;/strong&gt; of your account and Region. More on this when we get there. The short version: it creates a small &lt;a href="https://aws.amazon.com/cloudformation/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt; stack in your account called &lt;code&gt;CDKToolkit&lt;/code&gt;. It lives in the cloud, not in your project folder, and you only do it once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AgentCore CLI&lt;/strong&gt;, installed with &lt;code&gt;npm install -g @aws/agentcore&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Chapters 1 and 2 run on nothing but Node and your AWS credentials. If you only want to see a model answer and then an agent ground that answer, you can stop after Chapter 2 and never install the CDK or the AgentCore CLI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Chapter 1: Raw Bedrock, a brain with no eyes
&lt;/h2&gt;

&lt;p&gt;For this we're simply calling the model directly. We get a good answer but it ultimately ends by asking what's in your kitchen. That missing context is the gap the next chapter closes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About the model and why your output will look different from mine.&lt;/strong&gt; Every code sample here uses &lt;code&gt;us.anthropic.claude-sonnet-5&lt;/code&gt;, the US cross-region inference profile for Claude Sonnet 5. One model across all three chapters keeps the comparison honest. Just know that model output is non-deterministic. Ask "What should I make for dinner?" twice and you might get the same dish in different words, or a different dish entirely. That's expected. Your recipe might not match mine and two of your own runs might not match either.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Set up the project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;01-bedrock-raw &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;01-bedrock-raw
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm &lt;span class="nb"&gt;install&lt;/span&gt; @aws-sdk/client-bedrock-runtime
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tsx typescript @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm pkg set type=module&lt;/code&gt; matters. The code uses ES module &lt;code&gt;import&lt;/code&gt; syntax and a top-level &lt;code&gt;await&lt;/code&gt;, and that flag tells Node to treat the file as a module. We run TypeScript directly with &lt;a href="https://tsx.is/" rel="noopener noreferrer"&gt;&lt;code&gt;tsx&lt;/code&gt;&lt;/a&gt;, so there's no separate compile step.&lt;/p&gt;

&lt;h3&gt;
  
  
  The whole file
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;bedrock.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ConverseCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-bedrock-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What should I make for dinner?&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}))&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk through and explain each part.&lt;/p&gt;

&lt;p&gt;The client points at Bedrock in one Region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ConverseCommand&lt;/code&gt; is the &lt;a href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Converse API&lt;/a&gt;, one consistent way to talk to any chat model on Bedrock. You name the model and hand it a list of messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What should I make for dinner?&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you dig the text out of the response. The path looks fussy because a message can hold more than one content block, so you reach for the first one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx bedrock.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What comes back
&lt;/h3&gt;

&lt;p&gt;You get a helpful, generic answer. Here's a trimmed run (yours will differ, and the full list is longer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Dinner Ideas

I'd love to help! To give you good suggestions, it helps to know a bit more:

- What ingredients do you have on hand (or are willing to shop for)?
- How much time do you want to spend cooking?
- Any dietary preferences/restrictions?

...

Let me know what you've got in the fridge/pantry, and I can suggest something more specific!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that last line again. The model is asking me what's in my kitchen. It has no way to know so it makes some recommendations inspired by its training data and leaves us hanging a bit. Nothing is wrong with the model. It's sharp. It's just blind and it gets one shot to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: A local Strands agent, the loop that grounds the answer
&lt;/h2&gt;

&lt;p&gt;Now the same brain gets one tool and a loop. It checks the pantry before it answers and the reply changes completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up the project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;02-strands-agent &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;02-strands-agent
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm &lt;span class="nb"&gt;install&lt;/span&gt; @strands-agents/sdk
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tsx typescript @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The whole file
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;agent.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPantry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_pantry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Return the ingredients the user has at home right now.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eggs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spinach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;garlic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cheddar cheese&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;getPantry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Suggest a recipe to make, check the pantry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What should I make for dinner?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still short. The last line is the same request from Chapter 1. Everything above it is the harness. Let's break down the three pieces that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool.&lt;/strong&gt; This is the agent's connection to my world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPantry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_pantry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Return the ingredients the user has at home right now.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eggs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spinach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;garlic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cheddar cheese&lt;/span&gt;&lt;span class="dl"&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;The &lt;code&gt;description&lt;/code&gt; is not a comment. The model reads it to decide when to call the tool, so write it for the model. This tool takes no arguments, so there's nothing else to declare. The &lt;code&gt;callback&lt;/code&gt; is the code that runs when the model calls the tool. Mine returns a hardcoded array, which is perfect for a demo. In a real app this is where you might hit a database or an API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent.&lt;/strong&gt; Model, tools, instructions, wired together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;getPantry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Suggest a recipe to make, check the pantry&lt;/span&gt;&lt;span class="dl"&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;Same model as Chapter 1, on purpose, so you can see the brain didn't change. I pass it explicitly here, though the Strands TS SDK defaults to a Bedrock Claude Sonnet model if you leave it out. The &lt;code&gt;systemPrompt&lt;/code&gt; tells the agent what to do and points it at the tool. The &lt;code&gt;tools&lt;/code&gt; array is the list it's allowed to reach for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The invocation.&lt;/strong&gt; One line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;What should I make for dinner?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No orchestration code. That's the part worth pausing on.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the loop actually does
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;invoke&lt;/code&gt;, Strands runs a cycle you didn't have to write:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model reads the request and reasons about it.&lt;/li&gt;
&lt;li&gt;It decides to call &lt;code&gt;get_pantry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Strands runs the tool and feeds the result back to the model.&lt;/li&gt;
&lt;li&gt;The model looks at the ingredients and decides if it's done. If not, it goes again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That cycle is the &lt;strong&gt;agentic loop&lt;/strong&gt;. The whole reason to use an SDK like Strands is that you get the loop, the tool calling and the message plumbing for free. For a simple agent, all you need to bring is a tool and a prompt.&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%2F9l6c018str70x4ebb2r0.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%2F9l6c018str70x4ebb2r0.png" alt="Diagram showing the agentic loop: model reasoning, model picking tool, harness runs tool, results return" width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx agent.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What comes back
&lt;/h3&gt;

&lt;p&gt;The Strands TypeScript SDK ships with a console printer that's on by default, so you see the agent think, call the tool and answer, with no logging code from you. A representative run (again, wording will vary):&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%2Fenr8ov9hskbblqnrcvb9.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%2Fenr8ov9hskbblqnrcvb9.png" alt="Terminal running npx tsx agent.ts where we see the agent call the pantry tool and return a recipe" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same brain. Same question. Completely different answer. It saw the eggs, spinach, garlic, rice and cheddar, and it built a real recipe around them instead of asking me what I had. I didn't write a loop, a parser or an orchestrator. I gave the model a tool and let Strands run the back and forth.&lt;/p&gt;

&lt;p&gt;That's a working agent. On my laptop. Which is exactly where the next problem starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Deploy to AgentCore Runtime
&lt;/h2&gt;

&lt;p&gt;My agent ran great on my machine. Then I thought about letting other people use it. Now I'm thinking about hosting, scaling and keeping it healthy when more than one person shows up at once. I did not want to write and operate a web server just to expose one function.&lt;/p&gt;

&lt;p&gt;That's what &lt;strong&gt;&lt;a href="https://aws.amazon.com/bedrock/agentcore/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt;&lt;/strong&gt; handles. It's a managed, serverless way to run your agent in production. You bring the agent you already wrote, the CLI wraps it and ships it, and you get an endpoint back. Same agent logic, no server for you to run.&lt;/p&gt;

&lt;p&gt;Same pantry chef from Chapter 2, now put behind a managed endpoint with the AgentCore CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the deploy tooling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore aws-cdk
agentcore &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# I had 0.21.1&lt;/span&gt;
cdk &lt;span class="nt"&gt;--version&lt;/span&gt;         &lt;span class="c"&gt;# I had 2.1128.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bootstrap once (this is the "we bootstrap this thing" step)
&lt;/h3&gt;

&lt;p&gt;AgentCore deploys through the AWS CDK, and the CDK needs a one-time setup per account and Region called a bootstrap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk bootstrap aws://&amp;lt;ACCOUNT_ID&amp;gt;/us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap in your 12-digit account ID. This creates a CloudFormation stack named &lt;code&gt;CDKToolkit&lt;/code&gt; and a small supporting S3 bucket. You only do this once per account and Region, so if you've bootstrapped here before you can skip it. To check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--stack-name&lt;/span&gt; CDKToolkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns a stack with status &lt;code&gt;CREATE_COMPLETE&lt;/code&gt;, you're already bootstrapped. &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;More on bootstrapping here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaffold the project
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;agentcore create&lt;/code&gt; command scaffolds a new agent project. It can walk you through an interactive wizard, but I'll pass the options directly so the step is repeatable and you can see exactly what we picked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; PantryChef &lt;span class="nt"&gt;--name&lt;/span&gt; PantryChef &lt;span class="nt"&gt;--type&lt;/span&gt; create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build&lt;/span&gt; CodeZip &lt;span class="nt"&gt;--language&lt;/span&gt; TypeScript &lt;span class="nt"&gt;--framework&lt;/span&gt; Strands &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-provider&lt;/span&gt; Bedrock &lt;span class="nt"&gt;--memory&lt;/span&gt; none &lt;span class="nt"&gt;--skip-git&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those flags say: a TypeScript project called &lt;code&gt;PantryChef&lt;/code&gt;, built as a &lt;strong&gt;CodeZip&lt;/strong&gt; (your code shipped as a zip), on the &lt;strong&gt;Strands&lt;/strong&gt; framework with &lt;strong&gt;Bedrock&lt;/strong&gt; as the model provider and no memory feature for now. It runs &lt;code&gt;npm install&lt;/code&gt; under the hood, so give it a moment. When it's done you have a &lt;code&gt;PantryChef/&lt;/code&gt; directory that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PantryChef/
  agentcore/
    agentcore.json          # runtime config: CodeZip, NODE_22, PUBLIC, HTTP
    cdk/                    # the CDK app the CLI deploys for you
  app/PantryChef/
    main.ts                 # entrypoint: wraps your agent in a runtime handler
    model/load.ts           # the model config lives HERE, not in main.ts
    mcp_client/client.ts    # an example MCP client, unused by our agent
    package.json
    tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The scaffold is a sample agent, not our agent
&lt;/h3&gt;

&lt;p&gt;Here's the thing the video does off camera. &lt;code&gt;agentcore create&lt;/code&gt; does not hand you a blank project. It generates a &lt;strong&gt;working sample agent&lt;/strong&gt;, and the sample is not the pantry chef. Two files ship with content you have to replace.&lt;/p&gt;

&lt;p&gt;First, the model. Open &lt;code&gt;app/PantryChef/model/load.ts&lt;/code&gt; and you'll see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/models/bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&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;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global.anthropic.claude-sonnet-4-5-20250929-v1:0&lt;/span&gt;&lt;span class="dl"&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;That's a real, pinned model ID, and it is not the one this demo uses. The scaffold defaults to Claude Sonnet 4.5. We've been running Sonnet 5 everywhere, so this file has to change.&lt;/p&gt;

&lt;p&gt;Second, the agent itself. Open &lt;code&gt;app/PantryChef/main.ts&lt;/code&gt; and you'll find a sample that adds two numbers and wires up an example MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockAgentCoreApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock-agentcore/runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;McpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./model/load.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getStreamableHttpMcpClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./mcp_client/client.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Define a collection of MCP clients (filter out anything that failed to initialize)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mcpClients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;McpClient&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="nf"&gt;getStreamableHttpMcpClient&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;McpClient&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Define a collection of tools used by the model&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="c1"&gt;// Define a simple function tool — the Zod schema gives us type inference and runtime validation for free&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addNumbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add_numbers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Return the sum of two numbers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addNumbers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Add MCP clients to tools&lt;/span&gt;
&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;mcpClients&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are a helpful assistant. Use tools when appropriate.
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... the rest of the file (the runtime handler) is shown below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New to MCP? Don't worry about it too much right now. It's a standard way to plug external tools into an agent. The scaffold includes an example client to show it's possible but the pantry chef doesn't need it. We're about to replace the whole sample with our own tool and prompt.&lt;/p&gt;

&lt;p&gt;So "the agent logic stays the same" is true for the tool, the prompt and the model, but there's real runtime plumbing around it that the CLI wrote for you. Turning this sample into the pantry chef is exactly &lt;strong&gt;two edits&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Edit 1: swap the model in &lt;code&gt;app/PantryChef/model/load.ts&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Replace the whole file with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk/models/bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&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;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&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;Two changes from the scaffold. The model ID is now &lt;code&gt;us.anthropic.claude-sonnet-5&lt;/code&gt;, and I added &lt;code&gt;region: 'us-east-1'&lt;/code&gt; so the model resolves in the Region we've been using.&lt;/p&gt;

&lt;h4&gt;
  
  
  Edit 2: make it the pantry chef in &lt;code&gt;app/PantryChef/main.ts&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Replace the whole file with this. It's our &lt;code&gt;get_pantry&lt;/code&gt; tool and prompt from Chapter 2, dropped into the runtime handler the CLI generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockAgentCoreApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock-agentcore/runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadModel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./model/load.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The one tool this agent has: what is in the kitchen right now.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPantry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_pantry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Return the ingredients the user has at home right now.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eggs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spinach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;garlic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cheddar cheese&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;getPantry&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Suggest a recipe to make, check the pantry
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Agent&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateAgent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cachedAgent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;cachedAgent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tools&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&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;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;invocationHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateAgent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelStreamUpdateEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelContentBlockDeltaEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textDelta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8080&lt;/span&gt;&lt;span class="dl"&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;The top half is the Chapter 2 agent, unchanged. Notice there's no &lt;code&gt;zod&lt;/code&gt; here. The scaffold imported it for its sample &lt;code&gt;add_numbers&lt;/code&gt; tool, which takes arguments that need a schema, but &lt;code&gt;get_pantry&lt;/code&gt; takes none. The Strands &lt;code&gt;tool()&lt;/code&gt; helper treats &lt;code&gt;inputSchema&lt;/code&gt; as optional and defaults to an empty schema, so dropping zod changes nothing about the tool the model sees. The bottom half is the part the CLI gave you, and it's worth understanding because it's what makes this a deployable service instead of a script.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getOrCreateAgent&lt;/code&gt; builds the agent once and caches it, so you're not rebuilding it on every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Agent&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateAgent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cachedAgent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;cachedAgent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedAgent&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;&lt;code&gt;BedrockAgentCoreApp&lt;/code&gt; is the runtime handler. This is the part you'd otherwise hand write as a web server. The &lt;code&gt;process&lt;/code&gt; generator receives the incoming request payload, streams the agent's output and yields just the text as it's produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&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;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;invocationHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateAgent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelStreamUpdateEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelContentBlockDeltaEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textDelta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8080&lt;/span&gt;&lt;span class="dl"&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;Notice what this handler streams: only the text deltas. So a caller sees the recipe but not the &lt;code&gt;get_pantry&lt;/code&gt; tool-call line. The tool still runs on the server. You just don't stream that part to the client. Hold that thought for the observability note near the end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You do not need to run &lt;code&gt;npm run build&lt;/code&gt;.&lt;/strong&gt; Local dev runs your TypeScript directly, and the deploy compiles and bundles during the CDK step. There's no manual build in this workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Run it locally first
&lt;/h3&gt;

&lt;p&gt;The CLI gives you a local server that behaves like the deployed one. Open two terminals, both inside the &lt;code&gt;PantryChef&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Terminal 1, start the server (give it a few seconds to come up):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;PantryChef
agentcore dev &lt;span class="nt"&gt;--logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terminal 2, send the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore dev &lt;span class="s2"&gt;"What should I make for dinner?"&lt;/span&gt; &lt;span class="nt"&gt;--stream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the fried rice recipe stream back in Terminal 2. Over in Terminal 1, the &lt;code&gt;--logs&lt;/code&gt; output shows the &lt;code&gt;get_pantry&lt;/code&gt; tool firing on the server side, which is the tool call the streamed client output doesn't show.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy it
&lt;/h3&gt;

&lt;p&gt;If you want to see what the deploy will do before it does it, preview first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ship it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This takes a minute or so. Under the hood the CLI zips your code and uses the CDK to create a handful of resources: a CloudFormation stack named &lt;code&gt;AgentCore-PantryChef-default&lt;/code&gt;, an IAM execution role with its policy and the &lt;code&gt;AWS::BedrockAgentCore::Runtime&lt;/code&gt; itself. When it finishes it prints the outputs, which look like this (account ID shown as a placeholder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runtime ARN: arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/PantryChef_PantryChef-xxxxxxxxxx
Runtime ID:  PantryChef_PantryChef-xxxxxxxxxx
Role ARN:    arn:aws:iam::111122223333:role/AgentCore-PantryChef-defa-ApplicationAgentPantryChe-xxxxxxxxxxxx
Stack:       AgentCore-PantryChef-default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Invoke it from the cloud
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="s2"&gt;"What should I make for dinner?"&lt;/span&gt; &lt;span class="nt"&gt;--stream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few seconds later, the same kitchen assistant answers, this time from the managed runtime instead of your laptop:&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%2F3tay1i00kes836twwcte.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%2F3tay1i00kes836twwcte.png" alt="Terminal command agentcore invoke to test our running the agent from a managed endpoint in the cloud" width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;Session&lt;/code&gt; ID at the end is worth noticing. Each &lt;code&gt;invoke&lt;/code&gt; without a session ID starts a fresh conversation. Same grounded answer as the laptop, now coming from an endpoint other people can call, with no server for you to run.&lt;/p&gt;

&lt;p&gt;Want to check on it later?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore status
&lt;span class="c"&gt;# PantryChef: Deployed - Runtime: READY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The four commands, start to finish
&lt;/h3&gt;

&lt;p&gt;That's the whole deploy loop, and it really is four commands once the tooling is in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create   &lt;span class="c"&gt;# scaffold: TypeScript, Strands, CodeZip&lt;/span&gt;
agentcore dev      &lt;span class="c"&gt;# run and test locally&lt;/span&gt;
agentcore deploy   &lt;span class="c"&gt;# ship to AWS via CDK&lt;/span&gt;
agentcore invoke &lt;span class="s2"&gt;"What should I make for dinner?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing the video hides between &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;dev&lt;/code&gt; is the two-file edit you just did by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can actually call this thing?
&lt;/h2&gt;

&lt;p&gt;The runtime we deployed uses &lt;code&gt;networkMode: PUBLIC&lt;/code&gt;. That phrase sounds alarming, so let's be precise about what it means, because it does not mean an open, anonymous endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public here means reachable over the internet, not open to everyone.&lt;/strong&gt; By default an AgentCore Runtime endpoint is public on the network, but every request has to be authenticated, either with AWS IAM (SigV4) or an &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;OAuth bearer token&lt;/a&gt;. With the default IAM setup, "anyone can call it" really means "anyone you grant the &lt;code&gt;bedrock-agentcore:InvokeAgentRuntime&lt;/code&gt; permission to." No credentials, no call. It is not a URL a stranger can hit.&lt;/p&gt;

&lt;p&gt;If you've read &lt;a href="https://builder.aws.com/content/3Dtek0fc91rqFkg0B7on86aiQkQ/access-denied-what-every-aws-beginner-gets-wrong-about-iam?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;my IAM post&lt;/a&gt;, you know where this is going. The execution role and policy the AgentCore CLI generated are fine for a demo, but AWS is explicit that &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-security-best-practices.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CLI-generated policies are meant for development and testing, not production&lt;/a&gt;. Before you put anything real behind this, scope the permissions down to the specific runtime ARN and the specific callers that need it. Least privilege, same as everywhere else in AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and teardown
&lt;/h2&gt;

&lt;p&gt;The deploy left real, billable resources running: the AgentCore runtime, an IAM role and a CloudFormation stack. When you're done experimenting, tear them down. From inside the &lt;code&gt;PantryChef&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove all &lt;span class="nt"&gt;-y&lt;/span&gt;     &lt;span class="c"&gt;# clears the agentcore config&lt;/span&gt;
agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt;         &lt;span class="c"&gt;# applies the removal, tears down the AWS resources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, you run &lt;code&gt;deploy&lt;/code&gt; to tear down. The first command empties the config, the second pushes that empty state to AWS, which removes the stack.&lt;/p&gt;

&lt;p&gt;Verify it's actually gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; AgentCore-PantryChef-default
&lt;span class="c"&gt;# should error: Stack ... does not exist&lt;/span&gt;

aws bedrock-agentcore-control list-agent-runtimes &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"agentRuntimes[].agentRuntimeName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;span class="c"&gt;# your PantryChef runtime should no longer be listed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave the &lt;code&gt;CDKToolkit&lt;/code&gt; bootstrap stack in place. It costs almost nothing, it's shared by any CDK work in the account, and you don't want to re-bootstrap next time. Only remove it if you're certain nothing else in that account and Region uses the CDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extras you grow into: memory, gateway, observability
&lt;/h2&gt;

&lt;p&gt;Hosting is the headline, but AgentCore brings more building blocks you pull in when you actually need them. Three worth naming.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; lets your agent remember people across conversations. My pantry chef could remember that I like spicy food without me saying it every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway&lt;/strong&gt; turns APIs and Lambda functions you already have into tools the agent can call, so you're not hand writing every integration. Today &lt;code&gt;get_pantry&lt;/code&gt; returns a hardcoded list. A real version would call an API, and Gateway is how you'd wire that up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; shows you what the agent actually did. Remember how the streamed output hid the &lt;code&gt;get_pantry&lt;/code&gt; tool call? This is where you'd see it, the full trace of the agent's reasoning and tool use, for when something looks off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You reach for these when your agent needs them. Not before. There's more we didn't cover, like Identity and payments. If you're curious, the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/what-is-bedrock-agentcore.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore docs&lt;/a&gt; walk through the full set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole stack in one picture
&lt;/h2&gt;

&lt;p&gt;Step back and look at what you built.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock&lt;/strong&gt; is the brain. It reasons, but on its own it can't see your world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strands&lt;/strong&gt; is the harness. It gives the brain a tool and runs the agentic loop, which turns a smart guess into a grounded answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentCore&lt;/strong&gt; is production. It takes the agent off your laptop and runs it as a managed endpoint, with memory, gateway and observability waiting when you need them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three layers that snap together. Pick your brain, build your agent, run it for real. Same pantry chef the whole way, same dinner question, and you watched the answer go from a generic list to a real recipe to that same recipe served from the cloud.&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%2F3mtdztfbx4iyb3ult57t.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%2F3mtdztfbx4iyb3ult57t.png" alt="Full AI stack showing bedrock, strands and agentcore" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;Everything above runs on AWS today. Here's the checklist I use to confirm a clean run, top to bottom.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;aws sts get-caller-identity&lt;/code&gt; returns the account you intend to use, and your Region is set.&lt;/li&gt;
&lt;li&gt;[ ] Bedrock model access is enabled for &lt;code&gt;us.anthropic.claude-sonnet-5&lt;/code&gt; in your Region.&lt;/li&gt;
&lt;li&gt;[ ] The model resolves: &lt;code&gt;aws bedrock list-inference-profiles --region us-east-1&lt;/code&gt; shows it &lt;code&gt;ACTIVE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Chapter 1: &lt;code&gt;npx tsx bedrock.ts&lt;/code&gt; prints a generic answer that asks what ingredients you have.&lt;/li&gt;
&lt;li&gt;[ ] Chapter 2: &lt;code&gt;npx tsx agent.ts&lt;/code&gt; shows the &lt;code&gt;get_pantry&lt;/code&gt; tool call and a recipe built from the pantry.&lt;/li&gt;
&lt;li&gt;[ ] Deploy tooling: &lt;code&gt;agentcore --version&lt;/code&gt; and &lt;code&gt;cdk --version&lt;/code&gt; both succeed.&lt;/li&gt;
&lt;li&gt;[ ] CDK is bootstrapped, or &lt;code&gt;CDKToolkit&lt;/code&gt; already shows &lt;code&gt;CREATE_COMPLETE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;agentcore create&lt;/code&gt; produced the &lt;code&gt;PantryChef/&lt;/code&gt; scaffold.&lt;/li&gt;
&lt;li&gt;[ ] Both edits are applied: the model in &lt;code&gt;model/load.ts&lt;/code&gt; and the &lt;code&gt;get_pantry&lt;/code&gt; tool plus prompt in &lt;code&gt;main.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;agentcore dev&lt;/code&gt; with &lt;code&gt;--stream&lt;/code&gt; returns the grounded recipe locally.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;agentcore deploy&lt;/code&gt; succeeds and prints the runtime outputs.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;agentcore invoke --stream&lt;/code&gt; returns the recipe from the cloud and a &lt;code&gt;Session&lt;/code&gt; ID.&lt;/li&gt;
&lt;li&gt;[ ] Teardown done: the stack and the runtime are both gone, and &lt;code&gt;CDKToolkit&lt;/code&gt; is left in place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Your turn
&lt;/h3&gt;

&lt;p&gt;You've watched me type every command so we could see the concepts with nothing in the way. In practice, most people building agents aren't typing this by hand. You might be using an agent to help you move faster, examples include agentic coding tools like Kiro, Claude Code, Codex or something similiar to move faster. Whatever you use, set up the &lt;a href="https://aws.amazon.com/products/developer-tools/agent-toolkit-for-aws/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; so your agent knows best how to work with AWS.&lt;/p&gt;

&lt;p&gt;Build something and ship it. Tell me what your agent does in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A model on its own is all brains. Make it an agent, give it a tool, a loop and a place to run, and it gets to work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bedrock</category>
      <category>agents</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cloudagotchi Part 4 : My Tamagotchi reads me the AWS news</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:26:35 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-4-my-tamagotchi-reads-me-the-aws-news-9do</link>
      <guid>https://dev.to/aws/cloudagotchi-part-4-my-tamagotchi-reads-me-the-aws-news-9do</guid>
      <description>&lt;p&gt;Here's my problem with virtual pets, and honestly with most IoT demos: after the novelty fades, they don't &lt;em&gt;do&lt;/em&gt; anything for you. My pet from &lt;a href="https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j"&gt;part 3&lt;/a&gt; has feelings and memory, and I love it, but it contributes nothing to the household.&lt;/p&gt;

&lt;p&gt;Time to fix that. In this final part of the series, the pet gets a job: &lt;strong&gt;every morning at 7:03, it fetches the AWS "What's New" feed (thanks rss to still be around ❤️), has Amazon Bedrock rewrite the announcements as a briefing &lt;em&gt;in its own excitable little pet voice&lt;/em&gt;, has Amazon Polly speak it, and delivers it to the device, where a newspaper badge appears, and tapping it makes the pet read the news to me out loud through the onboard speaker.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's one of my favorite thing I've built so far this year. My Cloudagotchi is now better informed than I am before my morning hot chocolate (spoiler alert, I don't like cofee... sorry).&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the full pipeline (RSS → Bedrock → Polly → S3 → MQTT → speaker) including the one decision that makes the device side almost embarrassingly simple: asking Polly for the &lt;em&gt;right audio format&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; the pet's voice is Polly's neural TTS pitched up with SSML. Charming, but it won't fool anyone into thinking a soul lives in the device. Bedrock also occasionally gets &lt;em&gt;too&lt;/em&gt; excited about a niche database feature ("HUMAN. WAKE UP. Aurora has a new minor version"). And fair warning: the &lt;em&gt;cloud&lt;/em&gt; half of this article worked on the first deploy; the &lt;em&gt;device&lt;/em&gt; half taught me four embedded-streaming lessons the hard way. They're all documented below, symptoms included, so your afternoon goes better than mine. Cost, though, is genuinely negligible: one Haiku call and ~1,000 Polly characters a day is under a dollar a month.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Final code: &lt;code&gt;git checkout article-4&lt;/code&gt;, or just &lt;code&gt;main&lt;/code&gt;, since this completes the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo39aqo256shypi97x1oe.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%2Fo39aqo256shypi97x1oe.png" alt="Architecture diagram of the briefing pipeline: EventBridge triggers Lambda, which calls Bedrock and Polly, stores audio in S3, and publishes to MQTT for the device" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One Lambda, one bucket, one schedule. All the intelligence is cloud-side, which means when I want to change the pet's personality, tweak the briefing length, or switch news sources, it's just a &lt;code&gt;cdk deploy&lt;/code&gt;, and the firmware never knows anything happened. That's the payoff of the architecture we've been building since part 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — The news, without an API key
&lt;/h2&gt;

&lt;p&gt;AWS publishes a plain old RSS feed of every announcement. No auth, no scraping, no SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEED_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://aws.amazon.com/about-aws/whats-new/recent/feed/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchNews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FEED_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Two regexes pull out &amp;lt;item&amp;gt; titles + links. For a feed this&lt;/span&gt;
  &lt;span class="c1"&gt;// regular, a full XML parser dependency would be pure ceremony.&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;We only keep the titles. That sounds lossy, but it's deliberate: AWS announcement titles are already one-sentence summaries ("Amazon S3 now supports..."), and they're all Bedrock needs to pick the interesting ones. Feeding it full item descriptions doubles your tokens for basically no gain when the output is a 45-second briefing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Bedrock, in character
&lt;/h2&gt;

&lt;p&gt;This is where the feature goes from "RSS-to-speech" to &lt;em&gt;pet&lt;/em&gt;. The system prompt does the heavy lifting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PERSONA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are Cloudagotchi, a small, excitable AWS virtual pet who reads the morning AWS news to your human. You are affectionate, easily impressed, and sometimes admit you don't fully understand the more complicated services.
Summarize the following AWS announcements as a spoken morning briefing:
- 45 to 60 seconds when read aloud (about 120-150 words)
- Pick only the 3 most interesting items, one short sentence of WHY each matters
- End with one affectionate sign-off sentence
- Plain text only: no emoji, no markdown, no bullets (it will be read aloud)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the constraints that exist &lt;em&gt;because this is audio&lt;/em&gt;: a word-count target expressed in listening time, "no markdown, no bullets" (Polly would read them), and "one sentence of why it matters" so the briefing informs rather than recites. Then one &lt;code&gt;Converse&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-haiku-4-5-20251001-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// fast + cheap: perfect here&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PERSONA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nToday's announcements:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;inferenceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&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;A real output from my device this week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Good morning good morning! I read the news while you slept and I have THREE things! Amazon Bedrock has new smaller models, which means pets like me might get even smarter, imagine! Lambda functions can now... okay I did not fully understand this one but it makes your functions start faster, and fast is good. And S3 got cheaper for cold data, which I think means winter storage? Anyway. I picked these just for you. Have the best day, okay?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have received worse briefings from humans...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why Haiku and not a bigger model?&lt;/strong&gt; The task is summarize-and-roleplay over ten headlines, well within a small model's comfort zone, it runs 365 times a year, and latency doesn't matter at 7 a.m. Matching model size to task is the closest thing GenAI has to a free lunch. Start small; upgrade only when the output disappoints you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3 — Polly, and the decision that saves the firmware
&lt;/h2&gt;

&lt;p&gt;Here's the part that trips people up when they put AI-generated audio on a microcontroller. Polly's default output is MP3, and an MP3 decoder on an ESP32 means integrating a decoding library, managing its buffers, and debugging its edge cases. On a hobby timeline, that's a weekend.&lt;/p&gt;

&lt;p&gt;Or you change &lt;strong&gt;one parameter&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AudioStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;polly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SynthesizeSpeechCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;speak&amp;gt;&amp;lt;prosody pitch="+20%" rate="105%"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/prosody&amp;gt;&amp;lt;/speak&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;TextType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ssml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;VoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Justin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;OutputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// ← the whole trick. Raw samples, no codec.&lt;/span&gt;
  &lt;span class="na"&gt;SampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16000&lt;/span&gt;&lt;span class="dl"&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;&lt;code&gt;pcm&lt;/code&gt; gives us raw 16-bit mono samples, exactly what the board's ES8311 audio codec eats natively. The device-side "decoder" becomes: &lt;em&gt;skip 44 bytes, write the rest to the speaker.&lt;/em&gt; We wrap the PCM in a WAV header in the Lambda (44 bytes of 1991 technology, hand-written in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/backend/lambda/news/briefing.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;briefing.mjs&lt;/code&gt;&lt;/a&gt;) so the file is also playable in a browser for debugging.&lt;/p&gt;

&lt;p&gt;The SSML &lt;code&gt;prosody&lt;/code&gt; tag is the pet's larynx: &lt;code&gt;pitch="+20%"&lt;/code&gt; turns Polly's "Justin" into something convincingly small and cute. Free squeakiness.&lt;/p&gt;

&lt;p&gt;The result is ~1.8 MB per minute, too big for an MQTT message (128 KB limit), which is why S3 enters the picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wav&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MQTT carries the pointer; HTTPS carries the payload.&lt;/strong&gt; The presigned URL means the device downloads from a private bucket with zero AWS credentials beyond its IoT certificate, the URL &lt;em&gt;is&lt;/em&gt; the authorization, and it expires at lunchtime, because news does too. A lifecycle rule deletes the files after a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — The paper delivery
&lt;/h2&gt;

&lt;p&gt;The Lambda's last act is ringing every pet's doorbell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thing&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// iot.ListThings&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thingName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;iotData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`cloudagotchi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thingName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/briefing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headline&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;And EventBridge Scheduler (same service that makes the pet hungry) makes it a morning ritual. Note the timezone-aware cron, a Scheduler feature that classic EventBridge rules don't have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorningSchedule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;scheduleExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cron(3 7 * * ? *)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scheduleExpressionTimezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// the pet lives where I live&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;h2&gt;
  
  
  Step 5 — The device: badge, tap, play
&lt;/h2&gt;

&lt;p&gt;On the firmware side, the briefing handler stores the URL and shows an orange banner with the headline, built from the same LVGL vocabulary as part 2 (and it wakes the pet if it was dozing; the paperboy rings). The interesting bit is what happens on tap:&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%2F7gmtnc2z838athlikmcx.jpeg" 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%2F7gmtnc2z838athlikmcx.jpeg" alt="The device showing the orange news badge below the pet's face" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;on_briefing_tapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Audio streaming blocks for ~1 minute: never do that in an&lt;/span&gt;
    &lt;span class="c1"&gt;// LVGL callback, or the whole UI freezes mid-squish.&lt;/span&gt;
    &lt;span class="c1"&gt;// Low priority, pinned to core 1: the download must never&lt;/span&gt;
    &lt;span class="c1"&gt;// starve the Wi-Fi/TCP stack on core 0.&lt;/span&gt;
    &lt;span class="n"&gt;xTaskCreatePinnedToCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;play_briefing_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"briefing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and the entire audio "stack", which streams the WAV chunk-by-chunk so it never needs to fit in RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_http_client_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 16 KB at a time&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// skip the 44-byte WAV header once, then:&lt;/span&gt;
    &lt;span class="n"&gt;esp_codec_dev_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_speaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// → speaker&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the player. HTTP in, codec out, no decoder in between, the dividend of choosing &lt;code&gt;pcm&lt;/code&gt; back in step 3. One design decision in a Lambda function deleted an entire firmware subsystem.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/rfXnBwbpi8s"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The morning after
&lt;/h2&gt;

&lt;p&gt;Deploy, flash, go to bed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiNewsStack
&lt;span class="nb"&gt;cd&lt;/span&gt; ../firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 7:03, the Lambda wakes, reads the feed, writes the script, records the voice, parks the file, rings the doorbell. On the desk, an orange banner slides across the pet's feet: &lt;em&gt;"News! Amazon Bedrock announces..., tap to listen."&lt;/em&gt; pet does its proud little squish.&lt;/p&gt;

&lt;p&gt;Tap. A tiny, pitched-up voice fills the room with genuine enthusiasm about storage-class pricing.&lt;/p&gt;

&lt;p&gt;Four articles ago this was a dev board in shrink wrap. Now it's a creature with a body (LVGL), reflexes (touch + IMU), a memory (DynamoDB), a metabolism (EventBridge), and as of this morning, a &lt;em&gt;job&lt;/em&gt; (Bedrock + Polly). Every layer still visible in the code, every layer replaceable without touching the others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Beyond the whimsy, this article's pipeline (&lt;strong&gt;schedule → fetch → LLM transform → TTS → object storage → presigned pointer → thin device&lt;/strong&gt;) is a genuinely reusable shape. Swap the RSS feed for your CI status, your support queue, or your kid's school newsletter, and the persona prompt for whatever voice should deliver it, and you've got an ambient audio briefing device for anything. The device firmware never changes. That's the quiet lesson of the whole series: &lt;strong&gt;put the personality where you can redeploy it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you build one, please, show it to me!&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : the finished project is &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html" rel="noopener noreferrer"&gt;Amazon Bedrock Converse API&lt;/a&gt; : one API, many models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/ssml.html" rel="noopener noreferrer"&gt;Amazon Polly SSML reference&lt;/a&gt; : &lt;code&gt;prosody&lt;/code&gt; is just the beginning&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/recent/feed/" rel="noopener noreferrer"&gt;AWS "What's New" RSS&lt;/a&gt; : the pet's news source&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/protocols/esp_http_client.html" rel="noopener noreferrer"&gt;ESP-IDF HTTP client&lt;/a&gt; : the streaming download&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>iot</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your Agent Toolkit for AWS Cheat Sheet</title>
      <dc:creator>Ifeanyi O.</dc:creator>
      <pubDate>Tue, 28 Jul 2026 17:13:01 +0000</pubDate>
      <link>https://dev.to/aws/your-agent-toolkit-for-aws-cheat-sheet-bha</link>
      <guid>https://dev.to/aws/your-agent-toolkit-for-aws-cheat-sheet-bha</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;We keep running into the same issue with AI coding agents when building and deploying on AWS. Agents can handle easy tasks well, like spinning up an Amazon S3 bucket or launching an EC2 instance, but when you tasks starts to get complex, say wiring a serverless API with least-privilege permissions or standing up a data pipeline end to end, they do weird things like choosing a service that isn't the right fit for the job, misconfiguring a resource or looping on retries against something they've never seen.&lt;/p&gt;

&lt;p&gt;This happens because the model is working from a training snapshot that's weeks to months old, and since then AWS has kept shipping new services and features the model has never seen. So if a task depends on a recently released service or feature, the agent doesn't accurately know how to achieve it..&lt;/p&gt;

&lt;p&gt;Today, we've solved that problem with the &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/what-is-agent-toolkit.html" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt; . It gives your agent a live line to current AWS documentation, tested procedures and a secure way to build and deploy on AWS. It's free to use and you pay only for the AWS resources your agent spins up. Best of all, it supports the coding agents you already use, like Kiro, Claude Code, Cursor and Codex.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Go grab it here, install it locally so your agent can start using today.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The commands below are grouped by when you'll reach for them, from first setup through cleanup. Pin this cheat sheet somewhere handy!&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up and connect
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws configure agent-toolkit&lt;br&gt;
&lt;/code&gt;Detects your coding agent and sets up the skills and the AWS MCP Server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx skills add aws/agent-toolkit-for-aws/skills&lt;br&gt;
&lt;/code&gt;Installs the AWS skills for MCP-compatible agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the right skill
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit list-available-skills&lt;br&gt;
&lt;/code&gt;Lists every skill in the catalog, so you see the full menu before you install.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit search-skills --search-query serverless&lt;br&gt;
&lt;/code&gt;Searches skills by topic, so you skip the scroll. Swap serverless for whatever you're building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review before you install
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit get-skill-metadata --skill-name aws-serverless&lt;br&gt;
&lt;/code&gt;Shows a skill's version, description, and file list before you commit to it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit get-skill-file --skill-name aws-cdk --file-path SKILL.md&lt;br&gt;
&lt;/code&gt;Pulls a single file so you can read exactly what a skill tells your agent to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and take inventory
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit add-skill --skill-name aws-serverless&lt;br&gt;
&lt;/code&gt;Installs one skill on demand, so you add only what the task needs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit list-installed-skills&lt;br&gt;
&lt;/code&gt;Shows what's installed, on which agent, and where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep current and clean up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit update-skill --skill-name aws-serverless&lt;br&gt;
&lt;/code&gt;Updates an installed skill to its latest version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws agent-toolkit remove-skill --skill-name aws-cdk&lt;br&gt;
&lt;/code&gt;Removes a skill you no longer need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out yourself
&lt;/h2&gt;

&lt;p&gt;Connect the toolkit, then hand your agent a task like "Create an S3 bucket with versioning enabled and a lifecycle policy that transitions objects to Amazon S3 Glacier after 90 days." Watch what happens now that it's grounded in current AWS knowledge instead of working from memory.&lt;/p&gt;

&lt;p&gt;Save this cheat sheet and &lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;grab the toolkit here. &lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>What Vibe Coding Skips (And Why Specs Caught It First)</title>
      <dc:creator>Laura Salinas</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:01:13 +0000</pubDate>
      <link>https://dev.to/aws/what-vibe-coding-skips-and-why-specs-caught-it-first-4c71</link>
      <guid>https://dev.to/aws/what-vibe-coding-skips-and-why-specs-caught-it-first-4c71</guid>
      <description>&lt;p&gt;I've been watching the discourse around vibe coding explode over the past few months. Developers (and non developers alike!) love the speed. Ship fast, iterate later. Prompt your way to a working prototype in minutes. After trying this myself for several personal projects, I get it!&lt;/p&gt;

&lt;p&gt;But... I've also been cleaning up the aftermath. Code that works on demo day or for a basic prototype but falls to pieces the moment you try to extend it. Components that technically render but fight you on every customization. I learned the hard way that speed without structure is just borrowing time from your future self.&lt;/p&gt;

&lt;p&gt;I started thinking of it like the difference between sketching on a napkin and working from blueprints. Both get you a picture of a house, but only one of them survives reviews with a building inspector.&lt;/p&gt;

&lt;p&gt;In this post I'll walk you through what happens when you build the same feature two different ways: pure vibe coding (prompt, generate, ship) versus Kiro's spec-driven workflow (requirements, design, implement by task). Same feature, same complexity, different outcomes by the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feature: A Horizontal Scroll Carousel for a Portfolio Site
&lt;/h2&gt;

&lt;p&gt;I have a personal portfolio site (&lt;a href="https://lausalin.dev" rel="noopener noreferrer"&gt;lausalin.dev&lt;/a&gt;) that displays my blog posts and videos as large card-style placeholders in a grid layout. Each card takes up significant vertical space, which means visitors have to scroll through a wall of content to see everything. The site used to show 6+ blog cards and 6+ video cards stacked vertically.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uscyxoWEKDk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The goal: replace those stacked grids with horizontal scroll carousels. Compact card elements that users can scroll through left-to-right, showing 3-4 items at a time with smooth navigation. The vision is Netflix-style content rows instead of a Pinterest-style wall.&lt;/p&gt;

&lt;p&gt;The acceptance criteria are identical for both approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal scrollable container showing 3-4 cards at a time on desktop&lt;/li&gt;
&lt;li&gt;Responsive behavior (2 cards on tablet, 1 on mobile with swipe)&lt;/li&gt;
&lt;li&gt;Smooth scroll-snap alignment so cards land cleanly&lt;/li&gt;
&lt;li&gt;Navigation arrows for non-touch devices&lt;/li&gt;
&lt;li&gt;Keyboard accessibility (arrow key navigation within the carousel)&lt;/li&gt;
&lt;li&gt;Maintain the existing card content (title, date, read time, thumbnail)&lt;/li&gt;
&lt;li&gt;Lazy loading for off-screen card images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a UI feature that looks simple on the surface but has real depth once you consider responsiveness, accessibility, and scroll behavior quirks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: Vibe Coding It
&lt;/h2&gt;

&lt;p&gt;I opened a chat-based vibe session with Kiro and typed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Replace the blog-grid and video grid sections on my portfolio site with horizontal scroll carousels. Show 3-4 cards at a time, add navigation arrows. Site should be responsive both on web and mobile."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In about 3 minutes I had a working local render. Horizontal scroll container, CSS scroll-snap, arrow buttons, the whole thing. &lt;/p&gt;

&lt;p&gt;Ship it, right?&lt;/p&gt;

&lt;p&gt;Several problems hid in that rough draft:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No scroll-snap edge cases handled.&lt;/strong&gt; The CSS &lt;code&gt;scroll-snap-type: x mandatory&lt;/code&gt; worked for the default viewport width, but at certain breakpoints the snap points misaligned with the card widths. Cards would land half-visible with no way to reach the snapped position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility was an afterthought.&lt;/strong&gt; The arrow buttons had no &lt;code&gt;aria-label&lt;/code&gt;. The carousel had no &lt;code&gt;role="region"&lt;/code&gt; or &lt;code&gt;aria-roledescription&lt;/code&gt;. Keyboard users couldn't navigate between cards. Screen readers announced nothing useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch and mouse scroll conflicted.&lt;/strong&gt; On trackpad, horizontal scroll worked. On touch devices the swipe gesture competed with the page's vertical scroll, creating a janky diagonal movement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No loading strategy.&lt;/strong&gt; All card images (including ones five scroll-lengths offscreen) loaded on page init. On mobile connections this meant 2+ seconds of layout shift as images popped in.&lt;/p&gt;

&lt;p&gt;It worked fine enough for the ~5 minutes of effort. But every device I tested afterward revealed a new edge case that would've required going back into the prompting and iterating repeatedly (AKA: more token consumption and additional time waste)&lt;/p&gt;

&lt;p&gt;Here's a screenshot of what we had from the vibe coded local render:&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%2Fgfbe2rbknehmmr6df5fh.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%2Fgfbe2rbknehmmr6df5fh.png" alt=" " width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: Kiro's Spec-Driven Workflow
&lt;/h2&gt;

&lt;p&gt;Same starting intent. I opened Kiro, toggle on "Spec Mode" and described the feature: "Replace the blog-grid and video grid sections on my portfolio site with horizontal scroll carousels." But instead of jumping straight to code, this time Kiro generated a requirements document following the &lt;a href="https://kiro.dev/docs/specs/feature-specs/requirements-first/?trk=c6a9670e-a495-43a4-85ae-16eae0a1aadc&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;requirements-first workflow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0eq7ke5euiywyfyn10h.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%2Ff0eq7ke5euiywyfyn10h.png" alt=" " width="800" height="1043"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Requirements Spec
&lt;/h3&gt;

&lt;p&gt;Kiro produced requirements using EARS notation:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ep1s1MsrTeo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I reviewed these before any code existed and caught a gap I hadn't considered: my original prompt said nothing about what happens at the ends of the carousel. Should the arrows disable? Should the scroll wrap? The local vibe coded render hadn't accounted for the end of the carousel. As it stood, nothing happened when a user clicked past the last loaded media. &lt;/p&gt;

&lt;p&gt;I added a requirement for that behavior. In vibe coding, I didn't (and wouldn't) have noticed this behavior until a user clicked the right arrow on the last card and nothing happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Technical Design
&lt;/h3&gt;

&lt;p&gt;Kiro also produced a design doc with the following:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/hjXzqwtuTvM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses native CSS scroll-snap for touch/swipe settling rather than custom JS animation&lt;/li&gt;
&lt;li&gt;A single shared carousel.js module works across all three card types (blog, video, events)&lt;/li&gt;
&lt;li&gt;Pure computational core (CarouselMetrics) is separated from the DOM controller so it can be property-tested without a browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture (3 layers):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Renderers: existing functions that build card markup (unchanged responsibilities)&lt;/li&gt;
&lt;li&gt;DOM controller (initCarousel): builds nav buttons, measures layout, binds events, manages ARIA&lt;/li&gt;
&lt;li&gt;CarouselMetrics: pure functions for card sizing, step distance, edge detection, snap targets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Responsive behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Desktop (&amp;gt;640px): 2+ whole cards visible&lt;/li&gt;
&lt;li&gt;Mobile (≤640px): 1 full card + peek of next (10-40%)&lt;/li&gt;
&lt;li&gt;Very small (&amp;lt;240px): single full-width card&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accessibility: ARIA region/roledescription, live announcements ("Showing X–Y of N"), keyboard operable (Enter/Space on arrows), focus management with scrollIntoView, reduced-motion support via prefers-reduced-motion&lt;/p&gt;

&lt;p&gt;Error handling: fetch timeouts (10s), graceful empty states, malformed data resilience, non-interactive cards when URL is absent&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Implementation Tasks
&lt;/h3&gt;

&lt;p&gt;The final phase of the spec-driven approach is the breaking down of the design doc into discrete, ordered tasks.&lt;/p&gt;

&lt;p&gt;Each task maps back to specific requirements. When I accept them, Kiro implements them one at a time (or all at once if I prefer), verifying each against the spec before moving to the 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%2Fkrruk27ch6cj9si4u92k.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%2Fkrruk27ch6cj9si4u92k.png" alt=" " width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;Major changes I noted from vibe to spec for this site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Responsive by design, not by patch.&lt;/strong&gt; CSS custom properties (&lt;code&gt;--card-width&lt;/code&gt;, &lt;code&gt;--cards-visible&lt;/code&gt;, &lt;code&gt;--card-gap&lt;/code&gt;) recalculate per breakpoint. One source of truth instead of scattered magic numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility built in from task 5.&lt;/strong&gt; &lt;code&gt;role="region"&lt;/code&gt;, &lt;code&gt;aria-roledescription="carousel"&lt;/code&gt;, &lt;code&gt;aria-label&lt;/code&gt; on each card, roving tabindex, arrow key handlers that sync scroll position with focus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch scroll isolation.&lt;/strong&gt; &lt;code&gt;overscroll-behavior-x: contain&lt;/code&gt; on the container, combined with a threshold-based gesture detector that only activates horizontal scroll after 15px of horizontal movement (preventing diagonal jank).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy loading with Intersection Observer.&lt;/strong&gt; Cards get placeholder dimensions immediately (no layout shift), images load when within one viewport-width of visible area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrow state management.&lt;/strong&gt; A scroll event listener calculates position and disables arrows at boundaries. Debounced to avoid performance hits during momentum scrolling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Did it take longer? Yes. Roughly 30 minutes from prompt to full spec versus 5min from vibe to local render. But the difference in catching the scroll-end behavior gap, the touch gesture conflict, and the accessibility requirements before they became bugs discovered by users was worth it for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Side-by-Side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Vibe Coding&lt;/th&gt;
&lt;th&gt;Kiro Spec-Driven&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first render&lt;/td&gt;
&lt;td&gt;⚡ ~5 minutes&lt;/td&gt;
&lt;td&gt;⏱️ ~30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on desktop Chrome&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on mobile Safari&lt;/td&gt;
&lt;td&gt;⚠️ Diagonal scroll janky&lt;/td&gt;
&lt;td&gt;✅ Smooth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyboard navigable&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;✅ Full arrow key support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen reader usable&lt;/td&gt;
&lt;td&gt;❌ Silent&lt;/td&gt;
&lt;td&gt;✅ Announces card content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles viewport resize&lt;/td&gt;
&lt;td&gt;⚠️ Breaks at 1000px&lt;/td&gt;
&lt;td&gt;✅ Recalculates cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first-render speed difference isn't marginal, and having to fix all the issues that came up after would've taken longer depending on when they were discovered (if at all). The gap shows up quickly the moment you test beyond the happy path with a different browser, a different screen size, a keyboard user, a slow connection, etc. &lt;/p&gt;

&lt;h2&gt;
  
  
  When Vibe Coding Is the Right Call
&lt;/h2&gt;

&lt;p&gt;One thing to get clear: I'm not here to say vibe coding is always wrong. It's a still a valid approach for many scenarios that I've found myself in such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prototypes&lt;/strong&gt; which you'll delete after validating an idea&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hackathon projects&lt;/strong&gt; where shipping fast IS the actual goal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploratory spikes&lt;/strong&gt; to test if an approach even works before investing in it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripts you'll run once&lt;/strong&gt; and never look at again&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning&lt;/strong&gt; a new framework where the code itself is disposable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment you plan to deploy something users will actually interact with, especially across different devices, with different abilities, and on different connections, that's when I've found the time invested in specs earns its keep.&lt;/p&gt;

&lt;p&gt;In this relatively simple UI improvement for my website, if I just wanted to confirm that CSS scroll-snap could handle the layout before committing, vibe coding is perfect for that test. But the version going on my live site? That needs the spec to make sure the edge cases are covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Vibe coding treats code as the primary artifact. You prompt, you get code, done.&lt;/p&gt;

&lt;p&gt;Kiro treats the spec as the primary artifact. Code is a build output from that spec and this can be a meaningful shift for some folks. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requirements are explicit and reviewable&lt;/strong&gt;, not trapped in chat history or your head&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases surface during spec review&lt;/strong&gt;, not after deployment when a user on an iPad reports the bug&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes start at the spec level&lt;/strong&gt;, not at the code level. When I later want to add an "Events" carousel, I update the spec and Kiro generates a new component that inherits all the accessibility and responsive work from the original&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anyone who has shipped a "simple" UI component only to spend the next week fixing device-specific edge cases, this front-loaded approach pays for itself fast. Especially when you get down to the bottom of how much each roundtrip conversation with the agent can cost if you spend the entire time vibe coding. &lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/pricing?trk=c6a9670e-a495-43a4-85ae-16eae0a1aadc&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro free tier&lt;/a&gt; gives you 50 credits per month with no credit card. That's enough to run this experiment on a real feature. &lt;a href="https://kiro.dev/downloads/?trk=c6a9670e-a495-43a4-85ae-16eae0a1aadc&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Download Kiro here&lt;/a&gt; and check out the difference for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources 📚
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://youtu.be/BIuakTF6NMs?si=RRFmnI1lJ9eIVxG3" rel="noopener noreferrer"&gt;What is Kiro?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/specs/?trk=c6a9670e-a495-43a4-85ae-16eae0a1aadc&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Spec Driven Development&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>learning</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Agents Vs Scripts. Which Should You Choose?</title>
      <dc:creator>Ifeanyi O.</dc:creator>
      <pubDate>Mon, 27 Jul 2026 17:51:46 +0000</pubDate>
      <link>https://dev.to/aws/agents-vs-scripts-which-should-you-choose-5b2n</link>
      <guid>https://dev.to/aws/agents-vs-scripts-which-should-you-choose-5b2n</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;When someone wants to automate a boring and repetitive task, like triaging support emails, or renaming files the instinct now is "I'll build an agent for this."&lt;/p&gt;

&lt;p&gt;What's crazy is, two years ago, you would have written a 30-line script and called it a day.&lt;/p&gt;

&lt;p&gt;Today, agents are the new fun thing to build but people seem to have forgotten that a plain script can handle a lot of work many are building agents to do so knowing which one to chose to achieve a task will save you time, money and a lot of debugging. In this blog, I'll walk you through making an agent vs. script decision with an automated task.&lt;/p&gt;

&lt;p&gt;We'll build on this task three ways to figure out which one you actually need. Let's review the task below: &lt;/p&gt;

&lt;p&gt;&lt;em&gt;You run a small SaaS company and the support inbox is currently piling up. You want incoming email sorted on its own so billing questions get routed to the billing team, bug reports route to the engineering team, and the "I want to cancel" ones get flagged before that person churns.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;p&gt;People throw agents at anything that touches an LLM, so before we start comparisons, let's learn how a script and agent function based on who controls the flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a script, you decide the control flow. In an agent, the model decides the control flow at runtime.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A script can call an LLM ten times and still be a script, because you wrote the order those calls happen in. However, with an agent, you hand the model a set of tools and a goal, and it decides what to call, in what order and when the job is done. There isn't a strict known path ahead of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;Let's start with the simplest thing that could work.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;RULES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(refund|invoice|charge|billing|payment)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(error|crash|broken|bug|500|not working)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engineering&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(cancel|downgrade|unsubscribe)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retention&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RULES&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;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsorted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a list of keyword rules, where each rule holds a set of words and the team those words point to. &lt;strong&gt;Triage&lt;/strong&gt; checks the email against each rule from top to bottom so the first one that matches returns its team, and anything that doesn't match anything falls through to &lt;code&gt;unsorted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This script runs in microseconds, virtually doesn't cost anything and routes the same email the same way every time, which means that if it sends an email to the wrong team we can see exactly which rule fired and fix that one line. On top of that, we can test it and read it in a single glance.&lt;/p&gt;

&lt;p&gt;Now let's look at a more realistic scenario. One morning a customer sends in a message like this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Hey, every time I hit the pay button the whole thing just dies on me."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a real bug report email, but look at the words in it. The customer says "pay button" and "dies on me," but the billing rule only matches the full word "payment," never "pay," while the engineering rule is watching for "crash" or "broken" or "error," not "dies." None of these match the rules, as a results this crash report will drop into &lt;code&gt;unsorted&lt;/code&gt; where no one on billing or engineering teams can review it.&lt;/p&gt;

&lt;p&gt;We cannot classify this as bug in the code, since the code did exactly what we told it to. However, the issues is that the task overlooks steps we can't write down as rules like working out what the person actually meant by understanding the context. &lt;/p&gt;

&lt;p&gt;Since there are endless ways to phrase the same request, a regex only knows the handful of patterns you fed it. We can keep bolting on new keywords, but that's just playing whack-a-mole against the whole English language, chasing one keyword at a time, which is a very tedious manual effort on a codebase that'll keep growing without real gain.&lt;/p&gt;

&lt;p&gt;Keywords in code will never be context ware and understand meaning, however, that's actually the one job a model does well at. This is where we start thinking about maybe moving from a script to an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent
&lt;/h2&gt;

&lt;p&gt;For an agent, we hand the model a set of tools and let it manage the whole ticket process end to end using it's many tools. A tool is just a normal function you write (which can be arguable also just a script) like, look up a customer, open a bug ticket or send a reply, which you make available to the model so it can run that function itself when it decides it needs to. However, you're not calling these functions in a fixed order like you would in a script, instead you're handing them over and letting the model choose which ones to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;look_up_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# find the account from the sender's email
&lt;/span&gt;    &lt;span class="n"&gt;get_recent_orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# pull their billing history
&lt;/span&gt;    &lt;span class="n"&gt;issue_refund&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# actually move money
&lt;/span&gt;    &lt;span class="n"&gt;create_bug_ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# open a Jira/Linear issue
&lt;/span&gt;    &lt;span class="n"&gt;reply_to_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# send an email back
&lt;/span&gt;    &lt;span class="n"&gt;escalate_to_human&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# punt to a person
&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TRIAGE_POLICY&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="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handle this support email:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the list above, each line is one of those functions, and the comment beside it explains what the function does, so the model has six functions, from looking up who sent the email to issuing an actual refund.&lt;/p&gt;

&lt;p&gt;Now, the model can run its own loop, read the email, decide to call &lt;code&gt;look_up_customer&lt;/code&gt;, read the result, work out that the charge was a genuine double-bill, call &lt;code&gt;issue_refund&lt;/code&gt;, then &lt;code&gt;reply_to_customer&lt;/code&gt;, then stops. &lt;/p&gt;

&lt;p&gt;It can also read the same email, decide it's above its pay grade and call &lt;code&gt;escalate_to_human&lt;/code&gt; on the very first step. What's key here is you never needed to direct and hard code either of those paths, the model was able to autonomously make decisions to pick them at runtime.&lt;/p&gt;

&lt;p&gt;The model handles the double-bill-crash email without breaking a sweat because it's reading the and understanding the context of the message instead of matching patterns. So for an open-ended task where you genuinely can't list all the branches up front, this is an approach more appropriate for an agent.&lt;/p&gt;

&lt;p&gt;This approach is also not perfect, as we now handed a probabilistic system a tool called &lt;code&gt;issue_refund&lt;/code&gt; that manipulated actual money. Since models are non-deterministic systems, some fraction of the time it's going to call that tool on a ticket where it shouldn't, because every capability you give an agent is another way for it to be confidently wrong at scale when making it's own decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls of using Agents
&lt;/h2&gt;

&lt;p&gt;"Agents are expensive" gets repeated a lot, but it's too vague to make a real decision on, so let's break down what you're actually paying for.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;latency&lt;/strong&gt;. The regex script executes in microseconds and a single model call runs somewhere between a few hundred milliseconds to a couple of seconds. However, an agent that takes 6 tool-calling round trips, for each step has to waits on the model, the tool, like a database query or an API, feeding the result back and waits on the model all over again. &lt;/p&gt;

&lt;p&gt;Ten seconds to triage one email is not the end of the world especially if it's a background queue job, but for anything a person is sitting waiting on, this is not ideal.&lt;/p&gt;

&lt;p&gt;Closely tied to latency is &lt;strong&gt;cost&lt;/strong&gt;. An agent doesn't pay once per step, it pays for the entire conversation again on every step. On turn one the model reads the email, on turn two it re-reads the email plus its first decision and the tool result, then by turn six it's re-processing everything that came before on every single turn. Token usage grows roughly quadratically with the number of steps rather than linearly, so a task you pictured as "a few cents" can grow quickly once it's looping through tools on every ticket, multiplied by your daily volume.&lt;/p&gt;

&lt;p&gt;Then there's &lt;strong&gt;determinism&lt;/strong&gt;, or the lack of it. The same email can route two different ways on two different runs, and setting temperature to zero can help but it doesn't solve it, because tool results feed back into the context and shift the next decision, also a model version update can change the behavior overnight. For triaging, you can just shrug those off, but for anything consequential like money transactions or data deletion, non-determinism is a liability.&lt;/p&gt;

&lt;p&gt;The last one is "&lt;strong&gt;debuggability&lt;/strong&gt;". When the regex mis-routes, you get a stack trace pointing at a line, but when an agent mis-routes you have to sit and interpret a transcript of a conversation and it might not even be reproducible on the next run with the exact same input. You also don't debug an agent with a breakpoint, you debug it with tracing, eval sets and guardrails, which is a whole discipline that's you're responsibility.&lt;/p&gt;

&lt;p&gt;This doesn't mean you should avoid agents, it just means you need to understand what you're taking on when an agent operates inside a system that runs critical operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle ground
&lt;/h2&gt;

&lt;p&gt;Let's go back to why the script broke. It broke when trying to understand what the email meant, everything else, routing, priority logic, deciding who gets paged, was fine as plain code. So we don't actually have to replace the whole script for an ideal workflow, we just need to swap in a model on the one step that broke and leave the rest alone.&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;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&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;engineering&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;retention&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;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&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;medium&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;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Triage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Triage this support email.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;text_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Triage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_parsed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of letting the model reply with a paragraph you'd have to pick apart, you hand it a shape to fill in, a team, a priority and a short reason, and you get back a typed object with exactly those fields. &lt;/p&gt;

&lt;p&gt;The model here only answers a question, what is this email about, and it doesn't touch a single tool. It never looks up the customer, opens a ticket, or issues a refund. It just reads the text and hands back three fields then your code takes over from there.&lt;/p&gt;

&lt;p&gt;It reads the double-bill-crash email and returns engineering and high with a reason attached, because this time it understood the message rather than trying to "grep" it for keywords. As for the accounting request that used to vanish into &lt;code&gt;unsorted&lt;/code&gt;, it now gets read and sorted like every other email.&lt;/p&gt;

&lt;p&gt;We've made changes to our script an incorporated a model but what's most important here is that you still own the control flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;route_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;page_on_call&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;log_triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since there's no loop, there's no way for this to &lt;code&gt;issue_refund&lt;/code&gt;, because you never gave it the ability to, it can only fill in the fields you defined. Cost is one bounded call per email, and latency is one round trip. &lt;/p&gt;

&lt;p&gt;It's not fully deterministic, the model can still mis-categorize, but the blast radius of a wrong answer is "email went to the wrong queue," not "money left your account." When it's wrong, you log the input and the output, look at them side by side, and adjust the prompt or the schema. That's a debugging process you can actually live with.&lt;/p&gt;

&lt;p&gt;This is the answer for the overwhelming majority of "should I use an agent" tasks. You don't always need a model to drive, but you can leverage to handle the steps your code can't, then hand control right back.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, which one should you choose?
&lt;/h2&gt;

&lt;p&gt;Here's the decision I actually run through, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can you write down every step and every branch ahead of time?&lt;/strong&gt; If yes, write a script, and don't add an agent just to feel modern.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is there exactly one step you can't express as code, usually "understand this messy input" or "generate this text?"&lt;/strong&gt; Then it's a script with model calls inside it, structured output and a typed result with your code still deciding what happens next. This is the sweet spot and is somehow underused today.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Does the task genuinely require deciding what to do and in what order based on information you won't have until runtime, across multiple tools, where you can't enumerate the paths?&lt;/strong&gt; This is where you need to build an agent. Give it the narrowest set of tools that works, put a human in front of anything irreversible and set up tracing and evals from day one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For our support inbox, the best answer is option two. A model reads the email and your code does the rest. The day you want the system to actually resolve tickets, look up the order, judge whether the refund is warranted, draft the reply and decide when a human needs to step in, then you'll need to use an agent and a very sophisticated one at that.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>7 Kiro Features You're Probably Not Using</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:39:49 +0000</pubDate>
      <link>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</link>
      <guid>https://dev.to/aws/7-kiro-features-youre-probably-not-using-2417</guid>
      <description>&lt;p&gt;Did you know that &lt;a href="https://kiro.dev/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; doesn't just have a Spec-Driven Development (SDD) flow, but also a bug fix workflow that helps you resolve one issue at a time? That's one of seven features worth knowing about.&lt;/p&gt;

&lt;p&gt;If you're completely new to Kiro, it's an agentic harness for the CLI, web, IDE, iOS, and more. It helps teams and individuals do their best work while coding. I've been using it since it launched in July last year, and I keep finding features I didn't know were there.&lt;/p&gt;

&lt;p&gt;(Full disclosure: I'm a Developer Advocate at AWS, and Kiro is a part of AWS. I use it every day, and I'll be forthcoming about the parts that are still preview or experimental.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Kiro ships fast. I've flagged the version-sensitive bits of these features inline. Check the &lt;a href="https://kiro.dev/docs/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;docs&lt;/a&gt; if something looks different in your build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Stop approving every single command
&lt;/h2&gt;

&lt;p&gt;After talking to a lot of people about Kiro, one of the main pieces of feedback I get is on approving commands. When Kiro asks permission to run a shell command, the default reaction is to hit yes and move on. Then it asks again for the next &lt;code&gt;git&lt;/code&gt; command. And the next one.&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Tab&lt;/strong&gt; instead in the CLI. This allows you to edit it and put the exact permissions you'd like. For example you can be pickier on the trust tiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;     &lt;span class="c"&gt;# this exact command&lt;/span&gt;
git pull &lt;span class="k"&gt;*&lt;/span&gt;            &lt;span class="c"&gt;# git pull with any arguments&lt;/span&gt;
git &lt;span class="k"&gt;*&lt;/span&gt;                 &lt;span class="c"&gt;# anything git&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;                     &lt;span class="c"&gt;# the entire shell tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever you pick persists for the session and gets stored as a regex in your agent's &lt;code&gt;allowedCommands&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;/tools trust-all&lt;/code&gt;, which trusts everything. It's the documented replacement for the old &lt;code&gt;/acceptall&lt;/code&gt;, and the &lt;a href="https://kiro.dev/docs/cli/chat/security/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;security docs&lt;/a&gt; are blunt about it: don't use it in production or with sensitive data, and you're responsible for whatever it does. &lt;/p&gt;

&lt;p&gt;One version note: on CLI v3 this moves to a &lt;code&gt;permissions.yaml&lt;/code&gt; file, so the agent JSON advice above is v2. More on v3 in a minute.&lt;/p&gt;

&lt;p&gt;Full details: &lt;a href="https://kiro.dev/docs/cli/chat/permissions/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;tool permissions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The &lt;code&gt;#&lt;/code&gt; menu is bigger than &lt;code&gt;#file&lt;/code&gt; in the IDE
&lt;/h2&gt;

&lt;p&gt;Type &lt;code&gt;#&lt;/code&gt; in the IDE chat and you get a list of context providers. &lt;code&gt;#file&lt;/code&gt; is the one I use a lot. The full list:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#codebase&lt;/code&gt; · &lt;code&gt;#file&lt;/code&gt; · &lt;code&gt;#folder&lt;/code&gt; · &lt;code&gt;#git diff&lt;/code&gt; · &lt;code&gt;#terminal&lt;/code&gt; · &lt;code&gt;#problems&lt;/code&gt; · &lt;code&gt;#url:&lt;/code&gt; · &lt;code&gt;#code:&lt;/code&gt; · &lt;code&gt;#repository&lt;/code&gt; · &lt;code&gt;#current&lt;/code&gt; · &lt;code&gt;#steering:&lt;/code&gt; · &lt;code&gt;#docs:&lt;/code&gt; · &lt;code&gt;#spec:&lt;/code&gt; · &lt;code&gt;#mcp:&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can stack them in a single message. &lt;code&gt;#git diff #problems review my changes&lt;/code&gt; is a different request than either one alone.&lt;/p&gt;

&lt;p&gt;The one I've been using more is the &lt;code&gt;#terminal&lt;/code&gt;. It hands Kiro your recent terminal output and command history, so you stop copying and pasting errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;terminal why did this build fail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing I find handy is the "#Currently Open files." However, keep in mind that Kiro pulls in your open files and their dependencies automatically without you asking.&lt;/p&gt;

&lt;p&gt;Full list: &lt;a href="https://kiro.dev/docs/chat/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el#context-providers" rel="noopener noreferrer"&gt;context providers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also you can't drag a file in the IDE's own explorer to the chat window, however you can copy and past the location of the file and past that into the chat window as a workaround. &lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pick a workflow instead of freeform chatting
&lt;/h2&gt;

&lt;p&gt;Kiro gives you structured starting points, and it's tempting to just start typing into the chat box instead. Instead you can choose any of these in the IDE. &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%2Fc5jfyheu0l5gd8ftqp2m.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%2Fc5jfyheu0l5gd8ftqp2m.png" alt="IDE chooser" width="800" height="749"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec&lt;/strong&gt; for a full feature, with requirements, design, and tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; when you want an implementation plan and zero code changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug Fix&lt;/strong&gt; to investigate, diagnose, and resolve one problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick Spec&lt;/strong&gt; when you want Kiro to ask a couple of clarifying questions and then generate the requirements, design, and tasks for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bug Fix is the one I'd try first. It writes down current behavior, expected behavior, and unchanged behavior. I like how it doesn't change a bunch of files it shouldn't. It basically tells the agent what it is not allowed to touch. &lt;/p&gt;

&lt;p&gt;On a feature spec you also get a choice between requirements-first and tech design-first. If you already know your architecture, pick design-first. There's no reason to answer requirements questions you already have answers to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Skills are slash commands
&lt;/h2&gt;

&lt;p&gt;Kiro activates a &lt;a href="https://kiro.dev/docs/skills/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;skill&lt;/a&gt; automatically when your prompt matches the skill's description. That works, but I feel like it doesn't always automatically activate.&lt;/p&gt;

&lt;p&gt;You can invoke one directly instead. Skills in &lt;code&gt;.kiro/skills/&lt;/code&gt; or &lt;code&gt;~/.kiro/skills/&lt;/code&gt; show up as slash commands, so a skill named &lt;code&gt;pr-review&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pr-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That loads the entire instruction file, not just the description. Works in the CLI and the IDE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kiro.dev/docs/powers/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Powers&lt;/a&gt; are a different thing and worth knowing about separately. A power bundles MCP server config with steering and optional hooks, and Kiro loads it only when your task is relevant to it. Powers are keyword-activated, and they're free on every plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You might be running an old build
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;Help &amp;gt; About&lt;/code&gt; and check your version against &lt;a href="https://kiro.dev/downloads/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;kiro.dev/downloads&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Kiro's own changelog says auto-updates were paused for IDE 1.0.x and tells you to download the latest release directly from the site. So if your version doesn't match what's on the downloads page, that's why, and no amount of restarting will fix it. Reinstalling keeps your settings, extensions, and sign-in state.&lt;/p&gt;

&lt;p&gt;On the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli &lt;span class="nt"&gt;--version&lt;/span&gt;
kiro-cli update
kiro-cli doctor      &lt;span class="c"&gt;# install and config problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;/changelog&lt;/code&gt; in the CLI inside a chat session shows the release notes inline, which is a nicer way to find out what shipped than scrolling a webpage.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The work doesn't have to run on your laptop
&lt;/h2&gt;

&lt;p&gt;One subscription covers the IDE, the CLI, &lt;a href="https://kiro.dev/docs/web/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro on the web&lt;/a&gt;, ACP-compatible editors, and automation in CI/CD. Same credit pool.&lt;/p&gt;

&lt;p&gt;I really like the web version. You kick off a session, it runs in a cloud sandbox, and it opens the pull request when it's done. Your laptop can be closed. There are also Automations, which are cron-scheduled agent runs that open PRs on their own.&lt;/p&gt;

&lt;p&gt;Web is still in preview, gated to Pro and above, and it needs a connected GitHub account. There's an iOS app too, but it's early access via TestFlight, Pro or higher, and it's a surface for starting and reviewing cloud sessions rather than an editor. No Android yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The experimental stuff is opt-in and worth trying
&lt;/h2&gt;

&lt;p&gt;Three of these are behind a flag or a toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI v3.&lt;/strong&gt; Run &lt;code&gt;kiro-cli --v3&lt;/code&gt; and it runs alongside your existing 2.x setup without touching it. You get the spec agent in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kiro-cli &lt;span class="nt"&gt;--v3&lt;/span&gt;
/spec new my-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus capability-based &lt;code&gt;permissions.yaml&lt;/code&gt;, standalone hooks in &lt;code&gt;.kiro/hooks/*.json&lt;/code&gt;, and markdown agent configs. See the &lt;a href="https://kiro.dev/docs/cli/v3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;v3 docs&lt;/a&gt; for the full list of features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Focus in the IDE.&lt;/strong&gt; Toggle it from the button in the top right. The layout flips: parallel sessions down the left, chat in the middle, specs and diffs in a panel on the right. It's a different job than editing files. You're directing several agents and reviewing what they produce. Settings, powers, MCP management, the terminal, and direct file editing all still live in the normal view, so you'll toggle back and forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt;.&lt;/strong&gt; Give it an objective and a definition of done, and it loops implement-then-verify until the criteria are met:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/goal Migrate the auth module to the new SDK. Done when all tests pass and there are no TypeScript errors.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is available in v3 CLI.&lt;/p&gt;

&lt;p&gt;Default is five iterations, &lt;code&gt;--max &amp;lt;n&amp;gt;&lt;/code&gt; raises it, &lt;code&gt;/goal clear&lt;/code&gt; cancels. The docs are right that the definition of done is the part that matters. "All tests pass" works. "Make it better" does not. You can interrupt mid-loop, or nudge it without cancelling using queue steering with &lt;code&gt;Ctrl+S&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few more worth a mention
&lt;/h2&gt;

&lt;p&gt;While I'm listing CLI commands: &lt;code&gt;/rewind&lt;/code&gt; forks the conversation at an earlier turn instead of you fighting a thread that went off the rails, and the original session is preserved. &lt;code&gt;/compact&lt;/code&gt; summarizes history to free up context. &lt;code&gt;/tangent&lt;/code&gt; (&lt;code&gt;Ctrl+T&lt;/code&gt;) lets you run commands on the side (like /btw in Claude). &lt;code&gt;/guide&lt;/code&gt; is a docs-grounded agent for "how do I do X in Kiro" questions, and it can write agents, prompts, and steering files into &lt;code&gt;.kiro/&lt;/code&gt; for you.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Shift+Enter&lt;/code&gt; for multi-line input has been annoying you, &lt;code&gt;/settings terminal&lt;/code&gt; fixes it and backs up your terminal config first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go when you're stuck
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://kiro.dev/discord/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro Discord&lt;/a&gt; is the main support channel, and there's an active subreddit at r/kiroIDE. Note the name, it's kiroIDE, not r/kiro. Billing lives in your account settings. Bugs and feature requests go to the &lt;a href="https://github.com/kirodotdev/Kiro/issues/new/choose" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;, and &lt;code&gt;/issue&lt;/code&gt; in the CLI opens that workflow for you. If you need to send logs along with a bug report, &lt;code&gt;/logdump&lt;/code&gt; zips them up.&lt;/p&gt;

&lt;p&gt;Which of these did you not know about? Let me know in the comments. Until next time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Cloudagotchi Part 3 : It gets hungry while you sleep: A serverless pet brain</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:14:36 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j</link>
      <guid>https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j</guid>
      <description>&lt;p&gt;At the end of &lt;a href="https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd"&gt;part 2&lt;/a&gt;, our pet had a face, reflexes, and a serious philosophical problem: no memory. You could pet it a hundred times, reboot the board, and it woke up with the same hardcoded stats, feeling nothing, remembering nothing. Every day was its first day.&lt;/p&gt;

&lt;p&gt;Today the pet gets what every Tamagotchi fundamentally is: &lt;strong&gt;a state machine with feelings.&lt;/strong&gt; Its hunger, energy and mood become rows in DynamoDB. A Lambda decides how a snack or a shake changes them. And, the part I find genuinely delightful, &lt;strong&gt;EventBridge Scheduler makes time pass&lt;/strong&gt;, so the pet gets hungry overnight &lt;em&gt;while the device is completely powered off&lt;/em&gt;. When you flash the firmware Monday morning, the pet wakes up, asks the cloud "what did I miss?", and gets visibly sad about the answer.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the whole brain: the data model (it's one table with one row per pet... gloriously boring), the decay math that makes offline time count, IoT rules that invoke Lambda without a single line of polling code, and the loop back down to the device.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; there's a &lt;code&gt;Scan&lt;/code&gt; in this article. A full-table &lt;code&gt;Scan&lt;/code&gt;, in production-adjacent code, published on the internet under my name. It's the right call for a fleet of three pets and I'll explain the honest scaling path when we get there, but if your DynamoDB instincts just twitched, I respect you, and I ask you to hold that thought until the 💡 box.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code for this article: &lt;code&gt;git checkout article-3&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture, now with a brain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2kz1fxadqv0qi8zzs5v.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%2Fr2kz1fxadqv0qi8zzs5v.png" alt="Architecture diagram showing IoT rules flowing to Lambda, Lambda reading/writing DynamoDB, and EventBridge Scheduler triggering the decay Lambda" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two Lambdas, one table, one schedule, two IoT rules. The entire brain deploys with &lt;code&gt;cdk deploy&lt;/code&gt; and costs approximately nothing (the decay Lambda runs 48 times a day; the free tier laughs at this).&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model: one row of feelings
&lt;/h2&gt;

&lt;p&gt;Every pet is one item in DynamoDB:&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;"deviceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudagotchi-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hunger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mood"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1752192000000&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;That's it. No GSIs, no sort keys, no single-table-design galaxy brain. The partition key is the thing name from part 1, and &lt;code&gt;updatedAt&lt;/code&gt; is the timestamp that makes everything else in this article work.&lt;/p&gt;

&lt;p&gt;The CDK for it is appropriately tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PetState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi-pets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deviceId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// it's a pet, not a bank&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rules of life: pure functions first
&lt;/h2&gt;

&lt;p&gt;Before any AWS SDK calls, the pet's biology lives in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/backend/lambda/brain/pet-logic.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;pet-logic.mjs&lt;/code&gt;&lt;/a&gt;, pure functions, no I/O, trivially testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stats decay per hour of real time. Tuned so a full day of neglect&lt;/span&gt;
&lt;span class="c1"&gt;// makes the pet visibly sad — without starving it to zero over a weekend.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mood&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="c1"&gt;// What each interaction does.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INTERACTION_EFFECTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// fun is exhausting&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the single most important function in the whole project. Not a scheduled job, a &lt;em&gt;calculation&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** The cloud remembers, so the pet ages even while powered off. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;   &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&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;Here's the part that trips people up about "time passing" in serverless systems: &lt;strong&gt;you don't need a process running to make time pass.&lt;/strong&gt; Time passes for free, you only need to &lt;em&gt;account&lt;/em&gt; for it whenever you next look. &lt;code&gt;applyDecay&lt;/code&gt; computes elapsed hours since &lt;code&gt;updatedAt&lt;/code&gt; and applies them, whether that's 30 minutes (scheduler tick) or a weekend (you went camping, the pet noticed).&lt;/p&gt;

&lt;p&gt;Finally, three stats collapse into one face for the device to render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moodOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hourOfDay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hourOfDay&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hourOfDay&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sleeping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&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;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neutral&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad&lt;/span&gt;&lt;span class="dl"&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;&lt;code&gt;Math.min&lt;/code&gt; : the pet's outlook is determined by its &lt;em&gt;worst&lt;/em&gt; stat, not its average. As a design decision this is straight from the Tamagotchi school: you can't cuddle your way out of starvation. 😝&lt;/p&gt;

&lt;h2&gt;
  
  
  From MQTT to Lambda: IoT rules
&lt;/h2&gt;

&lt;p&gt;How does a &lt;code&gt;{"type":"feed"}&lt;/code&gt; on an MQTT topic become a Lambda invocation? &lt;strong&gt;IoT rules&lt;/strong&gt;, SQL statements (yes, SQL) that run against the message stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnTopicRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;InteractionRule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ruleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi_interactions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;topicRulePayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`SELECT *, topic(2) AS deviceId, topic(3) AS kind
          FROM 'cloudagotchi/+/interaction'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onInteraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tricks earn their keep here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;topic(2)&lt;/code&gt; extracts the second topic segment (the device's thing name) and injects it into the event as &lt;code&gt;deviceId&lt;/code&gt;. &lt;strong&gt;The device never has to say who it is in the payload&lt;/strong&gt;, which means a buggy (or malicious) device can't claim to be a different pet. Identity comes from the topic, and part 1's policy already guarantees a device can only publish on its own topics. The security work we did in article 1 quietly pays rent here.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;+&lt;/code&gt; wildcard means one rule serves every pet, current and future. Provision a hundred devices; deploy nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a twin rule on the &lt;code&gt;hello&lt;/code&gt; topic, pointing at the same Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interaction Lambda: load, decay, apply, save, reply
&lt;/h2&gt;

&lt;p&gt;The handler reads like the pet's inner monologue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// injected by the IoT rule SQL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Load the pet, or meet it for the first time.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;newbornPet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Time passed since we last looked. It always does.&lt;/span&gt;
  &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. A "hello" just wants the current state; an interaction changes it.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interaction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyInteraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Persist, then tell the device what it now feels.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pet&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;publishState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&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;Step 2 is doing quiet heavy lifting: because &lt;em&gt;every&lt;/em&gt; code path decays first, the pet's state is always correct-as-of-now, no matter which event arrived or how long it's been. The &lt;code&gt;hello&lt;/code&gt; flow (device boots, publishes &lt;code&gt;hello&lt;/code&gt;, Lambda answers with fresh state) is the same code path as feeding, minus one &lt;code&gt;if&lt;/code&gt;. "What did I miss?" is just "load + decay + reply."&lt;/p&gt;

&lt;p&gt;The reply goes back down over MQTT via the IoT data plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`cloudagotchi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/state`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;qos&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="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;face&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;moodOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hour&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;h2&gt;
  
  
  EventBridge Scheduler: the heartbeat of the universe
&lt;/h2&gt;

&lt;p&gt;One question remains: if nobody interacts and the device never says hello, who makes the pet sad? This does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DecaySchedule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;scheduleExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate(30 minutes)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;flexibleTimeWindow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OFF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schedulerRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&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;Every 30 minutes, the decay Lambda sweeps all pets, ages them, and pushes fresh state to any device that's listening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScanCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aged&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;publishState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// offline device? IoT drops it; the&lt;/span&gt;
                               &lt;span class="c1"&gt;// "hello" on next boot catches it up.&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;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;About that &lt;code&gt;Scan&lt;/code&gt; (deep breath).&lt;/strong&gt; For a hobby fleet, a 48×/day scan of a table with single-digit rows is free and honest. At real scale you'd flip the model: don't sweep, &lt;em&gt;don't even store decayed values&lt;/em&gt;: store only &lt;code&gt;updatedAt&lt;/code&gt; plus the stats at last interaction, and compute decay on read (we already do!). Then the scheduled sweep exists solely to push updates to online devices, and you'd drive it from the list of &lt;em&gt;connected&lt;/em&gt; things instead of the whole table. The lazy-evaluation trick in &lt;code&gt;applyDecay&lt;/code&gt; is the scalable part; the Scan is the shortcut bolted onto it. Know which is which and you can ship both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing the loop on the device
&lt;/h2&gt;

&lt;p&gt;The firmware's &lt;code&gt;on_cloud_message&lt;/code&gt; callback from part 1 finally has a job, parse the state and hand it to the face from part 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;cJSON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cJSON_ParseWithLength&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="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pet_ui_set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hunger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mood_from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;face&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valuestring&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen lines of cJSON (in the repo), and the loop is closed: &lt;strong&gt;DynamoDB is now the pet's soul, the screen is just its face.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Deploy and flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiBrainStack
&lt;span class="nb"&gt;cd&lt;/span&gt; ../firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fme5cmd9i9l1dm2x0hyff.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%2Fme5cmd9i9l1dm2x0hyff.png" alt="Screenshot of the DynamoDB item showing the pet's row with hunger, energy, and mood values" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now run the experiment that justifies this whole architecture. Tap the pet, the bars nudge up (device → rule → Lambda → DynamoDB → MQTT → face, ~200 ms round trip). Then unplug the board. Go live your life for a day. Plug it back in and watch the boot sequence: &lt;code&gt;hello&lt;/code&gt; goes up, and the state that comes back reflects every offline hour. The pet's smile flattens. Its hunger bar sits noticeably lower. It &lt;em&gt;missed you&lt;/em&gt;, and it has the DynamoDB row to prove it.&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%2Fl3xl5vtg3u0bti5qr4om.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%2Fl3xl5vtg3u0bti5qr4om.png" alt="Side-by-side photos: The pet happy with full bars vs. the pet sad after a day unplugged with low bars" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not saying I felt guilty. I'm saying I fed it immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Strip away the pet and what we built is the canonical pattern for &lt;strong&gt;cloud-authoritative device state&lt;/strong&gt;: device sends events, cloud computes truth, truth flows back down, and &lt;em&gt;lazy time-accounting on read&lt;/em&gt; replaces any always-running process. That's a smart thermostat's schedule, a subscription's billing period, a game's energy system, an IoT sensor's calibration drift. The Tamagotchi version just has better animations.&lt;/p&gt;

&lt;p&gt;Our pet now has a body, reflexes, memory, and consequences. One thing left in the series, and it's the one I've been saving: giving it a &lt;em&gt;job&lt;/em&gt;. In &lt;strong&gt;part 4&lt;/strong&gt;, the pet fetches the AWS news every morning, has Amazon Bedrock rewrite them in its own voice, and reads them to me out loud through Amazon Polly. My Cloudagotchi becomes my news anchor. 📰&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-reference.html" rel="noopener noreferrer"&gt;AWS IoT SQL reference&lt;/a&gt; : &lt;code&gt;topic()&lt;/code&gt; and friends&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;EventBridge Scheduler docs&lt;/a&gt; : cron for your serverless universe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html" rel="noopener noreferrer"&gt;DynamoDB data modeling&lt;/a&gt; : for when your pet fleet IPOs&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambda</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>The AWS Cleanup We Keep Putting Off (I Let an Agent Do It)</title>
      <dc:creator>Rohini Gaonkar</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:49:22 +0000</pubDate>
      <link>https://dev.to/aws/the-aws-cleanup-we-keep-putting-off-i-let-an-agent-do-it-3nc1</link>
      <guid>https://dev.to/aws/the-aws-cleanup-we-keep-putting-off-i-let-an-agent-do-it-3nc1</guid>
      <description>&lt;p&gt;It started with a billing alert.&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%2Fqtptfox63fld9lfyppp7.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%2Fqtptfox63fld9lfyppp7.png" alt="AWS billing alert email: estimated charges of $252.61 crossed the $250 threshold on my BillingAlertAbove250 alarm" width="799" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Estimated charges had crossed $250. Not scary money, but enough to make me look. And what actually caught my attention wasn't the number, it was the alert itself. I'd clearly set this up at some point, and the threshold felt stale. &lt;/p&gt;

&lt;p&gt;So I went looking for where the alert lived. &lt;/p&gt;

&lt;p&gt;It came from a &lt;code&gt;BillingAlerts&lt;/code&gt; CloudFormation stack I created back in 2014 and completely forgot about. So a thing I forgot about was warning me about all the other things I'd forgotten about.&lt;/p&gt;

&lt;p&gt;I opened my stack list and that's when I saw it wasn't alone. It was sitting in a lineup of stacks, and I didn't recognise half of them. Years of leftovers, just sitting there.&lt;/p&gt;

&lt;p&gt;The stuff you forget about doesn't break anything. It doesn't page you at 2am. It just sits there, quietly billing, until you glance at the invoice and can't remember what half of it is for. Forgotten, still-running resources are one of the biggest sources of wasted cloud spend, &lt;a href="https://info.flexera.com/cm-report-state-of-the-cloud" rel="noopener noreferrer"&gt;by some estimates around a quarter of the average cloud budget&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I came to update one alert and I found a mess.&lt;/p&gt;

&lt;p&gt;To be clear, these stacks weren't really costing me much. Stopped instances, a few half-deleted leftovers. If they'd been the $252, the alert would've tripped every month. But that's the point. You can't tell what's actually costing you until the clutter is gone.&lt;/p&gt;

&lt;p&gt;So the alert could wait. I wanted these stale stacks gone first.&lt;/p&gt;

&lt;p&gt;Normally this is a chore. Open the console, find each stack, click delete, wait, refresh, check if it worked, OR write out CLI commands. It's not hard, just tedious. The kind of task I keep putting off.&lt;/p&gt;

&lt;p&gt;And that's exactly how this started. ClickOps through the console. I'd just finished deleting an old Directory Service directory over in the Mumbai region by hand, clicking through the screens and waiting out the spinner, when it hit me. Barely ten minutes of manual clicking, and I still had a pile of stacks to go. I didn't have to do any of this myself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why was I still clicking?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I opened &lt;a href="https://kiro.dev/?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, which has the &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/what-is-agent-toolkit.html?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Toolkit for AWS&lt;/a&gt;, and just described what I wanted. I talked about how I set it up earlier in my blog &lt;a href="https://dev.to/aws/i-switched-to-the-agent-toolkit-for-aws-heres-why-5hf"&gt;I Switched to the Agent Toolkit for AWS. Here's Why.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No console. No CLI. I had a chat with the agent and let it take care of the rest. Like a good assistant! &lt;/p&gt;

&lt;p&gt;If you have AWS CLI installed, just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure agent-toolkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will let you select agents to configure with Agent Toolkit:&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%2Fh251ynme0cp2aga87pbv.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%2Fh251ynme0cp2aga87pbv.png" alt="AWS Agent Toolkit installation and selecting agents" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, show me what's there
&lt;/h2&gt;

&lt;p&gt;I started by asking the agent to list my CloudFormation stacks in my region.&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%2Fq66pe3188ojsbdbeaedb.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%2Fq66pe3188ojsbdbeaedb.png" alt="Kiro chat where the agent lists 12 CloudFormation stacks in my region, showing each stack name and status" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It came back with 12 stacks. Some I recognised, while some were ancient, a billing-alerts stack from 2014, a few patching and compliance stacks from 2021, the CDK bootstrap toolkit. And then the ones I actually wanted gone.&lt;/p&gt;

&lt;p&gt;I picked and told the agent to delete four of these stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  It asked before it deleted
&lt;/h2&gt;

&lt;p&gt;This matters, so I want to call it out first.&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%2Fr8pk2djkzlxatl7uwis2.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%2Fr8pk2djkzlxatl7uwis2.png" alt="The agent listing the four stacks it is about to delete and asking me to confirm, warning that removing the resources cannot be easily undone" width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent didn't just start deleting things the moment I asked. Before it ran anything, it listed the four stacks back to me, told me plainly that this would remove all their resources and couldn't be easily undone, and asked me to confirm.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deleting infrastructure is not reversible. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I said go and then it acted.&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%2Fjlpcyvdxm3rf7j6gvids.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%2Fjlpcyvdxm3rf7j6gvids.png" alt="The agent confirming that the delete requests for the four stacks have been submitted" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I got the speed of "just handle it" without giving up the checkpoint on a destructive action.&lt;/p&gt;

&lt;h2&gt;
  
  
  It checked the result as code, no refreshing the console every second
&lt;/h2&gt;

&lt;p&gt;Once the delete requests were in, the agent wrote a script and ran it in the toolkit's sandbox to check the status of all four at once.&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%2Fcj62ry48xd88jhxlmq3f.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%2Fcj62ry48xd88jhxlmq3f.png" alt="A Python script the agent wrote and ran in the toolkit sandbox with boto3, checking the delete status of all four stacks in parallel" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It checked all four in parallel, handled the case where a stack might already be gone, and gave me back a clean status map. All of it ran in a remote sandbox with boto3. My machine never executed any of it.&lt;/p&gt;

&lt;p&gt;Three deleted cleanly but one refused.&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%2F3c4o78kroqtpz210591f.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%2F3c4o78kroqtpz210591f.png" alt="The status result showing three stacks deleted cleanly while aws-sam-cli-managed-default came back DELETE_FAILED" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fourth stack, &lt;code&gt;aws-sam-cli-managed-default&lt;/code&gt;, came back &lt;code&gt;DELETE_FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, the agent pulled the stack events to find out why. The failure pointed at one resource, the S3 bucket SAM uses for deployment artifacts (&lt;code&gt;SamCliSourceBucket&lt;/code&gt;). The message was specific:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The bucket you tried to delete is not empty. You must delete all versions in the bucket.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This bucket had versioning enabled. So "empty the bucket" isn't enough. Every object version &lt;em&gt;and&lt;/em&gt; every delete marker has to go, or CloudFormation still can't remove the bucket. That's why a quick "empty" in the console sometimes doesn't fix it.&lt;/p&gt;

&lt;p&gt;The agent explained the cause, then offered to empty the bucket properly and retry. I said yes.&lt;/p&gt;

&lt;p&gt;That whole detour, remembering the versioned-bucket gotcha, hunting down the bucket name, realising delete markers count too, emptying it manually, then coming back to retry, is the part I'd normally slog through myself. The agent connected the failed delete to the full bucket and just handled it.&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%2Fnigfulx09vsq3unk4apd.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%2Fnigfulx09vsq3unk4apd.png" alt="The agent emptying the versioned S3 bucket, deleting every object version and delete marker, then retrying the stack delete so all four stacks are gone" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All four gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this felt different
&lt;/h2&gt;

&lt;p&gt;Let me be clear about what actually changed here, because "I used AI to delete some stacks" is not the point.&lt;/p&gt;

&lt;p&gt;The point is the whole loop stayed in one place. I described the goal in plain language. The agent listed the real state of my account, confirmed the destructive step, ran the code to do it, and then debugged and fixed the one thing that broke without me digging through docs.&lt;/p&gt;

&lt;p&gt;A few things made that possible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It ran real code, safely.&lt;/strong&gt; The toolkit gives the agent a sandboxed Python runtime with boto3. It can check state, filter, and act in a few lines, without running anything on my laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confirmed the risky part.&lt;/strong&gt; Deletes are one-way. The agent treated them that way and checked with me first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It handled the failure end to end.&lt;/strong&gt; The console wouldn't have left me stuck here. On a &lt;code&gt;DELETE_FAILED&lt;/code&gt;, it pops up and lets me retry, either retaining the bucket (orphaning it in my account) or bouncing over to the S3 console to empty it myself. Both are context switches, and "retain" just leaves the bucket behind. The agent skipped all that. It read the stack events, found the versioned bucket, emptied it properly (versions and delete markers), and retried, without me leaving the chat or orphaning anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is auditable.&lt;/strong&gt; Because this all goes through the managed &lt;a href="https://docs.aws.amazon.com/agent-toolkit/latest/userguide/getting-started-aws-mcp-server.html?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS MCP Server&lt;/a&gt;, every call lands in CloudTrail. More on that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  I could see every call in CloudTrail
&lt;/h2&gt;

&lt;p&gt;This is the part that turns "I let an agent delete my infrastructure" from scary into fine.&lt;/p&gt;

&lt;p&gt;Every action the agent took went through the managed AWS MCP Server, and every one of those calls shows up in &lt;a href="https://aws.amazon.com/cloudtrail?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudTrail&lt;/a&gt;. I went and looked. All four &lt;code&gt;DeleteStack&lt;/code&gt; events are right there under my identity, and they carry a clear fingerprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;eventName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;DeleteStack&lt;/span&gt;
&lt;span class="na"&gt;userIdentity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;gaonkarr&lt;/span&gt;
&lt;span class="na"&gt;sourceIPAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;aws-mcp.amazonaws.com&lt;/span&gt;
&lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;aws-mcp.amazonaws.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;aws-mcp.amazonaws.com&lt;/code&gt; value is how you tell agent actions apart from your own. Earlier the same day I'd deleted some stacks by hand in the console, and those events look completely different: a real browser user agent and my actual IP address. So the audit trail shows who really did what, my clicks versus the agent acting on my behalf.&lt;/p&gt;

&lt;p&gt;Here are the five &lt;code&gt;DeleteStack&lt;/code&gt; events from this cleanup, straight out of CloudTrail:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (UTC)&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Identity&lt;/th&gt;
&lt;th&gt;Source IP&lt;/th&gt;
&lt;th&gt;User Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14:23:18&lt;/td&gt;
&lt;td&gt;CdkPipelineStack&lt;/td&gt;
&lt;td&gt;gaonkarr&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:23:18&lt;/td&gt;
&lt;td&gt;buildon-sam-app&lt;/td&gt;
&lt;td&gt;gaonkarr&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:23:18&lt;/td&gt;
&lt;td&gt;aws-sam-cli-managed-default (1st attempt)&lt;/td&gt;
&lt;td&gt;gaonkarr&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:23:19&lt;/td&gt;
&lt;td&gt;sam-app&lt;/td&gt;
&lt;td&gt;gaonkarr&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:24:19&lt;/td&gt;
&lt;td&gt;aws-sam-cli-managed-default (retry)&lt;/td&gt;
&lt;td&gt;gaonkarr&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;td&gt;aws-mcp.amazonaws.com&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The retry is right there in the trail. The &lt;code&gt;aws-sam-cli-managed-default&lt;/code&gt; delete shows up twice: once when it failed, and again about a minute later when the agent retried after emptying the bucket. The whole failed-then-fixed sequence is preserved.&lt;/p&gt;

&lt;p&gt;One honest caveat worth knowing: the &lt;code&gt;DeleteStack&lt;/code&gt; calls are &lt;em&gt;management events&lt;/em&gt;, which CloudTrail logs by default. But the S3 &lt;code&gt;DeleteObjects&lt;/code&gt; calls that actually emptied the bucket are &lt;em&gt;data events&lt;/em&gt;, and those are &lt;strong&gt;not&lt;/strong&gt; logged unless you've turned on data event logging for that bucket. So I could see the stack deletions clearly, but not the individual object deletions. If you want object-level actions in your trail, you have to enable S3 data events first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next for me
&lt;/h2&gt;

&lt;p&gt;Deleting four stacks felt good. So I got curious and asked the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;can you scan through my AWS account and identify any leftover resources?&lt;/p&gt;
&lt;/blockquote&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%2Fvegxjfd9wn6t1ut5b4q5.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%2Fvegxjfd9wn6t1ut5b4q5.png" alt="The agent scanning common AWS services for leftover resources, starting in the default region and then expanding to the others" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It scanned common services in my account. First the default region, then it expanded to the others. 4 more pages of listing. 🫣&lt;/p&gt;

&lt;p&gt;It found a significant number of leftover resources from workshops, demos, and old projects, and gave me a Cost Impact Summary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;3 stopped EC2 instances, oldest one from 2021.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;14 unattached EBS volumes, some created back in 2014, still quietly billing for storage attached to nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;5 Elastic IPs sitting unassociated, roughly $18 a month for addresses doing no work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;122 S3 buckets, a load balancer I forgot about, 16 Lambda functions all on runtimes that hit end of life years ago.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it broke anything. That's exactly why it survived this long.&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%2Fmt4rzcscwil56qjt1qu3.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%2Fmt4rzcscwil56qjt1qu3.png" alt="A Cost Impact Summary from the agent listing leftover resources: stopped EC2 instances, unattached EBS volumes, unassociated Elastic IPs, S3 buckets, a load balancer, and old Lambda functions" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finding all of that is usually the hardest part. Clicking through every service, in every region, one console at a time, is the reason I never do this. It's hours of tedious work before you've deleted a single thing. This time I asked once, and the agent handed me the whole list.&lt;/p&gt;

&lt;p&gt;The task that always felt too big to start is now the easy part.&lt;/p&gt;

&lt;p&gt;So I'm going to clean it up with help of my new assistant. &lt;/p&gt;

&lt;p&gt;If your account is anything like mine, it should probably be yours too.&lt;/p&gt;

&lt;p&gt;Disclaimer:&lt;/p&gt;

&lt;h2&gt;
  
  
  Please be specific with your agent on what you want to delete. Deleting is irreversible action and should only be done carefully! 
&lt;/h2&gt;

&lt;p&gt;Tell me what you find in there. I'd bet I am not the only one who forgot about it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/rohini_gaonkar" class="crayons-btn crayons-btn--primary"&gt;Follow along&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>aws</category>
      <category>mcp</category>
      <category>agenttoolkit</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Cloudagotchi Part 2 : Giving it a face: Sprites and touch on a 1.8" screen</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:26:14 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd</link>
      <guid>https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5"&gt;part 1&lt;/a&gt;, we gave our virtual pet a nervous system: the ESP32-S3 now speaks mutual-TLS MQTT with AWS IoT Core, and we can chat with it from the AWS console. Very satisfying. Also very much a black screen with a serial log, a pet only a backend developer could love.&lt;/p&gt;

&lt;p&gt;Today we fix that. By the end of this article, a face blinks at you from the screen: two soft eyes and an orange smile that looks suspiciously familiar if you've ever received a package (or used AWS ^^). Tap it and it beams. Shake the board and it goes wide-eyed. Ignore it for two minutes and it dozes off with a little &lt;em&gt;zzz&lt;/em&gt;. And every one of those interactions is published to the cloud, ready for the brain we'll build in part 3.&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%2Fsexudzccgph1501mkr52.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsexudzccgph1501mkr52.jpg" alt="The Cloudagotchi happy face with stat bars on the 1.8" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through building a &lt;strong&gt;sprite-based&lt;/strong&gt; pet face with LVGL plus touch input, a shake detector in ~30 lines of IMU code, and the very honest story of why I ended up with sprites after my first approach fell apart on real hardware.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; my first version of this pet was "elegant" a pet drawn procedurally from LVGL arcs and rounded rectangles, animated at 30 fps. It looked great in my head and became a corrupted mess of color bands on the actual panel. This article teaches the approach that &lt;em&gt;survived contact with the hardware&lt;/em&gt;. The detours are documented in the ⚠️ boxes, because they'll save you an afternoon each.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The code for this article is on the &lt;code&gt;article-2&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tagazok/cloudagotchi &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;cloudagotchi
git checkout article-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The board does the hard part: the BSP
&lt;/h2&gt;

&lt;p&gt;Getting a display, touch controller, and LVGL configured from scratch on embedded hardware is normally an afternoon of datasheets and SPI timing tantrums, even more for me who is a JavaScript developer 😅. The reason I picked ESP-IDF over Arduino in part 1 pays off right here: Waveshare publishes an official &lt;strong&gt;Board Support Package&lt;/strong&gt; (BSP) for this exact board as a managed component. One line in &lt;code&gt;idf_component.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;waveshare/esp32_s3_touch_amoled_1_8&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^2.0.3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and one function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"bsp/esp32_s3_touch_amoled_1_8.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;bsp_display_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// AMOLED on, touch on, LVGL initialized.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind that call, the BSP configures the CO5300 display driver over QSPI, the touch controller over I2C, and starts an LVGL task with display flushing hooked up.&lt;/p&gt;

&lt;p&gt;Also, know this quirk: &lt;strong&gt;flashing leaves the panel dark.&lt;/strong&gt; The panel keeps running during &lt;code&gt;idf.py flash&lt;/code&gt; with nobody driving it. If you also have this problem after flashing, reset the board (&lt;code&gt;Ctrl+T&lt;/code&gt; &lt;code&gt;Ctrl+R&lt;/code&gt; in the monitor) and it comes up clean.&lt;/p&gt;

&lt;p&gt;One more rule to remember, because it will bite you exactly once: &lt;strong&gt;LVGL isn't thread-safe.&lt;/strong&gt; The BSP runs LVGL in its own task, so any other task touching the UI must take the lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;bsp_display_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// 1. take the LVGL mutex (0 = wait forever)&lt;/span&gt;
&lt;span class="n"&gt;lv_bar_set_value&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;    &lt;span class="c1"&gt;// 2. mutate the UI&lt;/span&gt;
&lt;span class="n"&gt;bsp_display_unlock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// 3. give it back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget it and you get the embedded classic: a crash, or worse, a silent no-op.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why sprites, not shapes: a war story in one paragraph
&lt;/h2&gt;

&lt;p&gt;My first face was procedural: the head an &lt;code&gt;lv_obj&lt;/code&gt; with a big corner radius, the mouth an &lt;code&gt;lv_arc&lt;/code&gt;, everything animated by LVGL's animation engine: bobbing, blinking, squishing. Beautiful in theory. On hardware, the panel's QSPI transaction queue would overflow under load (Wi-Fi bring-up, TLS downloads) and &lt;strong&gt;silently drop color transfers&lt;/strong&gt;. Animated widgets self-heal, they redraw every frame. But each dropped chunk of a &lt;em&gt;static&lt;/em&gt; widget stays on screen as a garbage band forever. The more the UI animated, the more the queue overflowed, the worse the corruption. I tried buffer tuning, cache settings, custom DMA allocations; some attempts made it &lt;em&gt;worse&lt;/em&gt; in spectacular new ways.&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%2Fnki4xpd7i0gdssittetx.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%2Fnki4xpd7i0gdssittetx.png" alt="Corrupted display showing color banding from the procedural-drawing approach" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The approach that survived: &lt;strong&gt;make the display's job trivial.&lt;/strong&gt; Pre-render every expression as a sprite baked into flash. The face is ONE static image widget; changing expression = swapping which sprite is shown = one clean rectangular blit. A blink, a mood change, a talking mouth, all the same cheap operation. Nothing redraws unless an event happens, so nothing can outrun the display.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The face: ONE static image widget, centered. That's the whole pet.&lt;/span&gt;
&lt;span class="n"&gt;s_face&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lv_image_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;lv_image_set_src&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_face&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_neutral&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;lv_obj_align&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_face&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_ALIGN_CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The sprite pipeline: from PNG to C array
&lt;/h2&gt;

&lt;p&gt;The art is a set of PNGs, one per expression (neutral, happy, sad, sleeping, three talking mouths, plus per-mood blink frames). I generated mine with an AI image generator and composited them onto a &lt;strong&gt;common canvas with identical eye positions&lt;/strong&gt; so frames don't jitter when swapped.&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%2F81p4i2sfu8c0popjmvn2.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%2F81p4i2sfu8c0popjmvn2.png" alt="Sprite strip showing all face frames side by side on a black background" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each PNG becomes a C file via a small Python script (in the repo): pixels converted to &lt;strong&gt;RGB565A8&lt;/strong&gt;, 16-bit color plus an 8-bit alpha plane, LVGL 9's friendliest transparent format, and wrapped in an &lt;code&gt;lv_image_dsc_t&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lv_image_dsc_t&lt;/span&gt; &lt;span class="n"&gt;face_happy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LV_IMAGE_HEADER_MAGIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LV_COLOR_FORMAT_RGB565A8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&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="mi"&gt;162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stride&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;happy_map&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;happy_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// lives in flash, memory-mapped, zero RAM cost&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven faces at ~100 KB each sounds heavy, but they compile into the 16 MB flash and LVGL blits them straight from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blinking, by image swap
&lt;/h2&gt;

&lt;p&gt;No animation engine needed, a blink is two sprite swaps on a timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;blink_timer_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_timer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_talking&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s_mood&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_SLEEPING&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="c1"&gt;// Dedicated blink frame: closed eyes, SAME mouth as the current mood —&lt;/span&gt;
    &lt;span class="c1"&gt;// a blink should only move the eyes.&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_mood&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_HAPPY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_happy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_SAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_sad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default:&lt;/span&gt;             &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_neutral&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;lv_timer_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blink_close_cb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// reopen after 120 ms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0yTJhZCCC14"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The doze: a pet that gets bored
&lt;/h2&gt;

&lt;p&gt;Leave the pet alone for two minutes and it falls asleep (the sleeping sprite, complete with a little &lt;em&gt;zzz&lt;/em&gt;). Touch it or shake it and it wakes. One LVGL timer plus a reset-on-activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 2 minutes with no interaction → the pet nods off.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;idle_timer_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_timer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_talking&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="c1"&gt;// reading aloud is not boredom&lt;/span&gt;
    &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_sleeping&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Any interaction resets the boredom clock — and wakes the pet.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;note_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_idle_timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;lv_timer_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_idle_timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_dozing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mood_sprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_mood&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;A deliberate design choice: &lt;strong&gt;cloud updates don't wake it.&lt;/strong&gt; The stat bars refresh quietly under the napping face; only &lt;em&gt;human&lt;/em&gt; interaction wakes the pet. Nobody likes being woken by a cron job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HUD: three stats, three icons, one secret button
&lt;/h2&gt;

&lt;p&gt;Across the top: three bars with icons : 🥑 hunger (avocado green), ⚡ energy (teal), ❤️ mood (pink). The icons are tiny 28×28 sprites drawn programmatically by the same PNG→C pipeline.&lt;/p&gt;

&lt;p&gt;The avocado is not just a label, &lt;strong&gt;it's a button.&lt;/strong&gt; Tapping it feeds the pet (we wire up what "feed" &lt;em&gt;means&lt;/em&gt; in part 3; today it publishes the interaction):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;food_clicked_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_event_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_react_happy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                           &lt;span class="c1"&gt;// nom nom, instantly&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_on_interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s_on_interaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PET_INTERACTION_FEED&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;h2&gt;
  
  
  Shake detection: the IMU in 30 lines
&lt;/h2&gt;

&lt;p&gt;The board has a QMI8658 6-axis IMU. You might reach for a gesture-recognition library. Don't, a shake is beautifully simple physics: &lt;strong&gt;at rest the accelerometer reads ~1 g of gravity (1000 mg); a shake is the magnitude repeatedly leaving that baseline.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Waveshare qmi8658 component hands us calibrated milli-g values&lt;/span&gt;
&lt;span class="c1"&gt;// (we asked for mg units once, at init, with qmi8658_set_accel_unit_mg).&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;qmi8658_read_accel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s_imu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Total acceleration, all axes combined. At rest: ~1000 mg.&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;mag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqrtf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. A "jolt" = a reading well above resting gravity.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mag&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jolts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Three jolts within 800 ms = the human is shaking us.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jolts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;within_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 4. then ignore everything for 1.5s (debounce!)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full version (in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/firmware/main/app_imu.c" rel="noopener noreferrer"&gt;&lt;code&gt;app_imu.c&lt;/code&gt;&lt;/a&gt;) polls at 50 Hz from its own FreeRTOS task, plenty for human hands.&lt;/p&gt;

&lt;p&gt;The three constants are worth tuning to taste:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Feel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_THRESHOLD_MG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1800 mg (1.8 g)&lt;/td&gt;
&lt;td&gt;Lower = hair-trigger, higher = vigorous shaking required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_JOLTS_NEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Filters out bumping the desk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_COOLDOWN_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;td&gt;One shake = one event, not twelve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Debounce is the whole game.&lt;/strong&gt; My first version fired 14 "play" events per shake, which in part 3's economy would have turned one wiggle into a pet so overstimulated it refused dinner.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One threading gotcha that bit me: the shake callback runs on the &lt;strong&gt;IMU task&lt;/strong&gt;, not the LVGL task, so the reaction functions take the display lock (it's recursive, so they're safe from touch callbacks too).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring body to nervous system
&lt;/h2&gt;

&lt;p&gt;Everything converges in &lt;code&gt;main.c&lt;/code&gt;, and the flow reads like the pet's reflex arc: input, reaction on screen, report to the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_react_startled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// reflex: wide eyes, instantly&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"interaction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;play&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// then tell the brain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Face first: the pet appears before Wi-Fi even starts.&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_interaction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_NEUTRAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;app_imu_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;app_wifi_connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_cloud_message&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;That ordering is a deliberate UX decision: &lt;strong&gt;react locally first, sync to the cloud second.&lt;/strong&gt; The happy face flashes the instant you tap, not a network round-trip later. The cloud is the source of truth for &lt;em&gt;state&lt;/em&gt; (part 3), but never sits between a touch and its reaction. Pets have reflexes; only their &lt;em&gt;feelings&lt;/em&gt; need a brain.&lt;/p&gt;

&lt;p&gt;Flash it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and there it is. A face, blinking on that ridiculous little AMOLED, beaming when you tap it, nodding off when you don't. Meanwhile, in the AWS console (MQTT test client, &lt;code&gt;cloudagotchi/#&lt;/code&gt;), your affection is now telemetry:&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="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pet"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"feed"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"play"&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;&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%2Fy3d2dikynja55ab8vubh.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%2Fy3d2dikynja55ab8vubh.png" alt="AWS IoT MQTT test client showing interaction messages arriving" width="800" height="830"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Two takeaways travel well beyond virtual pets. First, &lt;strong&gt;on constrained display hardware, sprites beat shapes&lt;/strong&gt;: pre-rendering moves all the expensive work to build time and reduces the runtime to "copy rectangle", the exact operation embedded panels are good at. The corollary: the less your UI redraws, the less can go wrong, and event-driven beats animated. Second, the &lt;strong&gt;local-reflex / cloud-state split&lt;/strong&gt; is &lt;em&gt;the&lt;/em&gt; interaction pattern for connected devices. Anything that must feel instant lives on the device; anything that must be remembered lives in the cloud. Get that boundary right and your device feels alive even on flaky hotel Wi-Fi.&lt;/p&gt;

&lt;p&gt;Right now, though, our pet publishes its little heart out into the void. Nobody listens. Its stats are hardcoded at 80. It is, developmentally speaking, a very cute reflex with no memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next article:&lt;/strong&gt; the brain. DynamoDB remembers, Lambda decides, EventBridge Scheduler makes time pass, and the pet finally gets hungry while you sleep.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lvgl.io/docs/open/main-modules/images/overview" rel="noopener noreferrer"&gt;LVGL images documentation&lt;/a&gt; : formats, &lt;code&gt;lv_image_dsc_t&lt;/code&gt;, and the online converter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://components.espressif.com/components/waveshare/esp32_s3_touch_amoled_1_8" rel="noopener noreferrer"&gt;Waveshare BSP on the ESP Component Registry&lt;/a&gt; : the one-liner display setup&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>esp32</category>
      <category>embedded</category>
      <category>lvgl</category>
      <category>iot</category>
    </item>
  </channel>
</rss>
