<?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>Forem: Danny Teller</title>
    <description>The latest articles on Forem by Danny Teller (@danny_teller_8d3026771d11).</description>
    <link>https://forem.com/danny_teller_8d3026771d11</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3856238%2Fadd1a2c8-688f-4507-b5c9-9f801aef3d03.jpg</url>
      <title>Forem: Danny Teller</title>
      <link>https://forem.com/danny_teller_8d3026771d11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/danny_teller_8d3026771d11"/>
    <language>en</language>
    <item>
      <title>AgentCore Registry: 16 Skills, 1 Hour, Zero Downtime</title>
      <dc:creator>Danny Teller</dc:creator>
      <pubDate>Sun, 12 Apr 2026 05:43:03 +0000</pubDate>
      <link>https://forem.com/aws-builders/agentcore-registry-16-skills-1-hour-zero-downtime-4ne7</link>
      <guid>https://forem.com/aws-builders/agentcore-registry-16-skills-1-hour-zero-downtime-4ne7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The story of migrating our governance agent from hardcoded skills to dynamic Registry loading — the wins, the gotchas, and what we learned along the way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Bother?
&lt;/h2&gt;

&lt;p&gt;Our AWS governance agent has 16 domain skills — security analysis, cost optimization, network intelligence, the works. Every single one of them was baked into the system prompt on every request. Ask about a single S3 bucket? Here's all 16 skill descriptions anyway.&lt;/p&gt;

&lt;p&gt;That's a lot of tokens doing nothing useful.&lt;/p&gt;

&lt;p&gt;The goal was simple: move skill definitions to AgentCore Registry so the agent loads only what it needs per request. Less prompt bloat, smarter skill selection, and the foundation for per-thread skill loading down the road.&lt;/p&gt;

&lt;p&gt;The key constraint: Registry acts as a &lt;strong&gt;catalog only&lt;/strong&gt;. Tool implementations stay in the agent code where they belong (lowest latency). The Registry just stores metadata — name, description, instructions, which tools a skill can use.&lt;/p&gt;

&lt;p&gt;And because we're not reckless, we added a &lt;code&gt;USE_REGISTRY&lt;/code&gt; env var toggle. Flip it off, and everything goes back to the old local files. Zero drama.&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.amazonaws.com%2Fuploads%2Farticles%2Fsjt8mcaomwk67rdtt57x.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.amazonaws.com%2Fuploads%2Farticles%2Fsjt8mcaomwk67rdtt57x.png" alt="before_after" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;We used Claude Code with agentic planning to break it into 7 phases: SDK upgrades, plugin creation, upload scripts, IAM permissions, tests, this journal, and documentation. The whole thing — plan through production deployment — took about an hour. A few decisions we locked in early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registry as catalog, not runtime&lt;/strong&gt; — tools stay in-process for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One record per skill&lt;/strong&gt; — 16 skills, 16 records, clean mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session-scoped caching&lt;/strong&gt; — fetch the catalog once per session, not every turn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-approval enabled&lt;/strong&gt; — all uploads come from our repo, no human review gate needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup: Console and CLI Only
&lt;/h2&gt;

&lt;p&gt;As of April 2026, AgentCore Registry has no Terraform provider and no CloudFormation support. So we did everything through the AWS Console and CLI — which honestly worked fine for a one-time setup, but worth knowing if you're planning to IaC everything from day one.&lt;/p&gt;

&lt;p&gt;Created the registry in the console. We already had a working JWT auth setup with Azure Entra ID on our main AgentCore agent, so we matched that same configuration. Straightforward.&lt;/p&gt;

&lt;p&gt;Then we needed the Registry ID.&lt;/p&gt;

&lt;p&gt;You'd think there'd be a labeled field with a copy button, like every other AWS service. There isn't. The ID is buried inside the ARN — you have to extract it yourself, or notice it tucked into sample CLI commands at the bottom of the page.&lt;/p&gt;

&lt;p&gt;Not a showstopper, but the kind of thing that makes you squint at the screen for five minutes wondering what you're missing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Dear AWS&lt;/strong&gt;: A "Registry ID" field with a copy button would save everyone some confusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Phase 1–2: Foundation and Plugin
&lt;/h2&gt;

