<?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: What to Store and What to Throw Away</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Sat, 05 Sep 2026 02:37:13 +0000</pubDate>
      <link>https://dev.to/aws/ai-agent-memory-what-to-store-and-what-to-throw-away-196e</link>
      <guid>https://dev.to/aws/ai-agent-memory-what-to-store-and-what-to-throw-away-196e</guid>
      <description>&lt;p&gt;The best AI agent memory is selective: it keeps durable facts, preferences, and events and drops the small talk. Here is how to build that in Strands, three ways.&lt;/p&gt;

&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;Everyone is racing to make agents remember &lt;strong&gt;more&lt;/strong&gt;. Bigger context windows, longer histories, a vector store that keeps everything. But the agent that wins is not the one that remembers the most, it is the one that keeps the right things and throws the rest away. Store everything and your agent's memory becomes expensive, slow, and dirty; store nothing and it forgets its user between sessions. The earlier posts in this series covered &lt;em&gt;where&lt;/em&gt; memory lives (&lt;a href="https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-2i2l"&gt;key-value&lt;/a&gt;, &lt;a href="https://dev.to/aws/ai-agent-memory-add-semantic-search-without-a-vector-database-3g5c"&gt;vector&lt;/a&gt;, &lt;a href="https://dev.to/aws/graph-memory-when-vector-search-fails-2aeh"&gt;graph&lt;/a&gt;). This one is about the decision that comes before all of them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is worth storing, and what should you throw away?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That decision is called &lt;strong&gt;memory extraction&lt;/strong&gt; (or selective memory), and this post builds it three ways against the same planted conversation with deterministic ground truth (&lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws/tree/main/04-selective-memory-demo" rel="noopener noreferrer"&gt;companion demo&lt;/a&gt;). The first two run on Strands Agents' &lt;a href="https://strandsagents.com/docs/user-guide/concepts/memory/overview/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;native memory framework&lt;/a&gt;: you attach a &lt;code&gt;MemoryManager&lt;/code&gt;, and the SDK runs extraction, storage, retrieval, and injection for you. The third is fully managed by AWS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native MemoryManager, one store&lt;/strong&gt;: the framework does memory; you write one selection prompt that decides what to keep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native MemoryManager, four typed stores&lt;/strong&gt;: the same framework, one store and one prompt per memory type (Amazon Bedrock AgentCore Memory's partitioning, native SDK).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed memory&lt;/strong&gt;: you send raw turns and a managed service extracts asynchronously (&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/built-in-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Memory&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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;, &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; or &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/vector-search.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon DynamoDB Vector Search&lt;/a&gt; for the vector store, and &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/built-in-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Memory&lt;/a&gt; for the managed option.&lt;/p&gt;




&lt;h2&gt;
  
  
  What counts as memory extraction?
&lt;/h2&gt;

&lt;p&gt;Memory extraction, also called selective memory, is the step between a conversation and a memory store. It decides &lt;strong&gt;what to keep, which memory type it belongs to, and what to throw away&lt;/strong&gt;. It is separate from the storage backend: extraction decides &lt;em&gt;what&lt;/em&gt; enters memory, the backend decides &lt;em&gt;where&lt;/em&gt; it lives.&lt;/p&gt;

&lt;p&gt;The four memory types are the same across this whole series, and they map one-to-one to the four built-in strategies Amazon Bedrock AgentCore Memory offers (&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/built-in-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;built-in strategies&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;th&gt;AgentCore built-in strategy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;facts&lt;/td&gt;
&lt;td&gt;durable facts about the user's world&lt;/td&gt;
&lt;td&gt;&lt;code&gt;semanticMemoryStrategy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;preferences&lt;/td&gt;
&lt;td&gt;likes/dislikes the user reveals&lt;/td&gt;
&lt;td&gt;&lt;code&gt;userPreferenceMemoryStrategy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trip_summary&lt;/td&gt;
&lt;td&gt;rolling summary of the current task&lt;/td&gt;
&lt;td&gt;&lt;code&gt;summaryMemoryStrategy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;episodes&lt;/td&gt;
&lt;td&gt;notable events, one entry each&lt;/td&gt;
&lt;td&gt;&lt;code&gt;episodicMemoryStrategy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Strands does memory for you, natively
&lt;/h2&gt;

&lt;p&gt;You don't hand-roll memory tools, and you don't put memory logic in the chat agent's system prompt. Strands ships a native &lt;a href="https://strandsagents.com/docs/user-guide/concepts/memory/overview/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;&lt;code&gt;MemoryManager&lt;/code&gt;&lt;/a&gt; you attach to the agent. It handles three jobs across the stores you give it: &lt;strong&gt;recall&lt;/strong&gt; (a &lt;code&gt;search_memory&lt;/code&gt; tool the agent can call), &lt;strong&gt;injection&lt;/strong&gt; (folding relevant memory into the prompt before each call, without touching durable history), and &lt;strong&gt;extraction&lt;/strong&gt; (a &lt;a href="https://strandsagents.com/docs/api/python/strands.memory.extraction.model_extractor/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;&lt;code&gt;ModelExtractor&lt;/code&gt;&lt;/a&gt; that distills conversation into memories, off the turn, on a trigger). Recall and injection are on by default; extraction is opt-in.&lt;/p&gt;

&lt;p&gt;You own exactly two things: the extractor's &lt;strong&gt;selection prompt&lt;/strong&gt; (the keep/discard policy) and the &lt;strong&gt;store&lt;/strong&gt; (where memories live and how they're searched). Everything else is the framework's 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MemoryManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModelExtractor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExtractionConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IntervalTrigger&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIModel&lt;/span&gt;

&lt;span class="c1"&gt;# The selection prompt IS the keep/discard policy: the only memory logic you write.
&lt;/span&gt;&lt;span class="n"&gt;SELECTION_PROMPT&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;Extract durable memories worth keeping about a traveler: identity, dietary &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restrictions and allergies, stated travel preferences, and confirmed bookings. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discard small talk, weather, and passing opinions. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Return ONLY a JSON array of {&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="s"&gt;: string}, or [] if there is nothing to keep.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A store implementing the native MemoryStore contract, backed by a vector index.
&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VectorMemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traveler_memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extraction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ExtractionConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;IntervalTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;                 &lt;span class="c1"&gt;# when extraction runs (off the turn)
&lt;/span&gt;        &lt;span class="n"&gt;extractor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ModelExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                           &lt;span class="c1"&gt;# HOW selection happens
&lt;/span&gt;            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;OpenAIModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# a separate, optionally cheaper model
&lt;/span&gt;            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SELECTION_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;# &amp;lt;-- the policy you own
&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="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a flight assistant. Be concise.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# persona only, no memory logic
&lt;/span&gt;    &lt;span class="n"&gt;memory_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MemoryManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stores&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi, I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m Sam, vegetarian with a shellfish allergy.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# extraction happens automatically
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chat agent's system prompt stays about the agent's job. The selection policy lives in the &lt;code&gt;ModelExtractor&lt;/code&gt;, a separate model call the framework runs off the turn, so it never bloats the conversational prompt and can even run on a cheaper model than the chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each native piece does
&lt;/h3&gt;

&lt;p&gt;You only touch four things, and the &lt;a href="https://strandsagents.com/docs/user-guide/concepts/memory/overview/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;native memory framework&lt;/a&gt; handles the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MemoryManager&lt;/code&gt;&lt;/strong&gt;: the plugin you attach to the agent. It gives the agent a &lt;code&gt;search_memory&lt;/code&gt; tool, runs extraction in the background, and folds relevant memories into the prompt, all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ModelExtractor&lt;/code&gt;&lt;/strong&gt;: the piece that decides what to keep. Its &lt;code&gt;system_prompt&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; your keep/discard policy, and it runs as a separate model call from the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExtractionConfig&lt;/code&gt;&lt;/strong&gt;: ties the extractor and a trigger to a store, and quietly strips tool-call noise so tool JSON never lands in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IntervalTrigger&lt;/code&gt; / &lt;code&gt;InvocationTrigger&lt;/code&gt;&lt;/strong&gt;: decide &lt;em&gt;when&lt;/em&gt; extraction runs (every turn, or every N turns), off the conversation path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Injection is on by default too: before each model call the manager pulls relevant memories into the input without touching the durable history, and if retrieval fails it just skips injection instead of breaking the turn. The point: you declare &lt;em&gt;what to keep&lt;/em&gt; (the prompt) and &lt;em&gt;where&lt;/em&gt; (the store); the framework handles the plumbing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The store: your data, your backend
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MemoryManager&lt;/code&gt; needs somewhere to persist and search. That's a &lt;a href="https://strandsagents.com/docs/api/python/strands.memory.types/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;&lt;code&gt;MemoryStore&lt;/code&gt;&lt;/a&gt;, a small contract of &lt;code&gt;add(content)&lt;/code&gt; and &lt;code&gt;search(query)&lt;/code&gt;. The demo implements it over a vector index so recall is &lt;strong&gt;semantic&lt;/strong&gt;, with &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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VectorMemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;writable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;                        &lt;span class="c1"&gt;# the manager may write extracted memories here
&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# Titan V2
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# S3 Vectors OR DynamoDB
&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MemoryEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&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;hits&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the store is just this contract, &lt;strong&gt;the vector backend is a lever you set with one env var&lt;/strong&gt;: &lt;code&gt;VECTOR_BACKEND=s3&lt;/code&gt; (Amazon S3 Vectors) or &lt;code&gt;dynamodb&lt;/code&gt; (Amazon DynamoDB Vector Search). It's separate from &lt;em&gt;what&lt;/em&gt; gets remembered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which one, and why.&lt;/strong&gt; Both use the same Titan V2 embeddings, so recall quality is identical; the choice is about &lt;strong&gt;where the vectors live and how often you query them&lt;/strong&gt; (this is exactly how the AWS docs frame it):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3 Vectors&lt;/strong&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;docs&lt;/a&gt;): a dedicated vector bucket, separate from your operational data. AWS positions it for &lt;strong&gt;cost-optimized storage at massive scale with infrequent access&lt;/strong&gt;: query latency is &lt;strong&gt;sub-second, around 100 ms or less for frequent queries&lt;/strong&gt; and higher (up to a second or more) for cold ones. Pick it when memory is a standalone concern, you have a very large or archival vector corpus, and sub-second (not sub-10 ms) latency is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon DynamoDB Vector Search&lt;/strong&gt; (&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/VectorSearch.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;docs&lt;/a&gt;): the vector index lives &lt;em&gt;inside&lt;/em&gt; a DynamoDB table, so embeddings sit next to your operational data with no separate vector store to sync. AWS states &lt;strong&gt;single-digit-millisecond latency at 99%+ recall&lt;/strong&gt; for real-time search. You create it with the same &lt;code&gt;CreateTable&lt;/code&gt;/&lt;code&gt;UpdateTable&lt;/code&gt; APIs (a &lt;code&gt;VectorIndexes&lt;/code&gt; parameter) and query it with the &lt;code&gt;SearchVectors&lt;/code&gt; API, which needs a recent &lt;code&gt;boto3&lt;/code&gt;. Pick it when your agent already reads from DynamoDB, or you want real-time retrieval and one service for data and memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the AWS docs draw the line at access pattern: use S3 Vectors when memory is a standalone, large, or archival concern and sub-second latency is fine; use DynamoDB Vector Search when you are already on DynamoDB or want real-time retrieval with data and memory collocated. Neither is "faster memory" in a way the user feels, since the embedding call dominates end-to-end latency for both.&lt;/p&gt;

