<?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: Dennis Traub</title>
    <description>The latest articles on Forem by Dennis Traub (@dennistraub).</description>
    <link>https://forem.com/dennistraub</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%2F752988%2Fea2b8598-0758-4ffa-b8d7-d904360f46e4.png</url>
      <title>Forem: Dennis Traub</title>
      <link>https://forem.com/dennistraub</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dennistraub"/>
    <language>en</language>
    <item>
      <title>Your agent keeps using that word ...</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Thu, 21 May 2026 20:52:57 +0000</pubDate>
      <link>https://forem.com/aws/your-agent-keeps-using-that-word--4g36</link>
      <guid>https://forem.com/aws/your-agent-keeps-using-that-word--4g36</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"You keep using that word. I do not think it means what you think it means."&lt;/strong&gt; &lt;br&gt;
— Inigo Montoya, &lt;em&gt;The Princess Bride&lt;/em&gt; (1987)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two people using the same word while meaning completely different things has been a staple of comedy for centuries.&lt;/p&gt;

&lt;p&gt;Lewis Carroll built an entire scene around it in 1871:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"When I use a word,"&lt;/em&gt; Humpty Dumpty tells Alice, &lt;em&gt;"it means just what I choose it to mean - neither more nor less."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now imagine what happens if you give Humpty Dumpty a code editor and 1M tokens of context...&lt;/p&gt;

&lt;p&gt;An AI coding agent is the most confident version of Humpty Dumpty you'll ever encounter. It will interpret your vocabulary however it sees fit, and it will &lt;em&gt;never&lt;/em&gt; tell you it's confused. It won't raise its hand and ask &lt;em&gt;"wait, do you mean a&lt;/em&gt; purchase &lt;em&gt;order or a&lt;/em&gt; delivery &lt;em&gt;order?"&lt;/em&gt; It will pick one interpretation, generate plausible code, and move on. As confidently as ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI agents amplify vocabulary ambiguity
&lt;/h2&gt;

&lt;p&gt;The problem isn't new. Teams have always talked past each other. But in traditional development, ambiguity encounters friction: code reviews, pair programming, whiteboard sessions, the &lt;em&gt;"wait, what&lt;/em&gt; exactly &lt;em&gt;do you mean by 'order'?"&lt;/em&gt; conversation over coffee. Misunderstandings cascade, but they mostly get caught eventually.&lt;/p&gt;

&lt;p&gt;With AI agents, that friction disappears. The code compiles, the tests pass, and the semantic mismatch only surfaces when a downstream system interprets the same word differently. Or when the wrong action leads to a production incident.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.danielschleicher.com/software/engineering,/ai,/spec-driven/development/2026/01/04/removing-ambiguity-with-spec-driven-development.html" rel="noopener noreferrer"&gt;How Creating a Ubiquitous Language Ensures AI Builds What You Actually Want&lt;/a&gt;, Daniel Schleicher wrote: &lt;em&gt;"LLMs are amplifiers. When we give an AI agent ambiguous instructions where 'order' could mean a dozen different things, it amplifies the chaos by generating code that reflects our confusion."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The flip side is equally true: precise vocabulary gets amplified into correct class names, correct lifecycle states, and correct boundaries. The agent works with whatever you give it - if you give it ambiguity, then that's what it amplifies.&lt;/p&gt;

&lt;p&gt;Russell Miles tells a story about a finance agent that confused &lt;em&gt;booking&lt;/em&gt; (revenue recognition in one bounded context) and &lt;em&gt;reservation&lt;/em&gt; (risk allocation in another):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The agent mashed them like a bad DJ, and began taking action that would've landed the CFO in handcuffs."&lt;/em&gt;&lt;br&gt;
— Russell Miles, &lt;em&gt;&lt;a href="https://engineeringagents.substack.com/p/domain-driven-agent-design" rel="noopener noreferrer"&gt;Domain Driven Agent Design&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ambiguous words, different contexts, no clear definitions, and code that compiled just fine all the way to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ubiquitous Language: DDD's pattern for shared vocabulary
&lt;/h2&gt;

&lt;p&gt;The good news is that &lt;strong&gt;&lt;a href="https://www.domainlanguage.com/ddd/" rel="noopener noreferrer"&gt;Domain-Driven Design (DDD)&lt;/a&gt;&lt;/strong&gt;, a widely-adopted methodology, introduced by Eric Evans in 2003, provides both the vocabulary and the pattern for a solution. It's called &lt;strong&gt;Ubiquitous Language&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the model as the backbone of a language. Commit the team to exercising that language relentlessly in all communication within the team and in the code.&lt;br&gt;
— Eric Evans, &lt;em&gt;Domain-Driven Design&lt;/em&gt; (2003)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Martin Fowler &lt;a href="https://martinfowler.com/bliki/UbiquitousLanguage.html" rel="noopener noreferrer"&gt;describes it&lt;/a&gt; as &lt;em&gt;"the practice of building up a common, rigorous language between developers and users."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And Vaughn Vernon sharpens it even more: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"[...] a shared language developed by the team - a team comprised of both domain experts and software developers"&lt;/em&gt;&lt;br&gt;
— Vaughn Vernon, &lt;em&gt;Implementing Domain-Driven Design&lt;/em&gt; (2013)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important distinction here: It's not a glossary imposed from above, not industry jargon adopted wholesale. It is a language the team builds together through use.&lt;/p&gt;

&lt;p&gt;But note that - despite the name - the Ubiquitous Language is all &lt;em&gt;but&lt;/em&gt; ubiquitous. In Vernon's words: &lt;em&gt;"There is one Ubiquitous Language per Bounded Context."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "Customer" really mean?
&lt;/h2&gt;

&lt;p&gt;The term "Customer" seems universal - it shows up in the codebase of virtually every single business application.&lt;/p&gt;

&lt;p&gt;But look at how each subdomain actually refers to the same person: marketing calls them a &lt;em&gt;lead&lt;/em&gt;, sales calls them a &lt;em&gt;prospect&lt;/em&gt;, the warehouse calls them a &lt;em&gt;consignee&lt;/em&gt;, finance calls them a &lt;em&gt;debtor&lt;/em&gt;, and support calls them a &lt;em&gt;claimant&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;All these subdomains may refer to the same person, but the words they use aren't synonyms. They carry different attributes, different lifecycle states, different invariants, and different behavior. &lt;/p&gt;

&lt;p&gt;And nobody really "creates" or "deletes" anything: a lead is &lt;em&gt;captured&lt;/em&gt;, a prospect is &lt;em&gt;qualified&lt;/em&gt;, a purchase intent is &lt;em&gt;confirmed&lt;/em&gt;, a shipment is &lt;em&gt;dispatched&lt;/em&gt;. Each subdomain has its own language, refined over decades - or centuries - of practice.&lt;/p&gt;

&lt;p&gt;Flattening all of this into a &lt;code&gt;Customer&lt;/code&gt; class with a &lt;code&gt;type&lt;/code&gt; field and CRUD operations may &lt;em&gt;feel&lt;/em&gt; like it's simplifying things. But it actually erases the precision and clarity each domain has built, and replaces it with generic ambiguity, which the code, the database schema, the team, and - more often than not - the users of the software have to work around.&lt;/p&gt;

&lt;p&gt;When writing the original book, Evans described the Ubiquitous Language for teams of humans - developers, product managers, and domain experts - modelling a shared domain.&lt;/p&gt;

&lt;p&gt;But now, we have a new participant: the coding agent. And when the agent reads your prompt, your system instructions, your project rules, it's doing something analogous to a new developer onboarding onto the team. &lt;/p&gt;

&lt;p&gt;Except it onboards every session, with no memory of last time's disambiguation, and no instinct to ask when a term's exact meaning is unclear. Tell it &lt;em&gt;"delete the order"&lt;/em&gt; and it will. Generically, ambiguously, and very confidently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practitioners applying DDD's Ubiquitous Language to AI agents
&lt;/h2&gt;

&lt;p&gt;I'm not the only one seeing this. Across the industry, people are arriving at the same pattern from different directions.&lt;/p&gt;

&lt;p&gt;Martin Fowler described at the &lt;a href="https://www.youtube.com/watch?v=CZs8J1ZD0CE" rel="noopener noreferrer"&gt;Pragmatic Summit 2026&lt;/a&gt; how a colleague is &lt;em&gt;"developing a precise language to communicate about the domain with the agent... that's basically the kind of model building, language building, domain-driven design stuff that we're used to doing, but it makes him more efficient to talk to the agent."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.aihero.dev" rel="noopener noreferrer"&gt;Matt Pocock&lt;/a&gt; built a glossary skill that scans the codebase, extracts terminology into markdown, and feeds it to the agent. Reading the agent's thinking traces, he found it improved implementation alignment while even reducing verbosity - an unexpected benefit in terms of token use and cost.&lt;/p&gt;

&lt;p&gt;Daniel Schleicher formalized it with the &lt;em&gt;"Spec Ambiguity Resolver"&lt;/em&gt; pattern: a living glossary where AI flags ambiguous terms, proposes definitions, and waits for human approval. Once approved, consistency is enforced project-wide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The greatest leverage in modern software development with AI lies not in accelerating typing, but in establishing semantic agreement before implementation begins."&lt;/em&gt;&lt;br&gt;
— Daniel Schleicher, &lt;em&gt;&lt;a href="https://www.danielschleicher.com/software/engineering,/ai,/spec-driven/development/2026/01/04/removing-ambiguity-with-spec-driven-development.html" rel="noopener noreferrer"&gt;How Creating a Ubiquitous Language ...&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tim Oleson named &lt;em&gt;"Language Consistency - usage of ubiquitous language in agent communications"&lt;/em&gt; as a first-class &lt;a href="https://www.linkedin.com/pulse/domain-driven-designs-role-agentic-services-mcp-tools-tim-oleson-rwipe" rel="noopener noreferrer"&gt;domain alignment metric&lt;/a&gt; for agent systems.&lt;/p&gt;

&lt;p&gt;Whether they came to it through practicing Domain-Driven Design or arrived at it independently, the pattern is the same. And it shows up in places you might not expect: every MCP tool name is a vocabulary decision the LLM reasons over. &lt;code&gt;confirm_purchase_intent()&lt;/code&gt; and &lt;code&gt;submit_order()&lt;/code&gt; produce different agent behavior even when the underlying operation is identical. Chris Hughes &lt;a href="https://medium.com/@chris.p.hughes10/building-scalable-mcp-servers-with-domain-driven-design-fb9454d4c726" rel="noopener noreferrer"&gt;writes&lt;/a&gt; that the tools layer &lt;em&gt;"protects your domain from the LLM's interface requirements - translating between 'strings the LLM can reason about' and 'rich domain objects your code works with.'"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Marc Brooker's &lt;a href="https://brooker.co.za/blog/2025/12/16/natural-language.html" rel="noopener noreferrer"&gt;Specification Loop&lt;/a&gt; explains how this compounds over time: each resolved ambiguity becomes a stable term that enriches the shared vocabulary. The next conversation starts from a higher baseline and the loop gets cheaper over time as the vocabulary grows. &lt;em&gt;"I believe that the trips around the loop are fundamental to the success of the whole enterprise. They're what we've been doing all along."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to define domain vocabulary for your coding agent
&lt;/h2&gt;

