<?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>AI Agent Memory: Add Semantic Search Without a Vector Database</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Thu, 13 Aug 2026 01:31:01 +0000</pubDate>
      <link>https://dev.to/aws/do-ai-agents-need-a-vector-database-the-measured-answer-2nf6</link>
      <guid>https://dev.to/aws/do-ai-agents-need-a-vector-database-the-measured-answer-2nf6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📦 Clone and ⭐ &lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws" rel="noopener noreferrer"&gt;stop-ai-agents-losing-memory-sample-for-aws&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent's memory holds the answer. The user asks the question. And retrieval returns nothing.&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;stored:   dietary_notes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vegetarian;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;severe&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shellfish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;allergy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;strictly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
          &lt;span class="s"&gt;crustaceans&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mollusks."&lt;/span&gt;

&lt;span class="na"&gt;asked&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;avoid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;eating&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;go&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dinner&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trip?"&lt;/span&gt;

&lt;span class="na"&gt;keyword scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4 hits, answer found&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;False&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%2Fd0glgw5jd8kt36yo2y3u.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%2Fd0glgw5jd8kt36yo2y3u.png" alt="Cartoon: a robot librarian fails to match a semantic question with keyword scan, then retrieves the answer instantly with a vector embedding magnet: keyword scan fails, semantic search finds it" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a real run, not a thought experiment. The question names no key and shares no words with the stored note, so the key-value memory from the &lt;a href="https://dev.to/elizabethfuentes12/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-1n94"&gt;previous post&lt;/a&gt; never finds it. The answer was in the store the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the dividing line for semantic search: do you know the key, or only the intent?&lt;/strong&gt; When questions stop matching keys, you retrieve by &lt;em&gt;meaning&lt;/em&gt;: embed each memory once, embed the question, return the nearest neighbors by cosine similarity. This post measures two things (whether semantic search finds what keyword search misses, and which vector store fits your deployment) using the same embeddings and the same memories in 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;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Post 2 of a series; the &lt;a href="https://dev.to/aws/ai-agent-memory-types-your-agent-forgets-everything-fix-it-pcc"&gt;intro&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;
  
  
  Why does key-value memory miss the question?
&lt;/h2&gt;

&lt;p&gt;Because a key-value read is a lookup someone designed in advance, and this question maps to no key. The demo stores 10 memories about a traveler (profile facts, notes, episodes) and asks the dinner question against three stores. The key-value store has exactly two moves, and both fail honestly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyword scan&lt;/strong&gt;: match question words against keys and values. It returns 4 hits, none of them the allergy note, because "avoid eating at dinner" shares no words with &lt;code&gt;dietary_notes&lt;/code&gt; or "shellfish". Answer found: &lt;strong&gt;False&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump-all fallback&lt;/strong&gt;: give the model the entire memory and let it read. It works, at a price that grows with every memory you add. For these 10 memories that's 647 characters per question; for hundreds of notes it's thousands of tokens, every single question, forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqiqfsyl9go0qfmceoacz.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%2Fqiqfsyl9go0qfmceoacz.png" alt="One question hitting agent memory two ways: the keyword scan misses because no words match, vector similarity finds the allergy note by meaning" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't a bug in key-value memory. Profile lookups ("what's my preferred cabin?") stay exact, instant, and free of embedding costs, which is why the previous post built them that way. The limit only appears when the &lt;em&gt;question&lt;/em&gt; is semantic. That's the signal to add a second way in, not to replace the first.&lt;/p&gt;


&lt;h2&gt;
  
  
  How does semantic search find it?
&lt;/h2&gt;

&lt;p&gt;By comparing meanings instead of words. Every memory is embedded once at write time into a vector (here: &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Titan Text Embeddings V2&lt;/a&gt;, 1,024 dimensions). At query time, the question is embedded and the store returns the nearest neighbors by cosine similarity:&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;top hit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vegetarian;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;severe&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;shellfish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;allergy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;strictly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;crustaceans&lt;/span&gt;
          &lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mollusks."&lt;/span&gt;  &lt;span class="s"&gt;(score 0.231)&lt;/span&gt;
&lt;span class="na"&gt;answer found&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No shared words between question and note. They're close in &lt;em&gt;meaning&lt;/em&gt;, and meaning is what got indexed. Both backends below return this same top hit, because they use the same embeddings; what differs is everything around the query.&lt;/p&gt;


&lt;h2&gt;
  
  
  FAISS or Amazon S3 Vectors? Same accuracy, different deployment
&lt;/h2&gt;

&lt;p&gt;Both are embedding vector stores. They use the same model (Titan V2), the same algorithm (cosine similarity), and they return the same top hit with the same score. &lt;strong&gt;The accuracy is identical&lt;/strong&gt; this is not a quality trade-off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Finds the answer&lt;/th&gt;
&lt;th&gt;Similarity score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Key-value (keyword scan)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;— keyword miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt; — Facebook AI Similarity Search, Meta's in-process vector index&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.231&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3 Vectors&lt;/a&gt; (managed cloud)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.231&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference is deployment. FAISS is an in-process library — zero infrastructure, a pip install, runs local to the process. It provides disk persistence via &lt;code&gt;faiss.write_index&lt;/code&gt; / &lt;code&gt;faiss.read_index&lt;/code&gt;. In this demo the index is not persisted and is rebuilt from scratch each run. S3 Vectors is a managed AWS service: the index lives in a cloud bucket, reachable from any process with AWS credentials, with no cluster to run or scale.&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%2Fon7q1b1lv26m84dboom0.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%2Fon7q1b1lv26m84dboom0.png" alt="Semantic search flow: embedding the question takes ~510 ms for both backends, then FAISS queries in 0.09 ms (in-process, index rebuilt each run in this demo) and S3 Vectors in 195 ms (cloud index, always available)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo uses the same AWS credentials for both: Titan embeddings via &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Bedrock&lt;/a&gt; and S3 Vectors via boto3; the same &lt;code&gt;aws configure&lt;/code&gt; setup powers both, which is why this requires no extra setup inside a &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; workflow. The demo self-provisions the bucket and index on first run: &lt;code&gt;create_vector_bucket&lt;/code&gt; → &lt;code&gt;create_index&lt;/code&gt; (1,024 dims, cosine) → &lt;code&gt;put_vectors&lt;/code&gt; / &lt;code&gt;query_vectors&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost both backends share: embedding the question costs ~510 ms with Titan V2.&lt;/strong&gt; Index query time (0.09 ms for FAISS, 195 ms for S3 Vectors) is secondary to that. Plan for the embedding call in any latency-sensitive path, regardless of which vector store you pick.&lt;/p&gt;