&lt;p&gt;Bumped our SDK versions (&lt;code&gt;strands-agents&lt;/code&gt; and &lt;code&gt;boto3&lt;/code&gt;), added config fields, and built the new plugin — a drop-in replacement for the old &lt;code&gt;AgentSkills&lt;/code&gt; loader.&lt;/p&gt;

&lt;p&gt;One thing worth knowing: &lt;strong&gt;match your Registry auth to your agent's auth.&lt;/strong&gt; If your agent uses IAM, create an IAM-auth registry. If your agent uses OIDC, create a JWT registry. We use OIDC on our agent, so we went with JWT — and since the plugin runs with the agent's IAM execution role, we use the &lt;strong&gt;control plane&lt;/strong&gt; APIs (&lt;code&gt;list_registry_records&lt;/code&gt; + &lt;code&gt;get_registry_record&lt;/code&gt;) which accept IAM regardless of registry type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Upload Scripts (Where Things Got Interesting)
&lt;/h2&gt;

&lt;p&gt;We wrote scripts to push all 16 skill files to the Registry. Two things bit us:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frontmatter problem.&lt;/strong&gt; Our SDK's &lt;code&gt;Skill.instructions&lt;/code&gt; field helpfully strips the YAML frontmatter from skill files. The Registry's upload API helpfully requires it. We switched to reading raw file content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The async creation dance.&lt;/strong&gt; &lt;code&gt;create_registry_record&lt;/code&gt; returns immediately with an ARN but no record ID field. The record is in &lt;code&gt;CREATING&lt;/code&gt; state. You have to: extract the ID from the ARN, wait for it to reach &lt;code&gt;DRAFT&lt;/code&gt;, then submit it for approval as a separate step. None of this is one API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4–5: IAM and Tests
&lt;/h2&gt;

&lt;p&gt;IAM was uneventful — added registry permissions to the execution role, scoped to our specific registry ARN.&lt;/p&gt;

&lt;p&gt;Tests were thorough: 13 unit tests for the plugin (including XML injection protection and caching behavior), 6 for the upload scripts. All green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 6: The Moment of Truth
&lt;/h2&gt;

&lt;p&gt;Deployed with &lt;code&gt;USE_REGISTRY=true&lt;/code&gt;. Opened Slack. Asked Schwarzi a question.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;The logs told the story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skills loaded from AgentCore Registry ✓&lt;/li&gt;
&lt;li&gt;16 skills fetched ✓&lt;/li&gt;
&lt;li&gt;Same tool count as before (30) ✓&lt;/li&gt;
&lt;li&gt;Extra latency: ~1.2 seconds at agent creation, once per session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 1.2 seconds is the catalog fetch. It happens once, then individual skill fetches (~200ms each) are cached for the rest of the session. Acceptable trade-off for dynamic loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But Does It Cost More Tokens?"
&lt;/h2&gt;

&lt;p&gt;This was our first question too. We'd just added API calls and latency — surely we were also burning more tokens?&lt;/p&gt;

&lt;p&gt;No. The LLM sees exactly the same text either way.&lt;/p&gt;

&lt;p&gt;Both plugins inject an &lt;code&gt;&amp;lt;available_skills&amp;gt;&lt;/code&gt; XML block into the system prompt before every turn — 16 skill names and descriptions, same format, same size. When the agent activates a skill, both return the full SKILL.md content as a tool result. Same content, same tokens. The Registry is a different storage backend, not a different prompt.&lt;/p&gt;

&lt;p&gt;The actual cost delta is purely operational: ~1.2 seconds of latency at session start, ~200ms per uncached skill fetch, and 3–4 AWS API calls per session that didn't exist before. Those are real, but they're measured in milliseconds and pennies, not tokens.&lt;/p&gt;

&lt;p&gt;If anything, the Registry &lt;em&gt;sets up&lt;/em&gt; future token savings. Right now we load the full catalog into the prompt every turn. With per-thread skill loading (the next phase), we'd inject only the skills relevant to the current conversation. That's a prompt reduction, not an increase — but it requires the dynamic loading infrastructure we just built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Payoff: Skill Updates Without Redeployment
&lt;/h2&gt;

&lt;p&gt;The best part came after the migration. We needed to fix a skill's instructions — previously, that meant editing the markdown, rebuilding the container, and running &lt;code&gt;agentcore launch&lt;/code&gt;. A full redeployment cycle for a text change.&lt;/p&gt;