&lt;p&gt;The Ubiquitous Language isn't a document you have to write upfront and rigidly apply. It's a living artifact that evolves through use.&lt;/p&gt;

&lt;p&gt;A common practice in teams: whenever someone realizes there's an ambiguity - two people using the same word differently, or one word meaning two things - the team pauses, discusses, and agrees on a term. And this doesn't have to be the academically "correct" term. What matters is that everyone on the team knows exactly what it means. Then you write it down, and use it consistently. In conversations, in code, and in the documentation.&lt;/p&gt;

&lt;p&gt;This practice naturally extends to conversations with a coding agent. Whenever there's a misunderstanding, missing clarity, or ambiguity in what the agent produces, spend a moment to establish a specific term for the concept. Define it. Add it to whatever artifact represents the current shared state of the vocabulary: your &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENT.md&lt;/code&gt;, your cursor rules, or just a simple &lt;code&gt;domain-terms.md&lt;/code&gt;, referenced as the canonical glossary of terms. The agent will start using it from that point forward, each disambiguation compounds, and every new conversation starts from a higher baseline.&lt;/p&gt;

&lt;p&gt;But remember: each bounded context has its own vocabulary. If your application has a fulfillment module and a billing module, each gets its own language section in that module's CLAUDE.md (or whatever file your agent reads when entering a directory). The terms in one context don't need to match the terms in another - that's the whole point.&lt;/p&gt;

&lt;p&gt;Here's what a &lt;code&gt;modules/fulfillment/CLAUDE.md&lt;/code&gt; might contain:&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="gu"&gt;## Fulfillment Domain Language&lt;/span&gt;

&lt;span class="gs"&gt;**Consignee**&lt;/span&gt;:
The person or entity receiving a shipment. Identified by delivery
address, contact phone, and access instructions. A consignee may
differ from the purchaser.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Customer, Buyer, User

&lt;span class="gs"&gt;**Consignment**&lt;/span&gt;:
The full set of items to be delivered to a consignee. May be
fulfilled as one or more Shipments depending on stock location
and logistics.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Order, Delivery (a delivery is one shipment arriving,
not the full set)

&lt;span class="gs"&gt;**Shipment**&lt;/span&gt;:
A physical package dispatched to a consignee via a carrier. Has
tracking number, weight, dimensions, and a declared value.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Order, Package (ambiguous with code packages)

&lt;span class="gs"&gt;**Dispatch**&lt;/span&gt;:
The act of handing a shipment to a carrier. Irreversible - once
dispatched, the shipment is the carrier's responsibility.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Send, Ship (verb), Process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;modules/billing/CLAUDE.md&lt;/code&gt; in the same application, even though it might refer to the same physical person or purchase event, is using terms native to its own domain:&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="gu"&gt;## Billing Domain Language&lt;/span&gt;

&lt;span class="gs"&gt;**Debtor**&lt;/span&gt;:
The legal entity liable for payment. Identified by tax ID, billing
address, and agreed payment terms (net-30, net-60, etc.).
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Customer, Account, User

&lt;span class="gs"&gt;**Invoice**&lt;/span&gt;:
A legal demand for payment tied to one or more line items. Carries
a due date, VAT status, and regulatory classification. An invoice
is never deleted - it is reversed with a &lt;span class="gs"&gt;**Credit Note**&lt;/span&gt;.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Order, Bill (ambiguous with infrastructure billing)

&lt;span class="gs"&gt;**Settlement**&lt;/span&gt;:
The act of a debtor satisfying an invoice in full or in part.
Records payment method, date, and reconciliation reference.
&lt;span class="ge"&gt;*Avoid*&lt;/span&gt;: Payment (ambiguous - is it the act or the money?),
Transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both examples describe the same person making the same purchase. But the vocabularies are completely different, because the contexts model different responsibilities, different invariants, different behaviors, and different lifecycles.&lt;/p&gt;

&lt;p&gt;Tell an agent &lt;em&gt;"handle the customer's order"&lt;/em&gt; without this, and you get a generic &lt;code&gt;Customer&lt;/code&gt; class with a &lt;code&gt;type&lt;/code&gt; field. Tell it within a defined context, and you get a &lt;code&gt;Consignee&lt;/code&gt; with a delivery address or a &lt;code&gt;Debtor&lt;/code&gt; with payment terms.&lt;/p&gt;

&lt;p&gt;The vocabulary manifests in an architecture that captures the richness of the business domains it models. And - whichever context you're in - the agent knows exactly what you mean.&lt;/p&gt;

&lt;p&gt;And remember: You don't need a formal glossary to start. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All you need is one line the next time something goes wrong: *"In this project, 'order' means a confirmed purchase intent, not a request or a reservation."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add it. Use it. Let it accumulate.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want to learn more about how Domain-Driven Design maps to the age of coding agents, I recently wrote about how engineers building MCP servers are &lt;a href="https://dev.to/aws/rediscovering-domain-driven-design-one-mcp-server-at-a-time-1i79"&gt;reaching for DDD's Bounded Contexts and Anti-Corruption Layers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>software</category>
      <category>architecture</category>
      <category>ddd</category>
    </item>
    <item>
      <title>AI isn't replacing junior devs. Your org chart is.</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Tue, 19 May 2026 15:50:26 +0000</pubDate>
      <link>https://forem.com/aws/ai-isnt-replacing-junior-devs-your-org-chart-is-1edp</link>
      <guid>https://forem.com/aws/ai-isnt-replacing-junior-devs-your-org-chart-is-1edp</guid>
      <description>&lt;p&gt;Two of this year's most-shared pieces on AI's effect on junior software engineers read as direct contradictions.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://brooker.co.za/blog/2026/03/25/ic-junior.html" rel="noopener noreferrer"&gt;What about juniors?&lt;/a&gt;&lt;/strong&gt;, Marc Brooker (VP, Distinguished Engineer at AWS) argues that junior developers have a structural advantage. Their accumulated heuristics haven't calcified into reflex yet, so they don't have to unlearn anything. The field, he says, is &lt;em&gt;"more powerful than ever"&lt;/em&gt; for someone willing to expand their scope.&lt;/p&gt;

&lt;p&gt;Around the same time, Mark Russinovich (CTO, Deputy CISO at Microsoft Azure) and Scott Hanselman (VP, Member of Technical Staff at Microsoft) published &lt;strong&gt;&lt;a href="https://dl.acm.org/doi/10.1145/3779312" rel="noopener noreferrer"&gt;Redefining the Software Engineering Profession for AI&lt;/a&gt;&lt;/strong&gt;, a paper arguing the exact opposite: AI imposes a &lt;em&gt;"drag"&lt;/em&gt; on early-career developers. Juniors must steer, verify, and integrate AI output before they have an opportunity to develop the judgment required to do it well. If organizations focus only on short-term efficiency, they &lt;em&gt;"risk hollowing out the next generation of technical leaders."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both papers were widely discussed, both were treated as the definitive take, and they appear to flatly disagree.&lt;/p&gt;

&lt;p&gt;What if they don't?&lt;/p&gt;

&lt;h2&gt;
  
  
  What if both sides are right about AI and junior developers?
&lt;/h2&gt;

&lt;p&gt;Brooker is describing what &lt;em&gt;individual&lt;/em&gt; junior developers can do. The person who arrives without wrong heuristics, who's comfortable expanding scope into business context and system ownership, who carries a pager and learns economics alongside code. That person will thrive. The absence of accumulated assumptions becomes an advantage when the assumptions are expiring anyway.&lt;/p&gt;

&lt;p&gt;Russinovich and Hanselman are describing what happens to &lt;em&gt;the cohort&lt;/em&gt; of junior developers. When companies stop hiring juniors, or hire them and pair them with AI agents instead of experienced engineers, or measure only short-term output velocity, the organizational pipeline dries out, because the structure fundamental to developing judgment has been removed.&lt;/p&gt;

&lt;p&gt;Both claims are true, operating at different scales. Brooker is talking about individual disposition. Russinovich and Hanselman are talking about organizational design. The apparent contradiction doesn't exist, they're just zooming into different levels.&lt;/p&gt;

&lt;p&gt;Which means the question worth asking isn't &lt;em&gt;"will AI hurt or help junior developers?"&lt;/em&gt; but &lt;em&gt;"who is making that decision, and are we making it intentionally?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How automation has reorganized professions before
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Arthur_M._Wellington" rel="noopener noreferrer"&gt;Arthur M. Wellington&lt;/a&gt;, a 19th century railroad engineer known for his work on infrastructure economics, defined engineering as &lt;em&gt;"the art of doing well with one dollar what any bungler can do with two after a fashion."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The "art" in his example is the art of building &lt;em&gt;economically&lt;/em&gt;. When execution becomes cheap, the profession reorganizes around judgment, trade-offs, and constraint management.&lt;/p&gt;

&lt;p&gt;And this is a pattern, not a prediction.&lt;/p&gt;

&lt;p&gt;The clearest quantitative example comes from manufacturing. When CNC machines automated metalworking through the 1970s and onward, operators running hand-operated tools were displaced. But the profession didn't shrink. In &lt;strong&gt;&lt;a href="https://www.nber.org/papers/w30400" rel="noopener noreferrer"&gt;Computerized Machine Tools and the Transformation of US Manufacturing&lt;/a&gt;&lt;/strong&gt;, a 2022 NBER study tracking four decades of data, the research team found that employment for college graduates in affected industries rose 86% from a low base, while losses hit high school graduates at 7-8%.&lt;/p&gt;

&lt;p&gt;The tasks that grew in demand were, in the researchers' words, &lt;em&gt;"more conceptual and socially connected, and require more training, preparation, and learning"&lt;/em&gt; than the ones that declined.&lt;/p&gt;

&lt;p&gt;Execution became programmable, and the profession reorganized around judgment, customization, and system-level thinking.&lt;/p&gt;

&lt;p&gt;You probably already heard the spreadsheet version of this story. After Excel's introduction, bookkeepers and clerks shrank from 2 million to 1.5 million, but accountants, auditors, and financial managers surged. When the ledger work got cheap, the profession moved upstream into analysis and advisory.&lt;/p&gt;

&lt;p&gt;The most structurally similar case, though, is playing out in a different discipline right now: large law firms are shrinking junior ranks as discovery and contract review are being automated. &lt;/p&gt;

&lt;p&gt;The human center shifts to narrative construction, litigation strategy, and client counseling. But moving professions upstream isn't that easy: Discovery was historically where junior lawyers developed judgment. The grind of reviewing thousands of documents taught pattern recognition, relevance filtering, and adversarial thinking. Automating the execution layer removed the drudgery - along with the training ground.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;The pattern is consistent: when execution gets cheap, professions reorganize around judgment. The question that varies, case by case, is whether the reorganization &lt;em&gt;includes&lt;/em&gt; the next generation, or &lt;em&gt;discards&lt;/em&gt; them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hiring decisions you're already making - whether you're aware or not
&lt;/h2&gt;

&lt;p&gt;Every company that chose not to open a junior headcount this quarter made this decision. Every team that paired a senior engineer with an AI coding agent instead of an early-career developer made it too. Every manager who measures output velocity without accounting for the mentorship that isn't happening anymore makes it by default.&lt;/p&gt;

&lt;p&gt;These are decisions being made today. Mostly unintentionally.&lt;/p&gt;

