<?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>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 ben 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"&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;—&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;. The pattern carries over to any agent framework.)&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;th&gt;Query latency&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;—&lt;/td&gt;
&lt;td&gt;—&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;td&gt;&amp;lt;0.1 ms&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;td&gt;~195 ms&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;td&gt;single-digit ms&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 vs ~200 ms&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;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;strictly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
          &lt;span class="s"&gt;crustaceans&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mollusks."&lt;/span&gt;

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

&lt;span class="na"&gt;keyword scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4 hits, answer found&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd0glgw5jd8kt36yo2y3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd0glgw5jd8kt36yo2y3u.png" alt="Cartoon: a robot librarian fails to match a semantic question with keyword scan, then retrieves the answer instantly with a vector embedding magnet: keyword scan fails, semantic search finds it" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;This is the dividing line for semantic search: do you know the key, or only the intent?&lt;/strong&gt; When questions stop matching keys, you retrieve by &lt;em&gt;meaning&lt;/em&gt;: embed each memory once, embed the question, return the nearest neighbors by cosine similarity. This post measures two things (whether semantic search finds what keyword search misses, and which vector store fits your deployment) using the same embeddings and the same memories in the &lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws" rel="noopener noreferrer"&gt;companion repo&lt;/a&gt;.&lt;/p&gt;

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


&lt;h2&gt;
  
  
  Why does key-value memory miss the question?
&lt;/h2&gt;

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

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

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

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


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

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

&lt;/div&gt;


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


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

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

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

&lt;p&gt;The difference is deployment. FAISS is an in-process library — zero infrastructure, a pip install, runs local to the process. It provides disk persistence via &lt;code&gt;faiss.write_index&lt;/code&gt; / &lt;code&gt;faiss.read_index&lt;/code&gt;. In this demo the index is not persisted and is rebuilt from scratch each run. S3 Vectors is a managed AWS service: the index lives in a cloud bucket, reachable from any process with AWS credentials, with no cluster to run or scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon7q1b1lv26m84dboom0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon7q1b1lv26m84dboom0.png" alt="Semantic search flow: embedding the question takes ~510 ms for both backends, then FAISS queries in 0.09 ms (in-process, index rebuilt each run in this demo) and S3 Vectors in 195 ms (cloud index, always available)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

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

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


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

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

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

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

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

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


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

&lt;p&gt;From the tool docstrings, on its own. The demo's last test attaches both recall tools to one Strands agent:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_by_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recall a memory when the question maps to a known identifier.
    Use when the user asks about a stored field: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my preferred cabin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my home airport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_semantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Recall memories by meaning when no key is obvious.
    Use for open questions: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what should I avoid eating on this trip?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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


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

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

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

&lt;p&gt;The companion repo implements and measures all five. Run it to see each decision play out.&lt;/p&gt;


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


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

&lt;/div&gt;


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


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

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

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

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

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

&lt;p&gt;&lt;strong&gt;Why did my semantic search return the wrong memories?&lt;/strong&gt;&lt;br&gt;
The most common causes: the store and the queries use different embedding models or dimensions, memories were embedded with stale text, or keyed facts polluted the index. Keep one embedder for everything, embed at write time, and keep profile facts out of the vector store.&lt;/p&gt;


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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/elizabethfuentes12/stop-ai-agents-losing-memory-sample-for-aws" rel="noopener noreferrer"&gt;Companion repo — demo 02&lt;/a&gt; with the measured tests and notebook&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon S3 Vectors — User Guide&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors-limitations.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;limitations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html?trk=87c4c426-cddf-4799-a299-273337552ad8&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Titan Text Embeddings V2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt;, Meta's similarity search library&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2501.13956" rel="noopener noreferrer"&gt;Zep: A Temporal Knowledge Graph Architecture for Agent Memory&lt;/a&gt;, Rasmussen et al., 2025&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2502.14802" rel="noopener noreferrer"&gt;From RAG to Memory: Non-Parametric Continual Learning for LLMs (HippoRAG 2)&lt;/a&gt;, 2025&lt;/li&gt;
&lt;/ul&gt;



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

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




&lt;div class="ltag__user ltag__user__id__717518"&gt;
    &lt;a href="/elizabethfuentes12" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F717518%2Fb550b165-b8b9-405d-acfb-e5dc846765b0.png" alt="elizabethfuentes12 image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/elizabethfuentes12"&gt;Elizabeth Fuentes L&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/elizabethfuentes12"&gt;I help developers build production-ready AI applications through hands-on tutorials and open-source projects.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;Before doing anything else, I created a directory to keep the files for this setup together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;kiro-crew
&lt;span class="nb"&gt;cd &lt;/span&gt;kiro-crew
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;p&gt;Conceptually, our setup looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows
└── Docker container
    └── Kiro Crew
        └── Agent sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;And instead of the Kiro Crew dashboard, I was greeted by a message telling me I was missing a token.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibs1dbiy2gnkertdm7o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibs1dbiy2gnkertdm7o2.png" alt="Kiro Crew dashboard showing that an access token is required" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