&lt;p&gt;Now? Edit the SKILL.md, run the update script, and the agent picks it up on the next session. No container build, no deploy, no downtime. Skill content is decoupled from the agent runtime.&lt;/p&gt;

&lt;p&gt;That's the kind of workflow improvement that compounds over time. Every skill tweak, every prompt refinement, every new tool reference — just a markdown edit and an upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gotcha Tracker
&lt;/h2&gt;

&lt;p&gt;For the detail-oriented, here's everything that went sideways:&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;What Happened&lt;/th&gt;
&lt;th&gt;What We Did&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;Registry ID not labeled in console&lt;/td&gt;
&lt;td&gt;Extracted from ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Registry APIs missing in boto3 &amp;lt; 1.42.88&lt;/td&gt;
&lt;td&gt;Upgraded boto3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Upload API requires YAML frontmatter&lt;/td&gt;
&lt;td&gt;Sent raw file content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Record creation is async, no ID in response&lt;/td&gt;
&lt;td&gt;Parsed ID from ARN, polled for DRAFT status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Data-plane search requires matching auth type&lt;/td&gt;
&lt;td&gt;Used control-plane APIs (accept IAM regardless)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Registry takes ~45s to become READY&lt;/td&gt;
&lt;td&gt;Added polling before uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What We'd Tell Past Us
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Match your Registry auth to your agent.&lt;/strong&gt; IAM agent → IAM registry. OIDC agent → JWT registry. We matched our existing Azure Entra ID config and it worked on the first try.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Control plane is your friend.&lt;/strong&gt; The control-plane APIs (&lt;code&gt;list_registry_records&lt;/code&gt; + &lt;code&gt;get_registry_record&lt;/code&gt;) accept IAM regardless of registry auth type. Use them for catalog fetches from your plugin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Send the raw file, frontmatter and all.&lt;/strong&gt; The SDK strips it; the Registry needs it. Read the file from disk, not from the parsed object.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Record creation is a multi-step process.&lt;/strong&gt; Create → wait for DRAFT → submit for approval → wait for APPROVED. Budget for polling and retries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check your boto3 version.&lt;/strong&gt; Registry APIs landed in 1.42.88. Older versions have the clients but not the operations — a confusing failure mode.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache at the session level.&lt;/strong&gt; The catalog fetch is the slow part (~1.2s). Individual skill lookups are fast (~200ms) and cache well. Don't re-fetch what hasn't changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use an env var toggle for migrations.&lt;/strong&gt; &lt;code&gt;USE_REGISTRY=true/false&lt;/code&gt; with both code paths intact means you can roll back in seconds. No database flags, no deployment. Just flip the switch.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>agentcore</category>
      <category>ai</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>We Built the Same Agent Three Times Before It Worked</title>
      <dc:creator>Danny Teller</dc:creator>
      <pubDate>Wed, 01 Apr 2026 19:10:44 +0000</pubDate>
      <link>https://forem.com/aws-builders/we-built-the-same-agent-three-times-before-it-worked-4ml8</link>
      <guid>https://forem.com/aws-builders/we-built-the-same-agent-three-times-before-it-worked-4ml8</guid>
      <description>&lt;p&gt;Two months ago, our DevOps team set out to build an AWS governance agent. Something that could look across a multi-account AWS organization, find orphaned resources, flag security issues, check tag compliance, and tell you where you're bleeding money — in plain English.&lt;/p&gt;

&lt;p&gt;We had AWS Strands Agents SDK, Amazon Bedrock AgentCore, and a reasonable amount of optimism.&lt;/p&gt;

&lt;p&gt;What followed was two months of building, tearing down, and rebuilding. Three fundamentally different architectures. 18,000 lines of code written and then deleted. And a final system that's simpler than any of the ones that came before it.&lt;/p&gt;

&lt;p&gt;This is the story of how we got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Iteration 1: "The LLM Will Figure It Out"
&lt;/h2&gt;

&lt;p&gt;The first version was the obvious one. Give the LLM a set of AWS API tools — &lt;code&gt;describe_instances&lt;/code&gt;, &lt;code&gt;list_security_groups&lt;/code&gt;, &lt;code&gt;get_cost_and_usage&lt;/code&gt; — and let it call them directly.&lt;/p&gt;