&lt;p&gt;The complication that makes this harder than &lt;em&gt;"just hire juniors and pair them up"&lt;/em&gt;: Brooker's other essay from around the same time, &lt;strong&gt;&lt;a href="https://brooker.co.za/blog/2026/03/20/ic-leadership.html" rel="noopener noreferrer"&gt;My heuristics are wrong. What now?&lt;/a&gt;&lt;/strong&gt;, describes an &lt;em&gt;"extinction-level event for rules of thumb."&lt;/em&gt; Seniors are simultaneously revising their own heuristics while being asked to transmit judgment. The experienced engineer's mental model of system maintainability, code costs, API design, and service boundary assumptions is actively being invalidated in near-real time.&lt;/p&gt;

&lt;p&gt;This is why Russinovich and Hanselman's preceptor model (structured mentoring through paired practice) specifies a year-long pairing as &lt;em&gt;equals&lt;/em&gt;, not a senior dispensing wisdom to an intern. The model works because both parties are learning &lt;em&gt;together&lt;/em&gt;. The senior contributes pattern recognition and system context. The junior contributes comfort with AI tooling and freedom from heuristics that no longer apply. Neither is the teacher. Both are learning.&lt;/p&gt;

&lt;p&gt;Your choice, if you're hiring, is between &lt;strong&gt;making this structural decision consciously&lt;/strong&gt; or &lt;strong&gt;letting it happen unintentionally&lt;/strong&gt; - having a hundred headcount conversations without ever considering your pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you're a junior: How to build judgment without waiting for your org
&lt;/h2&gt;

&lt;p&gt;Everything above is about forces acting on you. Forces you can't change, like organizational decisions, cohort dynamics, and pipeline economics.&lt;/p&gt;

&lt;p&gt;But you can still optimize, regardless of which way your company chooses.&lt;/p&gt;

&lt;p&gt;Brooker's prescription is straightforward: expand scope. Engage with business context, customer needs, economics, and trade-offs. Own systems. Carry a pager. Don't wait for the structure to develop your judgment. Build it yourself by going where the ambiguity is.&lt;/p&gt;

&lt;p&gt;Albert Zhao, a fellow AWS engineer, &lt;a href="https://youtu.be/ZkzkdhmcIVQ" rel="noopener noreferrer"&gt;made this concrete in a recent video&lt;/a&gt;: rather than &lt;em&gt;"learn to code better,"&lt;/em&gt; the path is &lt;em&gt;"learn to make decisions about systems"&lt;/em&gt;: understand why a service boundary exists where it does, why a particular trade-off was chosen, and what the second-order effects are when requirements change.&lt;/p&gt;

&lt;p&gt;The organizational design question is one you can't control. What you &lt;em&gt;can&lt;/em&gt; control is whether you're building the kind of judgment that matters on either side of the decision.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>software</category>
      <category>career</category>
      <category>leadership</category>
    </item>
    <item>
      <title>Rediscovering Domain-Driven Design, one MCP server at a time</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Mon, 18 May 2026 13:17:09 +0000</pubDate>
      <link>https://forem.com/aws/rediscovering-domain-driven-design-one-mcp-server-at-a-time-1i79</link>
      <guid>https://forem.com/aws/rediscovering-domain-driven-design-one-mcp-server-at-a-time-1i79</guid>
      <description>&lt;p&gt;A few days ago, a devops engineer &lt;a href="https://www.reddit.com/r/devops/comments/1tc01ui/mcp_servers_just_showed_up_in_our_infrastructure/" rel="noopener noreferrer"&gt;posted on r/devops&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"MCP servers just showed up in our infrastructure and I genuinely have no idea how to secure them, anyone been through this?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Filesystem access, shell permissions, database connectors - all callable by agents without human approval. At the time I'm writing this, the thread has 76 upvotes and 39 comments from fellow engineers improvising solutions: "separate by blast radius," "don't mix list_files and execute_shell in one server," "three security surfaces, not one."&lt;/p&gt;

&lt;p&gt;They're all describing the same thing, rediscovering patterns that Eric Evans described in &lt;strong&gt;&lt;a href="https://www.domainlanguage.com/ddd/" rel="noopener noreferrer"&gt;Domain-Driven Design (DDD)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In his book, Eric introduced concepts like &lt;strong&gt;Bounded Contexts&lt;/strong&gt; and &lt;strong&gt;Anti-Corruption Layers&lt;/strong&gt;, which gave us the vocabulary we've been using for system boundaries ever since. They helped us survive the microservices transition, and they apply directly to the architectural problems AI systems are creating right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  We made the same mistakes a decade ago
&lt;/h2&gt;

&lt;p&gt;In the 2010s, many teams adopted microservices without understanding what made them work. They took monoliths, split them apart, and called the pieces "services." More often than not, the result was distributed monoliths - all the operational complexity of distribution with none of the architectural benefits of real, well-defined boundaries.&lt;/p&gt;

&lt;p&gt;The correction took years. We learned (painfully) that a microservice boundary isn't where you split the code. It's where you split the &lt;em&gt;mental model&lt;/em&gt; you have of the application. A payment service and a user service don't just live in different containers - they have different vocabularies, different invariants, different reasons to change.&lt;/p&gt;

&lt;p&gt;And the same mistake is happening again, this time with MCP servers. We wrap existing REST APIs one-to-one and call it AI integration. &lt;a href="https://experimentalworks.net/about/" rel="noopener noreferrer"&gt;David Soria Parra&lt;/a&gt;, one of the creators of MCP, said "it's a bit cringe, it just results in horrible things" at AI Engineer World's Fair 2025. The &lt;a href="https://www.thoughtworks.com/radar/techniques/mcp-by-default" rel="noopener noreferrer"&gt;Thoughtworks Technology Radar&lt;/a&gt; placed "MCP by default" as a Caution. And if you dive into the argument they make, they're both saying the same thing: we're building distributed monoliths again.&lt;/p&gt;

&lt;p&gt;But the correction doesn't have to take years this time. The vocabulary already exists - and has been battle-tested for more than 20 years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bounded Contexts: one server, one model, one language
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Bounded Context&lt;/strong&gt; defines where a particular (data or object) model is valid. Inside the boundary, terms have precise meanings: a "transaction" in the finance context means money changing hands, a "transaction" in the booking context means a reservation. Inside each boundary lives one language and one set of rules. Across boundaries, you expect translation.&lt;/p&gt;

&lt;p&gt;And MCP is particularly interesting from this angle: &lt;strong&gt;the protocol already enforces bounded contexts at the topology level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MCP's architecture uses a one-client-per-server model. The host spawns a separate client for each MCP server, and each client talks to exactly one server. An MCP server for your database cannot accidentally leak data to an MCP server for your file system. Unlike microservices, where any service can trivially call any other over the network, an MCP server has no protocol-level way to reach another server's tools. You have to deliberately build that bridge. Cross-boundary coupling becomes visible and intentional rather than accidental.&lt;/p&gt;

&lt;p&gt;But only if you &lt;em&gt;design&lt;/em&gt; your servers as bounded contexts.&lt;/p&gt;

&lt;p&gt;The failure mode is an MCP server that exposes everything, including filesystem access, shell execution, and database connectors in a single server. That's three separate concerns crammed into one boundary - the equivalent of a microservice that owns users, payments, and notifications.&lt;/p&gt;

&lt;p&gt;The commenter on Reddit who wrote "don't mix list_files and execute_shell in one server" was actually designing context boundaries, even if he didn't know the term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-Corruption Layers: separating the tools from domain logic
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;Anti-Corruption Layer&lt;/strong&gt; (ACL) prevents one system's model from contaminating another. It translates between two different worldviews.&lt;/p&gt;

&lt;p&gt;In AI systems, two fundamentally different models collide every time an agent calls a tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the LLM, everything is strings, parameters are simple, and context is a token window. It reasons in natural language to generate structured calls.&lt;/li&gt;
&lt;li&gt;The domain consists of rich types, configuration, state, complex error handling, and business invariants that must hold regardless of how they're invoked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools layer sits between these two worlds. In &lt;a href="https://chris-hughes10.github.io/posts/multi-agent-part1/" rel="noopener noreferrer"&gt;Chris Hughes’s&lt;/a&gt; words, it “protects your domain from the LLM’s interface requirements - translating between ‘strings the LLM can reason about’ and ‘rich domain objects your code works with’.”&lt;/p&gt;

&lt;p&gt;Here's a tool that ignores this principle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Everything in one function - LLM interface mixed with domain logic
&lt;/span&gt;
&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transfer_funds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_account&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;to_account&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;amount&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;amount_decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&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;from_acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_account&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;from_acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insufficient funds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_frozen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Account frozen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;audit_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transferred &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_account&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;to_account&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the same operation with a proper separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Tool layer: thin adapter (the ACL)
&lt;/span&gt;&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transfer_funds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_account&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;to_account&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;amount&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transfer_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;from_account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;from_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;to_account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;to_account&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;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_agent_summary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Service layer: domain logic, testable without the LLM
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TransferService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&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;from_account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_account&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TransferResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;accounts&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="n"&gt;from_account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate_transfer&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="c1"&gt;# raises on invariant violation
&lt;/span&gt;        &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initiate_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_account&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="k"&gt;await&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;transfers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&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;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TransferResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&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 version gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testability&lt;/strong&gt;: the service works without an LLM. Run it from tests, CLI, scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replaceability&lt;/strong&gt;: change the LLM interface (tool parameters, response format) without touching business logic. Change business rules without touching the tool layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability&lt;/strong&gt;: other MCP servers, other agents, or humans can call the same service through their own interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ACL protects both sides. The domain doesn't get contaminated by the LLM's string-based worldview. The LLM doesn't get overwhelmed by domain complexity it can't reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same vocabulary in a new domain
&lt;/h2&gt;

&lt;p&gt;Back to that Reddit thread.&lt;/p&gt;

&lt;p&gt;"Separate MCP servers by blast radius." That's bounded context design. Each server owns one domain. The blast radius is contained because the boundary is real.&lt;/p&gt;

&lt;p&gt;"Three security surfaces, not one - tool capability, tool description, and tool call chains." The ACL decomposed into its responsibilities. Tool capability is what the domain allows. Tool description is what the LLM thinks it can do. Tool call chains are cross-boundary interactions that need explicit orchestration.&lt;/p&gt;

&lt;p&gt;"The dangerous part is not one tool in isolation. It is the chain." In DDD terms: an &lt;em&gt;aggregate invariant violation&lt;/em&gt;. A sequence of operations crossing bounded contexts without coordination. Each operation succeeds locally while the system fails globally.&lt;/p&gt;

&lt;p&gt;Same patterns, same structural problem, discovered independently because the problem is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "abstraction tax" is the ACL doing its job
&lt;/h2&gt;

&lt;p&gt;One fair criticism is that MCP adds a layer. The &lt;a href="https://www.thoughtworks.com/radar/techniques/mcp-by-default" rel="noopener noreferrer"&gt;Thoughtworks Tech Radar&lt;/a&gt; calls this the "abstraction tax" - every protocol layer between an agent and an API loses fidelity. &lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt; notes that "almost everything I might achieve with an MCP can be handled by a CLI tool instead."&lt;/p&gt;