&lt;p&gt;You don't always have to write the store, either. The &lt;a href="https://strandsagents.com/integrations/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands integrations directory&lt;/a&gt; lists ready-made &lt;code&gt;MemoryStore&lt;/code&gt; backends: &lt;strong&gt;Amazon Bedrock Knowledge Base&lt;/strong&gt; and &lt;strong&gt;AgentCore Memory&lt;/strong&gt; from AWS, packaged &lt;strong&gt;S3 Vectors&lt;/strong&gt; (&lt;code&gt;s3-vectors-memory&lt;/code&gt;) and &lt;strong&gt;DynamoDB&lt;/strong&gt; (&lt;code&gt;strands-dynamodb-storage&lt;/code&gt;) stores, and partner options like &lt;strong&gt;Mem0&lt;/strong&gt;, &lt;strong&gt;Zep&lt;/strong&gt;, &lt;strong&gt;Vectorize&lt;/strong&gt;, and &lt;strong&gt;Neo4j&lt;/strong&gt; graph memory. Implementing the contract yourself, as this demo does, is the way to &lt;em&gt;understand&lt;/em&gt; it; in production you'd often drop in one of those.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three mechanisms, 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;Mechanism&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Selection prompt&lt;/th&gt;
&lt;th&gt;Partitions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A: native, one store&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MemoryManager&lt;/code&gt; + one &lt;code&gt;MemoryStore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;one general prompt&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B: native, four typed stores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MemoryManager&lt;/code&gt; + four &lt;code&gt;MemoryStore&lt;/code&gt;s&lt;/td&gt;
&lt;td&gt;one prompt per type&lt;/td&gt;
&lt;td&gt;four (facts / prefs / summary / episodes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C: Amazon Bedrock AgentCore Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fully managed by AWS&lt;/td&gt;
&lt;td&gt;AWS (managed, or override)&lt;/td&gt;
&lt;td&gt;managed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A vs B is granularity, not backend.&lt;/strong&gt; A is the simplest native setup: one store, one prompt. B reproduces AgentCore's per-type partitioning (four stores, four specialized prompts) with the native SDK, so the part AgentCore ships built-in (the selection criteria) becomes text you can read and tune. Both A and B run on S3 Vectors &lt;em&gt;or&lt;/em&gt; DynamoDB (the &lt;code&gt;VECTOR_BACKEND&lt;/code&gt; lever); the backend doesn't define the mechanism. &lt;strong&gt;C&lt;/strong&gt; is the fully managed counterpart to B: you send raw turns, AWS extracts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The measured results
&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%2Fuwylbir54np54vrstzxj.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%2Fuwylbir54np54vrstzxj.png" alt="Storing everything is not memory quality: a jar that stores everything reaches perfect recall but keeps all the junk, while a selective jar keeps recall high and drops the noise" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test conversation mixes &lt;strong&gt;5 keepers&lt;/strong&gt; (2 facts, 2 preferences, 1 episode) with &lt;strong&gt;3 decoys&lt;/strong&gt; to throw away (small talk, a passing opinion, ephemeral weather). The score is &lt;strong&gt;selection recall&lt;/strong&gt;: how many of the 5 keepers a mechanism stored, checked deterministically against that ground truth (no LLM judge). The decoys are there so a mechanism cannot win by hoarding: keeping everything would ace recall and still be useless.&lt;/p&gt;

&lt;p&gt;From 20 runs each for A and B, and repeated runs for C (gpt-4o-mini; the extractor is an LLM, so A's and B's exact recall varies slightly run to run, C's result was consistent, and your numbers will differ):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Selection recall&lt;/th&gt;
&lt;th&gt;Who owns the selection policy&lt;/th&gt;
&lt;th&gt;Retrieval granularity&lt;/th&gt;
&lt;th&gt;Turn latency&lt;/th&gt;
&lt;th&gt;When queryable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: native, one store&lt;/td&gt;
&lt;td&gt;~3.9/5 (3-5)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;you&lt;/strong&gt; (one prompt)&lt;/td&gt;
&lt;td&gt;one blended pool&lt;/td&gt;
&lt;td&gt;~3.2 s/turn&lt;/td&gt;
&lt;td&gt;when the turn returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: native, four typed stores&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~5/5 (4.95)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;you&lt;/strong&gt; (one prompt per type)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;per type&lt;/strong&gt; (query/inject/tune each alone)&lt;/td&gt;
&lt;td&gt;~3.5 s/turn&lt;/td&gt;
&lt;td&gt;when the turn returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: Amazon Bedrock AgentCore Memory&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;AWS&lt;/strong&gt; (managed, or override)&lt;/td&gt;
&lt;td&gt;managed per strategy&lt;/td&gt;
&lt;td&gt;~2.2 s/turn&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~20-55 s later (async)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The reproducible finding, across all those runs:&lt;/strong&gt; all three recall the keepers well. What differs is &lt;strong&gt;who writes the selection policy&lt;/strong&gt;, and that is a choice, not a verdict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;B is the sharpest when you want to own every criterion.&lt;/strong&gt; One specialized, non-overlapping prompt per type means each store keeps only its own kind of memory (facts vs preferences vs a confirmed-trip summary vs a completed action), so it lands recall ~5/5 on every run and drops the decoys. Writing four tight prompts is the work; per-type selection is the payoff. Choose B when the keep/discard rules are yours to define and tune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A is the same idea with the least setup.&lt;/strong&gt; One store, one prompt: you own the selection policy at a coarser grain. It rejects small talk, weather, and opinions; a single prompt covering everything recalls a touch less consistently (~3.9/5). Choose A when one flat memory and one prompt are enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C lets AWS do the selection for you.&lt;/strong&gt; You send raw turns and Amazon Bedrock AgentCore Memory's managed strategies extract, embed, and index them, with no extraction pipeline to maintain. It recalls the keepers (5/5) and runs the memory lifecycle server-side. Choose C when you would rather not own the selection logic. If you &lt;em&gt;do&lt;/em&gt; want to shape it, AgentCore supports &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-custom-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;custom strategies with prompt overrides&lt;/a&gt;: override a built-in strategy's default logic with your own prompt and model, so control is there on the managed path too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing here is instant. Every mechanism runs an extraction step, embeds the kept text, and writes it. A and B pay that cost &lt;strong&gt;inside the turn&lt;/strong&gt;, so the memory is queryable the moment the turn returns. C pays it &lt;strong&gt;asynchronously on AWS&lt;/strong&gt;: the turn is cheap (~2.2 s) but the extracted memory appears &lt;strong&gt;~20-55 seconds later&lt;/strong&gt; (measured, waiting for extraction to settle). Same work, moved off the turn, for a delay before the memory is usable.&lt;/p&gt;

&lt;p&gt;The takeaway is not "more stores is better." It is &lt;strong&gt;how much of the selection policy you want to hold&lt;/strong&gt;: A and B put the prompt in your hands (one prompt, or one per type for finer control); C hands the whole pipeline to AWS, with custom strategies as the way back in if you want it. Same goal, different amount of control, pick the one that fits your team.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on &lt;code&gt;flush()&lt;/code&gt;: when is a memory saved?
&lt;/h3&gt;

&lt;p&gt;Because extraction runs in the background, the last turn's memory might not be persisted yet when the agent finishes responding. &lt;code&gt;await manager.flush()&lt;/code&gt; closes that gap: it forces &lt;strong&gt;every&lt;/strong&gt; store to save its buffered messages (even one whose trigger hasn't fired, or one currently backed off) and waits for those writes to land. It's the synchronization point that guarantees nothing is lost on a graceful shutdown.&lt;/p&gt;

&lt;p&gt;When you call it depends on how you drive the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronous &lt;code&gt;agent("...")&lt;/code&gt;&lt;/strong&gt; (this demo): each call runs in its own event loop, so the framework flushes for you after every invocation. Memory is persisted by the time the call returns: &lt;strong&gt;you never flush manually.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async &lt;code&gt;agent.invoke_async(...)&lt;/code&gt; / &lt;code&gt;stream_async(...)&lt;/code&gt;&lt;/strong&gt;: these share your long-lived loop and don't flush, so extraction stays on its trigger cadence, and you &lt;code&gt;await memory_manager.flush()&lt;/code&gt; yourself at a shutdown boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two caveats from the docs: don't call &lt;code&gt;flush()&lt;/code&gt; every turn alongside a periodic trigger (it defeats the trigger's schedule), and a hard kill (&lt;code&gt;SIGKILL&lt;/code&gt;, timeout) can still drop the last unsaved turn since flush never runs, so a more frequent trigger narrows that window.&lt;/p&gt;




&lt;h2&gt;
  
  
  What B's four typed prompts look like
&lt;/h2&gt;

&lt;p&gt;Mechanism B is where owning the prompt pays off most, so it's worth seeing its policy. Each memory type maps to its own vector partition &lt;strong&gt;and&lt;/strong&gt; its own selection prompt, in one table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# {memory_type: (vector_partition, selection_prompt)}
# Each prompt keeps ONLY its own kind of memory and explicitly rejects the others,
# so the four stores never overlap: nothing lands in two stores, and no decoy slips
# in disguised as a "summary". That discipline is what makes B's per-type selection sharp.
&lt;/span&gt;&lt;span class="n"&gt;TYPED&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;facts&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;selective-facts&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;Extract ONLY durable FACTS about the traveler (name, home airport, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dietary restrictions, allergies). DISCARD preferences, opinions, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small talk, weather, and one-off events. If none, return [].&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&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;selective-prefs&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;Extract ONLY stated travel PREFERENCES (cabin, seat, layover rules, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget). DISCARD facts like allergies, one-off bookings, opinions, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small talk, weather. If none, return [].&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;trip_summary&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;selective-summary&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;Maintain a one-sentence summary of the CONFIRMED current trip ONLY &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(route, airline, date once booked). DISCARD small talk, weather, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opinions, and anything not part of the booked trip. If unchanged, return [].&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;episodes&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;selective-episodes&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;Record ONLY a concrete completed ACTION the traveler took this turn &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(a booking, a cancellation, a confirmed change). Not a comment, question, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opinion, weather remark, or small talk. If none, return [].&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;# Every prompt ends with the same output contract, appended when the extractor is built:
&lt;/span&gt;&lt;span class="n"&gt;JSON_CONTRACT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; Return ONLY a JSON array of {&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="s"&gt;: string}, or [] if none.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning &lt;code&gt;[]&lt;/code&gt; is a first-class answer; that's the discard half of selection. If the extractor keeps a decoy, you tune the prompt. On the managed path (C) those defaults live in the service, and you can still reach them through &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-custom-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;custom strategy overrides&lt;/a&gt;. (If you don't pass a prompt to the &lt;code&gt;ModelExtractor&lt;/code&gt;, it uses Strands' sensible default, but then you inherit its generic criteria.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Which mechanism should you pick?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You want the SDK to run memory for you with the least setup, and one selection prompt is enough&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A&lt;/strong&gt;: native, one store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You need per-type control of the keep/discard criteria (regulated domain, custom taxonomy)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;B&lt;/strong&gt;: native, four typed stores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production multi-user; asynchronous extraction (seconds of lag) is fine; you want AWS to run the whole memory pipeline for you (with custom strategies available if you later want to shape it)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;C&lt;/strong&gt;: Amazon Bedrock AgentCore Memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two levers cut across all of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: S3 Vectors vs DynamoDB Vector Search is a one-env-var choice for A and B; pick by where your operational data already lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination&lt;/strong&gt;: a &lt;code&gt;MemoryStore&lt;/code&gt; could just as well write facts to the &lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws/tree/main/03-graph-memory-demo" rel="noopener noreferrer"&gt;knowledge graph of Demo 03&lt;/a&gt; (as triples). Selection and storage compose.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical notes for the managed path
&lt;/h2&gt;

&lt;p&gt;A few things worth knowing when you wire up Amazon Bedrock AgentCore Memory through the &lt;a href="https://strandsagents.com/docs/integrations/session-managers/agentcore-memory/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;official Strands session manager&lt;/a&gt; (&lt;code&gt;AgentCoreMemorySessionManager&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give each strategy an explicit namespace at creation&lt;/strong&gt; (&lt;code&gt;/facts/{actorId}/&lt;/code&gt;, &lt;code&gt;/preferences/{actorId}/&lt;/code&gt;, &lt;code&gt;/summaries/{actorId}/{sessionId}/&lt;/code&gt;, &lt;code&gt;/episodes/{actorId}/{sessionId}/&lt;/code&gt;). The same namespace you set on the strategy is the one you reference in &lt;code&gt;RetrievalConfig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;RetrievalConfig(relevance_score=...)&lt;/code&gt; to control what comes back at recall.&lt;/strong&gt; It keeps only records above a relevance threshold per namespace, so the agent sees the most on-point memories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction is asynchronous.&lt;/strong&gt; In this demo the extracted memory became queryable &lt;strong&gt;~20 to 55 seconds&lt;/strong&gt; after the turn (measured, polling until extraction settled). Plan for eventual consistency: a fact written this turn may not be retrievable on the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A memory in &lt;code&gt;CREATING&lt;/code&gt; status isn't ready yet.&lt;/strong&gt; Wait until it reports &lt;code&gt;ACTIVE&lt;/code&gt; before sending events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to shape what the managed strategies keep?&lt;/strong&gt; Use &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/long-term-configuring-custom-strategies.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;custom strategies with prompt overrides&lt;/a&gt;: override a built-in strategy's default extraction/consolidation logic with your own prompt and model, so you get the managed pipeline and your own criteria.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service evolves quickly, so treat the exact behaviors above as current observations and check the docs for the latest.&lt;/p&gt;




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

&lt;p&gt;Everything runs from &lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws/tree/main/04-selective-memory-demo" rel="noopener noreferrer"&gt;Demo 04 of the companion repo&lt;/a&gt;: the three mechanisms against the same conversation, with the deterministic scorecard and the lag measurement. AWS resources (vector indexes/tables, the managed memory) are created automatically if missing, and the README covers cleanup and the exact native Strands pieces used.&lt;/p&gt;

&lt;p&gt;There is also one interactive chat per mechanism (&lt;code&gt;chat_single_store.py&lt;/code&gt;, &lt;code&gt;chat_typed_stores.py&lt;/code&gt;, &lt;code&gt;chat_agentcore.py&lt;/code&gt;): talk to the agent and watch memory fill turn by turn, with small talk discarded and keepers stored. The AgentCore chat lets you feel the async lag: right after you speak, &lt;code&gt;/memory&lt;/code&gt; shows nothing until extraction catches up.&lt;/p&gt;

&lt;p&gt;This post was about throwing away &lt;em&gt;noise&lt;/em&gt;. Next in the series, the higher-stakes version of the same instinct: what your agent must &lt;strong&gt;NOT&lt;/strong&gt; remember even when it looks legitimate, and how to defend the write path against prompt injection and memory poisoning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Research referenced
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paper&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2507.07957" rel="noopener noreferrer"&gt;MIRIX: Multi-Agent Memory System&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Typed memory (6 types, +35% accuracy, SOTA 85.4% on LOCOMO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;MemGPT: Towards LLMs as Operating Systems&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Core memory concept, virtual context management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We reproduce the &lt;em&gt;mechanism&lt;/em&gt; these papers describe (typed, selective memory), not their specific benchmark numbers.&lt;/p&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;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>How a Strands agent took Claude Opus 5 from 30% to 99.95% on ARC-AGI-3</title>
      <dc:creator>Morgan Willis</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:07:24 +0000</pubDate>
      <link>https://dev.to/aws/how-a-strands-agent-took-claude-opus-5-from-30-to-9995-on-arc-agi-3-4kel</link>
      <guid>https://dev.to/aws/how-a-strands-agent-took-claude-opus-5-from-30-to-9995-on-arc-agi-3-4kel</guid>
      <description>&lt;p&gt;AI’s most fervent and optimistic promoters promise a future where AI is innovating its way out of society’s biggest problems. AI systems will conduct scientific research, discover new drugs and materials, run engineering projects, and autonomously execute complex tasks usually requiring teams of experts working over long periods of time.&lt;/p&gt;

&lt;p&gt;But fulfilling that promise requires more than a good prompt and basic context engineering. AI systems need to pursue goals over long horizons, encounter situations they’ve never seen before, learn from failed attempts, adapt their strategy, and keep making progress without a human telling them what to do next.&lt;/p&gt;

&lt;p&gt;They need to solve novel problems that aren’t represented in their training data. But are today’s models actually capable of that?&lt;/p&gt;

&lt;p&gt;That’s one of the questions ARC Prize is trying to answer with ARC-AGI-3, a benchmark that drops AI systems into unfamiliar environments without giving them the rules or even telling them what the goal is. They have to experiment, figure out how the environment works, and learn what winning means in this ambiguous context.&lt;/p&gt;

&lt;p&gt;AWS engineers built &lt;a href="https://github.com/strands-labs/benchmark-harnesses/tree/main/arc-agi-3-agent" rel="noopener noreferrer"&gt;an agent&lt;/a&gt; to take it on. Using Claude Opus 5 and the open source Strands Agents SDK, the agent completed all 183 levels across ARC-AGI-3’s 25 public environments with a 99.95% relative human action efficiency (RHAE) score over one 8 hour run, spending about $830 in tokens to do it. NVIDIA has recently reported a similar result, with its &lt;a href="https://developer.nvidia.com/blog/nvidia-avo-reaches-100-on-arc-agi-3-demonstrating-a-frontier-level-general-purpose-architecture-for-long-horizon-autonomous-agents/" rel="noopener noreferrer"&gt;AVO agent&lt;/a&gt;, also using Opus 5, scoring 100% RHAE on the public game set.&lt;/p&gt;

&lt;p&gt;Then compare that to ARC Prize's standard evaluation of Opus 5, which scored &lt;a href="https://arcprize.org/results/anthropic-claude-opus-5" rel="noopener noreferrer"&gt;30.16%&lt;/a&gt; on ARC-AGI-3. The model across these runs is the same, but the system surrounding it is very different.&lt;/p&gt;

&lt;p&gt;These results give us some data showing how important agent harnesses are for getting what you want out of AI. A model by itself provides the reasoning and judgment, but the harness gives it a surrounding system for managing context, maintaining state, taking actions, observing their consequences, and generally interacting with the world around it. How you build the harness matters, so having open source examples of systems that successfully handle these kinds of long-horizon tasks gives us a chance to look beyond the benchmark score and understand which design choices actually made the difference.&lt;/p&gt;

&lt;p&gt;I work closely with the Strands team at AWS, and the entire Strands ARC-AGI-3 harness is open source. So let's dig into it to understand how they did it and how the agent works. You can find the code for yourself on &lt;a href="https://github.com/strands-labs/benchmark-harnesses/tree/main/arc-agi-3-agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The public vs private game set for ARC-AGI-3
&lt;/h2&gt;

&lt;p&gt;Before we get further into the details I do want to clarify one thing, especially for those of you who aren't deeply familiar with how ARC-AGI-3 works. &lt;/p&gt;

&lt;p&gt;The public game set has 25 games that are essentially the practice exam, and humans can play &lt;a href="https://arcprize.org/tasks/ls20" rel="noopener noreferrer"&gt;these games too&lt;/a&gt;. But the official ARC Prize rankings come from private, held-out evaluation, and the 2026 competition runs offline on Kaggle where hosted API models aren't allowed at all. The public set gives researchers a common set of environments for developing and experimenting with their agent systems, while the competition evaluates them against new unseen environments.&lt;/p&gt;

&lt;p&gt;That competition is still open. But that doesn't mean that the public results don't give us anything useful. Acing the public benchmark is still a strong signal about which agent architectures can successfully handle long-horizon tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inside the Strands harness
&lt;/h2&gt;

&lt;p&gt;First off, it's good to know that the agent starts with zero game knowledge in its system prompt. You can check the entire system prompt out by taking a look at the &lt;a href="https://github.com/strands-labs/benchmark-harnesses/blob/main/arc-agi-3-agent/prolong_agent/agent/prompts.py" rel="noopener noreferrer"&gt;code directly&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The available controls are presented as generic labels like ACTION1, ACTION2, and ACTION3, without telling the agent what those actions actually do. Even the board itself is represented as raw numeric values. If it wants to understand what anything does, it has to try something and observe what happens.&lt;/p&gt;

&lt;p&gt;This prompt is the same across all 25 games, and everything the agent knows about a game, it learned by playing it. So if it doesn't know anything, how does the agent navigate the game board?&lt;/p&gt;

&lt;p&gt;Before the agent takes its first turn, the harness writes the initial board state to a game log. Then the model is told to read that log, analyze the board, and decide what actions it wants to try.&lt;/p&gt;

&lt;p&gt;To do that, the harness gives the agent a small set of tools for working with files and code. It can read files, search them with grep and regex, write and edit files, and execute Python. So even though the board isn't pasted directly into the model's prompt, the agent can inspect it through the log and use these tools to start figuring out what it's looking at.&lt;/p&gt;

&lt;p&gt;Once the agent chooses an action, the harness executes it and records the action and resulting board state back to the log. The agent can then inspect what changed, form a theory about what its action did, and decide what it wants to try next. Each new action and resulting board state adds another piece of evidence to that growing history.&lt;/p&gt;

&lt;p&gt;Keeping that history in a file rather than continually adding it directly to the context window is important because things would get unruly pretty fast. A single board is 64x64, and over hundreds of actions the agent can build up a massive interaction log that can grow to tens of megabytes.&lt;/p&gt;

&lt;p&gt;The agent can use its tools to decide what parts of that history are actually useful, performing its own context engineering. It can search for previous actions, write Python scripts to compare board states or look for patterns, save notes about what it has figured out, and use those findings to decide what to try next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these tools?
&lt;/h2&gt;

&lt;p&gt;The specific tools the agent was given didn't come from nowhere. The approach was based on &lt;a href="https://github.com/alexisfox7/PRO-LONG" rel="noopener noreferrer"&gt;PRO-LONG&lt;/a&gt;, which tested how well an agent using GPT-5.5 performed on ARC-AGI-3 as it was progressively given more ways to work with its interaction history.&lt;/p&gt;

&lt;p&gt;With read-only access to its history, the agent scored 23.1%. Adding grep and regex brought that to 27.2%. But the biggest jump came when the agent was given access to Python, pushing the score to 38.3%. Finally, adding the ability to write and edit files brought it to 41.2%.&lt;/p&gt;

&lt;p&gt;That jump from giving the agent the Python tool suggests that giving an agent access to its history isn't enough, but giving it a way to programmatically analyze and derive insights from that history can make a significant difference in performance.&lt;/p&gt;

&lt;p&gt;For the Strands run, across the 25 games, the agent wrote &lt;a href="https://github.com/strands-labs/benchmark-harnesses/tree/main/arc-agi-3-agent" rel="noopener noreferrer"&gt;734 scripts&lt;/a&gt; for itself. Some of those scripts captured pieces of what the agent had learned about how a game worked. It built parsers for interpreting board states, renderers for representing them, and even simulators of game mechanics. &lt;/p&gt;

&lt;p&gt;The team describes some of these scripts as small world models. In 10 of the 25 games, the agent imported code it had written earlier and continued building on top of it.&lt;/p&gt;

&lt;p&gt;Those tools also run inside a bubblewrap sandbox with no network access and writes are restricted to the agent's workspace. That means that the agent can analyze everything it has learned through its own interactions, but it can't go online and find a walkthrough or inspect the underlying game implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative Human Action Efficiency
&lt;/h2&gt;

&lt;p&gt;The agent passed all the levels and scored 99.95% relative human action efficiency (RHAE). RHAE is a measure of how efficiently the agent solves the game by comparing the number of actions it takes on each level against a baseline from humans playing the game for the first time. This is arguably a more important metric than completion alone because &lt;a href="https://arcprize.org/arc-agi" rel="noopener noreferrer"&gt;ARC Prize&lt;/a&gt; defines AGI as “a system that can match the learning efficiency of humans.”&lt;/p&gt;

&lt;p&gt;An agent could eventually solve a game by taking thousands of actions and exhaustively trying every possibility, but that wouldn't demonstrate the same learning efficiency as a human. RHAE helps us measure that difference. This is the breakdown for the Strands agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compared with human baseline&lt;/th&gt;
&lt;th&gt;Levels&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beat human baseline by enough to reach maximum score&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;82.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beat human baseline&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matched human baseline&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used more actions than human baseline&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;12.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across 160 of the 183 levels, the Strands agent matched or beat the human baseline for action efficiency. And on 150 of those levels, it performed well enough to receive ARC Prize's maximum per-level efficiency score. So even though the overall score is 99.95%, it performed better than the human baseline most of the time. You can see how scoring works in &lt;a href="https://docs.arcprize.org/methodology" rel="noopener noreferrer"&gt;ARC Prize's methodology docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means
&lt;/h2&gt;

&lt;p&gt;This result on ARC-AGI-3 is impressive, and because it's open source we can all learn from how the pattern works. The takeaways for me are: externalize the history, give the agent tools to query it, let it write its own code to turn what it learns into insights and reusable programs, and let it decide what stays in active context for the next decision. &lt;/p&gt;

&lt;p&gt;The code is surprisingly easy to understand, and the patterns could be used directly with whatever agent you're building. Go check it out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strands Harness Code: &lt;a href="https://github.com/strands-labs/benchmark-harnesses/tree/main/arc-agi-3-agent" rel="noopener noreferrer"&gt;https://github.com/strands-labs/benchmark-harnesses/tree/main/arc-agi-3-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The PR, which is a great writeup on its own: &lt;a href="https://github.com/strands-labs/benchmark-harnesses/pull/12" rel="noopener noreferrer"&gt;https://github.com/strands-labs/benchmark-harnesses/pull/12&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Scorecard: &lt;a href="https://arcprize.org/scorecards/8a10b024-3560-448f-ac31-becc48affe5b" rel="noopener noreferrer"&gt;https://arcprize.org/scorecards/8a10b024-3560-448f-ac31-becc48affe5b&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PRO-LONG: &lt;a href="https://github.com/alexisfox7/PRO-LONG" rel="noopener noreferrer"&gt;https://github.com/alexisfox7/PRO-LONG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ARC-AGI-3: &lt;a href="https://arcprize.org/arc-agi/3" rel="noopener noreferrer"&gt;https://arcprize.org/arc-agi/3&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>programming</category>
    </item>
    <item>
      <title>Graph Memory: When Vector Search Fails</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Thu, 27 Aug 2026 22:48:51 +0000</pubDate>
      <link>https://dev.to/aws/graph-memory-when-vector-search-fails-2aeh</link>
      <guid>https://dev.to/aws/graph-memory-when-vector-search-fails-2aeh</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;AI agents accumulate facts across conversations. Key-value memory stores them as labeled blobs. Vector memory retrieves them by meaning. Neither can answer a question that spans multiple facts connected by relationships. Graph memory closes this gap by storing memories as nodes and typed edges.&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%2Fn73fpbkndzuffrlzx68i.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%2Fn73fpbkndzuffrlzx68i.png" alt="Graph memory architecture: Strands agent takes two paths, recall_semantic returns pieces (1/4), recall_graph traverses Maya Torres → Iberia → Madrid → Spain (4/4)" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post uses a travel assistant as the demo, but the failure is structural, not travel-specific. It shows up in any agent that accumulates facts about people, places, products, or events over time. Eventually a user asks something that can only be answered by following the edges between facts. And there are no edges to follow.&lt;/p&gt;

&lt;p&gt;The same structural gap causes agents to hallucinate answers to counting and aggregation questions. In &lt;a href="https://dev.to/aws/rag-vs-graphrag-when-agents-hallucinate-answers-2mcb?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el"&gt;RAG vs GraphRAG&lt;/a&gt;, I measured a hotel assistant that couldn't answer "how many hotels accept pets?" without inventing statistics, because it had no graph to compute over. Here the failure is multi-hop retrieval, but the root cause is the same: no edges to follow.&lt;/p&gt;

&lt;p&gt;Here is what that failure looks like in a real run, with the travel assistant after it accumulated four facts about its user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Facts in memory:
  Maya Torres works at Iberia.
  Iberia flies to Madrid.
  Madrid is in Spain.
  Iberia belongs to Oneworld.

Question: "Who do I know connected to flights to Spain?"

Top-3 vector similarity results:
  - Iberia. An airline.
  - Spain. A country.
  - Madrid. A city.

Recovers the person (Maya Torres)? False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Similarity found every &lt;em&gt;piece&lt;/em&gt;. It never found the &lt;em&gt;person&lt;/em&gt;, because a vector index has no notion of a relationship between its entries. &lt;strong&gt;Graph memory fixes this by storing memories as nodes and typed edges, so the answer is reached by traversal instead of resemblance.&lt;/strong&gt; This post builds it with Neo4j, measures the same four questions against both retrievers (1/4 vs 4/4), and shows the prompting technique that gets an AI assistant to build it right. Everything 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;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Post 3 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. This is the most advanced demo so far: it assumes the earlier posts and a Neo4j instance. 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;; the pattern carries over to any agent framework.)&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Strands Agents for this demo?
&lt;/h2&gt;

&lt;p&gt;Strands makes it simple to add graph memory to an agent. Creating an agent is just a few lines of code, and tools are functions with a decorator:&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;Search graph memory by traversing relationships.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph_retriever&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;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;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;recall_graph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recall_semantic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remember_fact&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 it. No custom integrations, no framework lock-in. The &lt;code&gt;@tool&lt;/code&gt; decorator is all you need to plug Neo4j retrievers into the agent. When &lt;code&gt;book_flight&lt;/code&gt; executes, it writes edges directly to the graph, and the knowledge graph grows with usage.&lt;/p&gt;

&lt;p&gt;The pattern shown here (external graph + tool-based access) works in any agent framework. Strands just makes it straightforward.&lt;/p&gt;


&lt;h2&gt;
  
  
  What is a multi-hop question?
&lt;/h2&gt;

&lt;p&gt;A question whose answer lives in no single memory, only in the chain between several. Stored as a graph, the assistant's four facts form one:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Maya&lt;/span&gt; &lt;span class="n"&gt;Torres&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="n"&gt;WORKS_AT&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Iberia&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="n"&gt;MEMBER_OF&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Oneworld&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
                                 &lt;span class="err"&gt;│&lt;/span&gt;
                            &lt;span class="n"&gt;FLIES_TO&lt;/span&gt;
                                 &lt;span class="err"&gt;▼&lt;/span&gt;
                             &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Madrid&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;&lt;span class="n"&gt;IN_COUNTRY&lt;/span&gt;&lt;span class="err"&gt;──▶&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Spain&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;"Who do I know connected to flights to Spain?" requires three hops: person → airline → city → country. Key-value memory can't express it (no key is "the chain"). Vector memory retrieves the three most similar fragments and stops. Only a store that &lt;em&gt;keeps the edges&lt;/em&gt; can walk 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%2F869bgbrl83sgn67awjo4.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%2F869bgbrl83sgn67awjo4.png" alt="Multi-hop question over agent memory: vector similarity surfaces Iberia, Madrid and Spain as disconnected pieces, graph traversal walks the edges back to Maya Torres" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How does graph memory answer it?
&lt;/h2&gt;

&lt;p&gt;In two moves: &lt;strong&gt;similarity finds the entry point, traversal finds the answer.&lt;/strong&gt; Both retrievers in the demo are official &lt;a href="https://neo4j.com/docs/neo4j-graphrag-python/" rel="noopener noreferrer"&gt;&lt;code&gt;neo4j-graphrag&lt;/code&gt;&lt;/a&gt; classes, sharing the same graph and the same vector index. The only variable is whether edges get walked:&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;neo4j_graphrag.retrievers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorRetriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VectorCypherRetriever&lt;/span&gt;

&lt;span class="c1"&gt;# Before: pure similarity, returns the nearest nodes, disconnected
&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VectorRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After: similarity finds an entry node, then Cypher walks back to the person
&lt;/span&gt;&lt;span class="n"&gt;RETRIEVAL_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
WITH node AS entry, score
MATCH (person:Person) WHERE person &amp;lt;&amp;gt; entry
MATCH path = shortestPath((person)-[*1..5]-(entry))
RETURN person.name AS who, [n IN nodes(path) | n.name] AS chain, max(score) AS score
ORDER BY score DESC
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VectorCypherRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_embeddings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;RETRIEVAL_QUERY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same question, second retriever, same run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- who='Maya Torres' chain=['Maya Torres', 'Iberia'] score=0.77
- who='Maya Torres' chain=['Maya Torres', 'Iberia', 'Madrid', 'Spain'] score=0.71

Recovers the person (Maya Torres)? True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note what the graph adds beyond the answer: &lt;strong&gt;the chain&lt;/strong&gt;. Every result carries the path that produced it (Maya → Iberia → Madrid → Spain). That receipt is what makes graph memory &lt;em&gt;traceable&lt;/em&gt;, and it becomes the star of a later post on auditing agent decisions.&lt;/p&gt;


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

&lt;p&gt;Four multi-hop questions, both retrievers, checked deterministically against the known graph (no LLM judge, so the numbers reproduce):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Vector similarity&lt;/th&gt;
&lt;th&gt;Graph traversal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who do I know that's connected to flights to Spain?&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who do I know connected to an airline that flies to Madrid?&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who works at the Oneworld airline I know?&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which person is linked to airlines in Spain?&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4/4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one similarity got right is worth pausing on: on question 3 the person node happened to rank high by resemblance alone. Similarity isn't always wrong on multi-hop questions; it's &lt;strong&gt;unreliable&lt;/strong&gt;, while traversal is consistent. That's the actual finding, and it matches what the graph-memory research measures at scale (&lt;a href="https://arxiv.org/abs/2601.03236" rel="noopener noreferrer"&gt;MAGMA&lt;/a&gt;, &lt;a href="https://arxiv.org/abs/2603.27910" rel="noopener noreferrer"&gt;GAAMA&lt;/a&gt;, &lt;a href="https://arxiv.org/abs/2501.13956" rel="noopener noreferrer"&gt;Zep&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The agent also &lt;em&gt;writes back&lt;/em&gt;: told "remember that Maya works at Iberia", the Strands agent calls a &lt;code&gt;remember_fact&lt;/code&gt; tool that MERGEs the edge into Neo4j and logs it to &lt;code&gt;agent.state&lt;/code&gt;. The memory grows as a graph, one fact per conversation.&lt;/p&gt;


&lt;h2&gt;
  
  
  When is a graph the wrong choice?
&lt;/h2&gt;

&lt;p&gt;When your memories are independent notes. A graph of disconnected nodes is a slow key-value store with extra steps, plus a database to run and a schema to think about. Skip a graph when nothing in your questions crosses more than one fact. The honest decision line, extending the series' table:&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&lt;/td&gt;
&lt;td&gt;Key-value (&lt;a href="https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13"&gt;post 1&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Exact, instant, zero infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search by meaning over independent notes&lt;/td&gt;
&lt;td&gt;Vector (&lt;a href="https://dev.to/aws/do-ai-agents-need-a-vector-database-the-measured-answer-2nf6"&gt;post 2&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Similarity is enough when nothing connects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Questions that hop across relationships&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Graph (this post)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only edges answer chain questions, with receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two more honest costs: you design the schema (every edge type must earn a real question: model "who do I know at X?", not everything), and connectivity cuts both ways, because one wrong fact contaminates every traversal that crosses it. That blast-radius problem gets its own post (memory hygiene).&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/03-graph-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
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# OPENAI_API_KEY + your NEO4J_* values&lt;/span&gt;
uv run python test_graph_memory.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Needs a running Neo4j (Desktop, Docker, or the free Aura tier) and &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; for model + embeddings (or swap to Amazon Bedrock; the README shows how). The repo's README also documents a real version-churn gotcha (&lt;code&gt;neo4j-graphrag&lt;/code&gt; 1.18 emits Cypher 25's &lt;code&gt;SEARCH&lt;/code&gt; clause, which fails on servers still defaulting to Cypher 5) and how the demo handles it automatically.&lt;/p&gt;


&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What is graph memory for AI agents?
&lt;/h3&gt;

&lt;p&gt;Agent memory stored as a knowledge graph: entities as nodes, facts as typed edges, with a vector index for finding entry points. It answers relationship questions ("who do I know connected to X?") that key-value lookup and vector similarity structurally cannot, and every answer carries the chain of facts that produced it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Knowledge graph vs vector memory: which does an agent need?
&lt;/h3&gt;

&lt;p&gt;Vector memory when questions match individual memories by meaning; graph memory when answers span &lt;em&gt;several&lt;/em&gt; memories connected by relationships. Measured here: vector similarity solved 1 of 4 multi-hop questions, graph traversal 4 of 4. Most production agents eventually want both, similarity to enter the graph and traversal to answer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is this the same as GraphRAG?
&lt;/h3&gt;

&lt;p&gt;Same mechanism, different corpus. GraphRAG builds a graph over your &lt;em&gt;documents&lt;/em&gt;; graph memory builds one over the user facts the agent accumulated across conversations. The retrieval pattern (vector entry point, then traversal) is identical, which is why the official graph-RAG retriever classes work unchanged here.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do I need an LLM to build the graph?
&lt;/h3&gt;

&lt;p&gt;Not for this pattern. I write facts as explicit MERGE statements from a tool the agent calls, which keeps results reproducible. LLM entity extraction, such as &lt;code&gt;SimpleKGPipeline&lt;/code&gt;, automates graph construction from raw text at the cost of determinism: a production option, not a requirement here.&lt;/p&gt;
&lt;h3&gt;
  
  
  How do you benchmark agent knowledge-graph memory?
&lt;/h3&gt;

&lt;p&gt;Deterministically: fix a known graph, write multi-hop questions whose answers you can verify by construction, run each retriever, and count. An LLM judging its own retrieval adds noise. The demo's 1/4 vs 4/4 scorecard reproduces run after run because the check is structural, not judged.&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 03&lt;/a&gt; with the scorecard and notebook&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/aws/rag-vs-graphrag-when-agents-hallucinate-answers-2mcb"&gt;RAG vs GraphRAG: When Agents Hallucinate Answers&lt;/a&gt;, how graph structure prevents hallucinated connections in RAG retrieval&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://neo4j.com/docs/neo4j-graphrag-python/" rel="noopener noreferrer"&gt;neo4j-graphrag for Python&lt;/a&gt;, the two retriever classes used here&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2601.03236" rel="noopener noreferrer"&gt;MAGMA: A Multi-Graph based Agentic Memory Architecture for AI Agents&lt;/a&gt;, Jiang et al., 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2603.27910" rel="noopener noreferrer"&gt;GAAMA: Graph Augmented Associative Memory for Agents&lt;/a&gt;, Paul et al., 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2605.01688" rel="noopener noreferrer"&gt;GRAVITY: Structured Anchoring for Long-Horizon Conversational Memory&lt;/a&gt;, Sun et al., 2026&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;/ul&gt;



&lt;p&gt;Which of the five prompting rules surprised you most? Share in the comments.&lt;/p&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>aws</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Build a Full-Stack Music Station with OpenRouter, Amazon Bedrock, and Nuxt</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Wed, 26 Aug 2026 00:48:03 +0000</pubDate>
      <link>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</link>
      <guid>https://dev.to/aws/build-a-full-stack-music-station-with-openrouter-amazon-bedrock-and-nuxt-5fh4</guid>
      <description>&lt;p&gt;Have you ever been coding and then gotten into that flow state? You know where hours pass by , and it feels to you it's only been a few minutes? Me too. One thing that really helps me get into that state is music. So I create my own music Lo-Fi server called compile and chill. &lt;/p&gt;

&lt;p&gt;As a part of this project, I created three radio stations. Each station can generate a 16:9 scene with &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, compose an instrumental loop with ElevenLabs, and turn an illustration into a six-second video through OpenRouter. Generated files live in private &lt;a href="https://aws.amazon.com/s3/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; storage and return to the browser through the Nuxt server.&lt;/p&gt;

&lt;p&gt;I also added a Stream Deck API interface! &lt;/p&gt;

&lt;p&gt;This tutorial shows how to build this radio station from start to finish. &lt;/p&gt;

&lt;p&gt;The complete source code is available in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill repository&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6tEVRqptHmg" width="100%" height="390"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=6tEVRqptHmg" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;You need the following tools for the complete build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.19 or newer. The locked Nuxt 4.5.2 release requires Node 22.19+, 24.11+, or 26+.&lt;/li&gt;
&lt;li&gt;npm 10 or newer.&lt;/li&gt;
&lt;li&gt;An AWS account and a configured &lt;a href="https://aws.amazon.com/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Command Line Interface (AWS CLI)&lt;/a&gt; profile.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Serverless Application Model (AWS SAM) CLI&lt;/a&gt; for the private storage stack.&lt;/li&gt;
&lt;li&gt;Access to Stability AI Stable Image Ultra through Amazon Bedrock in &lt;code&gt;us-west-2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An ElevenLabs API key for music generation.&lt;/li&gt;
&lt;li&gt;An OpenRouter API key for animated scenes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider credentials are optional. Without them, the UI, bundled scene, station switching, player, and Focus Block timer still work.&lt;/p&gt;

&lt;p&gt;The identity running the app needs &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; plus bucket-scoped permissions for &lt;code&gt;s3:GetObject&lt;/code&gt;, &lt;code&gt;s3:PutObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObject&lt;/code&gt;, &lt;code&gt;s3:DeleteObjectVersion&lt;/code&gt;, and &lt;code&gt;s3:ListBucketVersions&lt;/code&gt;. Use a role or profile scoped to the station bucket rather than an administrator identity.&lt;/p&gt;

&lt;p&gt;For this project I included infrastructure as code with SAM to help setup the AWS parts. It's also included in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Run the station without credentials
&lt;/h3&gt;

&lt;p&gt;Pull down the repo and get started!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ErikCH/compile-and-chill.git
&lt;span class="nb"&gt;cd &lt;/span&gt;compile-and-chill
npm ci
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:8231&lt;/code&gt;. You should see the station UI with the bundled placeholder scene.&lt;/p&gt;

&lt;p&gt;The environment file separates each feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ELEVENLABS_API_KEY=
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
AWS_PROFILE=default
STATION_S3_BUCKET=
STATION_S3_REGION=us-west-2
OPENROUTER_API_KEY=
STATION_BIND_HOST=127.0.0.1
NUXT_CONTROL_TOKEN=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add &lt;code&gt;NUXT_PUBLIC_&lt;/code&gt; to these names. Nuxt exposes public runtime configuration to browser code.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;STATION_BIND_HOST&lt;/code&gt; on loopback unless you need remote control. For Stream Deck access from another machine, bind to a private VPN interface address, never &lt;code&gt;0.0.0.0&lt;/code&gt; or a public IP, and set a long random &lt;code&gt;NUXT_CONTROL_TOKEN&lt;/code&gt;. The server refuses a non-loopback binding without that token.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put provider calls behind Nuxt server routes
&lt;/h3&gt;

&lt;p&gt;The browser should call your application. Compile &amp;amp; Chill keeps provider credentials in Nuxt &lt;code&gt;runtimeConfig&lt;/code&gt; and places provider code under &lt;code&gt;server/api/&lt;/code&gt; and &lt;code&gt;server/utils/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouhe9zlk3lg60u72i7m6.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%2Fouhe9zlk3lg60u72i7m6.png" alt="Architecture diagram showing the browser calling Nuxt server routes, which connect to Amazon Bedrock, ElevenLabs, OpenRouter, and private Amazon S3 storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relevant section of &lt;code&gt;nuxt.config.ts&lt;/code&gt; looks like 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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;elevenLabsApiKey&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;ELEVENLABS_API_KEY&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="na"&gt;imageGenerationRegion&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;BEDROCK_IMAGE_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageGenerationModel&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;BEDROCK_IMAGE_MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stability.stable-image-ultra-v1: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;stationStorageBucket&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;STATION_S3_BUCKET&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="na"&gt;stationStorageRegion&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;STATION_S3_REGION&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openRouterApiKey&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;OPENROUTER_API_KEY&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boundary gives you one place to validate requests, clamp paid parameters, redact signed URLs, and translate provider errors into useful HTTP responses.&lt;/p&gt;

&lt;p&gt;You can inspect the complete routes in &lt;a href="https://github.com/ErikCH/compile-and-chill/tree/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/api" rel="noopener noreferrer"&gt;&lt;code&gt;server/api&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Generate a scene with Amazon Bedrock
&lt;/h3&gt;

&lt;p&gt;Authenticate with your AWS profile first. This example uses &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&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;aws sso login &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the profile and model settings to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_PROFILE=your-profile
BEDROCK_IMAGE_REGION=us-west-2
BEDROCK_IMAGE_MODEL_ID=stability.stable-image-ultra-v1:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image route validates the station mode, visual style, developer presentation, and optional seed. It then calls &lt;code&gt;generateStationScene()&lt;/code&gt; in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-generation.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-generation.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The provider payload is small:&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildScenePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;diversitySeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// Shortened here. The source contains the complete negative prompt.&lt;/span&gt;
  &lt;span class="na"&gt;negative_prompt&lt;/span&gt;&lt;span class="p"&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;logos, readable text, watermark, rear view, motion blur, extra fingers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;visualStyle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;illustrated&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;photograph, live action, photorealistic skin, 3D render&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;cartoon, anime, cel shading, flat illustration&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-to-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;output_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;seed&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;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;InvokeModelCommand&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="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageGenerationModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&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;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;payload&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 server normalizes the seed before constructing the payload. This matters because &lt;code&gt;Number(undefined)&lt;/code&gt; becomes &lt;code&gt;NaN&lt;/code&gt;, and JSON serializes &lt;code&gt;NaN&lt;/code&gt; as &lt;code&gt;null&lt;/code&gt;. Stable Image Ultra expects an integer.&lt;/p&gt;

&lt;p&gt;The prompt also ties visual style to output type. Static scenes use a realistic style. Animated scenes use a clearly illustrated 2D style so the source is visibly non-photorealistic. I added that distinction after a provider's person-likeness classifier refused one of the illustrated anchors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; restart the app, open Visual settings, choose Realistic static, and generate one scene. If the model is unavailable in the configured region, the route returns the provider error and model ID instead of silently switching models.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add looping music with ElevenLabs
&lt;/h3&gt;

&lt;p&gt;The UI sends a 60-second duration, and the server should reject or normalize nonnumeric input before applying its 10-to-60-second bounds. This hardened version avoids sending &lt;code&gt;null&lt;/code&gt; to the provider if another client calls the route with an invalid value:&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;requestedDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;60&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;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestedDuration&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&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;xi-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elevenLabsApiKey&lt;/span&gt;&lt;span class="p"&gt;,&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;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="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildMusicPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;music_length_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;durationSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;music_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generation_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;force_instrumental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sign_with_c2pa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;&lt;code&gt;generation_mode: 'loop'&lt;/code&gt; is one part of making the repeat sound natural. The prompt also asks for no intro pickup, ending cadence, or fade-out. A track written like a normal song sounds broken when it jumps from the ending back to the first beat.&lt;/p&gt;

&lt;p&gt;Compile &amp;amp; Chill keeps one generated track per station mode. Switching from Deep Work to Rainy Debug and back reuses the existing track instead of making another paid request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; add &lt;code&gt;ELEVENLABS_API_KEY&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, restart the server, and generate one 60-second track. Switch modes and confirm that returning to the original mode reuses its audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy the private media library
&lt;/h3&gt;

&lt;p&gt;The repository includes an AWS SAM template managed by &lt;a href="https://aws.amazon.com/cloudformation/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudFormation&lt;/a&gt;. Validate and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2

sam deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/compile-and-chill-private-station.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-confirm-changeset&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-fail-on-empty-changeset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the media bucket output:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that value into &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATION_S3_BUCKET=YOUR_STACK_OUTPUT
STATION_S3_REGION=us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack creates two versioned buckets. One stores generated station media. The other stores &lt;a href="https://aws.amazon.com/cloudtrail/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CloudTrail&lt;/a&gt; data-event logs encrypted with &lt;a href="https://aws.amazon.com/kms/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS Key Management Service (AWS KMS)&lt;/a&gt;. An &lt;a href="https://aws.amazon.com/cloudwatch/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon CloudWatch&lt;/a&gt; alarm watches delete requests against the media bucket.&lt;/p&gt;

&lt;p&gt;The application assigns each browser profile a random station ID in an HttpOnly, SameSite cookie. Objects use this layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stations/&amp;lt;station-id&amp;gt;/manifest.json
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.png
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/scene.mp4
stations/&amp;lt;station-id&amp;gt;/&amp;lt;mode&amp;gt;/music.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser receives same-origin URLs such as &lt;code&gt;/api/library/assets/deepWork/scene&lt;/code&gt;. The route reads the matching S3 object into server memory and returns it. The current implementation is a proxy, not a streaming pass-through, so account for memory when increasing the 50 MiB video limit.&lt;/p&gt;

&lt;p&gt;The storage implementation is in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/station-library.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/station-library.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint:&lt;/strong&gt; regenerate a scene or track, reload the page in the same browser profile, and confirm that the media returns. Open a private browsing window and confirm that it starts with a separate station.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Turn one illustration into a seamless video loop
&lt;/h3&gt;

&lt;p&gt;Animated mode generates a fresh illustrated anchor with Amazon Bedrock, saves it to S3, and creates a 300-second presigned read URL. OpenRouter needs that temporary URL because its video provider must download the input image.&lt;/p&gt;

&lt;p&gt;The trick is to submit the same anchor as both frame constraints:&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;body&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aspect_ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16:9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;generate_audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&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;Animate this 2D cartoon illustration of a fictional character.&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;Use a locked-off camera and keep the illustrated composition unchanged.&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;The provided illustration is both the first and last frame.&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;Allow only tiny two-hand typing motion and one gentle blink.&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;No zoom, pan, cut, identity change, new objects, or geometry changes.&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;Create a silent seamless six-second ambient loop.&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;frame_images&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_frame&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;frame_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last_frame&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;&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%2F0ryc46qkmezlhtirqt03.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%2F0ryc46qkmezlhtirqt03.png" alt="Seven-step animation pipeline showing an Amazon Bedrock anchor saved to Amazon S3, signed for five minutes, used as both video endpoints, polled, downloaded, and saved" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model can move away from the anchor, but the matching frame constraints request a return to the same composition at the end. The prompt limits movement to a small typing motion and one blink, with no camera movement, reframing, cuts, new objects, or identity changes.&lt;/p&gt;

&lt;p&gt;The app discovers compatible video models at runtime. A candidate must support six seconds, 720p, &lt;code&gt;first_frame&lt;/code&gt;, and &lt;code&gt;last_frame&lt;/code&gt;. It prefers Seedance 2.0 Fast, then checks a short fallback list.&lt;/p&gt;

&lt;p&gt;The fallback policy is intentionally narrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP &lt;code&gt;402&lt;/code&gt; stops because credits are unavailable.&lt;/li&gt;
&lt;li&gt;HTTP &lt;code&gt;429&lt;/code&gt; stops because the account is rate limited.&lt;/li&gt;
&lt;li&gt;An input-image moderation refusal may try the next compatible model because no video job was created.&lt;/li&gt;
&lt;li&gt;Other failures stop rather than risk submitting and billing a duplicate job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the complete policy in &lt;a href="https://github.com/ErikCH/compile-and-chill/blob/21c60e6508f6f9c1fbda21ceab18b60a14b20aa1/server/utils/openrouter-video.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/utils/openrouter-video.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Video generation takes minutes rather than seconds, and provider timing and prices change. Treat the request as a paid background job even if the first version runs inside one long HTTP request.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Report real progress during a long request
&lt;/h3&gt;

&lt;p&gt;A spinner cannot tell the user whether a three-minute provider job is moving or stuck. Compile &amp;amp; Chill keeps a process-local job registry with six phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creating anchor
preparing source
submitting video
rendering loop
downloading loop
saving privately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser creates a job ID, starts the generation request, and polls the Nuxt status endpoint every 750 milliseconds. Nuxt polls the provider every 15 seconds, up to 48 times. These are separate loops.&lt;/p&gt;

&lt;p&gt;The registry also rejects a second active job for the same station with HTTP &lt;code&gt;409&lt;/code&gt;. That protects the user from duplicate clicks, but it is not a distributed queue. A process restart loses status, and multiple Nuxt instances would each have their own registry. For a multi-instance deployment, move job state and concurrency control to a shared data store and worker queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Validate the build
&lt;/h3&gt;

&lt;p&gt;Run the same checks used for the source project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current repository has 26 passing tests plus a clean type check and production build.&lt;/p&gt;

&lt;p&gt;Then test one provider at a time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with no credentials and verify the UI.&lt;/li&gt;
&lt;li&gt;Add Amazon Bedrock and generate one static scene.&lt;/li&gt;
&lt;li&gt;Add ElevenLabs and generate one track.&lt;/li&gt;
&lt;li&gt;Deploy the storage stack and verify reload persistence.&lt;/li&gt;
&lt;li&gt;Add OpenRouter last, acknowledge the paid operation, and generate one animated loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order keeps failures small. If the video path fails, you already know that image generation, storage, and browser identity work independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;The cleanup steps remove data permanently. Download anything you want to keep before continuing. The buckets and KMS key use retention policies, so they can remain after stack deletion and may continue to incur charges.&lt;/p&gt;

&lt;p&gt;First, use &lt;strong&gt;Delete saved station&lt;/strong&gt; inside each browser profile whose generated media should be removed. This purges object versions and delete markers under that profile's station prefix.&lt;/p&gt;

&lt;p&gt;Before deleting the stack, record the retained resource names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MEDIA_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='StationMediaBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;AUDIT_BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='CloudTrailLogBucketName'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws kms describe-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="nb"&gt;alias&lt;/span&gt;/compile-and-chill-cloudtrail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; KeyMetadata.KeyId &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete the non-retained stack resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; compile-and-chill-private-station &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The media bucket, audit bucket, and KMS key are retained by design. Empty &lt;strong&gt;all versions and delete markers&lt;/strong&gt; from both versioned buckets in the S3 console, then delete the buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MEDIA_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
aws s3api delete-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDIT_BUCKET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule the retained KMS key for deletion after confirming that you no longer need the encrypted audit logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws kms schedule-key-deletion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KMS_KEY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pending-window-in-days&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KMS key deletion has a waiting period. After deletion, data encrypted only by that key cannot be recovered.&lt;/p&gt;

&lt;p&gt;For local cleanup:&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules .nuxt .output
&lt;span class="nb"&gt;rm&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;This has been a very fun project. Let me know if you try it out!&lt;/p&gt;

&lt;p&gt;You can explore every route, prompt, infrastructure resource, and test in the &lt;a href="https://github.com/ErikCH/compile-and-chill" rel="noopener noreferrer"&gt;Compile &amp;amp; Chill source code&lt;/a&gt;. For another Nuxt project that keeps AI actions behind explicit user approval, read &lt;a href="https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42"&gt;How to Build an AI Agent That Asks Permission First&lt;/a&gt;. Leave a comment on what you think! Thank!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>aws</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Observability for AI Agents with OpenTelemetry</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:32:49 +0000</pubDate>
      <link>https://dev.to/aws/observability-for-ai-agents-with-opentelemetry-3e72</link>
      <guid>https://dev.to/aws/observability-for-ai-agents-with-opentelemetry-3e72</guid>
      <description>&lt;p&gt;AI agent observability means capturing your agent's reasoning cycles, tool calls, and token usage as metrics, traces, and logs. In this guide I build it in three layers with OpenTelemetry (OTEL), then take the same agent to production on Amazon Bedrock AgentCore.&lt;/p&gt;

&lt;p&gt;Your AI agent is in production. A user asks it a question, and it takes thirty seconds, calls five tools, and gives an answer you can't explain. What did it actually do? Which tools did it call? How many times did it "think" before answering? If you can't answer that, you're running agents blind. Traditional monitoring won't help you here: CPU, RAM, and uptime watch the machine, not the reasoning.&lt;/p&gt;

&lt;p&gt;In this post I make a travel-booking agent's &lt;em&gt;normal&lt;/em&gt; behavior visible. No injected failures, no chaos experiments. A real agent doing its job, seen through four increasingly capable lenses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent metrics&lt;/strong&gt;: what the run cost, with zero extra configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry traces&lt;/strong&gt;: the path the agent took, step by step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom trace attributes&lt;/strong&gt;: your business context, on the same trace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt;: the same visibility in Amazon CloudWatch via Amazon Bedrock AgentCore&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything comes from a runnable sample repository: &lt;a href="https://github.com/elizabethfuentes12/observability-for-agents-sample-for-aws" rel="noopener noreferrer"&gt;observability-for-agents-sample-for-aws&lt;/a&gt;. Each demo is keyed to a specific section of the &lt;a href="https://strandsagents.com/docs/user-guide/observability-evaluation/observability/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands Agents observability documentation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on the stack.&lt;/strong&gt; The demos use &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 that emits OpenTelemetry natively. Metrics, hierarchical traces, and span attributes are general agent-observability concepts. The same patterns carry over to other agent frameworks, and Strands is model-agnostic: works with any LLM provider (Amazon Bedrock, Anthropic, local models via Ollama, or others).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What agent are we observing?
&lt;/h2&gt;

&lt;p&gt;All four demos instrument the &lt;strong&gt;same travel agent&lt;/strong&gt;: it searches real sandbox flight fares (Duffel API), checks real weather (Open-Meteo), and books flights into a local SQLite ledger. The only thing that changes, demo to demo, is how much of the agent's internal behavior becomes visible, and where that visibility lives:&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%2Fiyak3m5qkcvf2tzikzjs.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%2Fiyak3m5qkcvf2tzikzjs.png" alt="Four observability lenses: metrics, traces, attributes, production" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 / What metrics do you get with zero configuration in Strands?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Every Strands agent run already carries its own metrics: reasoning cycle count, token usage, and per-tool call counts and timings, exposed through &lt;code&gt;result.metrics.get_summary()&lt;/code&gt;.&lt;/strong&gt; No extra install, no exporter, no setup. Every AI agent run has a &lt;em&gt;shape&lt;/em&gt;, and that shape is captured before you configure anything.&lt;/p&gt;

&lt;p&gt;Compare two lenses on the same run. First, traditional logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEBUG | strands.tools.executors._executor | tool_use=&amp;lt;...name': 'search_flights'...&amp;gt; | streaming
DEBUG | strands.tools.executors._executor | tool_use=&amp;lt;...name': 'get_weather'...&amp;gt; | streaming
DEBUG | strands.tools.executors._executor | tool_use=&amp;lt;...name': 'book_flight'...&amp;gt; | streaming
John Doe's flight from JFK to MIA has been successfully booked ... booking reference BK-JSFPJ5 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Useful for "did this run". Useless for "how much did it cost". Now the built-in metrics, one method call:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Book a one-way flight from JFK to MIA...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight 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;"total_cycles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_duration_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accumulated_usage"&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;"inputTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2520&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outputTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;209&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"totalTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2729&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"search_flights"&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;"call_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"average_time_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.721&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"get_weather"&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;"call_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"average_time_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.434&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"book_flight"&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;"call_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"average_time_s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.006&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is real output from an agent run, and every field answers a question a log line can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;total_cycles: 3&lt;/code&gt;&lt;/strong&gt;. An agent is not a single function call, it's a loop: the model calls a tool, thinks again with the result, calls another. Three cycles here. If this number is ever ten for a basic question, something's wrong, and now you can &lt;em&gt;see&lt;/em&gt; it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;accumulated_usage&lt;/code&gt;&lt;/strong&gt;. 2,729 tokens for the whole booking. Notice input is roughly ten times output; that's typical for agents, because every tool result gets fed back into the model. This is the number that tells you how heavy each request really is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_usage&lt;/code&gt;&lt;/strong&gt;. Three tools, three completely different performance profiles: &lt;code&gt;search_flights&lt;/code&gt; at 0.7 s (a real API call), &lt;code&gt;get_weather&lt;/code&gt; at 1.4 s (another API), &lt;code&gt;book_flight&lt;/code&gt; at 6 &lt;em&gt;milliseconds&lt;/em&gt; (a local write). Without this breakdown, "the agent is slow" is a mystery. With it, it's a diagnosis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more habit worth building from day one: the demo also queries the booking database directly, so you can cross-check what the agent &lt;em&gt;said&lt;/em&gt; ("booked!") against what actually &lt;em&gt;persisted&lt;/em&gt;. In this run, the agent's claim and the ground truth agreed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Honest caveat:&lt;/strong&gt; in Strands 1.47.0, &lt;code&gt;accumulated_metrics.latencyMs&lt;/code&gt; reads &lt;code&gt;0&lt;/code&gt; for some LLM providers. It ships as a &lt;code&gt;TODO&lt;/code&gt; in the provider streaming code (I verified this by reading the installed SDK source). Token counts and per-tool timings are accurate everywhere; treat the top-level &lt;code&gt;latencyMs&lt;/code&gt; as not-yet-implemented.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj3mtgo0zpy4z728aalk.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%2Flj3mtgo0zpy4z728aalk.png" alt="Metrics breakdown showing tool performance" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 2 - How do you trace an AI agent with OpenTelemetry?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Metrics are a flat snapshot, traces are the path.&lt;/strong&gt; A trace records the full hierarchy of one request: which reasoning cycle called which model invocation, which invocation triggered which tool, in what order, with timestamps. In Strands, turning on OpenTelemetry tracing is two lines:&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.telemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StrandsTelemetry&lt;/span&gt;

&lt;span class="n"&gt;strands_telemetry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StrandsTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;strands_telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup_console_exporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# print the span tree to stdout
# strands_telemetry.setup_otlp_exporter()    # or send it to a collector (Jaeger, CloudWatch, ...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;StrandsTelemetry&lt;/code&gt; wires up the OpenTelemetry SDK and registers it as the global tracer provider. Every &lt;code&gt;Agent(...)&lt;/code&gt; call after this is automatically instrumented; there is no manual span-wrapping of your own agent loop. Run the same travel query, and the console prints the documented span hierarchy:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invoke_agent Strands Agents      # the whole run (top-level span)
  execute_event_loop_cycle       # one reasoning cycle
    chat                         # the model invocation for that cycle
    execute_tool search_flights  # one span per tool call
    execute_tool get_weather
    execute_tool book_flight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each span carries attributes. The &lt;code&gt;invoke_agent&lt;/code&gt; span holds the totals (&lt;code&gt;gen_ai.usage.total_tokens: 2725&lt;/code&gt;, &lt;code&gt;gen_ai.request.model&lt;/code&gt;), and each &lt;code&gt;execute_tool&lt;/code&gt; span holds that one call's &lt;code&gt;gen_ai.tool.name&lt;/code&gt;, &lt;code&gt;gen_ai.tool.call.id&lt;/code&gt;, &lt;code&gt;tool.status&lt;/code&gt;, and the formatted tool result. That's enough to answer "did &lt;code&gt;book_flight&lt;/code&gt; fail, and what did it return?" from the trace alone, without re-running anything.&lt;/p&gt;

&lt;p&gt;And because this is standard OpenTelemetry, the console exporter is interchangeable with any OTEL backend. Want a visual UI locally? One Docker command starts Jaeger, one environment variable points the exporter at it, and the agent code doesn't change.&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%2Ffek5kharxp44wbig9jac.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%2Ffek5kharxp44wbig9jac.png" alt="Hierarchical span tree showing agent decision flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 3 — How do you add business context to agent traces?
&lt;/h3&gt;

&lt;p&gt;Out of the box, spans carry &lt;em&gt;technical&lt;/em&gt; attributes: tool name, token counts, status. None of those answer "was this a high-value booking?". That context is yours to add, and the &lt;a href="https://strandsagents.com/docs/user-guide/observability-evaluation/traces/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands traces guide&lt;/a&gt; documents two mechanisms. The demo uses both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static context.&lt;/strong&gt; Agent-level &lt;code&gt;trace_attributes&lt;/code&gt; attach metadata (session ID, user ID, tags) to &lt;em&gt;every&lt;/em&gt; span the agent produces:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_weather&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;trace_attributes&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;session.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo-03-custom-trace-attributes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Dynamic context.&lt;/strong&gt; A hook tags the &lt;em&gt;active span&lt;/em&gt; at the exact moment a business rule fires. An &lt;code&gt;AfterToolCallEvent&lt;/code&gt; callback runs right after each tool call finishes; at that moment, the currently open span &lt;em&gt;is&lt;/em&gt; that tool's &lt;code&gt;execute_tool&lt;/code&gt; span, so &lt;code&gt;trace.get_current_span()&lt;/code&gt; reaches it directly:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.hooks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AfterToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HookProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HookRegistry&lt;/span&gt;

&lt;span class="n"&gt;VIP_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;  &lt;span class="c1"&gt;# low on purpose, so sandbox fares cross it
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TagVipBookings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HookProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register_hooks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HookRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AfterToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tag_if_vip&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;_tag_if_vip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AfterToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_use&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;name&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;book_flight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_use&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;input&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="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;amount&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business.booking_amount_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business.vip_booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run the agent, find the &lt;code&gt;execute_tool book_flight&lt;/code&gt; span, and the custom attributes sit right alongside the SDK's own:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"execute_tool book_flight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gen_ai.tool.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"book_flight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gen_ai.tool.status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"business.booking_amount_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;88.73&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"business.vip_booking"&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="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The detail that matters: this lives on the &lt;strong&gt;trace&lt;/strong&gt;, not in the &lt;strong&gt;conversation&lt;/strong&gt;. The model never sees it. Trace attributes are OpenTelemetry span metadata, entirely separate from the message list, so they add exactly zero tokens to the agent's context. But six months from now, "show me every VIP booking this quarter" is a search on your traces.&lt;/p&gt;
&lt;h2&gt;
  
  
  Production — where does agent observability live when you deploy?
&lt;/h2&gt;

&lt;p&gt;Everything so far lived in your terminal. That's fine while you're developing, but your agent isn't going to run in your terminal, and you won't be there watching console output. The payoff of building on an open standard: everything we made (metrics, traces, attributes) is OpenTelemetry data, and OTEL data is portable. Swap the exporter, and the agent code doesn't change.&lt;/p&gt;

&lt;p&gt;Demo 04 deploys the same travel agent to &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 Runtime&lt;/a&gt;. The production architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent runs on &lt;strong&gt;AgentCore Runtime&lt;/strong&gt; (the code change is one decorator: &lt;code&gt;@app.entrypoint&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The three tools become &lt;strong&gt;AWS Lambda functions&lt;/strong&gt; served through an &lt;strong&gt;AgentCore Gateway&lt;/strong&gt; (a Model Context Protocol endpoint with IAM auth).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;book_flight&lt;/code&gt; writes to &lt;strong&gt;Amazon DynamoDB&lt;/strong&gt; instead of SQLite. Same tool, same booking, real storage.&lt;/li&gt;
&lt;li&gt;One added dependency, &lt;code&gt;aws-opentelemetry-distro&lt;/code&gt; (the AWS Distro for OpenTelemetry), ships the OTEL data to CloudWatch. The Runtime runs your agent under its auto-instrumentation automatically.&lt;/li&gt;
&lt;li&gt;One-time account setup: turn on &lt;strong&gt;CloudWatch Transaction Search&lt;/strong&gt;. Without it, traces don't appear in the console (&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-configure.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el#observability-configure-builtin" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After invoking the deployed agent, open &lt;strong&gt;CloudWatch GenAI Observability&lt;/strong&gt; and you get three views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agents View&lt;/strong&gt;: every AgentCore agent in your account, with invocations, latency, and error rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions View&lt;/strong&gt;: every conversation. Remember the &lt;code&gt;session.id&lt;/code&gt; from Layer 3? This is where it pays off: it's how you go from "something went wrong" to "here's the exact conversation".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces View&lt;/strong&gt;: the same span tree you learned to read in your terminal (&lt;code&gt;invoke_agent&lt;/code&gt; → cycles → &lt;code&gt;chat&lt;/code&gt; + &lt;code&gt;execute_tool&lt;/code&gt;), now rendered as a visual timeline, with every attribute searchable, including &lt;code&gt;business.vip_booking&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo ships the deployment two ways: an AWS CDK stack (&lt;code&gt;cdk deploy&lt;/code&gt;, and &lt;code&gt;cdk destroy&lt;/code&gt; tears down &lt;em&gt;everything&lt;/em&gt;, DynamoDB table included) and a step-by-step boto3 notebook if you want to see every API call.&lt;/p&gt;
&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between logs, metrics, and traces for an AI agent?&lt;/strong&gt;&lt;br&gt;
Logs are timestamped text records of what happened ("tool X was called"). Metrics are measurements of those events (how many times, how long, how many tokens). Traces are the hierarchical timeline connecting them. A log tells you &lt;em&gt;that&lt;/em&gt; something happened, a metric tells you &lt;em&gt;how much&lt;/em&gt; it cost, a trace shows you &lt;em&gt;the path&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need OpenTelemetry for basic agent metrics?&lt;/strong&gt;&lt;br&gt;
No. In Strands, &lt;code&gt;result.metrics.get_summary()&lt;/code&gt; is part of the base SDK: no &lt;code&gt;[otel]&lt;/code&gt; extra, no exporter, no collector. OpenTelemetry comes in when you want traces (Layer 2 onward).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a collector to see traces?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;setup_console_exporter()&lt;/code&gt; prints the full span tree to your terminal. Use &lt;code&gt;setup_otlp_exporter()&lt;/code&gt; when you want a real backend: Jaeger locally, or CloudWatch in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do custom trace attributes cost extra tokens?&lt;/strong&gt;&lt;br&gt;
No. They're OpenTelemetry span metadata, entirely separate from the message list the model sees. The model never reads them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this only work with Strands Agents or AWS?&lt;/strong&gt;&lt;br&gt;
No. An agent loop, hooks, metrics, and OpenTelemetry tracing are general agent-observability concepts. The demos use Strands because these primitives are built in, and Strands is model-agnostic: works with any LLM provider with no change to the agent code. The same patterns carry over to other agent frameworks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Strands' built-in observability compare to manual instrumentation?&lt;/strong&gt;&lt;br&gt;
Strands emits OpenTelemetry spans natively with no manual wrapping. In frameworks without native OTEL support, you'd instrument each tool call and reasoning cycle yourself using the OpenTelemetry SDK directly. The data structure is identical — only the setup differs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrap-up: three layers, one standard
&lt;/h2&gt;

&lt;p&gt;Agent observability, as built here, is three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; tell you &lt;em&gt;what&lt;/em&gt; your agent did and how efficiently: cycles, tokens, tool timings. Free with the SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; show you the &lt;em&gt;path&lt;/em&gt; it took: every decision, in order, with full context. Two lines to turn on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace attributes&lt;/strong&gt; add &lt;em&gt;your&lt;/em&gt; context to that path, so you can search it by what matters to your business. A dictionary and a hook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You build all three once, they travel on OpenTelemetry, and a managed runtime takes them to production with minimal configuration.&lt;/p&gt;

&lt;p&gt;One deliberate boundary: this post is about &lt;strong&gt;observability&lt;/strong&gt;, seeing what an agent already does. It is not about resilience or chaos testing (injecting failures and recovering from them); that's a different, related story. And once you can &lt;em&gt;see&lt;/em&gt; what your agent does, the natural next step is to &lt;em&gt;validate&lt;/em&gt; it. Evaluation builds on exactly this data. You can't validate what you can't see.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The travel agent, all four demos (each self-contained, with a script and a Jupyter notebook), and both production deployment paths are in the sample repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/elizabethfuentes12/observability-for-agents-sample-for-aws" rel="noopener noreferrer"&gt;observability-for-agents-sample-for-aws&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You need Python 3.10+, &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;, an API key for your LLM provider (the demos support multiple providers), and a free &lt;a href="https://app.duffel.com" rel="noopener noreferrer"&gt;Duffel sandbox&lt;/a&gt; token. Demo 01 runs in under a minute:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/elizabethfuentes12/observability-for-agents-sample-for-aws.git
&lt;span class="nb"&gt;cd &lt;/span&gt;observability-for-agents-sample-for-aws/01-agent-metrics
uv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# fill in your LLM provider API key and DUFFEL_API_KEY&lt;/span&gt;
uv run python test_agent_metrics.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Clone it, run it, and stop running your agents blind. Which of your agents would surprise you most if you could see every cycle? Tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;References: &lt;a href="https://strandsagents.com/docs/user-guide/observability-evaluation/observability/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands Agents observability docs&lt;/a&gt; · &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; · &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-get-started.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AgentCore Observability&lt;/a&gt; · &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/view-observability-data-cloudwatch.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudWatch GenAI Observability&lt;/a&gt;&lt;/em&gt;&lt;/p&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>tutorial</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>Amazon DynamoDB Vector Search. No Separate Vector Store</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:40:38 +0000</pubDate>
      <link>https://dev.to/aws/ai-agent-memory-part-2-amazon-dynamodb-vector-search-no-separate-vector-store-35el</link>
      <guid>https://dev.to/aws/ai-agent-memory-part-2-amazon-dynamodb-vector-search-no-separate-vector-store-35el</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;&lt;a href="https://dev.to/aws/do-ai-agents-need-a-vector-database-the-measured-answer-2nf6"&gt;Part 1 of this post&lt;/a&gt; showed how keyword search misses semantic questions and measured two vector backends: FAISS and &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), on the same traveler memories. Both found the answer. The difference was deployment: local vs cloud-managed.&lt;/p&gt;

&lt;p&gt;This part adds a third vector backend: &lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/vector-search.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon DynamoDB Vector Search&lt;/a&gt;&lt;/strong&gt;, generally available since 2025. The question and the memories are identical. Only the backend changes.&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;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;DynamoDB Vector Search: top hit (score 0.231)  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;h2&gt;
  
  
  What is Amazon DynamoDB Vector Search?
&lt;/h2&gt;

&lt;p&gt;It is a vector index added to an existing DynamoDB table. Not a separate service. You define a &lt;code&gt;VectorIndexes&lt;/code&gt; block when you create (or update) the table, and DynamoDB stores the embeddings as a &lt;code&gt;List&lt;/code&gt; attribute on each item. Queries use the &lt;code&gt;SearchVectors&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;The key difference from S3 Vectors: &lt;strong&gt;the vectors live in the same table as your operational data&lt;/strong&gt;. If your agent already reads user preferences or travel records from DynamoDB, you can add a vector index to that same table and query by meaning without provisioning another service.&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;Amazon S3 Vectors&lt;/th&gt;
&lt;th&gt;Amazon DynamoDB Vector Search&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where vectors live&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dedicated vector bucket&lt;/td&gt;
&lt;td&gt;Inside a DynamoDB table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational data collocated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&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;~100-200 ms&lt;/td&gt;
&lt;td&gt;Single-digit ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Billing model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-query + storage&lt;/td&gt;
&lt;td&gt;On-demand (PAY_PER_REQUEST)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ same (same embeddings)&lt;/td&gt;
&lt;td&gt;✅ same (same embeddings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Survives restart&lt;/strong&gt;&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;&lt;strong&gt;Infrastructure to manage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&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;Dedicated vector memory, no operational data to manage&lt;/td&gt;
&lt;td&gt;Agents that already use DynamoDB, or want one service for data + embeddings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are valid choices. S3 Vectors is purpose-built for dedicated vector workloads and the right fit when you want memory completely separate from your operational data. DynamoDB Vector Search is the right fit when your agent data is already in DynamoDB and you want one service for both.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This demo 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;.)&lt;/em&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%2Fmhxcux6ji74jrdq2a3ms.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%2Fmhxcux6ji74jrdq2a3ms.png" alt="DynamoDB Vector Search stores embeddings inside the existing table alongside operational data, unlike S3 Vectors which uses a separate dedicated bucket" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How does the embedding comparison look?
&lt;/h2&gt;

&lt;p&gt;Same question, same Titan V2 embeddings, four backends side by side:&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 answer&lt;/th&gt;
&lt;th&gt;cos_sim&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;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FAISS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3 Vectors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon DynamoDB Vector Search&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.231&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three vector backends return the same top hit with the same score, because they use the same &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; model. The embedding call (~510 ms) still dominates end-to-end latency for all of them. What changes is the query after the embedding.&lt;/p&gt;


&lt;h2&gt;
  
  
  How do you add a vector index to a DynamoDB table?
&lt;/h2&gt;

&lt;p&gt;DynamoDB Vector Search requires &lt;strong&gt;on-demand billing&lt;/strong&gt; (&lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;). The vector index is declared when creating the table:&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-memory-demo-ddb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BillingMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# required for vector indexes
&lt;/span&gt;    &lt;span class="n"&gt;KeySchema&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;AttributeName&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;memory_key&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;KeyType&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;HASH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;AttributeDefinitions&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;AttributeName&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;memory_key&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;AttributeType&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;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;VectorIndexes&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;IndexName&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;memory-vector-index&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;VectorAttribute&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;AttributeName&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;embedding&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;Dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DistanceFunction&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;COSINE&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;Projection&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;ProjectionType&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;ALL&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 self-provisions the table and index if missing: no console steps, no CDK required.&lt;/p&gt;


&lt;h2&gt;
  
  
  How do you write and query vectors?
&lt;/h2&gt;

&lt;p&gt;Embeddings are stored as a DynamoDB &lt;code&gt;List&lt;/code&gt; attribute alongside the rest of the item:&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-memory-demo-ddb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Item&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;memory_key&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;S&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;dietary_notes&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;text&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;S&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;Vegetarian; severe shellfish allergy...&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;embedding&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;L&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;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;  &lt;span class="c1"&gt;# 1024 floats
&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;Querying uses the &lt;code&gt;SearchVectors&lt;/code&gt; API with the same &lt;code&gt;AttributeValue&lt;/code&gt; format:&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;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="nf"&gt;search_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-memory-demo-ddb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IndexName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory-vector-index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SearchVector&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;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;question_vector&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;TopK&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Score note:&lt;/strong&gt; &lt;code&gt;SearchVectors&lt;/code&gt; returns a cosine &lt;em&gt;distance&lt;/em&gt; (lower = more similar). The demo converts it to cosine similarity (&lt;code&gt;1.0 − score&lt;/code&gt;) so the output is directly comparable to FAISS and S3 Vectors.&lt;/p&gt;


&lt;h2&gt;
  
  
  Does the index survive a restart?
&lt;/h2&gt;

&lt;p&gt;Yes. It's DynamoDB. A fresh client instantiated after the demo runs still sees every item:&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;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBVectorStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;   &lt;span class="c1"&gt;# True, all 10 memories are there
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the same restart test run in Part 1 for S3 Vectors. Both pass.&lt;/p&gt;


&lt;h2&gt;
  
  
  How do you run Test 4?
&lt;/h2&gt;

&lt;p&gt;Test 4 runs as part of the existing &lt;code&gt;test_vector_memory.py&lt;/code&gt; in the companion repo:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/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 (Bedrock), S3 Vectors, and DynamoDB. &lt;strong&gt;The demo creates the DynamoDB table and vector index automatically if they don't exist.&lt;/strong&gt; Requires &lt;code&gt;boto3&amp;gt;=1.43.72&lt;/code&gt; (&lt;code&gt;SearchVectors&lt;/code&gt; was added in that release).&lt;/p&gt;


&lt;h2&gt;
  
  
  When do you pick DynamoDB over S3 Vectors?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You have&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No existing DynamoDB table; memory is the only use case&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;S3 Vectors&lt;/strong&gt;, purpose-built for dedicated vector workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An existing DynamoDB table with user data&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;DynamoDB Vector Search&lt;/strong&gt;, add the index to the same table; one service, one billing model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need sub-100 ms query latency &lt;em&gt;after&lt;/em&gt; the embedding call&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;DynamoDB Vector Search&lt;/strong&gt;, single-digit ms where S3 Vectors is subsecond&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;Dedicated vector database (OpenSearch, Qdrant, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Can I add a vector index to an existing DynamoDB table?&lt;/strong&gt;&lt;br&gt;
Yes. Use &lt;code&gt;update_table&lt;/code&gt; with &lt;code&gt;VectorIndexUpdates&lt;/code&gt; to add the index to a table that already has data. Existing items without the embedding attribute won't appear in vector queries until you backfill their embeddings and update the items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does DynamoDB Vector Search work in all regions?&lt;/strong&gt;&lt;br&gt;
Check &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/vector-search.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;regional availability&lt;/a&gt;; the feature is GA but not in every region on launch day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the cost compared to S3 Vectors?&lt;/strong&gt;&lt;br&gt;
DynamoDB Vector Search uses on-demand billing: you pay for read/write capacity units and storage on the table. S3 Vectors charges per query and per stored vector. For agent memory workloads (infrequent queries, small number of vectors per user) both are low cost; the deciding factor is architecture, not price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;SearchVectors&lt;/code&gt; return a distance and not a similarity?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SearchVectors&lt;/code&gt; returns cosine distance (&lt;code&gt;1 − cosine_similarity&lt;/code&gt;), where 0 means identical and 1 means opposite. The demo converts with &lt;code&gt;1.0 − score&lt;/code&gt; to get cosine similarity for easy comparison with FAISS (which returns inner product of normalized vectors, equivalent to cosine similarity) and S3 Vectors (which also returns &lt;code&gt;1 − distance&lt;/code&gt;).&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/tree/main/02-vector-memory-demo" rel="noopener noreferrer"&gt;Companion repo, demo 02&lt;/a&gt; with the full 4-backend test&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/vector-search.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon DynamoDB Vector Search, Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/amazon-dynamodb-now-supports-real-time-vector-search-at-any-scale/?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon DynamoDB Vector Search GA announcement&lt;/a&gt;&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;&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://dev.to/aws/do-ai-agents-need-a-vector-database-the-measured-answer-2nf6"&gt;Part 1, FAISS and S3 Vectors&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;Which surprised you more: the single-digit millisecond DynamoDB latency, or the fact that the cosine similarity score is identical across all four backends? Share in the comments.&lt;/p&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>aws</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Watched My Coding Agent Refuse to Leak a Secret</title>
      <dc:creator>Jonathan Vogel</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:07:05 +0000</pubDate>
      <link>https://dev.to/aws/i-watched-my-coding-agent-refuse-to-leak-a-secret-5cj2</link>
      <guid>https://dev.to/aws/i-watched-my-coding-agent-refuse-to-leak-a-secret-5cj2</guid>
      <description>&lt;p&gt;&lt;strong&gt;A coding agent with AWS access grabs a secret the obvious way and drops the plaintext into its context. AWS Secrets Manager shipped a skill that blocks that and hands the agent a safe path instead. I ran it against a live account in Claude Code and pushed on the edges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give a coding agent shell access with AWS credentials, ask it to hit a database and watch what it does. It reaches for the password the obvious way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ aws secretsmanager get-secret-value --secret-id agent-toolkit-blog-demo/api-token --query SecretString --output text --region us-east-1
{"username":"demo_user","host":"demo-db.example.internal","password":"FAKE-pw-asdf","token":"FAKE-token-asdf"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That value is now definitely in the model's context window and conversation history. It could be sitting in a session transcript on disk and end up passing into whatever tool call the agent makes next. You thought you were using Secrets Manager because you wanted to follow best practices and not have a password sitting around.&lt;/p&gt;

&lt;p&gt;I'll admit I've done this. I've even pasted secrets straight up into the agent. When you gotta move fast, sometimes you let security take a backseat. Don't be me. Now when you're using agents to build on AWS, you can be fast and secure!&lt;/p&gt;

&lt;p&gt;AWS Secrets Manager shipped this nifty feature: a secret safety skill in the &lt;code&gt;aws-core&lt;/code&gt; plugin of 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;. The idea here is simple. An agent should be able to use a secret without ever seeing it. I spent an afternoon running it against a throwaway secret in a real account, then trying to get around it. Here is what happened.&lt;/p&gt;

&lt;p&gt;Before I installed the plugin I told Claude Code the secret was a fake throwaway and asked it to fetch the value and reply in chat. It did it without blinking. With the plugin enabled the same prompt died on the spot, and the part I did not expect is that the skill shut it down before the hook even ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the toolkit is, in one screen
&lt;/h2&gt;

&lt;p&gt;If you're doing stuff with AWS using an agent and have not yet set up the Agent Toolkit for AWS, you need to get it set up. Below is some of the "what" around the toolkit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS MCP Server.&lt;/strong&gt; A managed endpoint your agent talks to for all things AWS. Model training data can be stale, this gives you the capability of running the right AWS CLI commands, searching current AWS docs and logging calls in CloudTrail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills.&lt;/strong&gt; Task runbooks in markdown, loaded on demand, written by people who ran the workflow and found where agents get stuck. The secret safety skill is one of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins.&lt;/strong&gt; A single install that bundles the MCP Server config and a set of skills. &lt;code&gt;aws-core&lt;/code&gt; is the one to start with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using the toolkit is free. Skills and doc search work without credentials. API calls need AWS credentials. You pay for any AWS resources your agent deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;I created a throwaway secret with fake values so nothing real was ever at risk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager create-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; agent-toolkit-blog-demo/api-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-string&lt;/span&gt; &lt;span class="s1"&gt;'{"username":"demo_user","host":"demo-db.example.internal","password":"FAKE-pw-asdf","token":"FAKE-token-asdf"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You already saw the before. &lt;code&gt;get-secret-value&lt;/code&gt; hands back the plaintext and it enters the agent's world.&lt;/p&gt;

&lt;p&gt;Here's the command to install the toolkit in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin install aws-core@claude-plugins-official
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that comes back with &lt;code&gt;Plugin not found&lt;/code&gt;, your local marketplace index is stale. Run &lt;code&gt;/plugin marketplace update claude-plugins-official&lt;/code&gt; and install again. If the marketplace is missing rather than stale, update will not help and you need &lt;code&gt;/plugin marketplace add anthropics/claude-plugins-official&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;The skill activates on its own. So does a second piece I will come back to. One thing to know before you try it. Hooks load when the session starts, so restart your agent after installing or the block will not be there yet. Restart, ask the agent to fetch the secret and the request does not run.&lt;/p&gt;

&lt;h3&gt;
  
  
  The automatic block
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook sits in front of the agent's tool calls. When the call would fetch a secret value, the hook denies it before it executes and hands the agent a message that points at the safe path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Direct secret fetching is blocked. Use {{resolve:secretsmanager:secret-id:SecretString:key}} with asm-exec instead. Run /aws-secrets-manager for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being precise about which layer does what here. When I asked in plain language, the agent never tried at all. The skill had already told it not to, so it declined on its own and pointed me at the safe path, and the hook never fired. I only saw the message above once I insisted it actually run the command. The skill is the part that changes the agent's mind and the hook is the backstop for when it tries anyway.&lt;/p&gt;

&lt;p&gt;I wanted to know how much this actually covers. I tested ten different scenarios to see where the agent might want to reach for a secret and where I might get the skill to trip up when it shouldn't be flagging at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATUS | expected | got   | case
-------+----------+-------+-----------------------------------------
  OK   | deny     | deny  | Bash: aws secretsmanager get-secret-value
  OK   | deny     | deny  | Bash: aws secretsmanager batch-get-secret-value
  OK   | deny     | deny  | use_aws: secretsmanager GetSecretValue
  OK   | deny     | deny  | run_script: boto3 get_secret_value(...)
  OK   | deny     | deny  | Bash: direct SMA daemon curl localhost:2773
  OK   | deny     | deny  | Bash: python3 -c inline boto3 get_secret_value
  OK   | allow    | allow | Bash: asm-exec with {{resolve:...}}
  OK   | allow    | allow | Bash: grep that only mentions get-secret-value
  OK   | allow    | allow | use_aws: secretsmanager CreateSecret (a write, not a fetch)
  OK   | allow    | allow | use_aws: s3 ListBuckets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results are what I expected, well done agent toolkit. It catches the CLI fetch, the batch fetch, the structured API call, a boto3 call buried in a Python script, a &lt;code&gt;curl&lt;/code&gt; straight at the local Secrets Manager Agent daemon and an inline &lt;code&gt;python3 -c&lt;/code&gt; one-liner. It does not trip on a &lt;code&gt;grep&lt;/code&gt; for the bare string &lt;code&gt;get-secret-value&lt;/code&gt;, and it leaves writes and unrelated calls alone. The allowlist for read-only tools is narrower than I assumed though. Search for the full phrase &lt;code&gt;aws secretsmanager get-secret-value&lt;/code&gt; and you get denied whether you run it through &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;rg&lt;/code&gt; or &lt;code&gt;echo&lt;/code&gt;, because the CLI pattern is checked before the allowlist ever applies. I tripped that one myself while grepping my own notes for this post.&lt;/p&gt;

&lt;p&gt;What sold me is that both layers push the same direction. The skill has already taught the agent what to do instead, so whether it stops itself or gets stopped by the hook, it does not stall. It rewrites its own command to use the safe path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolve without seeing
&lt;/h3&gt;

&lt;p&gt;The safe path is a dynamic reference and a small wrapper called &lt;code&gt;asm-exec&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asm-exec &lt;span class="nt"&gt;--&lt;/span&gt; curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer {{resolve:secretsmanager:agent-toolkit-blog-demo/api-token:SecretString:token}}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.example.com/data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command the agent constructs holds a placeholder, not a value. &lt;code&gt;asm-exec&lt;/code&gt; scans the arguments, resolves each &lt;code&gt;{{resolve:...}}&lt;/code&gt; reference inside its own process, then hands the resolved arguments to the real command. The plaintext never lands in the model's context window or the session transcript.&lt;/p&gt;

&lt;p&gt;I pointed it at a local listener that records what it receives, so I could see both sides at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent-visible command:
  asm-exec -- curl -sS -H 'Authorization: Bearer {{resolve:secretsmanager:agent-toolkit-blog-demo/api-token:SecretString:token}}' http://127.0.0.1:8799/data

Response the agent sees:
  {"authenticated": true, "msg": "token accepted"}

Header the listener actually received:
  Bearer FAKE-token-asdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token reached the target. The agent saw a placeholder going in and an API response coming back. It never saw the token.&lt;/p&gt;

&lt;p&gt;To be sure I was not fooling myself, I resolved the reference and piped it straight into &lt;code&gt;shasum&lt;/code&gt; instead of a command, then compared against the hash of the known fake token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ printf %s "FAKE-token-asdf" | shasum -a 256
4953fa5206a3d4a298e6aab9f9c2f74ae50ec2d6f07f374a2df478386674593e  -

$ asm-exec -- sh -c 'printf %s "{{resolve:secretsmanager:agent-toolkit-blog-demo/api-token:SecretString:token}}" | shasum -a 256'
4953fa5206a3d4a298e6aab9f9c2f74ae50ec2d6f07f374a2df478386674593e  -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same hash. The real value was resolved, used and never printed.&lt;/p&gt;

&lt;p&gt;Then the part that closes the loop. I grepped the agent-visible transcript from both runs for the fake values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- unguarded path (aws secretsmanager get-secret-value) --
   FAKE-pw-asdf     found
   FAKE-token-asdf  found
-- safe path (asm-exec + {{resolve}}) --
   FAKE-pw-asdf     not found
   FAKE-token-asdf  not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One path writes your secret into a file on disk. The other runs the exact same task without ever writing it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pieces fit
&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%2Fc85qouasgagj4bxdc4lq.webp" 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%2Fc85qouasgagj4bxdc4lq.webp" alt="An agent's request to use a secret hits the PreToolUse hook. A direct fetch is denied and the agent rewrites it to use asm-exec with a resolve reference. asm-exec calls Secrets Manager, resolves the real value inside its own process where the plaintext stays, runs the target command, and returns only the output to the model context so the secret is never exposed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing I like about the design. &lt;code&gt;asm-exec&lt;/code&gt; never shells out to &lt;code&gt;aws secretsmanager get-secret-value&lt;/code&gt; itself, so the plaintext is never written to a local process's stdout where something could scrape it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this bites
&lt;/h2&gt;

&lt;p&gt;I've talked about how cool this is but I also promised to go over some rough edges. I want to be clear the skill is useful but it is also a best-effort defense. Even AWS says so in the docs. It stops the common leak but it doesn't mean you should stop thinking about security as high priority in your day-to-day actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reduces exposure, it does not prove a negative.&lt;/strong&gt; The block covers the fetch shapes I threw at it and more. A determined agent or a creative prompt can still find a path the hook does not model. Treat this as one layer. For real production stuff, you still need to keep IAM least-privilege underneath it, keep CloudTrail on and scope who can read which secret. The skill lowers the odds of a dumb leak. It does not turn the agent into a trusted process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safe path still calls GetSecretValue.&lt;/strong&gt; It is not read-free. &lt;code&gt;asm-exec&lt;/code&gt; resolves by calling Secrets Manager, it just does it in its own process instead of handing the value back to the agent. Your identity still needs &lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt; on the secret, and the read still shows up in CloudTrail. When it goes through the MCP endpoint the event lists &lt;code&gt;invokedBy&lt;/code&gt;, &lt;code&gt;sourceIPAddress&lt;/code&gt; and &lt;code&gt;userAgent&lt;/code&gt; all as &lt;code&gt;aws-mcp.amazonaws.com&lt;/code&gt;, so you can tell an agent read from one you ran yourself. The difference is where the plaintext lands, not whether the API gets called.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;asm-exec&lt;/code&gt; times out on slow connections&lt;/strong&gt;. I got &lt;code&gt;Failed to resolve&lt;/code&gt; on a fresh network even with valid creds. The read timeout is 10 seconds. A retry cleared it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on something you were going to build anyway
&lt;/h2&gt;

&lt;p&gt;To make sure you get the automatic block you saw in the demo above, install the plugin in Claude Code with this command: &lt;code&gt;/plugin install aws-core@claude-plugins-official&lt;/code&gt;, then restart your session so the hook loads.&lt;/p&gt;

&lt;p&gt;From 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; site, you'll see this one shot command that works for most agents:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This is the easy way to install the toolkit but it does require &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;AWS CLI 2.35+&lt;/a&gt;. This method will auto-detect your agents, install skills, and configure the MCP Server. One catch I hit: it does not install the secret-safety skill you saw here. That one ships with the aws-core plugin (it lives under &lt;code&gt;plugins/aws-core/skills&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Repo and docs below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/agent-toolkit-for-aws" rel="noopener noreferrer"&gt;Agent Toolkit for AWS on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets-ai-agents.html?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Secret safety skill docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/safe-secrets-handling-in-agent-toolkit-for-aws/?trk=23ae1f57-152e-4145-9aa7-04a603514f54&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;What's new post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start read-only. Use the &lt;code&gt;aws:CalledViaAWSMCP&lt;/code&gt; condition key to keep the agent to reads through the MCP Server so people can get used to it, then open up write access once you trust the pattern. Loosening later is easy. Walking back a broad grant is not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have any feedback on this or general thoughts on the toolkit, drop a comment below. Feel free to connect/reach out on &lt;a href="https://www.linkedin.com/in/jonathan-vogel-sw/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>agents</category>
      <category>agentskills</category>
      <category>aws</category>
    </item>
    <item>
      <title>3 Ways to Manage Files in CloudShell</title>
      <dc:creator>Sean Boult</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:48:35 +0000</pubDate>
      <link>https://dev.to/aws/3-ways-to-manage-files-in-cloudshell-3j8n</link>
      <guid>https://dev.to/aws/3-ways-to-manage-files-in-cloudshell-3j8n</guid>
      <description>&lt;p&gt;So you've probably had to open &lt;a href="https://aws.amazon.com/cloudshell/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CloudShell&lt;/a&gt; from time to time to poke at something quickly. Say I wanted to test pulling down an artifact from a private &lt;a href="https://aws.amazon.com/codeartifact/?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;CodeArtifact&lt;/a&gt; registry. To do that, I'd need to tweak my &lt;code&gt;~/.npmrc&lt;/code&gt; so npm points at the right registry.&lt;/p&gt;

&lt;p&gt;Which raises the question: what's the best way to edit a file once you're in CloudShell? Let's look at a few options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using&lt;/strong&gt; &lt;strong&gt;vim&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use Neovim btw (sorry emacs users), so naturally I'm starting with vim.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vim ~/.npmrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you're already comfortable with Vim, this is probably the fastest option. You're already in the terminal, you get all the navigation and search you're used to, and there's nothing else to open.&lt;/p&gt;

&lt;p&gt;Make your changes, hit ESC, and :wq (write + quit) to save the file.&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%2Fbwdh5jfvrkx0kt9mcu90.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%2Fbwdh5jfvrkx0kt9mcu90.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using&lt;/strong&gt; &lt;strong&gt;nano&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ah the old classic. I've been using this for decades to edit files on remote boxes.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/.npmrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Unlike Vim, there's not much to learn before you can start typing. Nano even shows you the important shortcuts at the bottom of the screen.&lt;/p&gt;

&lt;p&gt;Make your changes, hit CTRL+O to write them to disk, and CTRL+X to exit.&lt;/p&gt;

&lt;p&gt;If you just need to make a quick edit and get out, nano is hard to beat.&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%2Fd395dds4l2p9o2c2e2dd.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%2Fd395dds4l2p9o2c2e2dd.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using&lt;/strong&gt; &lt;strong&gt;edit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Okay, but what if you don't want to use a terminal editor at all?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cloudshell/latest/userguide/getting-started.html?trk=02c7b25c-78f4-4968-8fa2-241bdf0bcf97&amp;amp;sc_channel=el#edit-run" rel="noopener noreferrer"&gt;CloudShell has an &lt;code&gt;edit&lt;/code&gt;&lt;/a&gt; command that opens the file directly in its built-in editor.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;edit ~/.npmrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of editing inside the terminal, CloudShell opens the file in a familiar graphical editor where you can point, click, type, and save with CMD+S (CTRL+S on Windows/Linux) or the save icon.&lt;/p&gt;

&lt;p&gt;If you're used to editing files in VS Code or another graphical editor, this is probably going to feel the most familiar.&lt;/p&gt;

&lt;p&gt;No Vim modes. No terminal keybindings to remember.&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%2Faklqayz4l5v6ksi0c4bt.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%2Faklqayz4l5v6ksi0c4bt.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which one should you use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestly, whichever gets you back to solving the actual problem fastest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Editor&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Use it when...&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vim&lt;/td&gt;
&lt;td&gt;You're comfortable with Vim and want to stay in the terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nano&lt;/td&gt;
&lt;td&gt;You want a simple terminal editor without learning Vim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;edit&lt;/td&gt;
&lt;td&gt;You'd rather use a familiar graphical editor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the next time you find yourself debugging something from CloudShell, you've got a few ways to quickly read or edit a file without leaving your browser.&lt;/p&gt;



&lt;p&gt;As always, happy coding 😄!&lt;/p&gt;

&lt;p&gt;Follow AWS for more articles like this and follow me for all things tech.&lt;br&gt;&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;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>aws</category>
      <category>cloudskills</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Build an AI Agent That Asks Permission First (Nuxt + AI SDK 7)</title>
      <dc:creator>Erik Hanchett</dc:creator>
      <pubDate>Tue, 18 Aug 2026 18:38:25 +0000</pubDate>
      <link>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</link>
      <guid>https://dev.to/aws/how-to-build-an-ai-agent-that-asks-permission-first-nuxt-ai-sdk-7-n42</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I did something stupid. I built a superhero-themed Nuxt app, connected it to an Anthropic model through &lt;a href="https://aws.amazon.com/bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, and gave it a tool that deletes files from my computer. In fact, if I wasn't careful, it could have deleted all my files!&lt;/p&gt;

&lt;p&gt;The first time I tried it, I didn't use any sort of approval mechanism. And as you expected it just deleted things. Then I looked into how my coding agent works, and I learned about tool approvals.&lt;/p&gt;

&lt;p&gt;I learned that &lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7&lt;/a&gt; has a tool approval at the model-call level. It works by pausing for an approval, showing an approval window, and then deleting it. I then put &lt;a href="https://kiro.dev/docs/cli/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; behind the same interface using Agent Client Protocol (ACP).&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=RktwtjobGI4" rel="noopener noreferrer"&gt;Watch the full video on YouTube&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or later. AI SDK 7 requires Node.js 22 and uses ECMAScript modules (ESM).&lt;/li&gt;
&lt;li&gt;npm 11 or another package manager that works with Nuxt 4.&lt;/li&gt;
&lt;li&gt;AWS credentials available through the standard provider chain.&lt;/li&gt;
&lt;li&gt;Access to an Amazon Bedrock model in your AWS Region.&lt;/li&gt;
&lt;li&gt;The AWS CLI if you want to list the inference profiles available to your account.&lt;/li&gt;
&lt;li&gt;An authenticated Kiro CLI installation for the optional ACP section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Nuxt app
&lt;/h2&gt;

&lt;p&gt;Create the project and install the versions used in the recorded demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi@latest init nuxt-agent-approval
&lt;span class="nb"&gt;cd &lt;/span&gt;nuxt-agent-approval

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  nuxt@4.5.2 &lt;span class="se"&gt;\&lt;/span&gt;
  vue@3.5.41 &lt;span class="se"&gt;\&lt;/span&gt;
  ai@7.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/vue@4.0.66 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/amazon-bedrock@5.0.57 &lt;span class="se"&gt;\&lt;/span&gt;
  @aws-sdk/credential-providers@3.1111.0 &lt;span class="se"&gt;\&lt;/span&gt;
  @nuxt/ui@4.10.0 &lt;span class="se"&gt;\&lt;/span&gt;
  zod@4.4.3

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @iconify-json/lucide@1.2.123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register Nuxt UI and expose the Amazon Bedrock settings through server-side runtime config:&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;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&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;@nuxt/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&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;~/assets/css/main.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;awsRegion&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;AWS_REGION&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bedrockModelId&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;NUXT_BEDROCK_MODEL_ID&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;Add the two Nuxt UI imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* app/assets/css/main.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"@nuxt/ui"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can compare your setup with the &lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;complete companion project&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Keep the file tool inside a fixture directory
&lt;/h2&gt;

&lt;p&gt;The video uses two fixture files, &lt;code&gt;old-draft.md&lt;/code&gt; and &lt;code&gt;keep-me.md&lt;/code&gt;. Create them before adding the tools:&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; &lt;span class="nt"&gt;-p&lt;/span&gt; fixtures
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can list or remove files in that directory, but it should not accept a path such as &lt;code&gt;../../package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Approval decides whether a tool runs. It does not decide what the tool can reach after it starts.&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;// server/utils/file-tools.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;lstat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rm&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;node:fs/promises&lt;/span&gt;&lt;span class="dl"&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;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isAbsolute&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;node:path&lt;/span&gt;&lt;span class="dl"&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;tool&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputPath&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;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&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;..&lt;/span&gt;&lt;span class="dl"&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;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Path escapes the fixtures directory: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inputPath&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;target&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;listFiles&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;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;List the files in the project fixtures directory.&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;execute&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&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;readdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&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;withFileTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entries&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="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&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;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;Permanently delete one file from the fixtures directory.&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;path&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;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&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;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveInsideFixtures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;info&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;lstat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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;info&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;isFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSymbolicLink&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not a regular file&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;root&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;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolve&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&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;canonicalTarget&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;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonicalTarget&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;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rel&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;..&lt;/span&gt;&lt;span class="dl"&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;isAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;createError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File resolves outside fixtures&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="nf"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonicalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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 second check happens after &lt;code&gt;realpath()&lt;/code&gt;. That catches a path that looked local before resolution but points outside the fixture directory through a symbolic link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect the route to Amazon Bedrock
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I used Amazon Bedrock, but you can use provider with AI-SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create the provider in &lt;code&gt;server/utils/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;createAmazonBedrock&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;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&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;fromNodeProviderChain&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/credential-providers&lt;/span&gt;&lt;span class="dl"&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;useBedrock&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsRegion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createAmazonBedrock&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="nx"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentialProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fromNodeProviderChain&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;&lt;code&gt;fromNodeProviderChain()&lt;/code&gt; uses the AWS credentials already available to your local environment, including &lt;a href="https://aws.amazon.com/iam/identity-center/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS IAM Identity Center&lt;/a&gt; sessions, named profiles, environment variables, and instance roles. You do not need to put a long-lived access key in the Nuxt project.&lt;/p&gt;

&lt;p&gt;Do not copy a model ID from this post. Available IDs vary by account and AWS Region. List the active &lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock inference profiles&lt;/a&gt; for your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws bedrock list-inference-profiles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'inferenceProfileSummaries[?status==`ACTIVE`].inferenceProfileId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set one of the returned profile IDs before starting Nuxt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-2
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Inference profile ID: "&lt;/span&gt; NUXT_BEDROCK_MODEL_ID
&lt;span class="nb"&gt;export &lt;/span&gt;NUXT_BEDROCK_MODEL_ID
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add an unguarded chat route. Starting without approval makes the failure visible:&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;// server/api/chat.post.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;streamText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createUIMessageStreamResponse&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;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AmazonBedrockProvider&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;@ai-sdk/amazon-bedrock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AmazonBedrockProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&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="nf"&gt;readBody&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRuntimeConfig&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;bedrock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBedrock&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&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="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&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;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toUIMessageStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createUIMessageStreamResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stream&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;stopWhen&lt;/code&gt; is important. AI SDK 7 stops after one step by default. The model can call &lt;code&gt;deleteFile&lt;/code&gt;, receive the result, and then stop before it tells the user what happened. Five steps leave room to list, delete, and summarize while keeping the loop bounded.&lt;/p&gt;

&lt;p&gt;At this point, &lt;code&gt;delete old-draft.md&lt;/code&gt; removes the file as soon as the model selects the tool. That is what happened in the first minute of the video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add approval to the delete tool
&lt;/h2&gt;

&lt;p&gt;Add one option to the &lt;code&gt;streamText()&lt;/code&gt; call (toolApproval):&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&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="nf"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bedrockModelId&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BedrockModelId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertToModelMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&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;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&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-approval&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 option lives on &lt;code&gt;streamText()&lt;/code&gt;, not inside the tool definition. The same &lt;code&gt;deleteFile&lt;/code&gt; tool might run unattended in a maintenance job and require a person in a customer-facing chat. &lt;/p&gt;

&lt;p&gt;AI SDK 7 supports more than a yes-or-no policy. A policy function can approve a call, deny it without asking, or send it to the user. This example uses the direct &lt;code&gt;user-approval&lt;/code&gt; status because every delete should stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Render the approval request in Nuxt
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI's chat documentation&lt;/a&gt; follows the same AI SDK message-part model. The recorded app uses &lt;code&gt;useChat()&lt;/code&gt; from &lt;code&gt;@ai-sdk/vue&lt;/code&gt; and the &lt;code&gt;isToolApprovalPending()&lt;/code&gt; helper from Nuxt UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isTextUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isToolUIPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&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;ai&lt;/span&gt;&lt;span class="dl"&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;useChat&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;@ai-sdk/vue&lt;/span&gt;&lt;span class="dl"&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;isToolApprovalPending&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;@nuxt/ui/utils/ai&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;addToolApprovalResponse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;transport&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;DefaultChatTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sendAutomaticallyWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastAssistantMessageIsCompleteWithApprovalResponses&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nf"&gt;sendMessage&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&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;&lt;code&gt;sendAutomaticallyWhen&lt;/code&gt; resumes the interrupted turn after the user answers. Without it, the approval state changes in the browser but the agent does not continue on its own.&lt;/p&gt;

&lt;p&gt;Render tool parts and attach the two decisions while approval is pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"message in messages"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"message.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt;
      &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(part, index) in message.parts"&lt;/span&gt;
      &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"`$&lt;/span&gt;{message.id}-${part.type}-${index}`"
    &amp;gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolUIPart(part)"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tool-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nf"&gt;getToolName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;pre&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"part.input"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isToolApprovalPending(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: true
            })"
          &amp;gt;
            Do it
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

          &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
            &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"addToolApprovalResponse(&lt;/span&gt;{
              id: part.approval!.id,
              approved: false,
              reason: 'The user declined this file deletion.'
            })"
          &amp;gt;
            Nope
          &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isTextUIPart(part)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"onSubmit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"delete old-draft.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"status !== 'ready'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FYI, An approval button is not useful when the person cannot see which path the agent wants to remove.&lt;/p&gt;

&lt;p&gt;The denial reason is also useful. &lt;code&gt;approved: false&lt;/code&gt; tells the model it cannot run the tool. The reason gives it enough context to explain what happened rather than trying the same call again.&lt;/p&gt;

&lt;p&gt;Run the prompt twice. Deny it once and confirm that &lt;code&gt;old-draft.md&lt;/code&gt; remains. Reset the fixtures, repeat the prompt, approve it, and confirm that the file disappears. The model received the same request both times. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Put Kiro CLI behind the same Nuxt UI
&lt;/h2&gt;

&lt;p&gt;The video switches the backend for the final demo. Instead of sending the prompt directly to an Anthropic model through Amazon Bedrock, the Nuxt app talks to Kiro CLI over &lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Agent Client Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ACP gives the app a common way to start an agent session, send a prompt, receive tool events, and answer permission requests. Kiro runs as a separate process and keeps its existing agent tools and Model Context Protocol (MCP) integrations. The Nuxt app remains responsible for the interface and the host tools it exposes.&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%2F343y1kh8jr47vrby673y.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%2F343y1kh8jr47vrby673y.png" alt="ACP Image" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install the AI SDK harness packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness@1.0.73 &lt;span class="se"&gt;\&lt;/span&gt;
  @ai-sdk/harness-acp@1.0.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kiro route creates a &lt;code&gt;HarnessAgent&lt;/code&gt; instead of calling &lt;code&gt;streamText()&lt;/code&gt; directly. The &lt;code&gt;createKiroHarness()&lt;/code&gt; ACP preset and &lt;code&gt;createUnsafeLocalSandbox()&lt;/code&gt; development adapter come from the companion project, so treat this as the route configuration rather than a standalone file:&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;execFileSync&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;node:child_process&lt;/span&gt;&lt;span class="dl"&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;HarnessAgent&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;@ai-sdk/harness/agent&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;kiroExecutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;which&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&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;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;trim&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;HarnessAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createKiroHarness&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="mi"&gt;4100&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createUnsafeLocalSandbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;hostBins&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;harnessId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kiro-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kiroExecutable&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;permissionMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow-reads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Manage files in the fixture project. List files before deleting. Never guess a filename.&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;listFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;toolApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deleteFile&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-approval&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;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents/server" rel="noopener noreferrer"&gt;complete Kiro route and ACP preset&lt;/a&gt; include session creation and approval continuation. When a response comes back from the browser, the route gathers pending approval responses and calls &lt;code&gt;continueStream()&lt;/code&gt; against the Kiro session.&lt;/p&gt;

&lt;p&gt;My recorded run asked more than once before deleting &lt;code&gt;keep-me.md&lt;/code&gt;. Kiro confirmed the target, its permission flow asked to run the tool, and the host &lt;code&gt;deleteFile&lt;/code&gt; policy asked for the final approval. It was a little repetitive, but it exposed an important boundary. Kiro's built-in permissions and AI SDK's host-tool approval are separate systems.&lt;/p&gt;

&lt;p&gt;The local sandbox in this sample is a development adapter. It limits file API paths to a temporary root, but processes still run as the current operating-system user. Replace it with an isolated sandbox provider before exposing a coding agent to untrusted prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Keep approval in its lane
&lt;/h2&gt;

&lt;p&gt;Tool approval is a product DX improvement, it isn't security.&lt;/p&gt;

&lt;p&gt;If a person approves the wrong path, the tool still removes the wrong path. If the tool can reach the rest of the filesystem, approval does not narrow that access. Keep the path checks from Step 2, apply authorization inside the tool, validate inputs on the server, and use an isolated runtime for agents that can run commands.&lt;/p&gt;

&lt;p&gt;The sample app also keeps its recording controls in development mode. The browser can turn approval off for the first demo, but the built app ignores that flag and requires approval. A client-controlled switch that disables confirmation should not ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Stop the Nuxt development server. If you deleted either fixture during the demo, recreate both files before your next run:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Old draft\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/old-draft.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'# Keep me\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/keep-me.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tutorial does not provision AWS resources. Amazon Bedrock requests can still incur charges, so stop sending test prompts when you finish.&lt;/p&gt;

&lt;p&gt;If you delete the local project directory, its files and any local session state are removed. Copy anything you want to keep before deleting it. AWS credentials loaded through the provider chain remain in their original profile or identity-center cache; this app does not write them into the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finale
&lt;/h2&gt;

&lt;p&gt;I learned a lot by adding this tool approval process. And even though I normally use Strands Agents, the AI SDK 7 worked really well.&lt;/p&gt;

&lt;p&gt;Make sure to leave a comment below if you got this far!&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/changelog/ai-sdk-7" rel="noopener noreferrer"&gt;AI SDK 7 changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sdk.vercel.ai/docs/getting-started/nuxt" rel="noopener noreferrer"&gt;AI SDK Nuxt guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ui.nuxt.com/docs/components/chat" rel="noopener noreferrer"&gt;Nuxt UI chat components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kiro.dev/docs/cli/acp/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Kiro CLI ACP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/getting-started-with-cross-region-inference-in-amazon-bedrock/?trk=1ad04439-1c50-4fdd-a845-d07d2655fe7a&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock cross-Region inference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ErikCH/blog-posts/tree/main/apps/ai-sdk-7-nuxt-agents" rel="noopener noreferrer"&gt;Companion code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nuxt</category>
      <category>ai</category>
      <category>aws</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Can't We Just Use Claude for Everything? Agent Creation vs. Agent Engineering</title>
      <dc:creator>Matheus Guimaraes</dc:creator>
      <pubDate>Tue, 18 Aug 2026 16:36:11 +0000</pubDate>
      <link>https://dev.to/aws/why-cant-we-just-use-claude-for-everything-agent-creation-vs-agent-engineering-5aaa</link>
      <guid>https://dev.to/aws/why-cant-we-just-use-claude-for-everything-agent-creation-vs-agent-engineering-5aaa</guid>
      <description>&lt;p&gt;Recently, I was talking to a friend who, much like the rest of the world, has started using AI extensively at work to create agents even though she's not a software developer or from a technical background.&lt;/p&gt;

&lt;p&gt;She has been experimenting with Claude a lot, building useful workflows for herself and her team, and getting increasingly excited about what these systems can do.&lt;/p&gt;

&lt;p&gt;At some point, our conversation moved onto the work I've been doing around agentic systems: agent architectures, context engineering, memory, orchestration, sub-agents and frameworks such as Strands Agents.&lt;/p&gt;

&lt;p&gt;And her reaction was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why do you need all of that? I'm already creating agents.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Great question", I thought to myself.&lt;/p&gt;

&lt;p&gt;And what made it particularly interesting was that I'd heard almost the same question from the opposite direction.&lt;/p&gt;

&lt;p&gt;After giving a talk about agentic architecture and building harnesses with Strands Agents SDK, a junior developer came up to me and asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What's the point of all of this? Why can't we just use Claude for everything?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two people approaching the subject from completely different directions had arrived at the same question.&lt;/p&gt;

&lt;p&gt;And I think that question exposes something important about where we are with agents today. We are using the same word &lt;strong&gt;"agent"&lt;/strong&gt; to describe two very different activities with very different use cases.&lt;/p&gt;

&lt;p&gt;The way I see it, there is a big difference between &lt;strong&gt;creating agents&lt;/strong&gt; and &lt;strong&gt;engineering agents&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent Creation vs. Agent Engineering
&lt;/h2&gt;

&lt;p&gt;Applications such as Claude, ChatGPT, Amazon Quick and others increasingly allow people to create surprisingly sophisticated agentic workflows without building the underlying infrastructure themselves.&lt;/p&gt;

&lt;p&gt;There is still a learning curve, though. You need to provide instructions and give the system knowledge and context in a way that produces results that are within expectation (and, hopefully, without hallucinations!). You may also need to learn about tools, set up integrations, schedule work or even create subagents so multiple pieces of functionality can work together.&lt;/p&gt;

&lt;p&gt;From the user's perspective, they have created an agent. And indeed they have! But when a software engineer looks at the same system... they see something different.&lt;/p&gt;

&lt;p&gt;They see an application providing a highly abstracted, opinionated agent runtime operating as a black box. You have little control over how execution happens, how context is assembled and maintained, how the system scales, or even where memory lives, among other architectural concerns.&lt;/p&gt;

&lt;p&gt;As an application user, you are creating an agent &lt;strong&gt;inside somebody else's agentic architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that's not a problem. In fact, this could be exactly what you need!&lt;/p&gt;

&lt;p&gt;Developers and software architects make trade-offs between abstraction and control all the time. We don't choose the lowest-level technology simply because it gives us the most control; we use abstractions because they remove decisions we don't want to make and complexity we don't need to own.&lt;/p&gt;

&lt;p&gt;The same principle applies here. If Claude gives you everything you need to create an agent, why &lt;em&gt;wouldn't&lt;/em&gt; you use it?&lt;/p&gt;

&lt;p&gt;As usual with system design, the important thing is understanding the trade-offs. The abstraction works because somebody else has made a set of architectural decisions for you. If those decisions suit your requirements, then awesome. If they don't, you may need to take ownership of some of them yourself.&lt;/p&gt;

&lt;p&gt;That's where I find the distinction between &lt;strong&gt;Agent Creation&lt;/strong&gt; and &lt;strong&gt;Agent Engineering&lt;/strong&gt; useful.&lt;/p&gt;

&lt;p&gt;For me, three things make that distinction particularly clear.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is the agent a feature or a component?
&lt;/h2&gt;

&lt;p&gt;This is perhaps my favourite way of thinking about the distinction.&lt;/p&gt;

&lt;p&gt;Let's say I create an agent in Claude to research holidays for me. It knows my preferences, researches destinations, compares options and perhaps uses tools to build an itinerary. It could be incredibly sophisticated... but Claude is still the application. I go to Claude, interact through Claude's interface and rely on Claude's infrastructure to make everything work.&lt;/p&gt;

&lt;p&gt;My agent is a &lt;strong&gt;feature of an application I'm using&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now imagine I want to offer that same experience through my own travel website. A customer comes to my application and asks for a holiday; somewhere behind the scenes an agent researches flights and hotels, applies their preferences, interacts with booking systems and returns the result.&lt;/p&gt;

&lt;p&gt;The capability sounds almost identical, but architecturally something fundamental has changed. The agent is now a &lt;strong&gt;component of a system I'm building&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suddenly I need to decide where it runs, how my application communicates with it, how it scales, how it authenticates against other systems, what happens when it fails, how it fits into traditional application flows, and how I observe what it's doing. The AI is no longer the destination; it's now part of the system I'm responsible for.&lt;/p&gt;

&lt;p&gt;That's very clearly Agent Engineering.&lt;/p&gt;

&lt;p&gt;The point is really about ownership. If an agent is a feature of Claude or another application, you're limited, quite deliberately, to what that application allows you to do. If your requirements fit inside those boundaries, great. When they don't... you need another abstraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do you need control over the economics?
&lt;/h2&gt;

&lt;p&gt;Another reason you might choose to engineer the agent is cost.&lt;/p&gt;

&lt;p&gt;There is a lot of conversation right now about just how many tokens agentic systems can consume, particularly as we build long-running workflows with large context windows, tool calls and multiple agents.&lt;/p&gt;

&lt;p&gt;Moving from Agent Creation to Agent Engineering doesn't automatically make any of this cheaper, by the way. Engineering has its own cost, and it would be rather ironic to spend £100,000 in developer time to save £500 in tokens.&lt;/p&gt;

&lt;p&gt;What engineering gives you is &lt;strong&gt;control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you own the harness around your agents, architectural decisions become available that simply aren't exposed when you're operating inside somebody else's application.&lt;/p&gt;

&lt;p&gt;You can decide that one task doesn't need the full conversation history, cache an expensive result, retrieve memory only when it's relevant, summarize context between steps, or route simpler tasks to cheaper models.&lt;/p&gt;

&lt;p&gt;You can start treating context and tokens as resources that you deliberately engineer.&lt;/p&gt;

&lt;p&gt;There's a much larger conversation here around context engineering, but that's a story for another post. For now, the important point is that &lt;strong&gt;the abstraction determines which levers are available to you&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you don't need those levers, there's little value in taking responsibility for them. If the economics of your system mean you do, Agent Engineering gives you that control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Do you need to own where your data goes?
&lt;/h2&gt;

&lt;p&gt;Then there is a question that tends to become important very quickly in enterprise environments: where does everything actually live?&lt;/p&gt;

&lt;p&gt;When I create an agent inside an application, I'm accepting that application's architecture and its mechanisms for handling context, state, memory, integrations and data. Again, that may be completely acceptable.&lt;/p&gt;

&lt;p&gt;But what if particular data cannot leave a certain environment? What if I need to know exactly where long-term memory is persisted, control which model receives which information, maintain my own audit trail, or guarantee that certain data never crosses a particular boundary?&lt;/p&gt;

&lt;p&gt;Now the black box really matters.&lt;/p&gt;

&lt;p&gt;This is partly a compliance question, partly a data ownership question and, yes, partly a vendor lock-in question. The more behaviour, knowledge and state I build around one application's abstractions, the more dependent I become on that platform's capabilities and constraints.&lt;/p&gt;

&lt;p&gt;But vendor lock-in isn't, by itself, a reason to engineer everything from scratch. We accept varying degrees of lock-in throughout modern software architecture because managed abstractions can provide enormous value.&lt;/p&gt;

&lt;p&gt;The question is simply whether that trade-off is acceptable &lt;strong&gt;for the requirements in front of you&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... Can't We Just Use Claude For Everything?
&lt;/h2&gt;

&lt;p&gt;A useful spreadsheet can eventually become an application, but that doesn't mean the spreadsheet was a mistake. It may have been exactly the right abstraction for the problem at the time, and it may even have helped us understand the problem well enough to know what was worth engineering later.&lt;/p&gt;

&lt;p&gt;I think Agent Creation and Agent Engineering should be seen in much the same way: not as levels on a maturity ladder, and certainly not as a journey where every agent should eventually become an engineered one, but as different architectural choices about how much of the underlying system we need to own. We might happily create an agent inside Claude for one task while engineering another from the ground up, perhaps even within the same system.&lt;/p&gt;

&lt;p&gt;Sometimes an agent created inside Claude, ChatGPT, Amazon Quick or another application will remain there because it does everything we need. Sometimes our requirements will mean taking ownership of the architecture ourselves and we'll find ourselves reaching for Strands Agents SDK or another agent harness.&lt;/p&gt;

&lt;p&gt;And perhaps the next time someone asks me, &lt;em&gt;"Why can't we just use Claude for everything?"&lt;/em&gt;, my answer will simply be...&lt;/p&gt;

&lt;p&gt;You can! For as long as its decisions are compatible with yours.**&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Understanding Async Programming</title>
      <dc:creator>Matheus Guimaraes</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:01:25 +0000</pubDate>
      <link>https://dev.to/aws/understanding-async-programming-4mei</link>
      <guid>https://dev.to/aws/understanding-async-programming-4mei</guid>
      <description>&lt;p&gt;Async programming, short for asynchronous programming, is one of those concepts that might sound intimidating at first but becomes surprisingly intuitive once you understand the problem it is trying to solve.&lt;/p&gt;

&lt;p&gt;And that problem is mostly waiting.&lt;/p&gt;

&lt;p&gt;Modern applications spend a lot of time waiting for things to happen. They wait for APIs to respond, databases to return queries, files to be read or written, and data to travel across a network. Async programming gives us a way to avoid blocking while that happens, allowing other useful work to make progress instead.&lt;/p&gt;

&lt;p&gt;In this post, I’ll cover the basic concepts behind async programming rather than focusing on a particular language. The exact implementation differs between languages and runtimes, but the underlying ideas are largely the same. So if you’re new to async, or have used async and await without ever being completely sure what they’re doing, this is a good place to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Async Programming?
&lt;/h2&gt;

&lt;p&gt;At its core, async programming allows your program to start an operation that may take some time and avoid blocking while it waits for that operation to complete.&lt;/p&gt;

&lt;p&gt;This contrasts with synchronous programming, where execution waits for an operation to finish before continuing past that point.&lt;/p&gt;

&lt;p&gt;Imagine you’re making an API call, a very common task in modern applications. Your application sends a request across the network and then has to wait for a response. Even if that response takes only a fraction of a second, that's a long time in computer terms.&lt;/p&gt;

&lt;p&gt;The important thing is that your application isn't necessarily doing useful computation during all that time. A lot of it is simply waiting for something external to happen.&lt;/p&gt;

&lt;p&gt;This is where async programming shines. Rather than blocking an execution resource while waiting for the API call to complete, asynchronous execution can allow other work to make progress and then continue the original operation when its result becomes available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Doesn't Necessarily Mean Faster
&lt;/h2&gt;

&lt;p&gt;One of the most common misconceptions about async programming is that it's about improving performance. A lot of people think that using async makes their code run faster. However...&lt;/p&gt;

&lt;p&gt;Async does not necessarily make the operation itself any faster.&lt;/p&gt;

&lt;p&gt;Your API call might take exactly the same amount of time to complete whether you call it synchronously or asynchronously. What async changes is what your application can do while it waits. Instead of being blocked by that wait, it can potentially make progress on other work.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is Async Programming Useful?
&lt;/h2&gt;

&lt;p&gt;There are three main benefits to using async programming:&lt;/p&gt;

&lt;p&gt;Improved throughput: Because your application doesn't have to block on one operation before making progress on other work, it can often handle more work over the same period of time. This is particularly valuable in applications such as web servers that may be handling many requests concurrently.&lt;br&gt;
Better resource utilization: Waiting for I/O doesn't necessarily require a thread to sit blocked until the operation completes. With asynchronous I/O, execution resources can potentially be used for other work while the application waits for the database, network, filesystem or another external resource.&lt;br&gt;
Better responsiveness: Async programming can also help applications remain responsive while longer-running operations are taking place. In a user interface, for example, you generally don't want the entire application to freeze while it waits for a network request or file operation to complete.&lt;/p&gt;

&lt;p&gt;The common thread between all three is the same: making better use of time that would otherwise be spent waiting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sync vs. Async: When to Use Each
&lt;/h2&gt;

&lt;p&gt;Async programming sounds great, but that doesn't mean everything should be asynchronous.&lt;/p&gt;

&lt;p&gt;The distinction becomes clearer when we think about I/O-bound and CPU-bound work.&lt;/p&gt;

&lt;p&gt;I/O-bound operations spend a significant amount of their time waiting for something outside the CPU: network requests, database calls, file operations and similar tasks. These are often excellent candidates for async programming because there is useful time to reclaim while the application waits.&lt;/p&gt;

&lt;p&gt;CPU-bound operations are different. If you're performing a heavy calculation, for example, the CPU is actively doing the work rather than waiting for something else to happen. Making that code asynchronous doesn't suddenly make the computation faster.&lt;/p&gt;

&lt;p&gt;CPU-bound work may instead benefit from parallelism, where computation is divided so that multiple pieces of work can execute at the same time, potentially across multiple CPU cores.&lt;/p&gt;

&lt;p&gt;Async programming can also introduce additional complexity and some runtime overhead, so there's little benefit in making code asynchronous simply for the sake of it. It is most useful when your application would otherwise spend meaningful time blocked waiting for I/O.&lt;/p&gt;

&lt;p&gt;Async Isn't the Same as Parallelism&lt;/p&gt;

&lt;p&gt;The distinction between async and parallelism is an important one. Async is primarily about allowing other work to make progress while something is waiting. Parallelism is about performing multiple pieces of work at the same time.&lt;/p&gt;

&lt;p&gt;And async doesn't necessarily mean multithreaded either. Different languages and runtimes implement asynchronous execution in different ways, so you shouldn't assume that an async operation means another thread is somewhere doing the work.&lt;/p&gt;

&lt;p&gt;This is why async can provide concurrency without necessarily providing parallelism: multiple operations can make progress over the same period of time without their code actually executing at the exact same moment.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Does Async Work in Code?
&lt;/h2&gt;

&lt;p&gt;The exact mechanics vary between programming languages, but many modern languages provide the familiar async and await keywords to make asynchronous code easier to write and reason about.&lt;/p&gt;

&lt;p&gt;At a conceptual level, async identifies code that participates in asynchronous execution, while await marks a point where that code depends on the result of an asynchronous operation.&lt;/p&gt;

&lt;p&gt;If that result is already available, execution may simply continue. If it isn't, the function can yield rather than blocking there, allowing other work to make progress. Once the awaited operation completes, execution can continue from that point.&lt;/p&gt;

&lt;p&gt;You can think of await as saying: "I need the result of this operation before I can continue from here, but there's no reason to block everything else while I'm waiting for it."&lt;/p&gt;

&lt;p&gt;The precise details of what happens underneath, whether that involves an event loop, tasks, promises, futures, state machines or something else, depend on the language and runtime. But you don't need to understand all of those implementation details to understand the basic async mental model.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ready to Dive Deeper?
&lt;/h2&gt;

&lt;p&gt;If async still feels a little abstract, I also explain the same ideas visually in the companion video below. Sometimes seeing the synchronous and asynchronous flows side by side is what finally makes the concept click.&lt;/p&gt;

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

</description>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <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;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 Strands Agents for this demo?
&lt;/h2&gt;

&lt;p&gt;Strands makes comparing vector backends straightforward. The demo tests three vector stores (FAISS, S3 Vectors, DynamoDB Vector Search) against the same memories and same embeddings, so the comparison isolates &lt;strong&gt;storage and retrieval performance&lt;/strong&gt;, not the agent framework.&lt;/p&gt;

&lt;p&gt;Adding semantic search to an agent is just a tool:&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;Search memory by meaning, not keywords.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Embed the query, find nearest neighbors
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_store&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;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;recall_memory&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;recall_memory&lt;/code&gt; tool wraps the vector store. Swap FAISS for S3 Vectors or DynamoDB, and the agent code stays the same.&lt;/p&gt;

&lt;p&gt;The pattern shown here (semantic recall as a tool) works in any agent framework. Strands just makes it simple to plug different backends and measure them.&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;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;
  
  
  Two implementations: FAISS to prototype, S3 Vectors to persist
&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;These are two implementations of the same idea for two different moments. &lt;strong&gt;FAISS&lt;/strong&gt; is an in-process library: zero infrastructure, a pip install, running local to the process. It is how you prototype semantic search on your machine (in this demo the index is rebuilt from scratch each run; FAISS can persist to disk with &lt;code&gt;faiss.write_index&lt;/code&gt;, but that is still a file you manage). &lt;strong&gt;Amazon S3 Vectors&lt;/strong&gt; is the managed step: the index lives in a cloud bucket, reachable from any process with AWS credentials, surviving restarts with no cluster to run or scale. You reach for it when the memory has to outlive the process.&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: embed the question with Titan V2, then query the vector store by cosine similarity; the same query returns the same answer whether the store is FAISS in-process or S3 Vectors in the cloud" 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 implementations share: embedding the question costs ~510 ms with Titan V2 in this demo.&lt;/strong&gt; The vector query itself is small next to that, so the embedding call, not the store, is what to budget for in any latency-sensitive path.&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;n/a&lt;/td&gt;
&lt;td&gt;n/a&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;Persists across restarts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (in-process)&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;&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;Is the vector store my latency bottleneck?&lt;/strong&gt;&lt;br&gt;
No. In this demo the vector query is small next to embedding the question (~510 ms with Titan V2), which both implementations pay. Whether you prototype with FAISS in-process or persist to S3 Vectors, budget for the embedding call, not the index lookup.&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>
  </channel>
</rss>