&lt;p&gt;We built an &lt;code&gt;AgentRouter&lt;/code&gt; that received user queries, a &lt;code&gt;CoordinatorAgent&lt;/code&gt; that managed multi-agent flow, and wired it all to boto3 calls. The LLM would receive a question like "find unused security groups in our production VPC," reason about which APIs to call, and chain them together.&lt;/p&gt;

&lt;p&gt;It worked. Sort of.&lt;/p&gt;

&lt;p&gt;The problem wasn't that the LLM couldn't call AWS APIs. It could. The problem was that AWS APIs are inconsistent, paginated, rate-limited, and return wildly different response shapes across services. A question about orphaned EBS volumes required the LLM to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List all volumes&lt;/li&gt;
&lt;li&gt;Filter for &lt;code&gt;available&lt;/code&gt; state&lt;/li&gt;
&lt;li&gt;Cross-reference with instance attachments&lt;/li&gt;
&lt;li&gt;Check if any are in use by ASGs or launch templates&lt;/li&gt;
&lt;li&gt;Handle pagination across all of these&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The LLM would sometimes get this right. Sometimes it'd miss the pagination. Sometimes it'd hallucinate an API parameter. Every query was a fresh adventure in whether the model remembered the exact shape of &lt;code&gt;describe_volumes&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;We were spending tokens on API exploration instead of governance analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: Giving an LLM raw AWS APIs is like giving someone a phone book and asking them to plan a city. The information is there, but the abstraction is wrong.&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fc77ttw243h6mi2elmflm.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.amazonaws.com%2Fuploads%2Farticles%2Fc77ttw243h6mi2elmflm.png" alt="confusion" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Iteration 2: The 8,300-Line Orchestrator
&lt;/h2&gt;

&lt;p&gt;If the LLM couldn't be trusted to navigate AWS APIs on its own, we'd give it structure.&lt;/p&gt;