&lt;p&gt;This is correct. And it's exactly the same argument people made against microservice boundaries, API gateways, and anti-corruption layers in traditional systems. The translation layer comes with costs: you lose directness.&lt;/p&gt;

&lt;p&gt;But this loss is &lt;em&gt;intentional&lt;/em&gt;. It's the ACL doing its job. The LLM doesn't need to know about your domain's internal types, retry logic, or state management. The domain doesn't need to accommodate the LLM's string-based reasoning model. The "tax" buys you isolation, replaceability, and, ultimately, peace of mind.&lt;/p&gt;

&lt;p&gt;It's only a mistake if we're paying this tax without getting the architectural benefit - which is exactly what REST-to-MCP 1:1 wrappers do. They add the layer without adding the boundary: all cost, no benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vocabulary already exists. Let's keep using it.
&lt;/h2&gt;

&lt;p&gt;We don't have to reinvent these patterns - DDD has 20+ years of battle scars. We've learned the hard way where to draw boundaries, how to enforce them, and what happens when we don't. AI or no AI, Eric Evans's &lt;a href="https://www.domainlanguage.com/ddd/" rel="noopener noreferrer"&gt;&lt;em&gt;Domain-Driven Design&lt;/em&gt;&lt;/a&gt; is still the canonical reference for complex software systems.&lt;/p&gt;

&lt;p&gt;MCP is already designed to establish bounded contexts; the tools layer is already an anti-corruption layer. Name your MCP servers after the domain they own, not the API they wrap, and when someone on your team says "separate by blast radius" - let them know that there are established patterns for what they're describing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're interested in how vocabulary ambiguity gets amplified by AI coding agents - and what you can do about it - I wrote a follow-up: &lt;a href="https://dev.to/aws/your-agent-keeps-using-that-word--4g36"&gt;Your agent keeps using that word ...&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>8 Agents Wrote Perfect Components - And Nothing Worked</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:50:44 +0000</pubDate>
      <link>https://forem.com/aws/8-agents-wrote-perfect-components-and-nothing-worked-2176</link>
      <guid>https://forem.com/aws/8-agents-wrote-perfect-components-and-nothing-worked-2176</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Parallel AI agents don't coordinate on shared contracts, such as column names, URL paths, parameter formats, or identifiers. Extract those contracts into a single reference file before generation, and run a review agent that traces end-to-end data flows once the parallel agents are done. This single step fixed all 17 bugs in one pass.&lt;/p&gt;




&lt;p&gt;I launched 8 AI agents in parallel to build a full-stack app on AWS: infrastructure stacks, a React frontend, and a Java backend. Each agent owned one piece, and they all delivered clean, compiling code. The CDK type-checked, the Java backend followed Spring Boot conventions, the React UI looked nice.&lt;/p&gt;

&lt;p&gt;But when I tried to wire them together I hit bugs at every single boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;A full-stack app on AWS with a lot of moving parts. Multiple CDK stacks for the infrastructure (IAM, VPC, DB with seed functions, Cognito, CodePipeline, CloudFront/WAF), a Spring Boot backend on ECS Fargate, and a React frontend hosted on S3.&lt;/p&gt;

&lt;p&gt;The implementation plan was thorough and covered every component. But it wasn't detailed enough for agents that need to agree on shared contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs
&lt;/h2&gt;

&lt;p&gt;The first two block everything. Bugs 3 through 5 only show up after you fix the previous ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: The Spring Boot app won't even start
&lt;/h3&gt;

&lt;p&gt;The seed data function creates a schema with &lt;code&gt;passenger_id&lt;/code&gt; and &lt;code&gt;full_name&lt;/code&gt;, but the Spring Boot entity maps to &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Agent 1: seed data function creates the schema&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;passengers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;passenger_id&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agent 2: The Spring Boot entity maps the table&lt;/span&gt;
&lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// Schema says "passenger_id"&lt;/span&gt;
&lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// Schema says "full_name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ddl-auto: validate&lt;/code&gt;, Hibernate checks the mapping on startup. But the columns don't exist, so the ECS task crashes before serving a single request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: Every call returns 404
&lt;/h3&gt;

&lt;p&gt;The CDK stack registers ALB routes for /approve and /generate while the Java client sends requests to /voucher/approve and /voucher/generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CDK ALB routes:  /approve, /generate
Java client:     /voucher/approve, /voucher/generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both agents wrote correct, working code in isolation, but the CDK stack used clean paths while the Java client added a service prefix. Neither checked the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: Missing request fields
&lt;/h3&gt;

&lt;p&gt;A downstream service validates four required fields. The Java client sends three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda expects:  escalationId, passengerId, amount, situation
Java sends:      escalationId, passengerId, amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with the URLs from bug 2 fixed, every approval returns 400.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 4: User lookup doesn't work
&lt;/h3&gt;

&lt;p&gt;This one was the most interesting: three systems work with the user, and each of them created their own identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cognito custom attribute:  custom:passenger_id = "pax-a1b2c3d4-e5f6-..."
RDS seed data:             passenger_id = "PAX-a1b2c3d4-e5f6-..."
JWT subject claim:         sub = "a1b2c3d4-e5f6-..."  (Cognito UUID)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend uses &lt;code&gt;jwt.getSubject()&lt;/code&gt; to look up the user. That's a Cognito UUID - neither prefixed with &lt;code&gt;pax-&lt;/code&gt; nor with &lt;code&gt;PAX-&lt;/code&gt;. No user lookup ever returns a result.&lt;/p&gt;

&lt;p&gt;Three agents. Three naming conventions. Zero coordination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 5: Every status lookup returns "not found"
&lt;/h3&gt;

&lt;p&gt;A downstream service returns JSON. The Java client parses XML:&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="nl"&gt;"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;"FOUND_LOCAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warehouse-B-Shelf-47"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extractXmlElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Looks for &amp;lt;status&amp;gt;...&amp;lt;/status&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No XML tags in a JSON string. &lt;code&gt;extractXmlElement&lt;/code&gt; returns empty for every single request.&lt;/p&gt;

&lt;p&gt;The agent that wrote the downstream service followed one spec (JSON). The agent that wrote the Java client followed a different spec (XML).&lt;/p&gt;

&lt;h3&gt;
  
  
  Bugs 6 to 17: SSM parameter path mismatches
&lt;/h3&gt;

&lt;p&gt;One CDK stack writes an SSM parameter. Another CDK stack reads it. But they never coordinated on paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer stack writes:  /${AppName}/test/data/rds-secret-arn
Consumer stack reads:   /${AppName}/${Env}/data/rds-password-secret-arn

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve SSM parameters mismatched between producer and consumer stacks. The app fails on every one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why parallel agents can't catch this
&lt;/h2&gt;

&lt;p&gt;Each agent had context about the overall plan and its own component. But none of them could see the implementation details that the others came up with.&lt;/p&gt;

&lt;p&gt;When I write an app, I hold the contracts in working memory. "The column is &lt;code&gt;passenger_id&lt;/code&gt;, so I'll use that in both the migration and the entity." But an AI agent writing the migration doesn't know what the entity agent chose for its column name - and vice versa.&lt;/p&gt;

&lt;p&gt;The plan contained all the high-level information, but the agents were reading different sections and making their own calls on the shared details.&lt;/p&gt;

&lt;p&gt;Each agent wrote correct code that followed good conventions. But they never coordinated. Like digging a tunnel from two sides of a mountain - without ever checking in with each other. &lt;/p&gt;

&lt;h2&gt;
  
  
  How I found all of them at once
&lt;/h2&gt;

&lt;p&gt;After generation, before actually deploying the app. I ran an architecture review agent with a simple instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trace the actual data flow from user login through form submission to the downstream service calls, following every cross-component boundary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It found every one of the bugs in a single pass.&lt;/p&gt;

&lt;p&gt;The review agent started at the user-facing entry point, traced the request through every boundary, and at each one checked whether what one component sent actually matched what the next one expected. Same thing integration tests do after deployment, but you catch it before deploying anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent seam bugs
&lt;/h2&gt;

&lt;p&gt;Before launching parallel agents, pull every shared contract out of the plan into a single reference file and pass it to every agent as mandatory context.&lt;/p&gt;

&lt;p&gt;Then, after your parallel agents did their thing, run a review agent that traces a few real user flows across all the boundaries.&lt;/p&gt;

&lt;p&gt;Fix the seam bugs in one pass, then deploy.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  What are seam bugs in AI-generated code?
&lt;/h3&gt;

&lt;p&gt;Seam bugs are integration defects at the boundaries between components built by different AI agents. Each agent writes correct, working code in isolation, but the components don't fit together because the agents each made their own decisions about shared details - things like what a column is called, what path an API lives at, or what format an identifier uses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does parallel AI code generation produce integration bugs?
&lt;/h3&gt;

&lt;p&gt;Each agent only sees its own component and the plan it was given. When two agents need to agree on something - say, what a database column is called - they each pick a reasonable name independently. Those names often don't match. The plan says what the column should represent, but not necessarily the exact string both sides should use.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you catch integration bugs from parallel AI agents?
&lt;/h3&gt;

&lt;p&gt;Run a single review agent after generation that traces real user flows across all the boundaries. Give it a prompt like "trace the data flow from user login through the frontend, backend, to databases and downstream service calls, checking every boundary." It will catch the mismatches in one pass.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My 8 Agents Wrote Perfect Components - And Nothing Worked</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:50:44 +0000</pubDate>
      <link>https://forem.com/aws/8-agents-wrote-perfect-components-and-nothing-worked-5h2b</link>
      <guid>https://forem.com/aws/8-agents-wrote-perfect-components-and-nothing-worked-5h2b</guid>
      <description>&lt;p&gt;I launched 8 AI agents in parallel to build a full-stack app on AWS: infrastructure stacks, a React frontend, and a Java backend. Each agent owned one piece, and they all delivered clean, compiling code. The CDK type-checked, the Java backend followed Spring Boot conventions, the React UI looked nice.&lt;/p&gt;

&lt;p&gt;But when I tried to wire them together I hit bugs at every single boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;A full-stack app on AWS with a lot of moving parts. Multiple CDK stacks for the infrastructure (IAM, VPC, DB with seed functions, Cognito, CodePipeline, CloudFront/WAF), a Spring Boot backend on ECS Fargate, and a React frontend hosted on S3.&lt;/p&gt;

&lt;p&gt;The implementation plan was thorough and covered every component. But it wasn't detailed enough for agents that need to agree on shared contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs
&lt;/h2&gt;

&lt;p&gt;The first two block everything. Bugs 3 through 5 only show up after you fix the previous ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: The Spring Boot app won't even start
&lt;/h3&gt;

&lt;p&gt;The seed data function creates a schema with &lt;code&gt;passenger_id&lt;/code&gt; and &lt;code&gt;full_name&lt;/code&gt;, but the Spring Boot entity maps to &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Agent 1: seed data function creates the schema&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;passengers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;passenger_id&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agent 2: The Spring Boot entity maps the table&lt;/span&gt;
&lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// Schema says "passenger_id"&lt;/span&gt;
&lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// Schema says "full_name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ddl-auto: validate&lt;/code&gt;, Hibernate checks the mapping on startup. But the columns don't exist, so the ECS task crashes before serving a single request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: Every call returns 404
&lt;/h3&gt;