&lt;h2&gt;
  
  
  So, do you need a vector database?
&lt;/h2&gt;

&lt;p&gt;It depends on the query pattern. AWS &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;positions S3 Vectors&lt;/a&gt; as "ideal for workloads where queries are less frequent", which describes agent memory exactly: an agent queries a user's memories a handful of times per conversation, not thousands of times per second.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;FAISS&lt;/th&gt;
&lt;th&gt;Amazon S3 Vectors&lt;/th&gt;
&lt;th&gt;Dedicated vector database&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-process library&lt;/td&gt;
&lt;td&gt;AWS vector storage&lt;/td&gt;
&lt;td&gt;Full database engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/opensearch-service/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;OpenSearch&lt;/a&gt;, &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;, &lt;a href="https://weaviate.io/" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;, &lt;a href="https://milvus.io/" rel="noopener noreferrer"&gt;Milvus&lt;/a&gt;, &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;, &lt;a href="https://www.trychroma.com/" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ same&lt;/td&gt;
&lt;td&gt;✅ same&lt;/td&gt;
&lt;td&gt;✅ same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None — pip install&lt;/td&gt;
&lt;td&gt;None — fully managed&lt;/td&gt;
&lt;td&gt;Self-hosted or managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max vectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Process memory&lt;/td&gt;
&lt;td&gt;Up to 2 billion per index&lt;/td&gt;
&lt;td&gt;Depends on deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0.09 ms&lt;/td&gt;
&lt;td&gt;~100–200 ms&lt;/td&gt;
&lt;td&gt;Sub-10 ms at high QPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hybrid search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ most support it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prototype / local agent&lt;/td&gt;
&lt;td&gt;Cloud agent, infrequent queries&lt;/td&gt;
&lt;td&gt;High QPS, advanced filtering, production search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decision:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Facts under known keys (profile, preferences)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Key-value&lt;/strong&gt; (&lt;a href="https://dev.to/elizabethfuentes12/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-1n94"&gt;post 1&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Exact and instant; don't pay ~510 ms of embedding for a lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic search, local / prototype&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero infrastructure, pip install, in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic search, cloud / infrequent queries&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;S3 Vectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Purpose-built AWS vector storage, subsecond latency, up to 2 billion vectors, no infrastructure to manage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High QPS, hybrid search, or advanced filtering&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dedicated vector DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/opensearch-service/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;OpenSearch&lt;/a&gt;, &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;, &lt;a href="https://weaviate.io/" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;, &lt;a href="https://milvus.io/" rel="noopener noreferrer"&gt;Milvus&lt;/a&gt;, &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;, &lt;a href="https://www.trychroma.com/" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-hop questions over relationships&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Graph&lt;/strong&gt; (next post)&lt;/td&gt;
&lt;td&gt;Semantic search finds pieces; it can't follow edges between them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this demo does not cover:&lt;/strong&gt; FAISS and S3 Vectors are storage backends. They store vectors and retrieve by similarity. Building what to remember (extracting specific facts from conversations, deduplication, structured memory across sessions) is handled by managed memory services like &lt;a href="https://aws.amazon.com/bedrock/agentcore/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Memory&lt;/a&gt;. That technique is the topic of a future post in this series.&lt;/p&gt;


&lt;h2&gt;
  
  
  How does the agent choose between key lookup and semantic search?
&lt;/h2&gt;

&lt;p&gt;From the tool docstrings, on its own. The demo's last test attaches both recall tools to one Strands agent:&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_by_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recall a memory when the question maps to a known identifier.
    Use when the user asks about a stored field: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my preferred cabin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my home airport&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&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;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;Recall memories by meaning when no key is obvious.
    Use for open questions: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what should I avoid eating on this trip?&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Asked the dinner question, the agent calls &lt;code&gt;recall_semantic&lt;/code&gt;; asked "what cabin do I prefer?", it calls &lt;code&gt;recall_by_key&lt;/code&gt;. No routing logic, no prompt engineering. The &lt;em&gt;when to use this&lt;/em&gt; sentence at the top of each docstring is what the model reads to decide. Write that sentence carelessly and the agent pays embedding latency for profile lookups.&lt;/p&gt;


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

&lt;p&gt;The quality of the semantic search implementation your assistant builds depends on the decisions you name in the prompt. Unnamed, it will default to embedding everything and querying one big index. These five instructions encode what this post measured:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Add semantic search only for questions that don't map to keys; keep profile facts in key-value state."&lt;/strong&gt; Otherwise the assistant defaults to embedding every query, including exact lookups that already have a known key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Embed each memory once, at write time; only the question gets embedded at query time."&lt;/strong&gt; Assistants love re-embedding the whole store per query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Use one embedding function for storage and queries, and state the model and dimensions."&lt;/strong&gt; Mixed embedders produce silent garbage similarity scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Give me two recall tools with 'when to use' docstrings: by key, and by meaning."&lt;/strong&gt; The agent routes per question from those sentences; no router code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Make persistence explicit: in-process index for a prototype, managed vector storage for anything that must survive a restart, and prove it with a fresh-client test that still sees every vector."&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The companion repo implements and measures all five. Run it to see each decision play out.&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/02-vector-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_vector_memory.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Needs AWS credentials (&lt;code&gt;aws configure&lt;/code&gt;) for Titan embeddings and S3 Vectors. &lt;strong&gt;The demo creates the vector bucket and index automatically if they don't exist.&lt;/strong&gt; &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; is only needed for the agent conversation in the notebook (or swap one line for Amazon Bedrock); the retrieval measurements run without any LLM.&lt;/p&gt;


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

&lt;p&gt;&lt;strong&gt;Is a vector database the same as AI agent memory?&lt;/strong&gt;&lt;br&gt;
No. A vector database is one possible backend for one memory type (retrieval by meaning). Agent memory is the whole system: key-value state, vector or graph storage, selection rules, and hygiene. Many production agents need vector &lt;em&gt;retrieval&lt;/em&gt; without a vector &lt;em&gt;database&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use a vector database as agent memory?&lt;/strong&gt;&lt;br&gt;
Yes, for memories you'll query by meaning. But route keyed facts (preferences, settings) to key-value storage first: a direct lookup costs nothing, while every vector query pays the question-embedding call (~510 ms with Titan V2) before the index is even touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When do I need something beyond S3 Vectors?&lt;/strong&gt;&lt;br&gt;
When your query pattern changes. Dedicated vector databases such as &lt;a href="https://aws.amazon.com/opensearch-service/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;OpenSearch&lt;/a&gt;, &lt;a href="https://qdrant.tech/" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;, &lt;a href="https://weaviate.io/" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;, &lt;a href="https://milvus.io/" rel="noopener noreferrer"&gt;Milvus&lt;/a&gt;, &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;, and &lt;a href="https://www.trychroma.com/" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt; are built for high QPS, hybrid keyword+vector search, aggregations, and advanced filtering. S3 Vectors is purpose-built for infrequent queries: it handles up to 2 billion vectors per index with subsecond latency, which covers agent memory workloads well past prototype scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the real latency difference between FAISS and S3 Vectors?&lt;/strong&gt;&lt;br&gt;
Measured on the same memories: 0.09 ms vs 195 ms per query. But embedding the question adds ~510 ms to both, so end-to-end it's ~0.5 s vs ~0.7 s. The index is rarely your bottleneck; the embedding call is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my semantic search return the wrong memories?&lt;/strong&gt;&lt;br&gt;
The most common causes: the store and the queries use different embedding models or dimensions, memories were embedded with stale text, or keyed facts polluted the index. Keep one embedder for everything, embed at write time, and keep profile facts out of the vector store.&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 02&lt;/a&gt; with the measured tests and notebook&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3 Vectors — User Guide&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-limitations.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;limitations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Titan Text Embeddings V2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt;, Meta's similarity search library&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2501.13956" rel="noopener noreferrer"&gt;Zep: A Temporal Knowledge Graph Architecture for Agent Memory&lt;/a&gt;, Rasmussen et al., 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2502.14802" rel="noopener noreferrer"&gt;From RAG to Memory: Non-Parametric Continual Learning for LLMs (HippoRAG 2)&lt;/a&gt;, 2025&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;Gracias!&lt;/p&gt;

&lt;p&gt;🇻🇪 &lt;a href="https://dev.to/elizabethfuentes12"&gt;Dev.to&lt;/a&gt; &lt;a href="https://www.linkedin.com/in/lizfue/" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt; &lt;a href="https://github.com/elizabethfuentes12/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; &lt;a href="https://twitter.com/elizabethfue12" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; &lt;a href="https://www.instagram.com/elifue.tech" rel="noopener noreferrer"&gt;Instagram&lt;/a&gt; &lt;a href="https://www.youtube.com/channel/UCr0Gnc-t30m4xyrvsQpNp2Q" rel="noopener noreferrer"&gt;Youtube&lt;/a&gt;&lt;/p&gt;




&lt;div class="ltag__user ltag__user__id__717518"&gt;
    &lt;a href="/elizabethfuentes12" 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%2F717518%2Fb550b165-b8b9-405d-acfb-e5dc846765b0.png" alt="elizabethfuentes12 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="/elizabethfuentes12"&gt;Elizabeth Fuentes L&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/elizabethfuentes12"&gt;I help developers build production-ready AI applications through hands-on tutorials and open-source projects.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Running Kiro Crew in Docker on Windows</title>
      <dc:creator>Matheus Guimaraes</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:12:47 +0000</pubDate>
      <link>https://dev.to/aws/running-kiro-crew-in-docker-on-windows-19j6</link>
      <guid>https://dev.to/aws/running-kiro-crew-in-docker-on-windows-19j6</guid>
      <description>&lt;p&gt;I've been playing with &lt;a href="https://kiro.dev/docs/crew/installation/" rel="noopener noreferrer"&gt;Kiro Crew&lt;/a&gt; recently, and when I went to get it running on my Windows machine I had a decision to make.&lt;/p&gt;

&lt;p&gt;Kiro Crew doesn't currently have a Windows desktop app. You can still run it natively on Windows, but the documented route involves installing and running the Gateway from source.&lt;/p&gt;

&lt;p&gt;But then, while looking through the &lt;a href="https://github.com/kirodotdev/KiroCrew" rel="noopener noreferrer"&gt;Kiro Crew GitHub repository&lt;/a&gt;, I noticed another option that immediately caught my attention: &lt;strong&gt;there's an official Docker image!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker is already a familiar part of the development workflow for many of us, and a container felt like a nice, clean way to get Kiro Crew running without installing it from source.&lt;/p&gt;

&lt;p&gt;So that's the route I took.&lt;/p&gt;

&lt;p&gt;It worked, but along the way I ran into an interesting security detail that made the setup slightly less straightforward than simply running a container.&lt;/p&gt;

&lt;p&gt;And, as it turns out, that's actually a good thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're going to do
&lt;/h2&gt;

&lt;p&gt;By the end of this post, we'll have Kiro Crew running in Docker on Windows, with its agent sandbox enabled and without giving the container more privileges than it actually needs.&lt;/p&gt;

&lt;p&gt;I'm using Docker Desktop with WSL 2, so you'll want to have those installed before following along.&lt;/p&gt;

&lt;p&gt;You can check your WSL installation with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that out of the way, let's get Kiro Crew running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with the official container image
&lt;/h2&gt;

&lt;p&gt;The Kiro Crew team publishes a public container image, so we don't need to build anything ourselves or even authenticate with a container registry. Nice and easy.&lt;/p&gt;

&lt;p&gt;Before doing anything else, I created a directory to keep the files for this setup together:&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;mkdir &lt;/span&gt;kiro-crew
&lt;span class="nb"&gt;cd &lt;/span&gt;kiro-crew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, at first glance, running Kiro Crew looks like it should be as simple as starting the official image.&lt;/p&gt;

&lt;p&gt;And technically, the Gateway itself will run.&lt;/p&gt;

&lt;p&gt;But there's another security boundary inside that container that we need to understand before we can start it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait... a sandbox inside a container?
&lt;/h2&gt;

&lt;p&gt;This was the interesting part for me.&lt;/p&gt;

&lt;p&gt;We're already putting Kiro Crew inside a Docker container, which gives us an isolation boundary between Kiro Crew and our Windows host.&lt;/p&gt;

&lt;p&gt;But Kiro Crew adds another boundary of its own.&lt;/p&gt;

&lt;p&gt;Agent commands aren't simply executed with access to everything available to the main Kiro Crew process. On Linux, Kiro Crew creates an internal user-namespace sandbox for those commands.&lt;/p&gt;

&lt;p&gt;Conceptually, our setup looks something 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;Windows
└── Docker container
    └── Kiro Crew
        └── Agent sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why sandbox an agent that's already inside a container?&lt;/p&gt;

&lt;p&gt;Because the two boundaries are protecting different things.&lt;/p&gt;

&lt;p&gt;The container isolates Kiro Crew from our host. The inner sandbox isolates &lt;strong&gt;agent commands from sensitive state available to Kiro Crew itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, Kiro Crew's sandbox hides credential directories such as &lt;code&gt;~/.aws&lt;/code&gt; and &lt;code&gt;~/.ssh&lt;/code&gt; from agent subprocesses by bind-mounting empty directories over them. So if an agent-executed command tries to access one of those directories, it simply sees an empty directory rather than our AWS credentials or SSH keys.&lt;/p&gt;

&lt;p&gt;That means the Gateway can have the credentials it needs to do its job without automatically making those credentials readable by commands executed by an agent.&lt;/p&gt;

&lt;p&gt;That's a pretty important distinction when we're giving an AI agent the ability to execute commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Docker gets in the way
&lt;/h2&gt;

&lt;p&gt;There's one complication.&lt;/p&gt;

&lt;p&gt;To create that inner sandbox, Kiro Crew needs Linux system calls including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unshare(CLONE_NEWUSER)
unshare(CLONE_NEWNS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker's default seccomp security policy blocks those operations.&lt;/p&gt;

&lt;p&gt;So if the Kiro Crew container can't create its inner sandbox, it doesn't quietly shrug its shoulders and run agent commands without one.&lt;/p&gt;

&lt;p&gt;It &lt;strong&gt;fails closed&lt;/strong&gt;, meaning that when the security mechanism can't be established, access is denied rather than silently falling back to a less secure mode.&lt;/p&gt;

&lt;p&gt;The Gateway and dashboard can still run, but agent command execution remains disabled.&lt;/p&gt;

&lt;p&gt;I actually really like this design! What initially looks like an extra hurdle in getting the container running is Kiro Crew refusing to silently weaken its security model.&lt;/p&gt;

&lt;p&gt;So how do we fix it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting solution and the better solution
&lt;/h2&gt;

&lt;p&gt;There are a few ways we could approach this.&lt;/p&gt;

&lt;p&gt;One option is to simply tell Kiro Crew that we accept running agent commands without the inner sandbox. Kiro Crew provides an environment variable for exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KIROCREW_ALLOW_UNSANDBOXED=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could pass that into our container and Kiro Crew would allow agent execution, leaving Docker itself as our only isolation boundary.&lt;/p&gt;

&lt;p&gt;We could also reach for the rather large hammer:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;That would make the sandbox work, but it does so by granting the container extremely broad privileges and removing many of Docker's normal isolation restrictions.&lt;/p&gt;

&lt;p&gt;That's a much bigger security concession than we actually need.&lt;/p&gt;

&lt;p&gt;What we really want is much narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allow the operations Kiro Crew needs to create its sandbox while keeping the rest of Docker's security restrictions in place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And this is another nice detail about the Kiro Crew implementation: the team already provides a seccomp profile specifically for this!&lt;/p&gt;

&lt;p&gt;So we don't have to create one ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downloading the Kiro Crew seccomp profile
&lt;/h2&gt;

&lt;p&gt;From the &lt;code&gt;kiro-crew&lt;/code&gt; directory we created earlier, download the &lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docker/seccomp/kirocrew-seccomp.json" rel="noopener noreferrer"&gt;official Kiro Crew seccomp profile&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kirodotdev/KiroCrew/main/docker/seccomp/kirocrew-seccomp.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; kirocrew-seccomp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kiro-crew/
└── kirocrew-seccomp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Having trouble downloading it from WSL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I happened to be dealing with a DNS issue in my WSL environment when I did this, so &lt;code&gt;curl&lt;/code&gt; couldn't resolve GitHub. If you happen to run into the same thing, there's nothing special about downloading the file through &lt;code&gt;curl&lt;/code&gt;: you can download &lt;code&gt;kirocrew-seccomp.json&lt;/code&gt; directly from the Kiro Crew GitHub repository in your browser and place it in this directory manually, which is what I did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we can start the container using that profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Kiro Crew
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; kirocrew &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:5476:5476 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; kirocrew-home:/home/kirocrew &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;seccomp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kirocrew-seccomp.json &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/kirodotdev/kirocrew:stable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a couple of things worth noticing here.&lt;/p&gt;

&lt;p&gt;We're only publishing port &lt;code&gt;5476&lt;/code&gt; on the host's loopback interface, so the dashboard isn't being exposed to the rest of our network.&lt;/p&gt;

&lt;p&gt;We're also creating a named Docker volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kirocrew-home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiro Crew keeps its persistent state under &lt;code&gt;/home/kirocrew&lt;/code&gt;, including its configuration and Kiro CLI credentials. That means our state can survive replacing or upgrading the container.&lt;/p&gt;

&lt;p&gt;And, most importantly for what we've just discussed, we're passing the Kiro Crew seccomp profile with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--security-opt seccomp=kirocrew-seccomp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container can now create the inner namespace sandbox while Docker's other default security restrictions remain in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's open it
&lt;/h2&gt;

&lt;p&gt;At this point I did what I imagine most developers would do.&lt;/p&gt;

&lt;p&gt;I opened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5476
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And instead of the Kiro Crew dashboard, I was greeted by a message telling me I was missing a token.&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%2Fibs1dbiy2gnkertdm7o2.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%2Fibs1dbiy2gnkertdm7o2.png" alt="Kiro Crew dashboard showing that an access token is required" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another security measure!&lt;/p&gt;

&lt;p&gt;Simply being able to reach port &lt;code&gt;5476&lt;/code&gt; isn't enough to get access to the dashboard. Kiro Crew also requires a bearer token, so someone who can reach the Gateway still needs to authenticate before they can use it.&lt;/p&gt;

&lt;p&gt;So, there's one more part of the setup we need to do.&lt;/p&gt;

&lt;p&gt;Actually, two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating Kiro CLI
&lt;/h2&gt;

&lt;p&gt;Kiro Crew's agent runtime uses &lt;code&gt;kiro-cli&lt;/code&gt;, so first we need to authenticate the CLI running inside our container.&lt;/p&gt;

&lt;p&gt;We can do that without opening an interactive shell ourselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; kirocrew kiro-cli login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the authentication flow and log in with the identity you use for Kiro.&lt;/p&gt;

&lt;p&gt;Once authentication succeeds, those credentials are stored in the &lt;code&gt;kirocrew-home&lt;/code&gt; volume we created earlier, so they survive container upgrades.&lt;/p&gt;

&lt;p&gt;But that authenticates the agent runtime.&lt;/p&gt;

&lt;p&gt;We still need access to the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting our dashboard token
&lt;/h2&gt;

&lt;p&gt;Every request to the Kiro Crew dashboard requires a token.&lt;/p&gt;

&lt;p&gt;We can mint a temporary login link from the running container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;kirocrew kirocrew token &lt;span class="nt"&gt;--ttl&lt;/span&gt; 2h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiro Crew will print a URL containing the token.&lt;/p&gt;

&lt;p&gt;It will look roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5476/?token=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that URL in your browser and...&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%2Fugtlxga5vgxv3hj4i2f8.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%2Fugtlxga5vgxv3hj4i2f8.png" alt="Kiro Crew running successfully in Docker on Windows" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're in!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And with that, we have Kiro Crew running nicely inside Docker on Windows, without having to build it from source and without unnecessarily weakening either of its isolation boundaries.&lt;/p&gt;

&lt;p&gt;Now I just need someone to explain why the Solarized theme is green instead of yellow. 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/crew/installation/" rel="noopener noreferrer"&gt;Kiro Crew installation documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kirodotdev/KiroCrew" rel="noopener noreferrer"&gt;Kiro Crew GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/guides/docker.md" rel="noopener noreferrer"&gt;Kiro Crew Docker guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>7 Tips to Make Your AI Agent More Predictable</title>
      <dc:creator>Salih Guler </dc:creator>
      <pubDate>Tue, 11 Aug 2026 10:20:35 +0000</pubDate>
      <link>https://dev.to/aws/7-tips-to-make-your-ai-agent-more-predictable-1ga4</link>
      <guid>https://dev.to/aws/7-tips-to-make-your-ai-agent-more-predictable-1ga4</guid>
      <description>&lt;p&gt;After months of building with AI coding tools, I found the difference between generated code that works and generated code that ships comes down to how you communicate with the AI. I have been sharing these lessons in a talk called "It's Dangerous to Code Alone! Take This: Developer's AI Survival Guide" and people keep asking me to write them down. How big is the gap? An &lt;a href="https://www.forbes.com/sites/josipamajic/2026/06/10/ai-coding-agents-write-180-more-code-but-ship-only-30-more-software/" rel="noopener noreferrer"&gt;MIT study across 100,000+ developers&lt;/a&gt; found that AI agents boosted code &lt;em&gt;written&lt;/em&gt; by ~180%, while code that actually &lt;em&gt;shipped to production&lt;/em&gt; rose by only ~30%.&lt;/p&gt;

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

&lt;p&gt;To demonstrate these tips, I built a link-sharing platform so my teammates can share resources without juggling multiple QR codes. I generated the frontend with Codex GPT 5.6 Sol and Figma MCP, and I am adding an AWS Blocks backend to swap out local mocks with real cloud infrastructure. You can find all the prompts in &lt;a href="https://github.com/salihgueler/some-useful-links" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All of these tips are applicable to greenfield projects as well. I can't promise you it is going to have the 42 effect :)&lt;/p&gt;

&lt;h2&gt;
  
  
  #1 Talk to AI in Clear Prompts
&lt;/h2&gt;

&lt;p&gt;Each model reacts to prompts differently. The clearer you get, the faster you achieve your goal.&lt;/p&gt;

&lt;p&gt;You also need to remember that now we don't only have models, we also have effort levels. If you are not mindful about which model you are running with which effort level, you will have a hard time getting the results you want.&lt;/p&gt;

&lt;p&gt;Here are some general rules I follow (you can check the &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt; and the AWS Blocks skill in the repo for the full picture):&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero-Shot
&lt;/h3&gt;

&lt;p&gt;Describe what you want. Works for simple, well-defined tasks.&lt;/p&gt;

&lt;p&gt;Here is my frontend build prompt. One sentence, clear outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Build "Some Useful Links" — a LinkTree-style web application using
Next.js with SSR enabled. The complete visual design specification
is in &lt;span class="sb"&gt;`DESIGN_SPEC.md`&lt;/span&gt; and reference screenshots are in the
&lt;span class="sb"&gt;`design-previews/`&lt;/span&gt; folder. Implement the design pixel-perfectly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for the backend migration to AWS Blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Replace the local JSON mock services in src/lib/services/local/ with
AWS Blocks implementations. The service registry (index.ts) is the
only file that should change in the existing codebase. Frontend must
not be modified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are zero-shot: one clear task, no ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Few-Shot
&lt;/h3&gt;

&lt;p&gt;Show examples. Input, output. Input, output. The model picks up the shape. Research shows the format matters more than whether the examples are perfectly correct.&lt;/p&gt;

&lt;p&gt;In my &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt;, I use this for the service registry pattern. I show the AI what an implementation swap looks like:&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="c1"&gt;// src/lib/services/index.ts&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;LocalAnalyticsStore&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;./local/analytics-store.local&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;LocalLinkStore&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;./local/link-store.local&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;LocalVisitTracker&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;./local/visit-tracker.local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// SWAP POINT: Replace these with cloud implementations&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyticsStore&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;LocalAnalyticsStore&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;const&lt;/span&gt; &lt;span class="nx"&gt;linkStore&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;LocalLinkStore&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;const&lt;/span&gt; &lt;span class="nx"&gt;visitTracker&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;LocalVisitTracker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ask the AI to build the AWS Blocks backend, it sees this pattern and knows the target: create a &lt;code&gt;BlocksAnalyticsStore&lt;/code&gt;, &lt;code&gt;BlocksLinkStore&lt;/code&gt;, and &lt;code&gt;BlocksVisitTracker&lt;/code&gt; that implement the same interfaces, then swap them in &lt;code&gt;index.ts&lt;/code&gt;. I do not need to explain the concept of dependency injection. The example IS the explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chain-of-Thought
&lt;/h3&gt;

&lt;p&gt;Force the model to reason step by step before acting. For debugging, architecture decisions, or anything multi-step, this cuts logical errors significantly.&lt;/p&gt;

&lt;p&gt;I use this when asking the AI to plan the AWS Blocks migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Before writing any code, analyze the existing service interfaces in
src/lib/services/interfaces/. For each interface method, determine:
&lt;span class="p"&gt;1.&lt;/span&gt; Which AWS Blocks building block maps to it (DistributedTable, KVStore, FileBucket, etc.)
&lt;span class="p"&gt;2.&lt;/span&gt; What the key schema should be to support the query patterns
&lt;span class="p"&gt;3.&lt;/span&gt; Whether the method needs authentication (check if the frontend
   calls it from an admin route or a public route)

Write your analysis as a numbered plan. I will review it before you
start implementing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI produces a plan I can review before it writes a single line of code. Without this step, it would just start building and often pick the wrong storage pattern for a given query.&lt;/p&gt;

&lt;h2&gt;
  
  
  #2 Set Absolute Boundaries
&lt;/h2&gt;

&lt;p&gt;Use words like &lt;strong&gt;MUST&lt;/strong&gt;, &lt;strong&gt;NEVER&lt;/strong&gt;, &lt;strong&gt;ALWAYS&lt;/strong&gt;, and &lt;strong&gt;STRICTLY FORBIDDEN&lt;/strong&gt;. Avoid weak phrasing like "Please try to," "It is preferred," or "Usually we do."&lt;/p&gt;

&lt;p&gt;Here is a comparison from my project. The frontend &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt; sets boundaries like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;All backend infrastructure must be local mocks — no cloud dependencies.
The mocks must be documented clearly enough that another AI agent or
developer can swap them for any cloud provider without restructuring.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the AWS Blocks skill sets boundaries for the backend side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Backend lives in &lt;span class="sb"&gt;`aws-blocks/index.ts`&lt;/span&gt;. Frontend imports from
'aws-blocks' (workspace package). The &lt;span class="sb"&gt;`client.js`&lt;/span&gt; is auto-generated
— never edit it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are direct, absolute, and leave no room for interpretation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prioritize negative boundaries (Guardrails)
&lt;/h3&gt;

&lt;p&gt;Telling the AI what NOT to do is often more effective than listing everything it should do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend (&lt;code&gt;AGENTS.md&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Do not add cloud SDKs, deployment configuration, external persistence,
  or source-controlled credentials unless the task explicitly requires them.
&lt;span class="p"&gt;-&lt;/span&gt; Import services from &lt;span class="sb"&gt;`@/lib/services`&lt;/span&gt;; components and route handlers
  must not import &lt;span class="sb"&gt;`src/lib/services/local/*`&lt;/span&gt; directly.
&lt;span class="p"&gt;-&lt;/span&gt; Keep unrelated refactors and generated-file churn out of focused changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backend (AWS Blocks skill):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Adding to an existing project? Scaffold into a temp dir, copy only
  &lt;span class="sb"&gt;`aws-blocks/`&lt;/span&gt; folder, then manually merge workspace config, scripts,
  and dependencies. The scaffolder overwrites root package.json,
  tsconfig.json, vite.config.ts, .gitignore.
&lt;span class="p"&gt;-&lt;/span&gt; Never edit index.cdk.ts, index.handler.ts, or client.js — these are
  auto-generated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  #3 Give Your AI Persistent Context
&lt;/h2&gt;

&lt;p&gt;You need to steer the agent in the correct direction. AI tools have dedicated files for this purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kiro steering files (&lt;code&gt;.kiro/steering/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Kiro skills (&lt;code&gt;.kiro/skills/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these are loaded every session (steering files), giving the AI persistent rules. Others are loaded on demand (skills), giving the AI specialized knowledge only when it needs it. Both keep your context window lean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always-loaded: Steering files
&lt;/h3&gt;

&lt;p&gt;For the frontend, I have an &lt;code&gt;AGENTS.md&lt;/code&gt; at the root that covers the full Next.js application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# AGENTS.md&lt;/span&gt;

&lt;span class="gu"&gt;## Project Overview&lt;/span&gt;
Some Useful Links is a local-first, multi-page link-sharing application
built with Next.js App Router, React, strict TypeScript, and Tailwind CSS.

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
npm install
npm run dev
npm run validate:links
npm run lint
npm run build

&lt;span class="gu"&gt;## Architecture&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; src/app: App Router pages, layouts, route handlers
&lt;span class="p"&gt;-&lt;/span&gt; src/components: UI grouped by admin, analytics, layout, links, share
&lt;span class="p"&gt;-&lt;/span&gt; src/lib/services/interfaces: Backend-neutral service contracts
&lt;span class="p"&gt;-&lt;/span&gt; src/lib/services/local: Local JSON-backed implementations
&lt;span class="p"&gt;-&lt;/span&gt; src/lib/services/index.ts: The only provider registration and swap point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also have two Kiro steering files that enforce cross-cutting rules regardless of the task:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript rules&lt;/strong&gt; (&lt;code&gt;.kiro/steering/typescript.md&lt;/code&gt;) — enforces strict typing and build validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# TypeScript Project Instructions&lt;/span&gt;

&lt;span class="gu"&gt;## Workflows &amp;amp; Validation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Pre-completion Check: Before completing any task or reporting success,
  you MUST run &lt;span class="sb"&gt;`npm run build`&lt;/span&gt; in the terminal.
&lt;span class="p"&gt;-&lt;/span&gt; Do not consider a task finished if the build command returns errors. Fix
  the errors first.

&lt;span class="gu"&gt;## Coding Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Strict typing is enforced. You are strictly forbidden from using the
  &lt;span class="sb"&gt;`any`&lt;/span&gt; type.
&lt;span class="p"&gt;-&lt;/span&gt; Always define and apply the exact, correct types and interfaces for all
  variables, function parameters, and return values.
&lt;span class="p"&gt;-&lt;/span&gt; You are STRICTLY FORBIDDEN from using @ts-ignore.
  If unavoidable, use @ts-expect-error with a detailed comment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent behavior rules&lt;/strong&gt; (&lt;code&gt;.kiro/steering/agent.md&lt;/code&gt;) — controls what the AI can and cannot do on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Agent Behavior Rules&lt;/span&gt;

&lt;span class="gu"&gt;## File and Folder Boundaries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; DO NOT create any new Markdown files unless explicitly instructed by
  the user.
&lt;span class="p"&gt;-&lt;/span&gt; STRICTLY FORBIDDEN to auto-generate changelogs or documentation.
&lt;span class="p"&gt;-&lt;/span&gt; You MUST update the existing README.md if your changes alter the
  project's public API or architecture.

&lt;span class="gu"&gt;## Technology Boundaries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; You MUST ALWAYS use the Strands Agents library with TypeScript for any
  agent development.
&lt;span class="p"&gt;-&lt;/span&gt; You are STRICTLY REQUIRED to use Claude Haiku 4.5 from Amazon Bedrock
  for all agent models.
&lt;span class="p"&gt;-&lt;/span&gt; You MUST ALWAYS use React and Vite with TypeScript for web development.
&lt;span class="p"&gt;-&lt;/span&gt; NEVER write or generate unit tests unless the user explicitly commands
  it.

&lt;span class="gu"&gt;## Security Boundaries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; NEVER commit or hardcode sensitive information (client secrets, API
  keys, client IDs, resource IDs).

&lt;span class="gu"&gt;## Git Boundaries&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Commits MUST stay under 150 lines of source code.
&lt;span class="p"&gt;-&lt;/span&gt; Every commit: single-sentence summary, blank line, detailed explanation
  (max 20 lines).
&lt;span class="p"&gt;-&lt;/span&gt; You MUST append &lt;span class="sb"&gt;`(Kiro)`&lt;/span&gt; to the author name using:
  git commit --author="[Git Username] (Kiro) &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="na"&gt;User&lt;/span&gt; &lt;span class="na"&gt;Email&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These prevent the common annoyances: AI generating unwanted test files, committing giant diffs, sneaking &lt;code&gt;any&lt;/code&gt; types past the compiler, or littering the repo with markdown files nobody asked for.&lt;/p&gt;

&lt;h3&gt;
  
  
  On-demand: Skills
&lt;/h3&gt;

&lt;p&gt;Steering files are always loaded. But what about capabilities that are only needed sometimes? You do not want to load everything upfront because that wastes context.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Skill&lt;/strong&gt; is a reusable, discoverable capability. The AI loads it only when it becomes relevant to the current task. The most important part of a Skill is the name and description. That is how the AI decides whether to use it.&lt;/p&gt;

&lt;p&gt;My AWS Blocks skill activates with this frontmatter:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;building-aws-blocks-apps&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Builds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fullstack&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TypeScript&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;applications&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using"&lt;/span&gt;
  &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="s"&gt;aws-blocks/blocks. Use when working with any Building Block&lt;/span&gt;
  &lt;span class="s"&gt;(KVStore, DistributedTable, Agent, AuthBasic...), ApiNamespace,&lt;/span&gt;
  &lt;span class="s"&gt;BlocksStack, or the create-blocks-app CLI.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main &lt;code&gt;SKILL.md&lt;/code&gt; is the overview: decision guides, project structure, quick start. Detailed reference lives in separate files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.kiro/skills/aws-blocks-development/
├── SKILL.md                    # Overview + decision guide (under 200 lines)
├── CORE-ARCHITECTURE.md        # Scope, ApiNamespace, JSON-RPC, CORS
├── TROUBLESHOOTING.md          # Common errors and fixes
└── blocks/
    ├── auth-basic.md           # AuthBasic patterns
    ├── distributed-table.md    # DistributedTable patterns
    ├── api-namespace.md        # ApiNamespace deep dive
    └── ... (20+ block files)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the AI needs to implement authentication, it loads &lt;code&gt;blocks/auth-basic.md&lt;/code&gt;. When it needs to set up a database, it loads &lt;code&gt;blocks/distributed-table.md&lt;/code&gt;. It does not carry all 122 KB of reference material in every conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep it lean
&lt;/h3&gt;

&lt;p&gt;Keep the root file &lt;strong&gt;under 200 lines&lt;/strong&gt;. My &lt;code&gt;AGENTS.md&lt;/code&gt; is 127 lines. The AWS Blocks &lt;code&gt;SKILL.md&lt;/code&gt; is the overview (under 200 lines), with detailed reference files loaded on demand.&lt;/p&gt;

&lt;p&gt;Use the "Router Pattern": a root file that points to detailed references when needed. The AWS Blocks skill does exactly this with its block reference table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate context from instructions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Project Context&lt;/strong&gt; tells the AI where it is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is a Next.js 14 App Router project using Tailwind CSS."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Actionable Rules&lt;/strong&gt; tell the AI what to do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Import services from &lt;code&gt;@/lib/services&lt;/code&gt;; components must not import &lt;code&gt;src/lib/services/local/*&lt;/code&gt; directly."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep these separate. Context helps the AI orient itself. Rules constrain its behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  #4 Manage Your Context Window
&lt;/h2&gt;

&lt;p&gt;Your AI sees everything in a stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System instructions + steering files&lt;/li&gt;
&lt;li&gt;Full conversation history (every message)&lt;/li&gt;
&lt;li&gt;Tool output (files, terminal, search results)&lt;/li&gt;
&lt;li&gt;Your current request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Performance degrades at 25% capacity, not 100%.&lt;/strong&gt; You do not have the full context window available. The degradation starts much earlier than you think.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context rot
&lt;/h3&gt;

&lt;p&gt;Long sessions lead to the model "forgetting" earlier decisions, fixing one thing and breaking two others. Hallucinations increase as context fills up and your original constraints stop being followed.&lt;/p&gt;

&lt;p&gt;I have seen this firsthand. On the frontend side, I asked my AI to follow the service registry pattern from my &lt;code&gt;AGENTS.md&lt;/code&gt;. After 15 turns of unrelated work, it started importing directly from &lt;code&gt;src/lib/services/local/&lt;/code&gt;, exactly what I told it not to do.&lt;/p&gt;

&lt;p&gt;On the backend side, I had a session where I was building multiple API methods with AWS Blocks. After building the analytics endpoints, I asked it to add authentication. It generated a whole custom auth system instead of using the &lt;code&gt;AuthBasic&lt;/code&gt; block that was in the skill file. The context was too full for it to reference back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fighting context rot
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Start &lt;strong&gt;fresh sessions&lt;/strong&gt; for each new task&lt;/li&gt;
&lt;li&gt;Keep steering files under 200 lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarize progress&lt;/strong&gt; in a file the AI can re-read, do not rely on conversation history&lt;/li&gt;
&lt;li&gt;Keep interactions to &lt;strong&gt;5–7 turns per task&lt;/strong&gt; max&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  #5 Follow a Spec-Driven Development Process
&lt;/h2&gt;

&lt;p&gt;Vibe coding skips everything we know about building software: planning, analysis, design, testing, maintenance. All of it gone.&lt;/p&gt;

&lt;p&gt;Vibe coding works for prototyping and tiny fixes. But for anything beyond that, you need structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-Driven Development (SDD)&lt;/strong&gt; is a methodology where detailed, unambiguous requirements are written and agreed upon before any actual coding begins. The spec is the contract between you and your AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend spec (from &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;task&amp;gt;&lt;/span&gt;
Build "Some Useful Links" — a LinkTree-style web application using Next.js
with SSR enabled. The complete visual design specification is in
&lt;span class="sb"&gt;`DESIGN_SPEC.md`&lt;/span&gt; and reference screenshots are in the &lt;span class="sb"&gt;`design-previews/`&lt;/span&gt;
folder.

All backend infrastructure must be local mocks — no cloud dependencies. The
mocks must be documented clearly enough that another AI agent or developer
can swap them for any cloud provider without restructuring.
&lt;span class="nt"&gt;&amp;lt;/task&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;architecture&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Framework: Next.js (App Router) with Server-Side Rendering enabled
&lt;span class="p"&gt;2.&lt;/span&gt; Styling: Tailwind CSS with a custom theme from the design spec
&lt;span class="p"&gt;3.&lt;/span&gt; Backend: Local mocks only — no cloud services, no external APIs
&lt;span class="nt"&gt;&amp;lt;/architecture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backend spec (from &lt;code&gt;MIGRATING_TO_CLOUD_PROMPT.md&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;task&amp;gt;&lt;/span&gt;
Replace the local JSON mock services in &lt;span class="sb"&gt;`src/lib/services/local/`&lt;/span&gt; with AWS
Blocks implementations. The existing service interfaces in
&lt;span class="sb"&gt;`src/lib/services/interfaces/`&lt;/span&gt; are the contract. The service registry
(&lt;span class="sb"&gt;`src/lib/services/index.ts`&lt;/span&gt;) is the only file that should change in the
existing codebase.

The frontend, routing, components, and design must remain untouched.
&lt;span class="nt"&gt;&amp;lt;/task&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;architecture&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Backend runtime: AWS Blocks (&lt;span class="sb"&gt;`aws-blocks/index.ts`&lt;/span&gt;)
&lt;span class="p"&gt;2.&lt;/span&gt; API layer: ApiNamespace with methods that mirror the existing service
   interface contracts
&lt;span class="p"&gt;3.&lt;/span&gt; Auth: AuthBasic for admin routes (analytics dashboard, page management)
&lt;span class="p"&gt;4.&lt;/span&gt; Data — Link pages: DistributedTable (stores page configurations and
   link entries)
&lt;span class="p"&gt;5.&lt;/span&gt; Data — Analytics: DistributedTable (stores daily aggregate snapshots
   per page slug and date)
&lt;span class="p"&gt;6.&lt;/span&gt; Data — Visit events: DistributedTable (stores raw page-view, link-click,
   and share events)
&lt;span class="p"&gt;7.&lt;/span&gt; Hosting: Blocks Hosting for the Next.js frontend
&lt;span class="nt"&gt;&amp;lt;/architecture&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;acceptance_criteria&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] All existing frontend functionality works unchanged
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt; starts both frontend and AWS Blocks local server on
      port 3000
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Admin routes require AuthBasic login
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Click tracking persists events to DistributedTable
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`npm run build`&lt;/span&gt; completes without TypeScript errors
&lt;span class="p"&gt;-&lt;/span&gt; [ ] &lt;span class="sb"&gt;`npm run deploy`&lt;/span&gt; deploys the full stack to AWS
&lt;span class="nt"&gt;&amp;lt;/acceptance_criteria&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI knows exactly what to build, what the constraints are, and what "done" looks like. No ambiguity on either side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing good specs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;strong&gt;Goals and Non-Goals&lt;/strong&gt;: what are you building? What are you explicitly NOT building?&lt;/li&gt;
&lt;li&gt;Define your &lt;strong&gt;tech stack and project structure&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;executable commands&lt;/strong&gt; so the AI knows how to build and run things&lt;/li&gt;
&lt;li&gt;Write clear &lt;strong&gt;acceptance criteria&lt;/strong&gt; so everyone knows when the work is done&lt;/li&gt;
&lt;li&gt;Set your &lt;strong&gt;boundaries&lt;/strong&gt;: what should the AI always do? What should it ask you first?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  #6 Use MCP to Extend AI Beyond Its Knowledge
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) is an open standard for connecting AI to the outside world. With MCP, your AI can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to data sources (local files, databases, APIs)&lt;/li&gt;
&lt;li&gt;Run tools (search engines, calculators, linters)&lt;/li&gt;
&lt;li&gt;Follow workflows and specialized prompts&lt;/li&gt;
&lt;li&gt;Access knowledge that did not exist when it was trained&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Right now there is an MCP server for almost everything: GitHub, Slack, databases, documentation, cloud services.&lt;/p&gt;

&lt;p&gt;In my project, I used this in two places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: I connected Figma MCP so the AI could reference my actual design system when generating components. Instead of describing colors and spacing in text, it pulled the tokens directly from the Figma file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;: AWS Blocks is a new framework. The AI does not know its API surface from training data. Instead of pasting documentation into the chat (wasting context), I added an MCP server that gives the AI access to the AWS Blocks docs and API references on demand. It queries what it needs, when it needs it.&lt;/p&gt;

&lt;p&gt;The result: zero hallucinated API calls. The AI uses &lt;code&gt;new ApiNamespace(scope, 'api', (context) =&amp;gt; ({...}))&lt;/code&gt; because it can look up the actual signature, instead of guessing something like &lt;code&gt;createApi(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  #7 Know When to Vibe Code and When to Spec
&lt;/h2&gt;

&lt;p&gt;Some tasks deserve a spec. Others work fine as a quick conversation with the AI. Knowing which approach to use is what saves you time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use vibe coding when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prototyping something&lt;/li&gt;
&lt;li&gt;Making smaller changes and tiny fixes&lt;/li&gt;
&lt;li&gt;Learning syntax and a new technology&lt;/li&gt;
&lt;li&gt;Automating one-off tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Spec-Driven Development when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Building production software&lt;/li&gt;
&lt;li&gt;Working in a team&lt;/li&gt;
&lt;li&gt;Designing complex architecture&lt;/li&gt;
&lt;li&gt;Working on brownfield projects (like adding AWS Blocks to an existing Next.js app)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my project, I vibe coded the initial design exploration with Figma MCP. But the moment I started building the actual app and the backend migration, I switched to specs. The frontend &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt; and the backend AWS Blocks skill together gave the AI everything it needed to produce consistent, predictable results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;I am currently finishing the AWS Blocks backend for the link-sharing project and deploying it. I will write up that process in the next post.&lt;/p&gt;

&lt;p&gt;You can find the full project, including the &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;MIGRATING_TO_CLOUD_PROMPT.md&lt;/code&gt;, and the AWS Blocks skill files in &lt;a href="https://github.com/salihgueler/some-useful-links" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>aidlc</category>
    </item>
    <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>
  </channel>
</rss>