&lt;p&gt;We built a five-stage deterministic pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt; — determine the intent (security audit, cost review, orphan detection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reason&lt;/strong&gt; — extract entities (account IDs, VPC names, resource types)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route&lt;/strong&gt; — select the right tools and agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt; — run the tools in the correct order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesize&lt;/strong&gt; — compile results into a coherent response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was a hybrid approach. Deterministic control flow with LLM reasoning at each stage. A &lt;code&gt;SemanticIntentClassifier&lt;/code&gt; replaced keyword routing. A &lt;code&gt;SupervisorAgent&lt;/code&gt; managed the pipeline. We added VPC disambiguation, cross-account routing, hallucination guards.&lt;/p&gt;

&lt;p&gt;It felt like progress. The pipeline was predictable. Tests could target each stage. We could reason about failure modes.&lt;/p&gt;

&lt;p&gt;But the orchestrator kept growing. Intent taxonomies needed constant updates. Every new query pattern required new routing logic. The classifier would misroute edge cases, and fixing one route would break another. VPC name resolution alone went through four bug-fix cycles.&lt;/p&gt;

&lt;p&gt;By late February, the orchestrator was 8,300 lines across seven modules. The &lt;code&gt;SupervisorAgent&lt;/code&gt; had been decomposed, recomposed, and decomposed again. We'd built an entire reasoning engine on top of an LLM that was already a reasoning engine.&lt;/p&gt;

&lt;p&gt;We were fighting the model instead of using it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: A deterministic pipeline that wraps an LLM is still deterministic. You get the rigidity of hardcoded flows with the unpredictability of language models. The worst of both worlds.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Iteration 3: The Satellite Architecture
&lt;/h2&gt;

&lt;p&gt;Around the same time, we had a data freshness problem. Direct API calls were slow, rate-limited, and gave you a point-in-time snapshot. We wanted something closer to a continuously updated inventory.&lt;/p&gt;

&lt;p&gt;So we built a satellite architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda scanners&lt;/strong&gt; deployed across every AWS account via StackSets&lt;/li&gt;
&lt;li&gt;Each scanner would enumerate resources on a schedule, enrich them with metadata and relationships&lt;/li&gt;
&lt;li&gt;Results flowed into &lt;strong&gt;S3 Vectors&lt;/strong&gt; — four buckets with 52 vector indexes, organized by domain (core, data, compute, ops)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;embedding service&lt;/strong&gt; (Titan Embed Text v2, 1024 dimensions) vectorized everything&lt;/li&gt;
&lt;li&gt;The agent would query the vector store instead of calling AWS APIs directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure was impressive on a whiteboard. Service-aware vector indexes. Cross-account routing. Relationship graphs embedded alongside resource metadata. A reconciliation pipeline to handle eventual consistency.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;: Titan embedding calls for every resource in every account on every scan. Lambda execution time kept climbing — we bumped timeouts from 10 to 15 to 20 minutes. 57 VPC endpoints to give Lambdas access to S3 Vectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt;: Vector deduplication was a constant battle. Resources would appear twice after re-scans. Cross-account vector routing had subtle bugs where resources from one account would surface in queries about another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshness&lt;/strong&gt;: The thing we built this to solve. Scans ran on schedules, so the vector store was always behind reality. Users would ask about a security group that was created an hour ago and get nothing back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity&lt;/strong&gt;: 3,700 lines of scanner code across eight modules. A 611-line S3 Vectors client. A 388-line relationship index. A 286-line embedding service. Infrastructure stacks for Step Functions, S3 Vectors buckets, scanner Lambdas, and all the IAM plumbing to connect them.&lt;/p&gt;

&lt;p&gt;We had built a distributed data pipeline to feed an LLM that still couldn't reliably answer "show me the unused EBS volumes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: When your data layer is more complex than your analysis layer, you've probably over-solved the wrong problem.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Moment It Clicked
&lt;/h2&gt;

&lt;p&gt;In late February, we stepped back and asked a different question: What if we stopped building infrastructure and started using what AWS already provides?&lt;/p&gt;

&lt;p&gt;AWS Config Aggregator already has a continuously updated inventory of resources across all accounts. It supports SQL queries. It's maintained by AWS. It doesn't need Lambdas, embeddings, or vector stores.&lt;/p&gt;

&lt;p&gt;And Strands SDK already handles tool selection, invocation, and chaining. It doesn't need a five-stage pipeline to decide which tool to call.&lt;/p&gt;

&lt;p&gt;On February 27, we deleted the 8,300-line orchestrator and replaced it with a single Strands agent. On March 2, we deleted the entire scanner infrastructure and replaced S3 Vectors with Config Aggregator queries.&lt;/p&gt;

&lt;p&gt;The diff was dramatic: 18,000 lines removed. The new agent was roughly 1,500 lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Built Instead
&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.amazonaws.com%2Fuploads%2Farticles%2Feq222joteni2dru0v6zh.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.amazonaws.com%2Fuploads%2Farticles%2Feq222joteni2dru0v6zh.png" alt="getting_there" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The current system has one Strands agent running Haiku. No multi-agent orchestration. No intent classification. No vector stores. The LLM picks tools from a registry, and the tools handle the complexity of talking to AWS.&lt;/p&gt;

&lt;p&gt;Here's what makes it work:&lt;/p&gt;

&lt;h3&gt;
  
  
  Data: Config Aggregator + Resource Explorer 2
&lt;/h3&gt;

&lt;p&gt;Instead of building our own data layer, we query AWS's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT resourceId, resourceType, configuration
WHERE resourceType = 'AWS::EC2::SecurityGroup'
AND accountId = 'XXXXXXXXXXXX'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config Aggregator covers ~85% of resource types. Resource Explorer 2 fills the gaps. For anything with zero results, a direct API fallback fires automatically. No Lambdas. No embeddings. No eventual consistency problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools: Dispatchers Over Individual Functions
&lt;/h3&gt;

&lt;p&gt;Early versions exposed 70+ individual tools to the model. Each tool call required the LLM to pick from a massive schema — roughly 30K tokens of tool definitions per request. The model would get confused, pick the wrong tool, or combine tools incorrectly.&lt;/p&gt;

&lt;p&gt;We consolidated into 8 domain dispatchers:&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;security_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&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;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Security operations: findings, sg_rules, nacl_analysis,
    kms_keys, compliance_check, ...&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ACTIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;security_tool&lt;/code&gt; with an &lt;code&gt;action&lt;/code&gt; parameter replaces 11 individual tools. The LLM sees 8 clear categories instead of 70 ambiguous options. Tool schema dropped by 64% — about 10K fewer tokens per request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analysis: Context Builders, Not LLM Reasoning
&lt;/h3&gt;

&lt;p&gt;The earlier architectures asked the LLM to analyze raw AWS API responses. "Here are 200 security groups, figure out which ones are orphaned."&lt;/p&gt;

&lt;p&gt;Now, deterministic context builders pre-process the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orphan detection applies known rules (no attachments, no references, default VPC)&lt;/li&gt;
&lt;li&gt;Security analysis flags known-bad patterns (0.0.0.0/0 ingress, missing encryption)&lt;/li&gt;
&lt;li&gt;Cost analysis identifies savings opportunities from utilization data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM receives pre-analyzed context and focuses on what it's good at: explaining findings in plain language and prioritizing recommendations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skills: Progressive Disclosure
&lt;/h3&gt;

&lt;p&gt;15 domain skills (cost optimization, security analysis, network intelligence, etc.) load on-demand based on query context. Each skill gates which tools the agent can access and provides domain-specific guidance.&lt;/p&gt;

&lt;p&gt;This means the model doesn't see irrelevant tool instructions. A cost question loads cost-optimization guidance and cost-related tools. The context stays focused.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Well-Architected Sub-Agent
&lt;/h3&gt;

&lt;p&gt;For deep assessments, we spawn a separate Strands agent running Sonnet (the main agent runs Haiku). This sub-agent has its own tool set — 20+ check functions for EBS, RDS, IAM, S3 — and its own system prompt tuned for Well-Architected Framework analysis.&lt;/p&gt;

&lt;p&gt;It's the one place where we use a more powerful model, and only when the user explicitly asks for an assessment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strands SDK: What Actually Matters
&lt;/h2&gt;

&lt;p&gt;After using Strands through three architectural iterations, here's what ended up being load-bearing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching&lt;/strong&gt; (&lt;code&gt;CacheConfig(strategy="auto")&lt;/code&gt;): Our system prompt is substantial — core orchestration plus dynamically loaded skills. Caching it across invocations cut latency and cost meaningfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Plugin API&lt;/strong&gt;: A single &lt;code&gt;GovernancePlugin&lt;/code&gt; class registers hooks for before/after tool calls, after model calls, and after invocation. This gives us telemetry, token tracking, and post-response validation without touching the agent's core logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;event.resume&lt;/code&gt;&lt;/strong&gt;: After the agent finishes responding, a hook can inspect the output and inject a follow-up. We use this for orphan analysis validation — if the agent's response doesn't match our deterministic findings, the hook sets &lt;code&gt;event.resume&lt;/code&gt; with a correction prompt and the agent self-corrects. No infinite loops, no separate validation agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming with &lt;code&gt;agent.stream_async()&lt;/code&gt;&lt;/strong&gt;: Real-time progress in Slack. Users see which tools are running, partial results, and the final analysis as it generates. This turned a 30-second wait into a 30-second experience of watching the agent work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent cancellation&lt;/strong&gt;: Long governance scans can take a while. Users can cancel mid-flight, and the agent cleans up gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversation summarization&lt;/strong&gt;: Governance conversations can run long — "now check the other account," "what about the network side." The SDK's &lt;code&gt;SummarizingConversationManager&lt;/code&gt; keeps conversation history manageable while preserving critical context like account IDs and prior findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  AgentCore: Production Without the Ops
&lt;/h2&gt;

&lt;p&gt;AgentCore handles the parts we didn't want to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime hosting&lt;/strong&gt;: Docker container on Graviton, managed by AgentCore. No ECS cluster to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: Short-term and long-term session memory with episodic strategies. Cross-session learning — the agent remembers what it found in previous conversations and injects those reflections into new sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT authentication&lt;/strong&gt;: Corporate identity provider integration for user identity. The agent knows who's asking and can scope responses to their permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt;: Bedrock Guardrails filter content to prevent secrets leakage in governance responses.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bringing It to Slack with AG-UI
&lt;/h2&gt;

&lt;p&gt;An agent that lives behind an API endpoint is useful. An agent that lives in the channel where your team already works is adopted.&lt;/p&gt;

&lt;p&gt;We built a Slack bot as a separate service — Slack Bolt running in Socket Mode on ECS Fargate. When a user messages the bot, it calls the AgentCore-hosted agent over HTTPS. But early versions had a problem: governance scans take 15–30 seconds. Users would type a question, stare at a typing indicator, and wonder if anything was happening.&lt;/p&gt;

&lt;p&gt;AgentCore supports AG-UI (Agent-User Interface), a streaming protocol that surfaces what the agent is doing in real time. We built a custom Strands-to-AG-UI adapter that translates the SDK's internal events into an SSE stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /invocations
Accept: text/event-stream

→ TOOL_CALL_START: discover_resources (scanning 3 accounts...)
→ TEXT_DELTA: "Found 47 security groups across..."
→ TOOL_CALL_START: security_tool (analyzing findings...)
→ TEXT_DELTA: "12 groups have unrestricted ingress..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same endpoint serves both modes. &lt;code&gt;Accept: text/event-stream&lt;/code&gt; gets the SSE stream; a normal request gets synchronous JSON. The Slack bot consumes the stream and progressively updates the Slack message — users see tools firing, partial results appearing, and the final analysis building in real time.&lt;/p&gt;

&lt;p&gt;A few things we learned the hard way about streaming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never clear your accumulated response text when a new tool call starts. The agent can call tools mid-response, and clearing the buffer silently drops everything it said before the tool call.&lt;/li&gt;
&lt;li&gt;Track unique tool &lt;em&gt;stages&lt;/em&gt;, not individual calls, for progress display. Otherwise you flood the Slack message with duplicate updates.&lt;/li&gt;
&lt;li&gt;Track text offsets after both tool-call-start and tool-call-end events. Text can appear between tool calls, and missing either offset creates gaps in the streamed output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: what used to be a 30-second black box is now a 30-second live feed of the agent working through the problem. Users trust it more because they can see it thinking.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Satellite Architecture&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;~25,000&lt;/td&gt;
&lt;td&gt;~7,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool definitions exposed to LLM&lt;/td&gt;
&lt;td&gt;70+&lt;/td&gt;
&lt;td&gt;25 (8 dispatchers + 17 individual)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool schema tokens per request&lt;/td&gt;
&lt;td&gt;~30K&lt;/td&gt;
&lt;td&gt;~10K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure components&lt;/td&gt;
&lt;td&gt;S3 Vectors (4 buckets), 52 indexes, Lambda scanners, Step Functions, 57 VPC endpoints&lt;/td&gt;
&lt;td&gt;Config Aggregator (AWS-managed), 20 VPC endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data freshness&lt;/td&gt;
&lt;td&gt;Minutes to hours (scan schedule)&lt;/td&gt;
&lt;td&gt;Real-time (Config Aggregator)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding costs&lt;/td&gt;
&lt;td&gt;Per-resource per-scan&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live test pass rate&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;109/109 (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What We'd Tell Ourselves Two Months Ago
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Not enough coverage" isn't always a good reason to build your own.&lt;/strong&gt; We dismissed Config Aggregator early because our research showed it didn't cover every resource type we wanted to query. So we built an entire scanning pipeline to get 100% coverage. What we discovered later: Config Aggregator covers ~85% of resource types, Resource Explorer 2 fills most of the gaps, and a simple direct API fallback handles the rest. Three lines of fallback logic replaced 3,700 lines of scanner code. Perfect coverage wasn't worth the complexity cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't build orchestration on top of orchestration.&lt;/strong&gt; Strands SDK is already a tool-use loop. Adding a five-stage pipeline on top of it added complexity without adding capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token budget is an architecture constraint.&lt;/strong&gt; 70 tools meant 30K tokens of schema per request. The dispatcher pattern wasn't just cleaner code — it was a 64% reduction in per-request cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic preprocessing, LLM synthesis.&lt;/strong&gt; The best division of labor: code handles data collection and known-rule analysis. The LLM handles explanation, prioritization, and natural language. Don't ask the model to do what a SQL query can do better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship the boring version first.&lt;/strong&gt; Every complex architecture we built was an attempt to solve problems we hadn't actually encountered yet. The current system handles real governance queries from real users. That's the bar.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://github.com/strands-agents/sdk-python" rel="noopener noreferrer"&gt;AWS Strands Agents SDK&lt;/a&gt; and &lt;a href="https://aws.amazon.com/bedrock/agentcore/" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