&lt;p&gt;The CDK stack registers ALB routes for /approve and /generate while the Java client sends requests to /voucher/approve and /voucher/generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CDK ALB routes:  /approve, /generate
Java client:     /voucher/approve, /voucher/generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both agents wrote correct, working code in isolation, but the CDK stack used clean paths while the Java client added a service prefix. Neither checked the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: Missing request fields
&lt;/h3&gt;

&lt;p&gt;A downstream service validates four required fields. The Java client sends three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda expects:  escalationId, passengerId, amount, situation
Java sends:      escalationId, passengerId, amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with the URLs from bug 2 fixed, every approval returns 400.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 4: User lookup doesn't work
&lt;/h3&gt;

&lt;p&gt;This one was the most interesting: three systems work with the user, and each of them created their own identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cognito custom attribute:  custom:passenger_id = "pax-a1b2c3d4-e5f6-..."
RDS seed data:             passenger_id = "PAX-a1b2c3d4-e5f6-..."
JWT subject claim:         sub = "a1b2c3d4-e5f6-..."  (Cognito UUID)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend uses &lt;code&gt;jwt.getSubject()&lt;/code&gt; to look up the user. That's a Cognito UUID - neither prefixed with &lt;code&gt;pax-&lt;/code&gt; nor with &lt;code&gt;PAX-&lt;/code&gt;. No user lookup ever returns a result.&lt;/p&gt;

&lt;p&gt;Three agents. Three naming conventions. Zero coordination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 5: Every status lookup returns "not found"
&lt;/h3&gt;

&lt;p&gt;A downstream service returns JSON. The Java client parses XML:&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="nl"&gt;"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;"FOUND_LOCAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warehouse-B-Shelf-47"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extractXmlElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Looks for &amp;lt;status&amp;gt;...&amp;lt;/status&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No XML tags in a JSON string. &lt;code&gt;extractXmlElement&lt;/code&gt; returns empty for every single request.&lt;/p&gt;

&lt;p&gt;The agent that wrote the downstream service followed one spec (JSON). The agent that wrote the Java client followed a different spec (XML).&lt;/p&gt;

&lt;h3&gt;
  
  
  Bugs 6 to 17: SSM parameter path mismatches
&lt;/h3&gt;

&lt;p&gt;One CDK stack writes an SSM parameter. Another CDK stack reads it. But they never coordinated on paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer stack writes:  /${AppName}/test/data/rds-secret-arn
Consumer stack reads:   /${AppName}/${Env}/data/rds-password-secret-arn

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve SSM parameters mismatched between producer and consumer stacks. The app fails on every one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why parallel agents can't catch this
&lt;/h2&gt;

&lt;p&gt;Each agent had context about the overall plan and its own component. But none of them could see the implementation details that the others came up with.&lt;/p&gt;

&lt;p&gt;When I write an app, I hold the contracts in working memory. "The column is &lt;code&gt;passenger_id&lt;/code&gt;, so I'll use that in both the migration and the entity." But an AI agent writing the migration doesn't know what the entity agent chose for its column name - and vice versa.&lt;/p&gt;

&lt;p&gt;The plan contained all the high-level information, but the agents were reading different sections and making their own calls on the shared details.&lt;/p&gt;

&lt;p&gt;Each agent wrote correct code that followed good conventions. But they never coordinated. Like digging a tunnel from two sides of a mountain - without ever checking in with each other. &lt;/p&gt;

&lt;h2&gt;
  
  
  How I found all of them at once
&lt;/h2&gt;

&lt;p&gt;After generation, before actually deploying the app. I ran an architecture review agent with a simple instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trace the actual data flow from user login through form 
submission to the downstream service calls, following every 
cross-component boundary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It found every one of the bugs in a single pass.&lt;/p&gt;

&lt;p&gt;The review agent started at the user-facing entry point, traced the request through every boundary, and at each one checked whether what one component sent actually matched what the next one expected. Same thing integration tests do after deployment, but you catch it before deploying anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent seam bugs
&lt;/h2&gt;

&lt;p&gt;Before launching parallel agents, pull every shared contract out of the plan into a single reference file and pass it to every agent as mandatory context.&lt;/p&gt;

&lt;p&gt;Then, after your parallel agents did their thing, run a review agent that traces a few real user flows across all the boundaries.&lt;/p&gt;

&lt;p&gt;Fix the seam bugs in one pass, then deploy.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  What are seam bugs in AI-generated code?
&lt;/h3&gt;

&lt;p&gt;Seam bugs are integration defects at the boundaries between components built by different AI agents. Each agent writes correct, working code in isolation, but the components don't fit together because the agents each made their own decisions about shared details - things like what a column is called, what path an API lives at, or what format an identifier uses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does parallel AI code generation produce integration bugs?
&lt;/h3&gt;

&lt;p&gt;Each agent only sees its own component and the plan it was given. When two agents need to agree on something - say, what a database column is called - they each pick a reasonable name independently. Those names often don't match. The plan says what the column should represent, but not necessarily the exact string both sides should use.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you catch integration bugs from parallel AI agents?
&lt;/h3&gt;

&lt;p&gt;Run a single review agent after generation that traces real user flows across all the boundaries. Give it a prompt like "trace the data flow from user login through the frontend, backend, to databases and downstream service calls, checking every boundary." It will catch the mismatches in one pass.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Missing from the MCP debate: Who holds the keys when 50 agents access 50 APIs?</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Wed, 18 Mar 2026 12:35:57 +0000</pubDate>
      <link>https://forem.com/aws/missing-from-the-mcp-debate-who-holds-the-keys-when-50-agents-access-50-apis-mb3</link>
      <guid>https://forem.com/aws/missing-from-the-mcp-debate-who-holds-the-keys-when-50-agents-access-50-apis-mb3</guid>
      <description>&lt;p&gt;There are two debates happening right now:&lt;/p&gt;

&lt;p&gt;CLI vs MCP - should agents call existing CLIs or use an MCP server? And API vs MCP - does wrapping a REST API in an MCP server add value, or just complexity?&lt;/p&gt;

&lt;p&gt;Both focus on how agents call tools. What both aren't asking is, who holds the credentials when they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty agents, fifty sets of keys
&lt;/h2&gt;

&lt;p&gt;When one developer runs one agent on one laptop, credentials are simple. You store them locally, maybe rotate them, and move on.&lt;/p&gt;

&lt;p&gt;But that's not where we're heading. Dozens of agents per team, each needing access to Slack, GitHub, Jira, Office 365, that legacy CRM, multiple SaaS tools, and all your internal APIs.&lt;/p&gt;

&lt;p&gt;Some of those have CLIs. Most don't - they're SaaS products with REST APIs. If you're lucky - who knows how many production systems still use a global, password-protected admin account.&lt;/p&gt;

&lt;p&gt;So every agent needs a separate API key, OAuth token, or username/password pair. For each downstream system. On every machine. And if you've ever managed API keys for a team, you know where this goes. Keys in &lt;code&gt;.env&lt;/code&gt; files, shared over Slack, committed to repos, never rotated.&lt;/p&gt;

&lt;p&gt;Now hand that problem to fifty autonomous agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened to SSO?
&lt;/h2&gt;

&lt;p&gt;Most organizations with any sense of security have established SSO, spent years consolidating identity. Every SaaS tool, every internal system, every third-party integration flows through one identity provider. &lt;/p&gt;

&lt;p&gt;When someone leaves, you disable a single account. When compliance asks about access controls, there's one answer - and you know exactly where to find it.&lt;/p&gt;

&lt;p&gt;And now, agents are about to blow a wide open hole into everything you've built. Whether your agent calls a CLI, hits a REST API, or talks to an MCP server, it needs credentials. And if those credentials live on the agent's machine, they live outside your identity boundary.&lt;/p&gt;

&lt;p&gt;Imagine a contractor wrapping up on Friday. You disable their SSO account, but their laptop still has three agents with API keys for your CRM, your internal docs, and your deployment pipeline. Those keys don't expire with the SSO account. Those agents can continue calling your APIs long after the contractor has moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote MCP servers are identity boundaries
&lt;/h2&gt;

&lt;p&gt;This is where remote MCP servers earn their place in both debates.&lt;/p&gt;

&lt;p&gt;The CLI vs MCP crowd argues about token efficiency. The API vs MCP crowd argues about unnecessary abstraction. Neither side is talking about the nightmare of decentralized credential management.&lt;/p&gt;

&lt;p&gt;Charles Chen makes this point well in &lt;a href="https://chrlschn.dev/blog/2026/03/mcp-is-dead-long-live-mcp/" rel="noopener noreferrer"&gt;MCP is Dead; Long Live MCP!&lt;/a&gt;. Most of the debate ignores the difference between MCP over &lt;code&gt;stdio&lt;/code&gt; (local, and yeah, mostly pointless compared to raw &lt;code&gt;curl&lt;/code&gt; or a CLI) and MCP over streamable HTTP (remote, centralized). Once MCP runs as a centralized server, users authenticate via OAuth and never touch the downstream keys. &lt;/p&gt;

&lt;p&gt;As he puts it: "An engineer leaves your team? Revoke their OAuth token and access to the MCP server; they never had access to other keys and secrets to start with."&lt;/p&gt;

&lt;p&gt;Now take that one step further. In most organizations, that OAuth isn't standalone - it flows through SSO. The MCP server becomes an identity boundary. Your users never store any API keys, custom tokens, or service accounts. One auth mechanism instead of one per machine per agent per API.&lt;/p&gt;

&lt;p&gt;Disable the SSO account, and every agent loses access. To everything.&lt;/p&gt;

&lt;p&gt;But we already learned this, right? Every microservice managing its own database credentials was a nightmare until we centralized secrets management. Agent credentials are the same problem, just one layer up.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>3 Things I Wish I Knew Before Setting Up a UV Workspace</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Fri, 27 Feb 2026 17:57:09 +0000</pubDate>
      <link>https://forem.com/aws/3-things-i-wish-i-knew-before-setting-up-a-uv-workspace-30j6</link>
      <guid>https://forem.com/aws/3-things-i-wish-i-knew-before-setting-up-a-uv-workspace-30j6</guid>
      <description>&lt;p&gt;I love &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, it's so much better than &lt;code&gt;pip&lt;/code&gt;, but I'm still learning the ins and outs. Today I was setting up a Python monorepo with &lt;a href="https://docs.astral.sh/uv/concepts/workspaces/" rel="noopener noreferrer"&gt;uv workspaces&lt;/a&gt; and ran into a few issues, the fixes of which were trivial once I knew about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give the Root a Distinct Name
&lt;/h2&gt;

&lt;p&gt;First, a virtual root (&lt;code&gt;package = false&lt;/code&gt;) still needs a &lt;code&gt;[project] name&lt;/code&gt; - and it can't match any member package.&lt;/p&gt;

&lt;p&gt;I had both the root and my core package using the same name, e.g. &lt;code&gt;my-app&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;my-app/                   # workspace root
  pyproject.toml          # name = "my-app" &amp;lt;- problem!
  packages/
    core/
      pyproject.toml      # name = "my-app"
      src/core/
    cli/
      pyproject.toml      # name = "my-app-cli"
      src/cli/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran &lt;code&gt;uv sync&lt;/code&gt;, it refused outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uv sync