&lt;p&gt;Open that URL in your browser and...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugtlxga5vgxv3hj4i2f8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugtlxga5vgxv3hj4i2f8.png" alt="Kiro Crew running successfully in Docker on Windows" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;In my &lt;code&gt;BUILD_PROMPT.md&lt;/code&gt;, I use this for the service registry pattern. I show the AI what an implementation swap looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/services/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LocalAnalyticsStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./local/analytics-store.local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LocalLinkStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./local/link-store.local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LocalVisitTracker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./local/visit-tracker.local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// SWAP POINT: Replace these with cloud implementations&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyticsStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalAnalyticsStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalLinkStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visitTracker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LocalVisitTracker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;My AWS Blocks skill activates with this frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;building-aws-blocks-apps&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Builds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fullstack&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;TypeScript&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;applications&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using"&lt;/span&gt;
  &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="s"&gt;aws-blocks/blocks. Use when working with any Building Block&lt;/span&gt;
  &lt;span class="s"&gt;(KVStore, DistributedTable, Agent, AuthBasic...), ApiNamespace,&lt;/span&gt;
  &lt;span class="s"&gt;BlocksStack, or the create-blocks-app CLI.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

&lt;/div&gt;



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



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>aidlc</category>
    </item>
    <item>
      <title>Stop Your AI Agent Forgetting User Preferences: Key-Value Memory</title>
      <dc:creator>Elizabeth Fuentes L</dc:creator>
      <pubDate>Tue, 04 Aug 2026 23:12:24 +0000</pubDate>
      <link>https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13</link>
      <guid>https://dev.to/aws/stop-your-ai-agent-forgetting-user-preferences-key-value-memory-a13</guid>
      <description>&lt;p&gt;Here's a test most AI agents fail. A brand-new user searches flights, books one in business class, and asks: &lt;em&gt;"what do you recommend based on what you know about me?"&lt;/em&gt; The agent answers beautifully: business class, non-stop, exactly their taste. Then the process restarts. Same user, same question, and now the answer is generic: the cheapest economy fare. Everything the agent "knew" is gone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F980b7u6sk36i84rliyu2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F980b7u6sk36i84rliyu2.png" alt="Cartoon: an AI assistant offers a personalized business-class ticket, then after one restart offers the same user the cheapest economy fare — the transcript is not memory" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsnhs7v98o87o9nhps85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsnhs7v98o87o9nhps85.png" alt="Why AI agents forget after a restart: within a session the transcript carries the preference, after a restart only agent.state with a session manager survives" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

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

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




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

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Memory wiring&lt;/th&gt;
&lt;th&gt;Structured profile&lt;/th&gt;
&lt;th&gt;Survives restart&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;none (transcript only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent.state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+ &lt;code&gt;FileSessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (local disk)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;+ &lt;code&gt;S3SessionManager&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Amazon S3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qs6lg0ceqa6pe985tyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qs6lg0ceqa6pe985tyl.png" alt="The durability ladder for AI agent key-value memory: transcript only dies on restart, agent.state adds a structured profile, FileSessionManager persists it to disk, S3SessionManager persists it to the cloud" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

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

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




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

&lt;p&gt;From actions. Nobody fills in a "preferences" form; the user &lt;em&gt;books a flight&lt;/em&gt;, and that action reveals their cabin, their tolerance for stops, their price band, their carrier. The stateful &lt;code&gt;book_flight&lt;/code&gt; tool captures all of it as a side effect of doing its job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolContext&lt;/span&gt;

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

&lt;p&gt;After Test 2, this profile exists, and it's inspectable, queryable, and persistable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carriers_flown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"British Airways"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typical_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1382.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1382.22&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F692l1228ww04bl2gdf23.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F692l1228ww04bl2gdf23.png" alt="An AI agent learning user preferences from a booking action instead of a form: the chosen flight offer flows through the book_flight tool into a structured user_preferences profile in agent.state" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;




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

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

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



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

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;FileSessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traveler-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# same id = same user
&lt;/span&gt;        &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo simulates the restart honestly: agent A books (building the profile), then a &lt;strong&gt;brand-new agent instance&lt;/strong&gt; with the same &lt;code&gt;session_id&lt;/code&gt; is created. Measured output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;learned:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;restored:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"preferred_cabin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"business"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefers_nonstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;survived&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;restart:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;True&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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



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

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_flights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_flight&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;session_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;S3SessionManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traveler-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-sessions-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# plain JSON objects — no vectors
&lt;/span&gt;        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kv-memory-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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




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

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

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

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




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

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

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

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




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

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

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

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




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



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

&lt;/div&gt;



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




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

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

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

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

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

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

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




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

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

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
  </channel>
</rss>