error: Two workspace members are both named `my-app`:
  `/path/to/my-app` and `/path/to/my-app/packages/core`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the root has &lt;code&gt;package = false&lt;/code&gt;, uv still registers its &lt;code&gt;name&lt;/code&gt; as a workspace member identity. Same name, two members, no way to disambiguate.&lt;/p&gt;

&lt;p&gt;The fix - give the root a workspace-specific name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Root pyproject.toml&lt;/span&gt;
&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-app-workspace"&lt;/span&gt;  &lt;span class="c"&gt;# NOT "my-app"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class="py"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"packages/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[dependency-groups]&lt;/span&gt;
&lt;span class="py"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"pytest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"ruff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to note: &lt;code&gt;package = false&lt;/code&gt; means "don't install me", not "don't need a name". And dev dependencies go in &lt;code&gt;[dependency-groups]&lt;/code&gt; (&lt;a href="https://peps.python.org/pep-0735/" rel="noopener noreferrer"&gt;PEP 735&lt;/a&gt;), not &lt;code&gt;[project.dependencies]&lt;/code&gt; - the root is virtual, so project dependencies are just metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use &lt;code&gt;workspace = true&lt;/code&gt; for Inter-Package Deps
&lt;/h2&gt;

&lt;p&gt;When one workspace package depends on another, you need two things: a normal dependency declaration &lt;em&gt;and&lt;/em&gt; a &lt;a href="https://docs.astral.sh/uv/concepts/workspaces/#workspace-sources" rel="noopener noreferrer"&gt;&lt;code&gt;[tool.uv.sources]&lt;/code&gt;&lt;/a&gt; entry telling uv to resolve it locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# packages/cli/pyproject.toml&lt;/span&gt;
&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-app-cli"&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;my-app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the &lt;code&gt;[tool.uv.sources]&lt;/code&gt; entry, &lt;code&gt;uv sync&lt;/code&gt; fails with a helpful but initially confusing error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uv sync
  x Failed to build `my-app-cli @ file:///path/to/packages/cli`
  |-- Failed to parse entry: `my-app`
  \-- `my-app` is included as a workspace member, but is missing
      an entry in `tool.uv.sources`
      (e.g., `my-app = { workspace = true }`)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At least uv tells you exactly what to add.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[project.dependencies]&lt;/code&gt; list stays &lt;a href="https://peps.python.org/pep-0621/" rel="noopener noreferrer"&gt;PEP 621&lt;/a&gt; compliant, so any standard Python tool can read it. The &lt;code&gt;[tool.uv.sources]&lt;/code&gt; table is uv-specific and only affects resolution. And &lt;code&gt;uv sync&lt;/code&gt; installs the local package as editable automatically - changes are immediately visible without reinstalling.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use &lt;code&gt;importlib&lt;/code&gt; Mode for pytest
&lt;/h2&gt;

&lt;p&gt;When running &lt;code&gt;pytest&lt;/code&gt; across a workspace where multiple packages have &lt;code&gt;tests/&lt;/code&gt; directories with same-named test files (e.g. both have &lt;code&gt;test_helpers.py&lt;/code&gt;), pytest's default import mode breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uv run pytest packages/ -v
collected 1 item / 1 error

ERROR collecting packages/core/tests/test_helpers.py
import file mismatch:
imported module 'test_helpers' has this __file__ attribute:
  /path/to/packages/cli/tests/test_helpers.py
which is not the same as the test file we want to collect:
  /path/to/packages/core/tests/test_helpers.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pytest's default &lt;a href="https://docs.pytest.org/en/stable/explanation/pythonpath.html#import-modes" rel="noopener noreferrer"&gt;&lt;code&gt;prepend&lt;/code&gt; import mode&lt;/a&gt; treats both &lt;code&gt;test_helpers.py&lt;/code&gt; as the same module. It imports the first one, caches it, then errors when the second file doesn't match.&lt;/p&gt;

&lt;p&gt;The fix - add &lt;code&gt;importlib&lt;/code&gt; mode to your root &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Root pyproject.toml&lt;/span&gt;
&lt;span class="nn"&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class="py"&gt;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"--import-mode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;importlib&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&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 plaintext"&gt;&lt;code&gt;$ uv run pytest packages/ -v
packages/cli/tests/test_helpers.py::test_cli_helper PASSED    [ 50%]
packages/core/tests/test_helpers.py::test_core_helper PASSED  [100%]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Don't add &lt;code&gt;__init__.py&lt;/code&gt; to your test directories as a workaround - with &lt;code&gt;importlib&lt;/code&gt; mode, that can actually cause a &lt;em&gt;silent&lt;/em&gt; bug where pytest resolves both files to the same cached module and runs the wrong tests without any error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't uv-specific - it's a Python monorepo thing. But uv workspaces make monorepos easy to set up, so you're likely to hit it early.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/concepts/workspaces/" rel="noopener noreferrer"&gt;uv Workspaces docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peps.python.org/pep-0735/" rel="noopener noreferrer"&gt;PEP 735 - Dependency Groups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peps.python.org/pep-0621/" rel="noopener noreferrer"&gt;PEP 621 - Project metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pytest.org/en/stable/explanation/goodpractices.html" rel="noopener noreferrer"&gt;pytest - Good Integration Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>uv</category>
      <category>monorepo</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to Use Strands Agents' Built-In Session Persistence</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Tue, 17 Feb 2026 16:41:10 +0000</pubDate>
      <link>https://forem.com/aws/til-strands-agents-has-built-in-session-persistence-3nhl</link>
      <guid>https://forem.com/aws/til-strands-agents-has-built-in-session-persistence-3nhl</guid>
      <description>&lt;p&gt;Today I learned that the &lt;a href="https://github.com/strands-agents/sdk-python?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Strands Agents SDK&lt;/a&gt; has a built-in persistence layer for conversation history.&lt;/p&gt;

&lt;p&gt;Pass a &lt;code&gt;SessionManager&lt;/code&gt; to the &lt;code&gt;Agent&lt;/code&gt; constructor, and every message and state change is persisted automatically through lifecycle hooks. No manual save/load calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;session_demo.py&lt;/code&gt; and run it with &lt;code&gt;uv run session_demo.py&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;# /// script&lt;/code&gt; block is &lt;a href="https://peps.python.org/pep-0723/" rel="noopener noreferrer"&gt;PEP 723 inline metadata&lt;/a&gt; - &lt;code&gt;uv run&lt;/code&gt; reads it to install dependencies automatically, no venv or pip needed. All you need is &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS credentials configured&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /// script
# requires-python = "&amp;gt;=3.10"
# dependencies = ["strands-agents"]
# ///
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.session.file_session_manager&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;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;user-abc-123&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# defaults to /tmp/strands/sessions
&lt;/span&gt;
&lt;span class="c1"&gt;# First agent instance - ask a question
&lt;/span&gt;&lt;span class="n"&gt;agent1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;global.anthropic.claude-haiku-4-5-20251001-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent_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;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;SESSION_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_DIR&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the capital of France?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;agent1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt1&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="c1"&gt;# Second agent instance - same session_id, loads conversation from disk
&lt;/span&gt;&lt;span class="n"&gt;agent2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;global.anthropic.claude-haiku-4-5-20251001-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agent_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;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;SESSION_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STORAGE_DIR&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What did I just ask you?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt2&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;agent2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt2&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;agent1&lt;/code&gt; and &lt;code&gt;agent2&lt;/code&gt; are separate &lt;code&gt;Agent&lt;/code&gt; instances - they share no memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent2&lt;/code&gt; can answer "What did I just ask you?" because &lt;code&gt;FileSessionManager&lt;/code&gt; restored the conversation from disk when the second instance was created&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;agent_id&lt;/code&gt; identifies which agent's state to save and restore - required when using a session manager&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Gets Persisted
&lt;/h2&gt;

&lt;p&gt;The session manager saves three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conversation history&lt;/strong&gt; - all user and assistant messages (the &lt;code&gt;messages/&lt;/code&gt; directory)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent state&lt;/strong&gt; - a JSON-serializable key-value dict you can use for your own data (&lt;code&gt;agent.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session metadata&lt;/strong&gt; - timestamps and session type (&lt;code&gt;session.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After running the script, here's what's on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sessions/
└── session_user-abc-123
    ├── agents
    │   └── agent_assistant
    │       ├── agent.json
    │       └── messages
    │           ├── message_0.json
    │           ├── message_1.json
    │           ├── message_2.json
    │           └── message_3.json
    ├── multi_agents
    └── session.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message is a separate JSON file:&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;"message"&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&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;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What's the capital of France?"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-17T14:45:31.439081+00:00"&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;User and assistant turns alternate through &lt;code&gt;message_0.json&lt;/code&gt; to &lt;code&gt;message_3.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-In Backends
&lt;/h2&gt;

&lt;p&gt;The example uses &lt;code&gt;FileSessionManager&lt;/code&gt;, but the SDK ships three backends:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Manager&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileSessionManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local development, single-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S3SessionManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Production, distributed, multi-container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RepositorySessionManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom backend (implement &lt;code&gt;SessionRepository&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Tips and Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Troubleshooting tips
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;uv: command not found&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Install uv: &lt;code&gt;curl -LsSf https://astral.sh/uv/install.sh | sh&lt;/code&gt; (macOS/Linux) or &lt;code&gt;powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"&lt;/code&gt; (Windows). See &lt;a href="https://docs.astral.sh/uv/getting-started/installation/" rel="noopener noreferrer"&gt;uv installation docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;NoCredentialError&lt;/code&gt; or &lt;code&gt;Unable to locate credentials&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
AWS credentials aren't configured. Run &lt;code&gt;aws configure&lt;/code&gt; to set up a default profile, or export &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;. See &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CLI configuration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AccessDeniedException&lt;/code&gt; when calling the model&lt;/strong&gt;&lt;br&gt;
Your AWS credentials don't have permission to invoke the Bedrock model. Make sure your IAM user or role has &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; and &lt;code&gt;bedrock:InvokeModelWithResponseStream&lt;/code&gt; permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good to know
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FileSessionManager&lt;/code&gt; is not safe for concurrent same-session writes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two API requests with the same &lt;code&gt;session_id&lt;/code&gt; writing to &lt;code&gt;FileSessionManager&lt;/code&gt; concurrently can corrupt the session data. The storage layer has no locking - it reads, appends, and writes the full JSON file without coordination.&lt;/p&gt;

&lt;p&gt;For development, this is fine. Single-process, single-user development (CLI, local testing) will never hit this. Sequential requests to the same session are safe.&lt;/p&gt;

&lt;p&gt;For production, you have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Per-session locking in your API layer - serialize requests per session_id before they reach the Agent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S3SessionManager&lt;/code&gt; - uses atomic S3 operations for safe concurrent writes&lt;/li&gt;
&lt;li&gt;A custom &lt;code&gt;SessionRepository&lt;/code&gt; - implement your own with proper concurrency handling (database-backed, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;agent_id&lt;/code&gt; is required with a session manager&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you omit &lt;code&gt;agent_id&lt;/code&gt; when using a &lt;code&gt;SessionManager&lt;/code&gt;, you'll get &lt;code&gt;ValueError: agent_id needs to be defined.&lt;/code&gt; The session system uses it as a directory key to separate state for different agents within the same session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;FileSessionManager&lt;/code&gt; defaults to &lt;code&gt;/tmp&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without an explicit &lt;code&gt;storage_dir&lt;/code&gt;, sessions are written to &lt;code&gt;/tmp/strands/sessions&lt;/code&gt; - which most operating systems wipe on reboot. Set it to a project-local path like &lt;code&gt;./sessions&lt;/code&gt; or &lt;code&gt;.data/sessions&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/session-management/?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Session Management docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/conversation-management/?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Conversation Management docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Sun, 15 Feb 2026 11:20:33 +0000</pubDate>
      <link>https://forem.com/dennistraub/-31b6</link>
      <guid>https://forem.com/dennistraub/-31b6</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/aws" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&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%2Forganization%2Fprofile_image%2F1726%2F1f5cc5bc-5f61-428d-9d35-ba0c39f8af2d.png" alt="AWS" width="500" height="500"&gt;
      &lt;div class="ltag__link__user__pic"&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%2Fuser%2Fprofile_image%2F348349%2F213d7254-998a-413f-b7af-c96c087508b3.png" alt="" width="800" height="800"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/aws/from-zero-to-agentic-coding-running-claude-code-with-amazon-bedrock-1f00" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;From Zero to Agentic Coding: Running Claude Code with Amazon Bedrock&lt;/h2&gt;
      &lt;h3&gt;Gunnar Grosch for AWS ・ Feb 15&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#aws&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#ai&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#beginners&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#productivity&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>aws</category>
      <category>ai</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Your Chatbot is the BlackBerry of the 2020s</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Fri, 13 Feb 2026 01:58:38 +0000</pubDate>
      <link>https://forem.com/aws/why-your-chatbot-is-the-blackberry-of-the-2020s-b5</link>
      <guid>https://forem.com/aws/why-your-chatbot-is-the-blackberry-of-the-2020s-b5</guid>
      <description>&lt;p&gt;This is my third major technological shift, and every time I hear the same question echo through the C-Suites:&lt;/p&gt;

&lt;p&gt;"But where's the ROI?"&lt;/p&gt;

&lt;p&gt;In the 90s, we put our print brochures and yellow pages on the web - and called it digital transformation. In the 2000s, we put tiny keyboards or a Windows start button on a phone and called it mobile computing.&lt;/p&gt;

&lt;p&gt;Both times, the answer wasn't better brochures or smaller buttons. The answer was Google and Facebook. It was the iPhone and Android. New ideas, by companies that didn't try to replicate the old world - with all its constraints - on a new medium. They built something entirely new. They embraced the new medium and built what couldn't have existed before.&lt;/p&gt;




&lt;p&gt;And now we're doing it again, with AI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every demo is a chatbot.&lt;/li&gt;
&lt;li&gt;Every pilot is "adding AI to our existing workflow."&lt;/li&gt;
&lt;li&gt;Every enterprise use case tries to optimize what already exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But this time, it kind of works. And that's the trap.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;The web's brochureware visibly broke. The stylus on Windows Mobile was physically painful. But AI chatbots deliver just enough value to feel like progress.&lt;/p&gt;

&lt;p&gt;We're settling for 10% of what's possible and call it "revolutionizing the [industry of your choice]".&lt;/p&gt;

&lt;p&gt;But the real inflection point isn't better support agents or travel booking chatbots. It's when we stop replicating what's already there and start building something that never existed before.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How a subtle MCP server bug almost cost me $230 a month</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Wed, 11 Feb 2026 18:23:12 +0000</pubDate>
      <link>https://forem.com/aws/how-a-subtle-mcp-server-bug-almost-cost-me-230-a-month-22ij</link>
      <guid>https://forem.com/aws/how-a-subtle-mcp-server-bug-almost-cost-me-230-a-month-22ij</guid>
      <description>&lt;p&gt;An important part of my job is to collect and distill feedback into recommendations for product and engineering teams. Sure, not quite as glamorous as traveling the world, but it's a lot of fun: I'm getting paid to experiment with brand new tech - and I get to directly influence the developer experience of our products.&lt;/p&gt;

&lt;p&gt;But - just like every job - there's also a lot of routine work involved, including the boring type. And if there's anything my ADHD brain hates - with a passion! - it's boring routine work. And there's one thing right at the top of the list: processing tasks in a project management tool.&lt;/p&gt;

&lt;p&gt;So I built an AI agent to help me: triage tasks, add context, draft comments, move items around - all through an MCP server.&lt;/p&gt;

&lt;p&gt;As I said. Routine work. Boring and predictable. Right?&lt;/p&gt;

&lt;p&gt;Right. Until I noticed the time it needed for what should be simple updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Calls, Zero Updates
&lt;/h2&gt;

&lt;p&gt;The MCP server has an &lt;code&gt;update_task&lt;/code&gt; tool, and the agent called it with a &lt;code&gt;custom_fields&lt;/code&gt; parameter. The server took the request, processed it, and returned success. &lt;/p&gt;

&lt;p&gt;But when the agent continued, the custom fields were unchanged. It tried updating again - with a different format. Success. But nothing changed. Third attempt. Success. Still nothing.&lt;/p&gt;

&lt;p&gt;3 success responses. Zero successful updates. And the API never told the agent that anything was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking It Down
&lt;/h2&gt;

&lt;p&gt;So after multiple failed attempts, the agent started investigating on its own. It checked whether it had the right access permissions. Or if it can use &lt;code&gt;curl&lt;/code&gt; to bypass the MCP layer entirely. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; worked, showing that the problem wasn't permissions. So it must be the tool itself.&lt;/p&gt;

&lt;p&gt;After some more back and forth, the agent discovered that &lt;code&gt;create_batch_request&lt;/code&gt; - a completely different MCP tool - is the only way to update custom fields. The &lt;code&gt;update_task&lt;/code&gt; tool accepts the &lt;code&gt;custom_fields&lt;/code&gt; parameter without complaint, but the parameter isn't actually in the tool's schema. The tool silently drops it, updates everything else, and returns a success message.&lt;/p&gt;

&lt;p&gt;Maybe a small issue, if it happened only once.&lt;/p&gt;

&lt;p&gt;But my logs showed 16 silently failed attempts across 7 tasks. The same cycle every time: try, get "success," see nothing changed, investigate, try again, finally find a workaround.&lt;/p&gt;

&lt;p&gt;The agent kept hitting the same wall because the API never told it the wall existed. &lt;/p&gt;

&lt;p&gt;A crash would have been so much better - the agent would see the error and immediately try a different tool. Instead, it got a success response and had to figure out through downstream verification that the "successful" call hadn't been sucessfull after all.&lt;/p&gt;

&lt;p&gt;Each time a new agent instance worked on a task, it went through the same process, wasting ~93K tokens just to figure out that there is a problem - and how to solve it. Learning wasn't possible, because there was no error to learn from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Look at the Math
&lt;/h2&gt;

&lt;p&gt;Every number below comes directly from my session logs.&lt;/p&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wasted tokens per failed attempt&lt;/td&gt;
&lt;td&gt;~93,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average failed attempts per task&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per attempt (Claude Opus at $5/MTok)&lt;/td&gt;
&lt;td&gt;~$0.47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost per task&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1.08&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now imagine a 5-person team with 10 tasks per person per day.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timeframe&lt;/th&gt;
&lt;th&gt;Per person&lt;/th&gt;
&lt;th&gt;Team of 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per day (10 tasks)&lt;/td&gt;
&lt;td&gt;$10.80&lt;/td&gt;
&lt;td&gt;$54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per month (22 working days)&lt;/td&gt;
&lt;td&gt;$238&lt;/td&gt;
&lt;td&gt;$1,190&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per year&lt;/td&gt;
&lt;td&gt;$2,856&lt;/td&gt;
&lt;td&gt;$14,280&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is one parameter, in one MCP tool, on a single workflow. And even that scales really fast.&lt;/p&gt;

&lt;p&gt;Two things are worth mentioning: this models the cost &lt;em&gt;before&lt;/em&gt; the workaround is discovered. Once you know to use &lt;code&gt;create_batch_request&lt;/code&gt;, the waste drops to zero. And it assumes every task hits the bug - which was true in my case, since every triage task needed custom field updates.&lt;/p&gt;

&lt;p&gt;The point isn't the exact dollar figure. It's that silent failures delay discovery - possibly indefinitely.&lt;/p&gt;

&lt;p&gt;A crash costs one attempt. The agent sees the error, adjusts, moves on. But silent acceptance costs multiple attempts, every time, until you realize there's a problem - if you realize it at all.&lt;/p&gt;

&lt;p&gt;In software engineering, there's a concept called "graceful degradation". But if your API - MCP server or not - accepts parameters it can't handle and returns success, you're not being graceful. You're being expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You can Do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you build APIs or MCP tools:&lt;/strong&gt; Add input validation. Reject or warn on unrecognized parameters. My data says that one check would have saved every token and every dollar in this story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you build agents:&lt;/strong&gt; Verify state after every state-changing call. Don't trust success responses - confirm the change actually happened. And make sure your agent surfaces anything it didn't expect.&lt;/p&gt;




&lt;p&gt;Some say we don't need to worry about architecture anymore, because AI agents are able to figure things out.&lt;/p&gt;

&lt;p&gt;But I think it's the opposite: Software architecture principles become more important with AI, not less.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 1 of "The Inconsistency Tax" - a 3-part series on what happens when AI agents meet inconsistent APIs. Next: why these failures aren't random, and why "just wrapping an API in an MCP server" doesn't automatically make it agent-ready.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>api</category>
    </item>
    <item>
      <title>From Local MCP Server to AWS Deployment in Two Commands - The Code-Only Version</title>
      <dc:creator>Dennis Traub</dc:creator>
      <pubDate>Mon, 26 Jan 2026 17:30:18 +0000</pubDate>
      <link>https://forem.com/aws/from-local-mcp-server-to-aws-deployment-in-two-commands-code-only-5c4d</link>
      <guid>https://forem.com/aws/from-local-mcp-server-to-aws-deployment-in-two-commands-code-only-5c4d</guid>
      <description>&lt;p&gt;In this walkthrough, I'll show you the simplest way to deploy an MCP server on AWS, using the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt; - and how to call it with your IAM credentials - no complicated OAuth setup required.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a code-only walkthrough. For detailed explanations, see the &lt;a href="https://dev.to/aws/from-local-mcp-server-to-aws-deployment-in-two-commands-ag4"&gt;full tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;Python and AWS setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To follow this walkthrough, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python 3.11&lt;/a&gt; or higher,&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;uv&lt;/a&gt; package manager (or pip),&lt;/li&gt;
&lt;li&gt;an &lt;a href="https://go.aws/3ZcwOK0" rel="noopener noreferrer"&gt;AWS account&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;and the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; installed and configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run these commands to verify the prerequisites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;--version&lt;/span&gt;
uv &lt;span class="nt"&gt;--version&lt;/span&gt;     
aws &lt;span class="nt"&gt;--version&lt;/span&gt;
aws sts get-caller-identity &lt;span class="nt"&gt;--no-cli-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To troubleshoot, please follow the links above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AgentCore Starter Toolkit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://aws.github.io/bedrock-agentcore-starter-toolkit/api-reference/cli.html?trk=ef8ca202-7071-4ec3-aff2-78ef3bddfabf&amp;amp;sc_channel=el" rel="noopener noreferrer"&gt;Bedrock AgentCore Starter Toolkit&lt;/a&gt; will simplify the entire process to two commands, &lt;code&gt;agentcore configure&lt;/code&gt; and &lt;code&gt;agentcore deploy&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;To install it, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; bedrock-agentcore-starter-toolkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Create a small MCP server
&lt;/h2&gt;

&lt;p&gt;Prepare a new Python project for the MCP server:&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="c"&gt;# Start with an empty folder for your project&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;mcp-on-agentcore-runtime
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-on-agentcore-runtime

&lt;span class="c"&gt;# Create and cd into a subdirectory for the MCP server&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;mcp-server
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-server

&lt;span class="c"&gt;# Initialize a new Python project and add the `mcp` package:&lt;/span&gt;
uv init &lt;span class="nt"&gt;--bare&lt;/span&gt;
uv add mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the MCP server&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="c"&gt;# Create a simple MCP server in `server.py`:&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; server.py
import random
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(host="0.0.0.0", stateless_http=True)

@mcp.tool()
def roll_d20(number_of_dice: int = 1) -&amp;gt; dict:
    """Rolls one or more 20-sided dice (d20)"""
    if number_of_dice &amp;lt; 1:
        return {
            "error": f"number_of_dice must be at least 1"
        }

    rolls = [random.randint(1, 20) for _ in range(number_of_dice)]
    return {
        "number_of_dice": number_of_dice,
        "rolls": rolls,
        "total": sum(rolls)
    }

def main():
    mcp.run(transport="streamable-http")

if __name__ == "__main__":
    main()
&lt;/span&gt;&lt;span class="no"&gt;
EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Test Locally
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In the first terminal:&lt;/strong&gt;&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="c"&gt;# Start the MCP server:&lt;/span&gt;
uv run server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In a second terminal:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;List 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="c"&gt;# Send a list tools request&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/mcp &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&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;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ 
           "jsonrpc": "2.0",
           "id": 1,
           "method": "tools/list"
         }'&lt;/span&gt;

&lt;span class="c"&gt;# Response:&lt;/span&gt;
&lt;span class="c"&gt;# event: message&lt;/span&gt;
&lt;span class="c"&gt;# data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{...}]}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call the tool:&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="c"&gt;# Send a tool call request&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/mcp &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&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;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
           "id": 1,
           "jsonrpc": "2.0",
           "method": "tools/call",
           "params": {
             "name": "roll_d20",
             "arguments": { "number_of_dice": 2 }
           }
         }'&lt;/span&gt;

&lt;span class="c"&gt;# Response:&lt;/span&gt;
&lt;span class="c"&gt;# event: message&lt;/span&gt;
&lt;span class="c"&gt;# data: {"jsonrpc":"2.0","id":1,"result":{"content":[{...}]}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you see connection errors, ensure the server is running in the other terminal&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;In the first terminal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl/Cmd+C&lt;/code&gt; to stop the server&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Configure the deployment AWS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Navigate back up to the project root&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..

&lt;span class="c"&gt;# Configure the MCP server deployment.&lt;/span&gt;
&lt;span class="c"&gt;# This will use the account and region configured in the AWS CLI&lt;/span&gt;
agentcore configure &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; mcp-server/server.py &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--requirements-file&lt;/span&gt; mcp-server/pyproject.toml &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--disable-memory&lt;/span&gt; &lt;span class="nt"&gt;--disable-otel&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--deployment-type&lt;/span&gt; container &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--protocol&lt;/span&gt; MCP &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--name&lt;/span&gt; my_mcp_server

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;
&lt;span class="c"&gt;# Config saved to: .../.bedrock_agentcore.yaml&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;

&lt;span class="c"&gt;# If you want to view the configuration, you can run:&lt;/span&gt;
agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Deploy to AWS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Now you can launch the deployment process&lt;/span&gt;
agentcore deploy

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# Launching Bedrock AgentCore (codebuild mode - RECOMMENDED)&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;
&lt;span class="c"&gt;# Setting up AWS resources (ECR repository, execution roles)&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;
&lt;span class="c"&gt;# Deployment completed successfully&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single command orchestrates the entire deployment process.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It creates an ECR Repository to store the container image,&lt;/li&gt;
&lt;li&gt;an IAM Role with permissions for the runtime to pull images from ECR, execute your container, and write logs to CloudWatch,&lt;/li&gt;
&lt;li&gt;and an IAM Role for CodeBuild to build the container and push it to ECR.&lt;/li&gt;
&lt;li&gt;Then it zips and uploads your server files to S3,&lt;/li&gt;
&lt;li&gt;runs the build process, pushes the image,&lt;/li&gt;
&lt;li&gt;and deploys the MCP server to AgentCore Runtime&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server is now ready to accept MCP requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test with the AWS CLI
&lt;/h2&gt;

&lt;p&gt;Get the MCP server ARN:&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="c"&gt;# Extract the server's ARN from `.bedrock_agentcore.yaml`:&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MCP_SERVER_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'agent_arn:'&lt;/span&gt; .bedrock_agentcore.yaml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\"\'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# To see if it worked, type:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MCP_SERVER_ARN=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$MCP_SERVER_ARN&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List 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="c"&gt;# Create a file with the list tools request&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; list-tools-request.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/list"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# To list the tools, send the request to the AgentCore Runtime&lt;/span&gt;
&lt;span class="c"&gt;# Note that, even though this is called `invoke-agent-runtime`,&lt;/span&gt;
&lt;span class="c"&gt;# it will call the deployed MCP server.&lt;/span&gt;
aws bedrock-agentcore invoke-agent-runtime &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--agent-runtime-arn&lt;/span&gt; &lt;span class="nv"&gt;$MCP_SERVER_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--content-type&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--accept&lt;/span&gt; &lt;span class="s2"&gt;"application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-cli-pager&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--payload&lt;/span&gt; fileb://list-tools-request.json &lt;span class="se"&gt;\&lt;/span&gt;
    ./list-tools-output.txt

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;
&lt;span class="c"&gt;# mcpSessionId: [MCP Session ID]&lt;/span&gt;
&lt;span class="c"&gt;# runtimeSessionId: [AgentCore Runtime ID - same as MCP Session ID]&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;

&lt;span class="c"&gt;# View the results:&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;list-tools-output.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call the tool:&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="c"&gt;# Create a file with the tool call request&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tool-call-request.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "roll_d20",
    "arguments": { 
      "number_of_dice": 2 
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;aws bedrock-agentcore invoke-agent-runtime &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--agent-runtime-arn&lt;/span&gt; &lt;span class="nv"&gt;$MCP_SERVER_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--content-type&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--accept&lt;/span&gt; &lt;span class="s2"&gt;"application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-cli-pager&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--payload&lt;/span&gt; fileb://tool-call-request.json &lt;span class="se"&gt;\&lt;/span&gt;
     ./tool-call-output.txt

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;
&lt;span class="c"&gt;# mcpSessionId: [MCP Session ID]&lt;/span&gt;
&lt;span class="c"&gt;# runtimeSessionId: [AgentCore Runtime ID - same as MCP Session ID]&lt;/span&gt;
&lt;span class="c"&gt;# [...]&lt;/span&gt;

&lt;span class="c"&gt;# View the results:&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;tool-call-output.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We're using the AWS CLI because &lt;code&gt;curl&lt;/code&gt; won't work anymore. The endpoint is protected by AWS IAM and can only be called with valid credentials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 6: Connect an AI Agent
&lt;/h2&gt;

&lt;p&gt;Extract the information required to build the MCP server URL:&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="c"&gt;# Extract the required information from `.bedrock_agentcore.yaml`:&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RUNTIME_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'agent_id:'&lt;/span&gt; .bedrock_agentcore.yaml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\"\'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'account:'&lt;/span&gt; .bedrock_agentcore.yaml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\"\'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'region:'&lt;/span&gt; .bedrock_agentcore.yaml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\"\'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# To see if it worked, type:&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"RUNTIME_ID=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$RUNTIME_ID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"AWS_ACCOUNT_ID=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"AWS_REGION=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prepare a new Python project for the AI agent:&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="c"&gt;# Create and cd into a subdirectory for the AI agent&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;agent
&lt;span class="nb"&gt;cd &lt;/span&gt;agent

&lt;span class="c"&gt;# Initialize a new Python project:&lt;/span&gt;
uv init &lt;span class="nt"&gt;--bare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the MCP Proxy for AWS (this enables IAM-based auth for MCP - &lt;a href="https://dev.to/aws/no-oauth-required-an-mcp-client-for-aws-iam-k1o"&gt;quick intro to using it with other frameworks, like LangChain, Llamaindex, etc.&lt;/a&gt;) and the &lt;a href="https://bit.ly/strands-agents-sdk" rel="noopener noreferrer"&gt;Strands Agents SDK&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;uv add mcp-proxy-for-aws strands-agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the agent:&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="c"&gt;# Create a simple MCP server in `agent.py`:&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; agent.py
from strands import Agent
from strands.tools.mcp import MCPClient
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client

runtime_id = "&lt;/span&gt;&lt;span class="nv"&gt;$RUNTIME_ID&lt;/span&gt;&lt;span class="sh"&gt;"
account_id = "&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="sh"&gt;"
region = "&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="sh"&gt;"

def main():
    host = f"https://bedrock-agentcore.{region}.amazonaws.com"
    path = f"runtimes/{runtime_id}/invocations"
    query_params = f"qualifier=DEFAULT&amp;amp;accountId={account_id}"
    url = f"{host}/{path}?{query_params}"

    mcp_client_factory = lambda: aws_iam_streamablehttp_client(
        terminate_on_close=False,
        aws_service="bedrock-agentcore",
        aws_region=region,
        endpoint=url
    )

    prompt = "Roll 3 dice"

    with MCPClient(mcp_client_factory) as mcp_client:
        mcp_tools = mcp_client.list_tools_sync()
        agent = Agent(tools=mcp_tools)

        print(f"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;User: {prompt}")

        print("&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Agent:")
        agent(prompt)

        print()

if __name__ == "__main__":
    main()
&lt;/span&gt;&lt;span class="no"&gt;
EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run agent.py

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="c"&gt;# User: Roll 3 dice&lt;/span&gt;
&lt;span class="c"&gt;# &lt;/span&gt;
&lt;span class="c"&gt;# Agent:&lt;/span&gt;
&lt;span class="c"&gt;# The agent will call the roll_d20 tool and display the results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You may see a deprecation warning: &lt;code&gt;DeprecationWarning: Use 'streamable_http_client' instead&lt;/code&gt;. This warning can be safely ignored. The official MCP library recently renamed their client. By the time you read this, it may already be fixed.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;When you're done experimenting, destroy the deployment to avoid ongoing costs:&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="c"&gt;# Navigate back to the project root&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..

&lt;span class="c"&gt;# This command removes all resources created during deployment&lt;/span&gt;
agentcore destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;If you learned something new, it would be great if you could like this post. For detailed explanations of each step, see the &lt;a href="https://dev.to/aws/from-local-mcp-server-to-aws-deployment-in-two-commands-ag4"&gt;full tutorial&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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