<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul SANTUS</title>
    <description>The latest articles on DEV Community by Paul SANTUS (@psantus).</description>
    <link>https://dev.to/psantus</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%2F1338515%2F8abdaf33-0c48-4f84-aa29-7a881090986e.jpeg</url>
      <title>DEV Community: Paul SANTUS</title>
      <link>https://dev.to/psantus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/psantus"/>
    <language>en</language>
    <item>
      <title>AWS Security Agent: 34 Findings in Under 10 Hours. A Real-World Test</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Mon, 08 Jun 2026 12:00:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-security-agent-34-findings-in-under-10-hours-a-real-world-test-2b4p</link>
      <guid>https://dev.to/aws-builders/aws-security-agent-34-findings-in-under-10-hours-a-real-world-test-2b4p</guid>
      <description>&lt;p&gt;Last week, I ran AWS Security Agent against an app I'm building for a client. The app is quite usual: a React front-end, and the backend is powered by a CMS (that's part of my customer's requirements) on top of which I built a custom API. Both run on Lambda, with DSQL as the database layer and quite a lot of AI inside (more on that below). The results were impressive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two scans, overnight
&lt;/h2&gt;

&lt;p&gt;I kicked off both scans in the evening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Review&lt;/strong&gt; completed in 1h26m:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For this, I had to grant read access to my application Github repository (you can provide write access to get fixes, but that was a step I wasn't ready to take just yet.)&lt;/li&gt;
&lt;li&gt;18 findings (9 High, 8 Medium, 1 Low) which covered SQL injection, SSRF, XSS, privilege escalation, secret exposure, IAM misconfigurations&lt;/li&gt;
&lt;li&gt;2h42m of agent task time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Penetration Test&lt;/strong&gt; completed in 7h56m:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pentest was not a full blackbox test. It started with 3 URLs I provided (the app's front-end, the API and the admin app) but I also submitted the repository. &lt;/li&gt;
&lt;li&gt;16 findings (2 Critical, 2 High, 12 Medium)&lt;/li&gt;
&lt;li&gt;29.16 hours of agent task time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time I woke up, I had a downloadable 60+ page report with reproduction steps, CVSS scores, and suggested fixes. And a pleasant UI to see results summary, but also findings details, test logs, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl54z6hmy533cm11mlxvh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl54z6hmy533cm11mlxvh.png" alt="AWS Security Agent PenTest result summary" width="799" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzinmwsj1fofvgi40m7an.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzinmwsj1fofvgi40m7an.png" alt="AWS Security Agent PenTest finding detail" width="799" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What impressed me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It thinks like a real Pentester
&lt;/h3&gt;

&lt;p&gt;The agent didn't just scan for known CVEs. It understood my application's architecture and chained vulnerabilities together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It discovered that some API method had no authentication&lt;/li&gt;
&lt;li&gt;It found that the some Lambda code called by a Step Function this method was triggering constructed a URL with user-supplied (non-protected) &lt;code&gt;lang&lt;/code&gt; parameter&lt;/li&gt;
&lt;li&gt;It crafted a payload using &lt;code&gt;#&lt;/code&gt; fragment to redirect requests to an attacker-controlled domain it owned&lt;/li&gt;
&lt;li&gt;It verified the SSRF by actually receiving DNS callbacks!!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a multi-step attack requiring deep understanding of Python's URL parsing, AWS Step Functions workflow, and the application's data flow from API to Lambda to Wikipedia API.&lt;br&gt;
(I provided the repo )&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL injection: genuinely clever
&lt;/h3&gt;

&lt;p&gt;The code review found a SQL injection vector I would never have caught manually. Our DSQL driver had a shortcut: if a value starts with &lt;code&gt;CAST(&lt;/code&gt;, it's passed through unescaped (intended for internal type conversions). The agent traced the full path from user input (&lt;code&gt;POST /api/my-route&lt;/code&gt; body) through the CMS' abstraction layer down to the raw &lt;code&gt;pg_query()&lt;/code&gt; call, proving the injection was reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  A unexpected category.
&lt;/h3&gt;

&lt;p&gt;I was expecting the Agent to report SSRF, path traversal, etc. One category I didn't expect was "Cost Abuse". Since my app runs on serverless, the Agent also provided valuable insights on path were attackers could make my AWS bill fat, especially via the use of Bedrock, Polly and other AI services. &lt;/p&gt;

&lt;h3&gt;
  
  
  Findings were validated through exploitation
&lt;/h3&gt;

&lt;p&gt;The penetration test didn't just flag theoretical issues. A bug let the CMS "Installation Wizard" accessible even once install once done. AWS Security Agent &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accessed the install page without auth&lt;/li&gt;
&lt;li&gt;Successfully connected to the production database (DSQL with IAM auth meant empty credentials worked)&lt;/li&gt;
&lt;li&gt;Enumerated installed plugins with exact version numbers&lt;/li&gt;
&lt;li&gt;Documented each step with HTTP requests and responses&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Both approaches brought original content
&lt;/h3&gt;

&lt;p&gt;Despite the white box approach, only half of findings were redundant. The code review caught architectural issues (IAM over-privilege due to several Lambdas sharing the same IAM role, hardcoded secrets, missing log retention) while the pentest found runtime exploitables (auth bypass, path traversal, IDOR). Together they covered more ground than either alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A word on cost
&lt;/h2&gt;

&lt;p&gt;Warning: AWS Security Agent CAN be expensive (yet cost-effective): at standard pricing of &lt;strong&gt;$50/agent-hour&lt;/strong&gt;, the total would have been:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code review: 2.7h × $50 = &lt;strong&gt;$135&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Pentest: 29.2h × $50 = &lt;strong&gt;$1,460&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: ~$1,595&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For context, a human pentest engagement of equivalent scope (3 URLs, mixed tech stack, 8 hours of active testing) would probably run $8,000–$20,000 and take 1-2 weeks to deliver results.&lt;/p&gt;

&lt;p&gt;But here's the kicker: &lt;strong&gt;AWS Security Agent includes a generous free tier&lt;/strong&gt;. New customers get a 2-month trial with up to &lt;strong&gt;400 pentesting task-hours per month&lt;/strong&gt;. Both my scans (31.8 task-hours total) fit comfortably within that allowance. So the first real-world security audit of my production application cost me exactly $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Findings to Fixes
&lt;/h2&gt;

&lt;p&gt;The actionable output let me fix all findings within a single day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL injection&lt;/strong&gt; → removed CAST bypass, added intval() on inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSRF&lt;/strong&gt; → URL scheme allowlist + private IP blocking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth bypass on install page&lt;/strong&gt; → overlay file blocking access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path traversal&lt;/strong&gt; → regex validation on URL path parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task token exposure&lt;/strong&gt; → stripped from API response&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Info disclosure&lt;/strong&gt; → CloudFront response headers policy removes version headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing logging&lt;/strong&gt; → API Gateway access logs + 30-day retention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The suggested fixes in the report were specific enough to implement directly, not generic "validate your inputs" advice, but exact code locations and replacement patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few more things
&lt;/h2&gt;

&lt;p&gt;To be exhaustive, I must share that&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it didn't find existing application logic bugs (due to the model being instructed to focus solely on security. Attention is all we need, right?)&lt;/li&gt;
&lt;li&gt;due to our white box nature of our pentest, a couple findings, while technically correct, required knowledge of our deployment model to be exploited. If you need to know the value of a secret "consider-i-m-an-admin" header, then maybe the risk is not high.. but again the agent thinks like security folks, and probably considered lateral movement after log access like a possible path.&lt;/li&gt;
&lt;li&gt;Some findings are CMS upstream issues that I can't fix without modifying vendor code. I submitted findings to their security team. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;AWS Security Agent is not a full replacement for security expertise: you still need to understand your architecture to prioritize and fix findings. But as a first pass that runs overnight and produces a professional-grade report? It's remarkably good; literally 0 findings were non-relevant. The multi-step attack chains, the code-level precision, and the actual exploitation validation put it well above traditional SAST/DAST tools.&lt;/p&gt;

&lt;p&gt;For a solo developer or small team shipping on AWS, this is a no-brainer at the free tier. Run it before every major release, fix what it finds, and sleep better.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>S3 zipper challenge: a parallel zip assembly that beats the single Lambda approach</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Mon, 01 Jun 2026 21:27:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/s3-zipper-challenge-a-parallel-zip-assembly-that-beats-the-single-lambda-approach-37gf</link>
      <guid>https://dev.to/aws-builders/s3-zipper-challenge-a-parallel-zip-assembly-that-beats-the-single-lambda-approach-37gf</guid>
      <description>&lt;p&gt;I recently read Jérémie Rodon's excellent article &lt;a href="https://rustysl.com/en/blog/s3-on-demand-archive" rel="noopener noreferrer"&gt;On-Demand Archives on S3&lt;/a&gt;, where he describes an elegant Rust solution for zipping 3,000 × 5MB files from S3 within a single Lambda function. &lt;/p&gt;

&lt;p&gt;His approach is impressive: streaming a ZIP archive through a custom Rotating Slab Buffer, saturating bandwidth with concurrent downloads, all within 512MB of RAM. The result: &lt;strong&gt;3 minutes 35 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I thought it was a good challenge to reach better performance. His article ends with an open invitation: &lt;em&gt;"do you think you can do better with your favorite language?"&lt;/em&gt; Well, my favorite language is not Rust nor Go nor.. however, I'm fluent in serverless ;) so I took a different angle entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Approach: Why Not Parallelize the Problem?
&lt;/h2&gt;

&lt;p&gt;Jérémie's constraint was a single Lambda. That's elegant, but it means you're bound by one machine's network bandwidth (~600 Mbps). No matter how perfect your streaming is, physics wins: 15GB at 600 Mbps ≈ 200 seconds minimum.&lt;/p&gt;

&lt;p&gt;My question was: &lt;strong&gt;what if we break that single-machine bottleneck?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The key insight is that ZIP files in STORE mode (no compression) have &lt;strong&gt;deterministic byte offsets&lt;/strong&gt;. Each entry is exactly &lt;code&gt;50 + len(filename) + filesize&lt;/code&gt; bytes (local header + ZIP64 extra field + data). If you know all filenames and sizes upfront, you can pre-calculate exactly where every file will land in the final archive, before downloading a single byte.&lt;/p&gt;

&lt;p&gt;This means independent workers can each build their portion of the zip in parallel, and S3's multipart upload lets them write their chunks independently (parts can be uploaded in any order by different processes sharing the same upload ID).&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planner Lambda → Step Functions Distributed Map → N Worker Lambdas → Finalizer Lambda
     │                        │ │ │                        │
     │ CreateMultipartUpload  │ │ │ UploadPart (parallel)  │ CompleteMultipartUpload
     ▼                        ▼ ▼ ▼                        ▼
                         S3 Output Bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0q5pzfth4of1231hcu2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0q5pzfth4of1231hcu2.png" alt=" " width="294" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Planner&lt;/strong&gt;: Lists all source files, computes zip byte offsets, initiates multipart upload, divides work into balanced batches (equal data volume per worker).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Workers&lt;/strong&gt; (N concurrent): Each downloads its assigned files, constructs zip local file headers + raw data, computes CRC32 on the fly, streams to S3 as multipart parts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Finalizer&lt;/strong&gt;: Builds the central directory with real CRC32 values, uploads it as the final part, calls CompleteMultipartUpload.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;With a quota-constrained training account (I had 10 concurrency limit so used only 5 concurrent Lambdas, 3008MB each), zipping &lt;strong&gt;6.9GB across 160 files&lt;/strong&gt;:&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;Single Lambda (Jérémie's Rust)&lt;/th&gt;
&lt;th&gt;Parallel (this project)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Approach&lt;/td&gt;
&lt;td&gt;Stream within 1 Lambda&lt;/td&gt;
&lt;td&gt;Fan-out N workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time (15GB, 3000 files)&lt;/td&gt;
&lt;td&gt;~215s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Estimated ~10-15s&lt;/strong&gt; with 100+ workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time (6.9GB, 160 files, 5 workers)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per worker&lt;/td&gt;
&lt;td&gt;512MB&lt;/td&gt;
&lt;td&gt;3008MB (could be lower)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust 🦀&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a production account (1000 concurrent Lambdas), the 3000 × 5MB scenario would complete in &lt;strong&gt;under 15 seconds&lt;/strong&gt; (each worker handles ~150MB, downloads take ~2s at 600Mbps, upload ~2s). The bottleneck shifts from bandwidth to Lambda cold start (~200ms for Go on ARM64).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Jérémie's approach is &lt;strong&gt;simpler to deploy&lt;/strong&gt; (one Lambda, no orchestration) and &lt;strong&gt;cheaper per invocation&lt;/strong&gt; (512MB × 215s vs N × 3008MB × few seconds). It's the right choice when you want minimal infrastructure.&lt;/p&gt;

&lt;p&gt;The parallel approach wins on &lt;strong&gt;wall-clock time&lt;/strong&gt;, and dramatically so. It's the right choice when the user is waiting and you want the archive ready in seconds, not minutes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Single Lambda&lt;/th&gt;
&lt;th&gt;Parallel Fan-Out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wall-clock time&lt;/td&gt;
&lt;td&gt;Bounded by bandwidth&lt;/td&gt;
&lt;td&gt;Bounded by slowest worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium (Step Functions + 3 Lambdas)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per archive&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Higher (more Lambda-seconds total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Fixed ceiling (~600Mbps)&lt;/td&gt;
&lt;td&gt;Linear with concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory efficiency&lt;/td&gt;
&lt;td&gt;Excellent (512MB)&lt;/td&gt;
&lt;td&gt;Good (3GB, could optimize)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I were to use it in prod, there are plenty of room for optimization (our current Lambda used at most 1875mb, well below our allocated 3Gb, we could use Jérémie's streaming optimizations to cut that to by 10). Yet, we'd probably still have some overhead compared to Jeremie's solutions (cold starts, TLS negociations...) and so far it's just a vanity project :)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ZIP STORE mode is embarrassingly parallel&lt;/strong&gt;: deterministic offsets mean zero coordination between workers during the data phase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 multipart upload is the perfect primitive&lt;/strong&gt;: parts uploaded out of order, by different processes, assembled by S3 at the end.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step Functions Distributed Map&lt;/strong&gt; is ideal for this pattern: it handles fan-out, concurrency limits, retries, and result collection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The real bottleneck at scale is Lambda concurrency limits&lt;/strong&gt;, not bandwidth or compute. With sufficient concurrency, you can zip 15GB in the time it takes to download one 5MB file.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The code is at &lt;a href="https://github.com/psantus/on-demand-archive-on-s3" rel="noopener noreferrer"&gt;github.com/psantus/on-demand-archive-on-s3&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you want to try Jérémie's challenge with the single-Lambda constraint, his demo project is at &lt;a href="https://github.com/RustyServerless/demo-s3-archiving" rel="noopener noreferrer"&gt;github.com/RustyServerless/demo-s3-archiving&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both approaches are valid, it just depends on whether you're optimizing for simplicity or speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the challenge going?
&lt;/h2&gt;

&lt;p&gt;So, « &lt;strong&gt;do you think you can do better with your favorite &lt;del&gt;language&lt;/del&gt; architecture?&lt;/strong&gt;» &lt;/p&gt;

&lt;p&gt;And what does "better" even mean for you? :)&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>s3</category>
      <category>zip</category>
    </item>
    <item>
      <title>Hackez votre AWS CLI pour ajouter le support CloudShell et transformer votre terminal en bastion</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 29 May 2026 12:43:23 +0000</pubDate>
      <link>https://dev.to/aws-builders/hackez-votre-aws-cli-pour-ajouter-le-support-cloudshell-et-transformer-votre-terminal-en-bastion-2hoo</link>
      <guid>https://dev.to/aws-builders/hackez-votre-aws-cli-pour-ajouter-le-support-cloudshell-et-transformer-votre-terminal-en-bastion-2hoo</guid>
      <description>&lt;p&gt;J'utilise AWS CloudShell depuis la Console depuis un moment. C'est pratique : un shell pré-authentifié dans votre navigateur, directement dans la Console AWS. Mais je me suis toujours demandé : pourquoi je ne peux pas l'utiliser depuis mon terminal ? Pourquoi n'y a-t-il pas de commande &lt;code&gt;aws cloudshell&lt;/code&gt; ?&lt;/p&gt;

&lt;p&gt;Il s'avère que c'est possible. L'API existe, elle n'est simplement pas publique. Et une fois que vous avez accès à CloudShell en CLI, vous pouvez faire des choses intéressantes avec, comme utiliser un CloudShell attaché à un VPC comme bastion pour atteindre vos instances RDS privées.&lt;/p&gt;

&lt;p&gt;Consultez le &lt;a href="https://github.com/psantus/cloudshell-cli" rel="noopener noreferrer"&gt;dépôt compagnon&lt;/a&gt; en lisant cet article.&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudShell : une API non documentée
&lt;/h2&gt;

&lt;p&gt;AWS CloudShell n'a pas de support officiel SDK ou CLI. Mais la Console doit bien communiquer avec &lt;em&gt;quelque chose&lt;/em&gt;, non ? En regardant ce que fait le navigateur quand vous ouvrez CloudShell, vous pouvez rétro-ingénierer l'API.&lt;/p&gt;

&lt;p&gt;Heureusement, &lt;a href="https://github.com/guyon-it-consulting/cloudshell-boto3" rel="noopener noreferrer"&gt;Jérôme Guyon&lt;/a&gt; a déjà fait ce travail et publié un modèle de service compatible boto3. Son travail a rendu tout cela possible.&lt;/p&gt;

&lt;p&gt;L'API est simple : créer des environnements, les démarrer/arrêter, créer des sessions, uploader/télécharger des fichiers. Le mécanisme de session utilise le protocole WebSocket de SSM sous le capot, ce qui signifie que &lt;code&gt;session-manager-plugin&lt;/code&gt; (le même binaire qui fait tourner &lt;code&gt;aws ssm start-session&lt;/code&gt;) peut se connecter aux sessions CloudShell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apprendre un nouveau tour à l'AWS CLI
&lt;/h2&gt;

&lt;p&gt;L'AWS CLI a une fonctionnalité peu connue : &lt;code&gt;aws configure add-model&lt;/code&gt;. Donnez-lui un modèle de service JSON, et soudain la CLI connaît un nouveau service. AWS utilise ça en interne pour les previews privées.&lt;/p&gt;

&lt;p&gt;(Le modèle boto3 du dépôt de Jérôme a juste besoin d'un champ &lt;code&gt;"version": "2.0"&lt;/code&gt; ajouté au niveau racine pour devenir compatible CLI.)&lt;/p&gt;

&lt;p&gt;Exécutez :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure add-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-model&lt;/span&gt; file://cloudshell-cli-model.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; cloudshell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C'est tout. Maintenant j'ai &lt;code&gt;aws cloudshell&lt;/code&gt; avec l'auto-complétion et tout :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws cloudshell &lt;span class="nb"&gt;help

&lt;/span&gt;AVAILABLE COMMANDS
       create-environment
       create-session
       delete-environment
       describe-environments
       get-environment-status
       start-environment
       stop-environment
       ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Se connecter à CloudShell depuis le terminal
&lt;/h2&gt;

&lt;p&gt;Le workflow est simple :&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;# Créer ou trouver un environnement&lt;/span&gt;
aws cloudshell create-environment &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Attendre qu'il soit RUNNING&lt;/span&gt;
aws cloudshell get-environment-status &lt;span class="nt"&gt;--environment-id&lt;/span&gt; &amp;lt;ID&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Créer une session et se connecter&lt;/span&gt;
session-manager-plugin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudshell create-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment-id&lt;/span&gt; &amp;lt;ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-type&lt;/span&gt; TMUX &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tab-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--q-cli-disabled&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'{SessionId:SessionId,TokenValue:TokenValue,StreamUrl:StreamUrl}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; eu-west-1 StartSession
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et vous y êtes. Un shell complet sur une instance CloudShell, depuis votre terminal. Pas besoin de navigateur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le problème des credentials
&lt;/h2&gt;

&lt;p&gt;Il y a un hic. Quand vous utilisez CloudShell depuis la Console, AWS injecte vos credentials automatiquement via un appel API &lt;code&gt;PutCredentials&lt;/code&gt;. Celui-ci utilise votre token de session console (l'auth par cookie de votre connexion navigateur) pour alimenter le endpoint de métadonnées du conteneur en credentials temporaires.&lt;/p&gt;

&lt;p&gt;Quand vous vous connectez par programme, ça ne se fait pas. Le endpoint de credentials du conteneur renvoie une erreur 500. Vous devez injecter les credentials vous-même :&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;# Exécutez localement, puis collez la sortie dans votre session CloudShell&lt;/span&gt;
aws configure export-credentials &lt;span class="nt"&gt;--profile&lt;/span&gt; my-profile &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pas idéal, mais ça fonctionne.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le cas d'usage bastion
&lt;/h2&gt;

&lt;p&gt;C'est là que ça devient intéressant. Vous pouvez créer un environnement CloudShell attaché à un VPC :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudshell create-environment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment-name&lt;/span&gt; db-access &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "VpcId": "vpc-abc123",
    "SubnetIds": ["subnet-private-1"],
    "SecurityGroupIds": ["sg-allowed-by-rds"]
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mettez-le dans le même security group que celui autorisé par votre RDS, et soudain vous pouvez vous connecter à votre base de données directement depuis le shell :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; my-instance.xxx.eu-west-1.rds.amazonaws.com &lt;span class="nt"&gt;-u&lt;/span&gt; admin &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pas d'instance EC2 bastion à maintenir. Pas de clés SSH à gérer. Pas de coût horaire quand vous ne l'utilisez pas (CloudShell est gratuit). L'environnement se suspend après 20 minutes d'inactivité et vous pouvez le maintenir en vie avec &lt;code&gt;aws cloudshell send-heart-beat&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qui ne marche pas (et j'ai essayé..)
&lt;/h2&gt;

&lt;p&gt;J'ai passé pas mal de temps à essayer de faire fonctionner CloudShell comme un vrai bastion de port-forwarding, pour pouvoir utiliser des outils locaux comme DBeaver contre un RDS distant à travers lui. Voici ce que j'ai trouvé :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le port forwarding basé sur SSM ne fonctionne pas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ECS, par exemple, enregistre les conteneurs comme cibles SSM. Son identifiant SSM n'est pas documenté mais une fois qu'on le connaît, ça marche bien, comme je l'ai décrit dans &lt;a href="https://dev.to/aws-builders/access-your-aws-database-using-local-port-forwarding-on-your-ecsfargate-container-4nk4"&gt;un précédent article&lt;/a&gt;. De cette façon vous pouvez lancer &lt;code&gt;aws ssm start-session --document-name AWS-StartPortForwardingSessionToRemoteHost&lt;/code&gt;.&lt;br&gt;
Les notebooks SageMaker ont un comportement similaire.&lt;/p&gt;

&lt;p&gt;Les instances/conteneurs CloudShell ne semblent pas être enregistrés comme instances managées SSM. Ou s'ils le sont, c'est caché et à ce jour, personne chez AWS n'a divulgué le format de leur ID :) J'ai essayé toutes les combinaisons d'ID d'environnement, d'ID de session et de format de préfixe auxquelles j'ai pu penser. Aucune ne fonctionne.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le port forwarding local à travers le PTY ne fonctionne pas non plus.&lt;/strong&gt; La session est un terminal, pas un flux TCP brut. Vous ne pouvez pas faire passer des données binaires du protocole MySQL à travers. J'ai même essayé de mettre en place un relais ncat à l'intérieur de CloudShell et de tunneler à travers la session. Le relais fonctionne bien en interne, mais il n'y a aucun moyen de l'exposer comme un port TCP local sur votre machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le hole punching UDP est théoriquement possible&lt;/strong&gt; mais nécessite que le CloudShell ait accès à internet (NAT Gateway sur son subnet), et même là vous vous battez contre des problèmes de symétrie NAT des deux côtés. J'ai réussi à faire fonctionner STUN depuis CloudShell, mais le hole punch complet est fragile et impraticable pour un usage en production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alors à quoi ça sert ?
&lt;/h2&gt;

&lt;p&gt;Honnêtement, à pas mal de choses :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accès rapide à la base de données&lt;/strong&gt; sans maintenir une instance EC2 bastion. Connectez-vous, exécutez vos requêtes, déconnectez-vous. Gratuit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatisation.&lt;/strong&gt; Vous pouvez scripter l'exécution de commandes sur CloudShell via Python + &lt;code&gt;session-manager-plugin&lt;/code&gt;. Utile pour exécuter des choses à l'intérieur d'un VPC sans déployer une Lambda ou une tâche Fargate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Débogage de connectivité réseau.&lt;/strong&gt; Lancez un CloudShell dans une combinaison subnet/SG spécifique et testez ce qui peut atteindre quoi.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transfert de fichiers&lt;/strong&gt; (depuis les environnements publics). Les APIs &lt;code&gt;get-file-upload-urls&lt;/code&gt; et &lt;code&gt;get-file-download-urls&lt;/code&gt; vous donnent des URLs S3 présignées.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La limitation principale est que vous êtes limité à exécuter des commandes &lt;em&gt;à l'intérieur&lt;/em&gt; du shell. Vous ne pouvez pas l'utiliser comme un tunnel transparent pour vos outils locaux. Pour ça, vous avez toujours besoin d'une instance EC2 avec l'agent SSM, ou d'une tâche ECS avec execute-command activé.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essayez vous-même
&lt;/h2&gt;

&lt;p&gt;J'ai publié le modèle et un script d'exemple ici : &lt;a href="https://github.com/psantus/cloudshell-cli" rel="noopener noreferrer"&gt;github.com/psantus/cloudshell-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;L'installation se fait en une commande. Le tout est un seul fichier JSON qui apprend un nouveau service à votre AWS CLI. Rappelez-vous juste : c'est une API non documentée. AWS peut la modifier ou la casser à tout moment. Ne construisez rien de critique dessus.&lt;/p&gt;

&lt;p&gt;Mais pour un accès VPC rapide depuis votre terminal ? C'est plutôt génial.&lt;/p&gt;

</description>
      <category>cloudshell</category>
      <category>cli</category>
      <category>aws</category>
    </item>
    <item>
      <title>Générer des données structurées avec un LLM : quelques astuces pour plus de fiabilité</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 29 May 2026 12:41:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/generer-des-donnees-structurees-avec-un-llm-quelques-astuces-pour-plus-de-fiabilite-1png</link>
      <guid>https://dev.to/aws-builders/generer-des-donnees-structurees-avec-un-llm-quelques-astuces-pour-plus-de-fiabilite-1png</guid>
      <description>&lt;p&gt;Les LLMs sont excellents pour générer du texte. Ils sont mauvais pour générer des données structurées de manière fiable. Si vous avez déjà essayé de faire produire à un agent un objet JSON avec un schéma précis, vous connaissez le douloureux résultat : champs manquants, clés hallucinées, types incohérents, et des sorties qui cassent votre pipeline en aval.&lt;/p&gt;

&lt;p&gt;Dépassant le stade du code de démo pour travailler sur de vraies applications IA en production, j'ai été confronté au problème et j'ai trouvé une approche qui fonctionne remarquablement bien pour une application IA que je développe : &lt;strong&gt;utiliser les outils comme le pattern Builder de la programmation orientée objet&lt;/strong&gt;. Au lieu de demander au modèle de produire un blob JSON final, vous lui donnez des outils qui construisent la sortie de manière incrémentale - comme appeler des méthodes sur un objet. Le modèle ne voit ni ne produit jamais la structure finale directement. Il appelle simplement des outils, et la sortie structurée émerge comme un effet de bord.&lt;/p&gt;

&lt;p&gt;C'est particulièrement important quand votre agent traite des documents volumineux (formulaires d'assurance, dossiers juridiques, dossiers médicaux) qui consomment la majeure partie de la fenêtre de contexte. Quand l'entrée est volumineuse et que la tâche comporte plusieurs étapes, vous ne pouvez pas vous permettre de réserver aussi de l'espace pour une sortie structurée massive à la fin. Le pattern accumulateur vous permet de compresser la conversation en cours de route sans perdre aucune des données structurées déjà collectées, car ces données vivent entièrement en dehors de la fenêtre de contexte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Défis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Génère-moi un gros JSON" : les soucis
&lt;/h3&gt;

&lt;p&gt;L'approche naïve - demander au modèle de produire une structure JSON complète - échoue de manière quasi systématique lorsque le volume augmente :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dérive de schéma.&lt;/strong&gt; Le modèle oublie des champs obligatoires, en invente de nouveaux, ou change les types d'une exécution à l'autre. Un champ &lt;code&gt;date&lt;/code&gt; peut être une chaîne une fois et un objet la suivante.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tout-ou-rien.&lt;/strong&gt; Si le modèle fait une seule erreur dans une sortie JSON de 200 lignes, l'ensemble est impossible à parser. Vous devez soit relancer toute la génération, soit écrire du code de correction fragile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de progrès incrémental.&lt;/strong&gt; Quand un agent doit collecter des informations &lt;em&gt;et&lt;/em&gt; produire une sortie structurée, lui demander de faire les deux en une seule passe signifie qu'il ne peut pas itérer. Il s'engage sur une structure avant d'avoir tous les faits.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pourquoi &lt;code&gt;response_format&lt;/code&gt; et les schémas de function-calling ne suffisent pas
&lt;/h3&gt;

&lt;p&gt;Les modes de sortie structurée (comme &lt;code&gt;response_format: json_schema&lt;/code&gt; d'OpenAI ou les schémas de résultats d'outils de Bedrock) aident avec la syntaxe - vous obtiendrez du JSON valide. Mais ils ne résolvent pas le problème sémantique. Le modèle doit toujours produire la structure entière en une seule passe, et il hallucine toujours du contenu pour remplir les champs obligatoires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un problème répandu
&lt;/h3&gt;

&lt;p&gt;Toute équipe qui construit des agents autonomes ou semi-autonomes fait face à ce problème, pas seulement moi. Kiro CLI, le compagnon de développement agentique d'AWS, par exemple, a beaucoup galéré avec les grandes structures de données à son lancement.&lt;/p&gt;

&lt;p&gt;Depuis, ses mainteneurs ont équipé son harnais de capacités JSON (manipulations &lt;code&gt;jq&lt;/code&gt;, par exemple) et de multiples stratégies (utilisation extensive de grep, glob, tail..) pour éviter de remplir la fenêtre de contexte.&lt;/p&gt;

&lt;p&gt;Ça fait quand même plaisir de savoir que je ne suis pas le seul à avoir galéré :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Mes solutions
&lt;/h2&gt;

&lt;p&gt;Voici quelques astuces que j'ai utilisées avec succès pour contrôler à la fois la sortie de l'agent et la fenêtre de contexte. Comme je ne prétends pas avoir toutes les recettes, n'hésitez pas à commenter les vôtres ou à me taguer dans vos propres posts :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Utiliser les outils comme des Builder méthodes
&lt;/h3&gt;

&lt;p&gt;L'idée centrale : définir des outils qui agissent comme des méthodes Builder en POO. Chaque appel d'outil ajoute un élément bien typé à un accumulateur. Le travail du modèle passe de "produis cette structure" à "appelle ces fonctions dans le bon ordre."&lt;/p&gt;

&lt;p&gt;Voici le pattern - imaginez un agent qui traite des sinistres d'assurance en lisant des documents et en construisant une évaluation structurée :&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# L'accumulateur - c'est votre sortie structurée
&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_output&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_party&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&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;policy_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Enregistrer une partie impliquée dans le sinistre.

    Args:
        name: Nom complet de la personne ou de l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;organisation.
        role: Un parmi : claimant, insured, witness, adjuster, third_party
        policy_id: Numéro de police si applicable.

    Returns:
        Confirmation avec les détails de la partie.
    &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;role&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claimant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insured&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;witness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adjuster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;third_party&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid role &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. Must be one of: claimant, insured, witness, adjuster, third_party&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;policy_id&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Added &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&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;date&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;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Enregistrer un événement chronologique pertinent pour le sinistre.

    Args:
        description: Ce qui s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;est passé (1-3 phrases).
        date: Date au format ISO (AAAA-MM-JJ).
        location: Où cela s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;est produit (optionnel).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recorded event on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; events total)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&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;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Enregistrer un poste de dommage avec le coût estimé.

    Args:
        item: Description de l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;élément endommagé ou du coût.
        amount: Coût estimé en dollars.
        category: Un parmi : property, medical, liability, lost_income
        evidence_ref: Référence à une preuve justificative (optionnel).
    &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;category&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lost_income&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid category &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Added damage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ($&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;). Running total: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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;L'agent reçoit ces outils et un prompt système qui lui dit de traiter un sinistre. Au fur et à mesure qu'il lit les documents et découvre des informations, il appelle &lt;code&gt;add_party&lt;/code&gt;, &lt;code&gt;add_event&lt;/code&gt; et &lt;code&gt;add_damage&lt;/code&gt;. La sortie structurée se construit de manière incrémentale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation à la frontière
&lt;/h3&gt;

&lt;p&gt;Chaque appel d'outil est un point de contrôle de validation. Vous pouvez rejeter les entrées invalides immédiatement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&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;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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;category&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lost_income&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid category &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;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;Error: amount must be positive, got &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;.&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;evidence_ref&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;evidence_ref&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: evidence &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;evidence_ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not registered. Call add_evidence first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le modèle reçoit un feedback instantané. S'il essaie de référencer une preuve qu'il n'a pas encore enregistrée, l'outil le lui dit. Le modèle se corrige au tour suivant. Comparez cela à la validation d'un blob JSON de 500 lignes après coup - à ce moment-là, le modèle est passé à autre chose et ne peut plus corriger ses erreurs dans le contexte.&lt;/p&gt;

&lt;h3&gt;
  
  
  Décorréler la phase de réflexion de la construction de la sortie
&lt;/h3&gt;

&lt;p&gt;Un avantage clé : le même agent peut avoir des outils de &lt;em&gt;lecture&lt;/em&gt; et des outils d'&lt;em&gt;écriture&lt;/em&gt;. Les outils de lecture récupèrent et explorent les données. Les outils d'écriture construisent la sortie. Le modèle les entrelace naturellement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;# Outils de lecture
&lt;/span&gt;        &lt;span class="n"&gt;read_document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;get_weather_report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# Outils d'écriture (méthodes Builder)
&lt;/span&gt;        &lt;span class="n"&gt;add_party&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;set_assessment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# Suivi de progression
&lt;/span&gt;        &lt;span class="n"&gt;mark_step_done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Un seul appel - l'agent lit les documents ET construit la sortie structurée
&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process this claim: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;claim_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# La sortie est prête
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le modèle lit un rapport de police, extrait une partie, lit une facture médicale, enregistre un poste de dommage, vérifie la police d'assurance, et ainsi de suite. Recherche et construction de la sortie sont entrelacées plutôt que séquentielles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suivi de progression et récupération
&lt;/h3&gt;

&lt;p&gt;Parce que la sortie s'accumule de manière incrémentale, vous obtenez la récupération après crash gratuitement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STEPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1. Identify all parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2. Establish timeline of events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3. Catalog damages with evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4. Cross-reference policy coverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5. Produce assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;completed_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_step_done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Marquer une étape de traitement comme terminée.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;completed_steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STEPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;completed_steps&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;Step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; done. Remaining: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&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;Si l'agent atteint une limite de fenêtre de contexte ou plante, vous avez déjà des résultats partiels - chaque partie identifiée, chaque événement enregistré, chaque poste de dommage catalogué jusqu'à ce point. Vous pouvez reprendre ou utiliser ce que vous avez.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gestion du contexte par injection d'état
&lt;/h3&gt;

&lt;p&gt;C'est là que ce pattern prend tout son sens. Quand votre agent ingère un document de 30 pages puis fait des dizaines d'appels d'outils pour récupérer des sources supplémentaires, la fenêtre de contexte se remplit vite. Dans une approche traditionnelle, vous perdriez votre sortie structurée en même temps que la conversation quand vous atteignez la limite. Mais parce que l'accumulateur vit dans la mémoire Python - pas dans l'historique des messages - vous pouvez compresser agressivement la conversation sans perdre un seul point de données.&lt;/p&gt;

&lt;p&gt;Un gestionnaire de conversation personnalisé (une possibilité offerte, par exemple, par le &lt;a href="https://strandsagents.com/docs/user-guide/concepts/agents/conversation-management/#creating-a-conversationmanager" rel="noopener noreferrer"&gt;SDK Strands Agents&lt;/a&gt;) remplace les anciens messages par un résumé d'état compact dérivé de l'accumulateur :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClaimConversationManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConversationManager&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_management&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;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Garder le premier message + les 2 derniers messages
&lt;/span&gt;        &lt;span class="c1"&gt;# Remplacer tout le reste par un résumé d'état
&lt;/span&gt;        &lt;span class="n"&gt;first_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_state_summary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;state_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[STATE]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Continue.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[:]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first_msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state_msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_state_summary&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Résumer ce qui a été fait en utilisant l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;état de l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accumulateur.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&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;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;parties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Parties: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parties&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Damages: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items, $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; total&lt;/span&gt;&lt;span class="sh"&gt;"&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;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Events: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; recorded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parce que la sortie structurée vit en Python (pas dans la conversation), la compression du contexte ne perd aucune donnée. Le modèle peut toujours voir ce qu'il a déjà produit en lisant le résumé d'état.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bénéfices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sûreté de typage sans coercition
&lt;/h3&gt;

&lt;p&gt;Chaque outil a des paramètres typés imposés par le framework. Le modèle doit fournir une &lt;code&gt;category&lt;/code&gt; parmi &lt;code&gt;property, medical, liability, lost_income&lt;/code&gt; - non pas parce que vous parsez du JSON et vérifiez après coup, mais parce que la signature de l'outil l'exige. Les appels invalides sont rejetés avec des messages d'erreur clairs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composabilité
&lt;/h3&gt;

&lt;p&gt;Les outils se composent naturellement. Vous pouvez ajouter de nouveaux champs de sortie en ajoutant de nouveaux outils sans modifier les existants. Vous voulez suivre les pièces justificatives ? Ajoutez un outil &lt;code&gt;add_evidence&lt;/code&gt;. Vous voulez une recommandation finale ? Ajoutez un outil &lt;code&gt;set_assessment&lt;/code&gt;. Le modèle découvre les nouvelles capacités via sa liste d'outils.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testabilité
&lt;/h3&gt;

&lt;p&gt;Chaque outil est une fonction pure (ou presque). Vous pouvez les tester unitairement de manière indépendante :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_damage_rejects_invalid_category&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;reset_output&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="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Roof repair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosmetic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_damage_tracks_total&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;reset_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Roof repair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Water damage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Schéma de sortie déterministe
&lt;/h3&gt;

&lt;p&gt;Le schéma de sortie est défini par votre code Python, pas par l'interprétation du modèle d'un prompt. &lt;code&gt;claim_output&lt;/code&gt; a toujours les mêmes clés avec les mêmes types. Les consommateurs en aval peuvent compter sur la structure de manière inconditionnelle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dégradation gracieuse
&lt;/h3&gt;

&lt;p&gt;Si le modèle manque de contexte ou rencontre une erreur, vous avez tout ce qu'il a produit jusqu'à ce point. Vous pouvez même détecter une sortie vide et relancer avec un coup de pouce :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You haven&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t started processing. Begin by identifying the parties involved.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Comportement naturel de l'agent
&lt;/h3&gt;

&lt;p&gt;Le modèle n'a pas besoin de basculer entre "réfléchir" et "formater." Il réfléchit en appelant des outils. La sortie structurée est un sous-produit du travail de l'agent, pas un fardeau de formatage supplémentaire ajouté par-dessus.&lt;/p&gt;

&lt;p&gt;Ce pattern - outils comme Builder, accumulateur comme sortie, validation à la frontière - est la manière la plus fiable que j'ai trouvée pour obtenir des données structurées d'un workflow agentique. Ça fonctionne parce que c'est aligné avec la façon dont les modèles à appels d'outils se comportent déjà : ils raisonnent, ils agissent, ils observent les résultats, et ils agissent à nouveau. Vous faites simplement en sorte que "agir" signifie "construire un morceau de la sortie."&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>LLMs suck at generating large, structured data. Tips on how to get your AI agent to do it reliably</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 29 May 2026 12:06:19 +0000</pubDate>
      <link>https://dev.to/aws-builders/llms-suck-at-generating-large-structured-data-tips-on-how-to-get-your-ai-agent-to-do-it-reliably-3mop</link>
      <guid>https://dev.to/aws-builders/llms-suck-at-generating-large-structured-data-tips-on-how-to-get-your-ai-agent-to-do-it-reliably-3mop</guid>
      <description>&lt;p&gt;LLMs are great at generating text. They're terrible at generating structured data reliably. If you've ever tried to get an agent to produce a JSON object with a specific schema, you know the pain: missing fields, hallucinated keys, inconsistent types, and outputs that break your downstream pipeline.&lt;/p&gt;

&lt;p&gt;As I got past toy examples and labs to work on real, production-grade AI apps, I faced the problem and found an approach that works remarkably well for an AI app I'm building: &lt;strong&gt;use tools like object-oriented programming Builder pattern&lt;/strong&gt;. Instead of asking the model to produce a final JSON blob, you give it tools that incrementally build the output - like calling methods on an object. The model never sees or produces the final structure directly. It just calls functions, and the structured output emerges as a side effect.&lt;/p&gt;

&lt;p&gt;This matters especially when your agent processes large documents (like insurance forms, legal filings, medical records) that eat up most of the context window. When the input is big and the task is multi-step, you can't afford to also reserve space for a massive structured output at the end. The accumulator pattern lets you compress the conversation mid-flight without losing any of the structured data you've already collected, because that data lives outside the token window entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The "generate JSON" problem
&lt;/h3&gt;

&lt;p&gt;The naive approach - asking a model to output a complete JSON structure - fails in predictable ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schema drift.&lt;/strong&gt; The model forgets required fields, invents new ones, or changes types between runs. A &lt;code&gt;date&lt;/code&gt; field might be a string one time and an object the next.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;All-or-nothing failure.&lt;/strong&gt; If the model makes one mistake in a 200-line JSON output, the entire thing is unparseable. You either retry the whole generation or write brittle fixup code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No incremental progress.&lt;/strong&gt; If the model hits a context limit or stops mid-generation, you lose everything. There's no partial result to recover from.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hallucination in structure.&lt;/strong&gt; Models are more likely to hallucinate when producing structured output in one shot. They fill in fields they're uncertain about rather than leaving them empty, because the structure demands completeness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Coupling research and output.&lt;/strong&gt; When an agent needs to gather information &lt;em&gt;and&lt;/em&gt; produce structured output, asking it to do both in one pass means it can't iterate. It commits to a structure before it has all the facts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;response_format&lt;/code&gt; and function-calling schemas aren't enough
&lt;/h3&gt;

&lt;p&gt;Structured output modes (like OpenAI's &lt;code&gt;response_format: json_schema&lt;/code&gt; or Bedrock's tool result schemas) help with syntax - you'll get valid JSON. But they don't solve the semantic problem. The model still has to produce the entire structure in one shot, and it still hallucinates content to fill required fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  A wide-spread issue
&lt;/h3&gt;

&lt;p&gt;Any team building autonomous or semi-autonomous agents face this, not just me. Kiro CLI, AWS' agentic dev companion, for instance, struggled hard with large data structures when first launched. &lt;/p&gt;

&lt;p&gt;Since then, its maintainers have equipped its harness with JSON capabilities (&lt;code&gt;jq&lt;/code&gt; manipulations, for instance) and multiples strategies (extensive use of grep, glob, tail..) to avoid filling the context window.&lt;/p&gt;

&lt;p&gt;Still, happy to know I'm not alone in facing this :)&lt;/p&gt;

&lt;h2&gt;
  
  
  My solutions
&lt;/h2&gt;

&lt;p&gt;Here are a few tricks I have used successfully to control both agent output and context window. As I don't claim to have all the recipes, don't hesitate to comment your own or tag my in your own posts :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools as Builder methods
&lt;/h3&gt;

&lt;p&gt;The core idea: define tools that act like OOP builder methods. Each tool call adds one well-typed element to an accumulator. The model's job shifts from "produce this structure" to "call these functions in the right order."&lt;/p&gt;

&lt;p&gt;Here's the pattern - imagine an agent that processes insurance claims by reading documents and building a structured claim assessment:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# The accumulator - this is your structured output
&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_output&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_party&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&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;policy_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Register a party involved in the claim.

    Args:
        name: Full name of the person or organization.
        role: One of: claimant, insured, witness, adjuster, third_party
        policy_id: Policy number if applicable.

    Returns:
        Confirmation with party details.
    &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;role&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claimant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insured&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;witness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adjuster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;third_party&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid role &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. Must be one of: claimant, insured, witness, adjuster, third_party&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policy_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;policy_id&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Added &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&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;date&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;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Record a chronological event relevant to the claim.

    Args:
        description: What happened (1-3 sentences).
        date: ISO date string (YYYY-MM-DD).
        location: Where it happened (optional).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recorded event on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; events total)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&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;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Register a damage item with estimated cost.

    Args:
        item: Description of the damaged item or cost.
        amount: Estimated cost in dollars.
        category: One of: property, medical, liability, lost_income
        evidence_ref: Reference to supporting evidence (optional).
    &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;category&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lost_income&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid category &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Added damage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ($&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="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;). Running total: $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&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;The agent is given these tools and a system prompt that tells it to process a claim. As it reads documents and discovers information, it calls &lt;code&gt;add_party&lt;/code&gt;, &lt;code&gt;add_event&lt;/code&gt;, and &lt;code&gt;add_damage&lt;/code&gt;. The structured output builds up incrementally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation at the boundary
&lt;/h3&gt;

&lt;p&gt;Each tool call is a validation checkpoint. You can reject bad input immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&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;evidence_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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;category&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lost_income&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: invalid category &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;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;Error: amount must be positive, got &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;.&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;evidence_ref&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;evidence_ref&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: evidence &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;evidence_ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not registered. Call add_evidence first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model gets instant feedback. If it tries to reference evidence it hasn't registered yet, the tool tells it. The model self-corrects on the next turn. Compare this to validating a 500-line JSON blob after the fact - by then, the model has moved on and can't fix its mistakes in context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating research from output construction
&lt;/h3&gt;

&lt;p&gt;A key benefit: the same agent can have &lt;em&gt;reading&lt;/em&gt; tools and &lt;em&gt;writing&lt;/em&gt; tools. Reading tools fetch and explore data. Writing tools construct the output. The model interleaves them naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;# Reading tools
&lt;/span&gt;        &lt;span class="n"&gt;read_document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;get_weather_report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# Writing tools (builder methods)
&lt;/span&gt;        &lt;span class="n"&gt;add_party&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;add_evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;set_assessment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# Progress tracking
&lt;/span&gt;        &lt;span class="n"&gt;mark_step_done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# One call - the agent reads documents AND builds structured output
&lt;/span&gt;&lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Process this claim: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;claim_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Output is ready
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model reads a police report, extracts a party, reads a medical bill, registers a damage item, cross-references the policy, and so on. Research and output construction are interleaved rather than sequential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progress tracking and recovery
&lt;/h3&gt;

&lt;p&gt;Because output accumulates incrementally, you get crash recovery for free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;STEPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1. Identify all parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2. Establish timeline of events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3. Catalog damages with evidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4. Cross-reference policy coverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5. Produce assessment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;completed_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_step_done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mark a processing step as completed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;completed_steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STEPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;completed_steps&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;Step &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; done. Remaining: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&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;If the agent hits a context window limit or errors out, you already have partial results - every party identified, every event recorded, every damage item cataloged up to that point. You can resume or use what you have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context management with state injection
&lt;/h3&gt;

&lt;p&gt;Here's where this pattern really pays off. When your agent ingests a 30-page document and then makes dozens of tool calls to fetch additional sources, the context window fills up fast. In a traditional approach, you'd lose your structured output along with the conversation when you hit the limit. But because the accumulator lives in Python memory - not in the message history - you can aggressively compress the conversation without losing a single data point.&lt;/p&gt;

&lt;p&gt;A custom conversation manager (a possibility offered, for instance, by the &lt;a href="https://strandsagents.com/docs/user-guide/concepts/agents/conversation-management/#creating-a-conversationmanager" rel="noopener noreferrer"&gt;Strands Agents SDK&lt;/a&gt;) replaces old messages with a compact state summary derived from the accumulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClaimConversationManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConversationManager&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_management&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;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Keep first message + last 2 messages
&lt;/span&gt;        &lt;span class="c1"&gt;# Replace everything in between with a state summary
&lt;/span&gt;        &lt;span class="n"&gt;first_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_state_summary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;state_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[STATE]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Continue.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[:]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first_msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state_msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_state_summary&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Summarize what&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s been done using the accumulator state.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&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;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;parties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Parties: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parties&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Damages: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items, $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; total&lt;/span&gt;&lt;span class="sh"&gt;"&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;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;Events: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; recorded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the structured output lives in Python (not in the conversation), context compression doesn't lose any data. The model can always see what it's already produced by reading the state summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Type safety without type coercion
&lt;/h3&gt;

&lt;p&gt;Each tool has typed parameters enforced by the framework. The model must provide a &lt;code&gt;category&lt;/code&gt; that's one of &lt;code&gt;property, medical, liability, lost_income&lt;/code&gt; - not because you're parsing JSON and checking after the fact, but because the tool signature demands it. Invalid calls get rejected with clear error messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composability
&lt;/h3&gt;

&lt;p&gt;Tools compose naturally. You can add new output fields by adding new tools without changing existing ones. Want to track evidence attachments? Add an &lt;code&gt;add_evidence&lt;/code&gt; tool. Want a final recommendation? Add a &lt;code&gt;set_assessment&lt;/code&gt; tool. The model discovers new capabilities through its tool list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testability
&lt;/h3&gt;

&lt;p&gt;Each tool is a pure function (or close to it). You can unit test them independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_damage_rejects_invalid_category&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;reset_output&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="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Roof repair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosmetic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_damage_tracks_total&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;reset_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Roof repair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;add_damage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Water damage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;damages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deterministic output schema
&lt;/h3&gt;

&lt;p&gt;The output schema is defined by your Python code, not by the model's interpretation of a prompt. &lt;code&gt;claim_output&lt;/code&gt; always has the same keys with the same types. Downstream consumers can rely on the structure unconditionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Graceful degradation
&lt;/h3&gt;

&lt;p&gt;If the model runs out of context or hits an error, you have everything it produced up to that point. You can even detect empty output and retry with a nudge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;claim_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You haven&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t started processing. Begin by identifying the parties involved.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Natural agent behavior
&lt;/h3&gt;

&lt;p&gt;The model doesn't have to context-switch between "thinking" and "formatting." It thinks by calling tools. The structured output is a byproduct of the agent doing its job, not an additional formatting burden layered on top.&lt;/p&gt;




&lt;p&gt;This pattern - tools as Builder, accumulator as output, validation at the boundary - has been the most reliable way I've found to get structured data out of an agentic workflow. It works because it aligns with how tool-calling models already behave: they reason, they act, they observe results, and they act again. You're just making "act" mean "build one piece of the output."&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Hack your AWS CLI to add CloudShell support and turn your terminal into a bastion</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Thu, 28 May 2026 10:10:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/hack-your-aws-cli-to-add-cloudshell-support-and-turn-your-terminal-into-a-bastion-2ed9</link>
      <guid>https://dev.to/aws-builders/hack-your-aws-cli-to-add-cloudshell-support-and-turn-your-terminal-into-a-bastion-2ed9</guid>
      <description>&lt;p&gt;I've been using AWS CloudShell from the Console for a while. It's convenient: a pre-authenticated shell in your browser, right there in the AWS Console. But I always wondered: why can't I use it from my terminal? Why is there no &lt;code&gt;aws cloudshell&lt;/code&gt; command?&lt;/p&gt;

&lt;p&gt;Turns out, you can make it happen. The API exists, it's just not public. And once you have CLI access to CloudShell, you can do interesting things with it, like using a VPC-attached CloudShell as a bastion to reach your private RDS instances.&lt;/p&gt;

&lt;p&gt;Checkout the &lt;a href="https://github.com/psantus/cloudshell-cli" rel="noopener noreferrer"&gt;companion repository&lt;/a&gt; as you read through this blog post. &lt;/p&gt;

&lt;h2&gt;
  
  
  CloudShell: an undocumented API
&lt;/h2&gt;

&lt;p&gt;AWS CloudShell has no official SDK or CLI support. But the Console has to talk to &lt;em&gt;something&lt;/em&gt;, right? By looking at what the browser does when you open CloudShell, you can reverse-engineer the API.&lt;/p&gt;

&lt;p&gt;Thankfully, &lt;a href="https://github.com/guyon-it-consulting/cloudshell-boto3" rel="noopener noreferrer"&gt;Jérôme Guyon&lt;/a&gt; already did that work and published a boto3-compatible service model. His work made this whole thing possible.&lt;/p&gt;

&lt;p&gt;The API is straightforward: create environments, start/stop them, create sessions, upload/download files. The session mechanism uses SSM's WebSocket protocol under the hood, which means &lt;code&gt;session-manager-plugin&lt;/code&gt; (the same binary that powers &lt;code&gt;aws ssm start-session&lt;/code&gt;) can connect to CloudShell sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching the AWS CLI a new trick
&lt;/h2&gt;

&lt;p&gt;The AWS CLI has a little-known feature: &lt;code&gt;aws configure add-model&lt;/code&gt;. Give it a JSON service model, and suddenly the CLI knows about a new service. AWS uses this internally for private previews.&lt;/p&gt;

&lt;p&gt;(The boto3 model from Jérôme's repo just needs a &lt;code&gt;"version": "2.0"&lt;/code&gt; field added at the top level to become CLI-compatible.)&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure add-model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-model&lt;/span&gt; file://cloudshell-cli-model.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; cloudshell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Now I have &lt;code&gt;aws cloudshell&lt;/code&gt; with tab completion and everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws cloudshell &lt;span class="nb"&gt;help

&lt;/span&gt;AVAILABLE COMMANDS
       create-environment
       create-session
       delete-environment
       describe-environments
       get-environment-status
       start-environment
       stop-environment
       ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting to CloudShell from the terminal
&lt;/h2&gt;

&lt;p&gt;The workflow is simple:&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 or find an environment&lt;/span&gt;
aws cloudshell create-environment &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Wait for it to be RUNNING&lt;/span&gt;
aws cloudshell get-environment-status &lt;span class="nt"&gt;--environment-id&lt;/span&gt; &amp;lt;ID&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1

&lt;span class="c"&gt;# Create a session and connect&lt;/span&gt;
session-manager-plugin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudshell create-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment-id&lt;/span&gt; &amp;lt;ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-type&lt;/span&gt; TMUX &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tab-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;uuidgen | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--q-cli-disabled&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'{SessionId:SessionId,TokenValue:TokenValue,StreamUrl:StreamUrl}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; eu-west-1 StartSession
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you're in. A full shell on a CloudShell instance, from your terminal. No browser needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The credentials problem
&lt;/h2&gt;

&lt;p&gt;There's a catch. When you use CloudShell from the Console, AWS injects your credentials automatically via a &lt;code&gt;PutCredentials&lt;/code&gt; API call. This uses your console session token (the cookie-based auth from your browser login) to feed temporary credentials into the container's metadata endpoint.&lt;/p&gt;

&lt;p&gt;When you connect programmatically, that doesn't happen. The container's credential endpoint returns a 500 error. You need to inject credentials yourself:&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;# Run locally, then paste the output into your CloudShell session&lt;/span&gt;
aws configure export-credentials &lt;span class="nt"&gt;--profile&lt;/span&gt; my-profile &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not ideal, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bastion use case
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. You can create a VPC-attached CloudShell environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudshell create-environment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment-name&lt;/span&gt; db-access &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "VpcId": "vpc-abc123",
    "SubnetIds": ["subnet-private-1"],
    "SecurityGroupIds": ["sg-allowed-by-rds"]
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put it in the same security group that your RDS allows, and suddenly you can connect to your database directly from the shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; my-instance.xxx.eu-west-1.rds.amazonaws.com &lt;span class="nt"&gt;-u&lt;/span&gt; admin &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No EC2 bastion instance to maintain. No SSH keys to manage. No hourly cost when you're not using it (CloudShell is free). The environment suspends after 20 minutes of inactivity and you can keep it alive with &lt;code&gt;aws cloudshell send-heart-beat&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work (and I tried..)
&lt;/h2&gt;

&lt;p&gt;I spent a fair amount of time trying to make CloudShell work as a proper port-forwarding bastion, so you could use local tools like DBeaver against a remote RDS through it. Here's what I found:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSM-based port forwarding doesn't work.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;ECS, for instance, registers containers as SSM targets. Its SSM identifier is undocumented but once you know it, it works well, as I have described in &lt;a href="https://dev.to/aws-builders/access-your-aws-database-using-local-port-forwarding-on-your-ecsfargate-container-4nk4"&gt;a previous blog post&lt;/a&gt;. This way you can run &lt;code&gt;aws ssm start-session --document-name AWS-StartPortForwardingSessionToRemoteHost&lt;/code&gt;.&lt;br&gt;
SageMaker notebooks have kinda the same behaviour. &lt;/p&gt;

&lt;p&gt;CloudShell instances/containers seem not to be registered as SSM managed instances. Or if they are, it's hidden and as of today, no one at AWS leaked their ID format :) I tried every combination of environment ID, session ID, and prefix format I could think of. None of them work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local port forwarding through the PTY doesn't work either.&lt;/strong&gt; The session is a terminal, not a raw TCP stream. You can't pipe binary MySQL protocol data through it. I even tried setting up an ncat relay inside CloudShell and tunneling through the session. The relay works fine internally, but there's no way to expose it as a local TCP port on your machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UDP hole punching is theoretically possible&lt;/strong&gt; but requires the CloudShell to have internet access (NAT Gateway on its subnet), and even then you're fighting NAT symmetry issues on both ends. I got STUN working from CloudShell, but the full hole punch is fragile and impractical for production use.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is it good for?
&lt;/h2&gt;

&lt;p&gt;Honestly, quite a lot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quick database access&lt;/strong&gt; without maintaining a bastion EC2 instance. Connect, run your queries, disconnect. Free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automation.&lt;/strong&gt; You can script command execution on CloudShell via Python + &lt;code&gt;session-manager-plugin&lt;/code&gt;. Useful for running things inside a VPC without deploying a Lambda or Fargate task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging network connectivity.&lt;/strong&gt; Spin up a CloudShell in a specific subnet/SG combination and test what can reach what.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File transfer&lt;/strong&gt; (from public environments). The &lt;code&gt;get-file-upload-urls&lt;/code&gt; and &lt;code&gt;get-file-download-urls&lt;/code&gt; APIs give you presigned S3 URLs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The main limitation is that you're stuck running commands &lt;em&gt;inside&lt;/em&gt; the shell. You can't use it as a transparent tunnel for local tools. For that, you still need an EC2 instance with SSM agent, or an ECS task with execute-command enabled.&lt;/p&gt;

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

&lt;p&gt;I published the model and a sample script here: &lt;a href="https://github.com/psantus/cloudshell-cli" rel="noopener noreferrer"&gt;github.com/psantus/cloudshell-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installation is one command. The whole thing is a single JSON file that teaches your AWS CLI a new service. Just remember: this is an undocumented API. AWS can change or break it at any time. Don't build anything mission-critical on top of it.&lt;/p&gt;

&lt;p&gt;But for quick VPC access from your terminal? It's pretty great.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudshell</category>
      <category>cli</category>
    </item>
    <item>
      <title>Ne lâchez pas la bride à votre LLM</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 15 May 2026 13:27:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/ne-lachez-pas-la-bride-a-votre-llm-b8i</link>
      <guid>https://dev.to/aws-builders/ne-lachez-pas-la-bride-a-votre-llm-b8i</guid>
      <description>&lt;p&gt;Shannon l'avait prédit : un bon prompt ne remplacera jamais une boucle de feedback. &lt;/p&gt;

&lt;p&gt;Une idée séduisante qui circule : donnez un bon prompt à un LLM, et il vous génère une application complète. Simple, rapide, magique. Vibe !&lt;/p&gt;

&lt;p&gt;Sauf que c'est physiquement impossible. Claude Shannon l'expliquait dès 1948.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le problème informationnel
&lt;/h2&gt;

&lt;p&gt;La théorie de l'information nous enseigne un principe fondamental : on ne peut pas créer de l'information à partir de rien. Un canal de communication ne peut pas produire en sortie plus d'information qu'il n'en reçoit en entrée.&lt;/p&gt;

&lt;p&gt;Or, regardons ce qu'on demande :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;En entrée&lt;/strong&gt; : un prompt de quelques lignes. Quelques centaines de bits d'information utile. Des intentions vagues, des contraintes implicites, des choix de design non formulés.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;En sortie attendue&lt;/strong&gt; : une application complète. Des milliers de décisions d'architecture, de design, d'UX, de gestion d'erreurs, de cas limites. Des millions de bits d'information.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ce que le LLM apporte (et ce qu'il n'apporte pas)
&lt;/h2&gt;

&lt;p&gt;Soyons honnêtes : le LLM &lt;em&gt;ajoute&lt;/em&gt; bien de l'information. Il ne tire pas à pile ou face. Il puise dans un immense corpus d'entraînement pour combler les vides ; ses milliards de paramètres enrichissent vos 300 tokens péniblement accouchés. &lt;/p&gt;

&lt;p&gt;Mais de quelle information s'agit-il, exactement ?&lt;/p&gt;

&lt;p&gt;Du &lt;strong&gt;boilerplate&lt;/strong&gt;. De la &lt;strong&gt;connaissance formelle&lt;/strong&gt;. Les patterns classiques d'une API REST. La façon idiomatique de connecter une base de données en Python. La structure standard d'un composant React. Les conventions de nommage. Les imports habituels.&lt;/p&gt;

&lt;p&gt;Tout ce qui relève du "comment fait-on cela généralement ?" est couvert, et c'est précieux. C'est ce qui rend le LLM si bluffant sur les démos : il produit du code qui &lt;em&gt;ressemble&lt;/em&gt; à une vraie application, parce que la coquille formelle est correcte.&lt;/p&gt;

&lt;p&gt;Mais votre logique métier ? Les compromis d'architecture spécifiques à votre contexte ? Le comportement exact attendu dans ce cas limite que seul votre utilisateur connaît ? Cette info n'est dans aucun corpus. C'est dans votre tête, et nulle part ailleurs.&lt;/p&gt;

&lt;p&gt;Le LLM fournit le squelette. Vous fournissez l'âme. En termes formels : la complexité de Kolmogorov de votre application (la quantité minimale d'information pour la décrire entièrement) est bien supérieure à celle de votre prompt. Le LLM comble l'écart avec de l'information générique. Mais l'information spécifique à votre contexte est incompressible. Seul vous pouvez la fournir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Une expérience de pensée.
&lt;/h2&gt;

&lt;p&gt;Prenez une application qui fonctionne. Demandez au meilleur LLM du monde : "écris-moi le prompt parfait pour générer cette application à l'identique." Puis soumettez ce prompt à ce même LLM. Vous n'obtiendrez pas la même application. Jamais. Vous pouvez recommencer une fois, deux fois, cent fois : cent résultats différents, aucun identique à l'original.&lt;/p&gt;

&lt;p&gt;Un prompt est à une application ce qu'un hash SHA1 est à un fichier : une réduction irréversible (un "hash"). Personne ne s'attend à reconstruire un fichier à partir de son empreinte. Pourquoi s'attendrait-on à reconstruire une application à partir de son prompt ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Comme une impression de déjà-vu
&lt;/h2&gt;

&lt;p&gt;Cette situation n'est pas nouvelle. En fait, le monde du logiciel l'a vécue pendant des décennies.&lt;/p&gt;

&lt;p&gt;Les projets "effet tunnel" des années 2000 fonctionnaient exactement sur ce principe : on rédigeait un cahier des charges (l'équivalent d'un gros prompt), on l'envoyait à une équipe de développement (l'équivalent d'un LLM), et on attendait le résultat final des mois plus tard.&lt;/p&gt;

&lt;p&gt;Le résultat ? Systématiquement décevant. Et pourtant, ces spécifications faisaient des centaines de pages, infiniment plus détaillées qu'un prompt. Des équipes entières passaient des mois à les rédiger. Malgré cela, le produit livré ne correspondait jamais aux attentes réelles.&lt;/p&gt;

&lt;p&gt;Pourquoi ? Parce que même des centaines de pages de spécifications ne contiennent pas assez d'information pour décrire un logiciel complet. Les vrais besoins émergent à l'usage. Les bonnes décisions se prennent face au concret, pas dans l'abstrait.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'agilité avait la réponse
&lt;/h2&gt;

&lt;p&gt;L'industrie a mis quinze ans à comprendre et à adopter la solution : &lt;strong&gt;raccourcir la boucle de feedback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;L'agilité ne dit pas "ne spécifiez pas". Elle dit : "spécifiez peu, livrez vite, observez, ajustez, recommencez". L'information manquante dans la spécification initiale est injectée itération après itération, par le retour du réel.&lt;/p&gt;

&lt;p&gt;C'est exactement le mécanisme qui compense le déficit informationnel de Shannon : chaque itération est un nouveau message sur le canal, qui apporte l'information que le message précédent ne contenait pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avec l'IA, le piège est le même, en pire
&lt;/h2&gt;

&lt;p&gt;Le LLM accélère spectaculairement la &lt;em&gt;génération&lt;/em&gt; de code. C'est indéniable. Mais cette vitesse crée une illusion dangereuse : puisque le code sort vite, on croit que le produit avance vite.&lt;/p&gt;

&lt;p&gt;Or le travail produit (comprendre le besoin, valider les choix, vérifier l'adéquation) reste incompressible. Il nécessite un humain dans la boucle, des retours fréquents, des corrections de trajectoire.&lt;/p&gt;

&lt;p&gt;Générer 2000 lignes de code en 30 secondes pour découvrir après coup que l'architecture est inadaptée, c'est du waterfall à la vitesse de la lumière. C'est &lt;em&gt;pire&lt;/em&gt; que l'effet tunnel classique parce que, le coût perçu étant faible, on recommence sans remettre en question l'approche.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'illusion de la fenêtre de contexte
&lt;/h2&gt;

&lt;p&gt;"Mais les LLM ont maintenant des fenêtres de contexte énormes !" Oui. Et ça aggrave le problème.&lt;/p&gt;

&lt;p&gt;Une grande fenêtre de contexte donne l'illusion que le LLM peut traiter un sujet en profondeur, tout seul, en accumulant du raisonnement interne. En pratique, sans apport externe, il s'enferme dans sa propre "intuition". Il tourne en boucle. Il reformule. Il essaie des variantes de la même mauvaise idée. Il crame des tokens.&lt;/p&gt;

&lt;p&gt;On a tous vu passer sur LinkedIn ces posts : "Claude a brûlé mon quota mensuel de tokens en 2h." Ce n'est pas un bug. C'est Shannon qui se manifeste : le LLM n'a pas reçu d'information nouvelle, donc il ne peut pas converger. Il génère du volume, pas de la valeur.&lt;/p&gt;

&lt;p&gt;J'en ai fait l'expérience à répétition : un LLM coincé dans une spirale infernale depuis vingt minutes, accumulant des tentatives de plus en plus alambiquées. La solution ? Réduire le contexte (une simple commande &lt;code&gt;/compact&lt;/code&gt; dans mon outil favori, &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;), puis injecter une minuscule correction humaine : "essaie plutôt ça" ou "je pense que X est l'origine du problème". Cinq mots. Et le LLM repart immédiatement dans la bonne direction.&lt;/p&gt;

&lt;p&gt;Ces cinq mots contiennent plus d'information utile que les 50 000 tokens que le LLM venait de se générer à lui-même. Parce que c'est de l'information &lt;em&gt;externe&lt;/em&gt;, qui brise la circularité. C'est exactement le signal sur le canal que Shannon décrit : sans nouveau message de l'émetteur, le récepteur ne peut pas corriger sa trajectoire.&lt;/p&gt;

&lt;h2&gt;
  
  
  La bonne posture
&lt;/h2&gt;

&lt;p&gt;Ne lâchez pas la bride à votre LLM. Travaillez avec lui comme vous travailleriez en agile.&lt;/p&gt;

&lt;p&gt;La cybernétique appelle ça la loi de la variété requise (Ashby, 1956) : pour piloter un système complexe, votre mécanisme de contrôle doit avoir au moins autant de variété que le système lui-même. Une application a une variété énorme (tous les comportements, états, cas limites possibles). Un seul prompt est une seule action de contrôle. Une action ne peut pas contraindre un système à millions d'états. Il en faut beaucoup, appliquées séquentiellement. Autrement dit : des itérations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Itérations courtes&lt;/strong&gt; : demandez un petit morceau, validez-le, puis passez au suivant. (Ce billet, par exemple ^^ : quelques idées en deux phrases, expansées par une IA, puis au moins cinq ou six itérations de feedback humain pour arriver à ce que vous lisez. CQFD.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback constant&lt;/strong&gt; : relisez, testez, corrigez la trajectoire à chaque étape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Décisions explicites&lt;/strong&gt; : chaque choix de design que vous ne formulez pas est un choix que le LLM fera à votre place. Probablement pas comme vous l'auriez voulu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incréments fonctionnels&lt;/strong&gt; : préférez un résultat partiel qui marche à un résultat complet qui ne correspond à rien.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Itérez !
&lt;/h2&gt;

&lt;p&gt;Shannon nous le dit depuis 78 ans : l'information ne se crée pas spontanément. Un prompt court ne peut pas produire une application complète qui corresponde à vos besoins. Le LLM comble le vide avec ce qu'il connaît (le boilerplate, les patterns, les conventions), mais pas avec ce qu'il ne peut pas connaître : &lt;em&gt;votre&lt;/em&gt; intention précise.&lt;/p&gt;

&lt;p&gt;L'écart informationnel doit être comblé quelque part, et ce quelque part, c'est la boucle de rétroaction entre vous et votre outil.&lt;/p&gt;

&lt;p&gt;L'IA accélère la frappe, pas la réflexion. Elle amplifie votre capacité d'exécution, pas votre capacité de décision. Gardez la main. Itérez. Ne confondez pas vitesse de génération et vitesse de création de valeur.&lt;/p&gt;

&lt;p&gt;Le vrai superpouvoir, ce n'est pas le prompt parfait. C'est la prise de décision continue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>specdrivendevelopment</category>
      <category>kiro</category>
      <category>information</category>
    </item>
    <item>
      <title>Remplacez AWS Transfer Family SFTP par S3 Files + Atmoz SFTP</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 08 Apr 2026 08:37:49 +0000</pubDate>
      <link>https://dev.to/aws-builders/remplacez-aws-transfer-family-sftp-par-s3-files-atmoz-sftp-49n</link>
      <guid>https://dev.to/aws-builders/remplacez-aws-transfer-family-sftp-par-s3-files-atmoz-sftp-49n</guid>
      <description>&lt;p&gt;AWS vient de lancer l'une des fonctionnalités de stockage les plus attendues : &lt;strong&gt;S3 Files&lt;/strong&gt;. S3 Files place une interface de système de fichiers compatible EFS directement devant vos buckets S3. Quand j'ai entendu parler de cette fonctionnalité (dans le cadre du &lt;a href="https://www.linkedin.com/posts/coreystrausman_aws-s3-cloudcomputing-activity-7447375367155351552-Uuyo?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAHqFuoBYjYsx4cq4zQ6SBklTKN3Pd_juYs" rel="noopener noreferrer"&gt;programme Community Builders&lt;/a&gt;) j'ai tout de suite pensé au cas d'usage du SFTP sur AWS.&lt;/p&gt;

&lt;p&gt;Si vous payez actuellement AWS Transfer Family pour donner à vos partenaires un accès SFTP à S3, lisez attentivement ce qui suit. Il existe désormais une alternative nettement moins chère et plus puissante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qu'est-ce que S3 Files ?
&lt;/h2&gt;

&lt;p&gt;S3 Files crée un système de fichiers NFS haute performance adossé à un bucket S3. Voyez-le comme une couche EFS qui lit et écrit directement dans les objets S3, avec une synchronisation bidirectionnelle automatique. Tout fichier écrit via le système de fichiers apparaît comme un objet S3, et tout objet uploadé dans S3 devient visible via le système de fichiers.&lt;/p&gt;

&lt;p&gt;Les propriétés clés :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latence sub-milliseconde&lt;/strong&gt; pour les opérations fichier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronisation automatique&lt;/strong&gt; entre le système de fichiers et le bucket S3 (via EventBridge sous le capot)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Montable sur ECS Fargate&lt;/strong&gt;, ECS Managed Instances, EKS et EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocole NFS standard&lt;/strong&gt; — pas de client spécial nécessaire côté compute (ECS/EKS le gèrent nativement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Points d'accès&lt;/strong&gt; avec contrôle d'identité POSIX (uid/gid)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Versioning&lt;/strong&gt; requis et exploité pour la cohérence&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Le problème avec AWS Transfer Family
&lt;/h2&gt;

&lt;p&gt;AWS Transfer Family est la solution "officielle" pour exposer des endpoints SFTP adossés à S3. Ça fonctionne, mais avec des inconvénients sérieux :&lt;/p&gt;

&lt;h3&gt;
  
  
  C'est cher
&lt;/h3&gt;

&lt;p&gt;Transfer Family facture &lt;strong&gt;0,30 $/heure&lt;/strong&gt; rien que pour l'endpoint — soit &lt;strong&gt;~216 $/mois&lt;/strong&gt; avant même de transférer un seul octet. Ajoutez les coûts de transfert de données par-dessus. Pour un service que beaucoup d'équipes utilisent pour quelques dépôts de fichiers quotidiens, c'est difficile à justifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  C'est une boîte noire
&lt;/h3&gt;

&lt;p&gt;Vous obtenez un endpoint SFTP, mais vous ne contrôlez pas le serveur. L'authentification personnalisée nécessite des hooks Lambda. Le logging est limité. Vous ne pouvez pas vous connecter en SSH pour débugger. Vous ne pouvez pas personnaliser le comportement du serveur SFTP, ajouter des scripts de pré/post-traitement, ou exécuter quoi que ce soit à côté.&lt;/p&gt;

&lt;h2&gt;
  
  
  La nouvelle architecture : atmoz/sftp + S3 Files sur ECS Fargate
&lt;/h2&gt;

&lt;p&gt;Voici ce que nous allons mettre en place :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j8tr2khodyf4lw8cmds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j8tr2khodyf4lw8cmds.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les composants :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bucket S3&lt;/strong&gt; avec versioning activé (requis par S3 Files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Système de fichiers S3 Files&lt;/strong&gt; pointant vers le bucket, avec des mount targets dans votre VPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume EFS&lt;/strong&gt; pour les clés SSH persistantes (empreinte stable entre les redémarrages et le scaling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service ECS Fargate&lt;/strong&gt; exécutant &lt;a href="https://github.com/atmoz/sftp" rel="noopener noreferrer"&gt;atmoz/sftp&lt;/a&gt; avec le volume S3 Files monté sur &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Load Balancer&lt;/strong&gt; exposant le port 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enregistrement DNS&lt;/strong&gt; pour &lt;code&gt;sftp.votredomaine.com&lt;/code&gt; (optionnel)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Les fichiers uploadés via SFTP atterrissent sur le montage S3 Files → apparaissent dans S3 en quelques secondes → déclenchent les notifications S3 pour le traitement en aval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparaison des coûts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Composant&lt;/th&gt;
&lt;th&gt;Transfer Family&lt;/th&gt;
&lt;th&gt;S3 Files + Fargate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coût de base&lt;/td&gt;
&lt;td&gt;0,30 $/h (~216 $/mois)&lt;/td&gt;
&lt;td&gt;NLB : ~16 $/mois&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Inclus&lt;/td&gt;
&lt;td&gt;Fargate 0.25 vCPU / 512 Mo : ~9 $/mois&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stockage&lt;/td&gt;
&lt;td&gt;Tarification S3&lt;/td&gt;
&lt;td&gt;Tarification S3 (identique)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfert de données&lt;/td&gt;
&lt;td&gt;0,04 $/Go via SFTP&lt;/td&gt;
&lt;td&gt;Tarification NLB standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimum mensuel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~216 $&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~25 $&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;C'est environ &lt;strong&gt;8 fois moins cher&lt;/strong&gt; au niveau de base. Pour les cas d'usage SFTP à trafic faible à moyen (c'est-à-dire la majorité), les économies sont significatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi c'est mieux que du SFTP sur EFS
&lt;/h2&gt;

&lt;p&gt;Avant S3 Files, l'approche DIY classique consistait à monter EFS sur Fargate et faire tourner atmoz/sftp. C'est exactement ce que nous faisions. Ça marchait, mais avec une limitation fondamentale : &lt;strong&gt;vos fichiers vivaient dans EFS, pas dans S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ça signifiait :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pas de notifications S3 à l'arrivée des fichiers&lt;/li&gt;
&lt;li&gt;Pas de politiques de cycle de vie S3&lt;/li&gt;
&lt;li&gt;Pas de réplication cross-region S3&lt;/li&gt;
&lt;li&gt;Pas d'accès direct aux fichiers via l'API S3&lt;/li&gt;
&lt;li&gt;Tarification EFS (0,30 $/Go pour Standard) vs S3 (0,023 $/Go)&lt;/li&gt;
&lt;li&gt;Stratégie de backup séparée nécessaire&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avec S3 Files, les données vivent dans S3. Vous bénéficiez de tout l'écosystème S3 — notifications, règles de cycle de vie, réplication, analytics, tiering Glacier — tout en ayant un système de fichiers montable pour votre serveur SFTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traitement événementiel des fichiers
&lt;/h2&gt;

&lt;p&gt;Transfer Family et notre approche S3 Files écrivent tous deux dans S3, donc vous obtenez les mêmes capacités événementielles dans les deux cas :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 → SQS/SNS/Lambda&lt;/strong&gt; pour un traitement immédiat à l'arrivée d'un fichier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 → EventBridge&lt;/strong&gt; pour des règles de routage complexes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Inventory&lt;/strong&gt; pour l'audit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Object Lock&lt;/strong&gt; pour la conformité&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Replication&lt;/strong&gt; pour répliquer les fichiers uploadés vers une autre région ou un autre compte&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La différence n'est pas dans les fonctionnalités — c'est dans le coût. Vous obtenez exactement le même pipeline événementiel S3 pour ~25 $/mois au lieu de ~216 $/mois.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'implémentation Terraform
&lt;/h2&gt;

&lt;p&gt;Comme &lt;code&gt;aws_s3files_file_system&lt;/code&gt; n'est pas encore dans le provider Terraform AWS (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; ouverte et priorisée), nous gérons les ressources S3 Files via &lt;code&gt;terraform_data&lt;/code&gt; avec des provisioners &lt;code&gt;local-exec&lt;/code&gt; appelant l'AWS CLI.&lt;/p&gt;

&lt;p&gt;Les ressources clés :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Système de fichiers S3 Files — créé via AWS CLI&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_file_system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-file-system \
        --bucket "$BUCKET_ARN" \
        --role-arn "$ROLE_ARN" \
        --accept-bucket-warning \
        --region "$REGION"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Mount targets dans chaque sous-réseau privé&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_mount_targets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-mount-target \
        --file-system-id "$FS_ID" \
        --subnet-id "${each.value}" \
        --security-groups "$SG_ID"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# La task definition ECS utilise s3filesVolumeConfiguration&lt;/span&gt;
&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sftp-home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;s3files_volume_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;file_system_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3files_fs_arn&lt;/span&gt;
      &lt;span class="nx"&gt;root_directory&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le code Terraform complet est disponible en tant que &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;module Terraform&lt;/a&gt;. Le provider Terraform AWS ne supporte pas encore &lt;code&gt;aws_s3files_file_system&lt;/code&gt; (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; ouverte et priorisée), donc les ressources S3 Files sont actuellement gérées via &lt;code&gt;terraform_data&lt;/code&gt; + AWS CLI. Je m'engage à mettre à jour ce module pour utiliser les ressources Terraform natives dès que le provider intégrera le support S3 Files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration IAM
&lt;/h2&gt;

&lt;p&gt;Deux rôles IAM sont nécessaires :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rôle de service S3 Files&lt;/strong&gt; — assumé par &lt;code&gt;elasticfilesystem.amazonaws.com&lt;/code&gt; pour synchroniser entre le système de fichiers et le bucket S3. Nécessite un accès S3 en lecture/écriture sur le bucket + des permissions EventBridge pour la détection des changements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rôle de tâche ECS&lt;/strong&gt; — nécessite &lt;code&gt;s3files:ClientMount&lt;/code&gt;, &lt;code&gt;s3files:ClientWrite&lt;/code&gt;, et &lt;code&gt;s3:GetObject&lt;/code&gt;/&lt;code&gt;s3:ListBucket&lt;/code&gt; sur le bucket pour des lectures optimisées.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quand Transfer Family reste pertinent
&lt;/h2&gt;

&lt;p&gt;Pour être honnête, Transfer Family n'est pas mort pour tous les cas d'usage :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gestion managée des clés SFTP et des utilisateurs&lt;/strong&gt; — Transfer Family intègre nativement des fournisseurs d'identité (AD, authentification Lambda custom). Avec atmoz/sftp, vous gérez les utilisateurs via la configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support du protocole AS2&lt;/strong&gt; — si vous avez besoin d'AS2, Transfer Family reste la seule option managée.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTPS&lt;/strong&gt; — Transfer Family supporte FTPS nativement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tolérance zéro aux opérations&lt;/strong&gt; — si vous ne pouvez vraiment pas gérer un conteneur, Transfer Family est entièrement managé.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mais pour la grande majorité des cas d'usage SFTP — des partenaires qui déposent des fichiers à traiter — l'approche S3 Files est moins chère, plus flexible, et offre une meilleure observabilité.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour démarrer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prérequis :&lt;/strong&gt; Les commandes &lt;code&gt;aws s3files&lt;/code&gt; nécessitent AWS CLI v2.34.26 ou ultérieur. Vous avez également besoin de &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; (utilisé par les scripts des provisioners Terraform). Mettez à jour la CLI avec &lt;code&gt;brew upgrade awscli&lt;/code&gt; ou consultez le &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;guide d'installation AWS CLI&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Créer un bucket S3 avec le versioning activé&lt;/li&gt;
&lt;li&gt;Créer un rôle IAM pour S3 Files avec les politiques de confiance et de permissions requises&lt;/li&gt;
&lt;li&gt;Créer un système de fichiers S3 Files via la console ou &lt;code&gt;aws s3files create-file-system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Créer des mount targets dans vos sous-réseaux VPC&lt;/li&gt;
&lt;li&gt;Créer un EFS pour les clés SSH persistantes&lt;/li&gt;
&lt;li&gt;Déployer un service ECS Fargate avec atmoz/sftp, en montant S3 Files sur &lt;code&gt;/home&lt;/code&gt; et EFS sur &lt;code&gt;/etc/ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Placer un NLB devant, pointer votre DNS dessus&lt;/li&gt;
&lt;li&gt;Configurer les notifications S3 sur le bucket pour le traitement en aval&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ou utilisez simplement le &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;module Terraform&lt;/a&gt; — le tout se déploie en moins de 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test de bout en bout
&lt;/h2&gt;

&lt;p&gt;Après &lt;code&gt;terraform apply&lt;/code&gt;, le serveur SFTP est prêt en environ 8 minutes (l'essentiel du temps est consacré à la mise à disposition des mount targets S3 Files). Voici un test rapide :&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;# Upload d'un fichier&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from S3 Files SFTP!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test.txt
sshpass &lt;span class="nt"&gt;-p&lt;/span&gt; demo sftp &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-P&lt;/span&gt; 22 demo@&amp;lt;sftp_endpoint&amp;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;
cd upload
put test.txt
bye
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Vérifier qu'il est arrivé dans S3 (attendre ~30-60s pour la synchro)&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://&amp;lt;sftp_bucket_name&amp;gt;/demo/upload/test.txt -
&lt;span class="c"&gt;# Output: Hello from S3 Files SFTP!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nous avons également vérifié que les clés SSH persistent entre les redémarrages de tâches — l'empreinte du serveur reste identique après un redéploiement forcé, grâce au volume EFS monté sur &lt;code&gt;/etc/ssh/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;S3 Files comble le fossé entre système de fichiers et stockage objet d'une manière qui rend beaucoup de services AWS coûteux redondants. Pour le SFTP en particulier, la combinaison atmoz/sftp + S3 Files sur Fargate vous offre :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~8x moins cher&lt;/strong&gt; que Transfer Family&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrôle total&lt;/strong&gt; sur le serveur SFTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 natives&lt;/strong&gt; pour le traitement événementiel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 comme source de vérité&lt;/strong&gt; — règles de cycle de vie, réplication, analytics fonctionnent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; avec Terraform (même avant le support natif du provider)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'époque où il fallait payer 216 $/mois minimum pour un endpoint SFTP managé est révolue pour la plupart des équipes. S3 Files est la pièce manquante qui rend le SFTP DIY sur AWS non seulement viable, mais ~8x moins cher.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3files</category>
      <category>sftp</category>
      <category>terraform</category>
    </item>
    <item>
      <title>AWS S3 Files just made Transfer Family SFTP obsolete for most use cases</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 08 Apr 2026 08:15:29 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me</link>
      <guid>https://dev.to/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me</guid>
      <description>&lt;p&gt;AWS just launched one of the most impactful storage features in years: &lt;strong&gt;S3 Files&lt;/strong&gt;. It puts an EFS-compatible file system interface directly in front of your S3 buckets. When I was introduced to S3 Files (as part of &lt;a href="https://www.linkedin.com/posts/coreystrausman_aws-s3-cloudcomputing-activity-7447375367155351552-Uuyo?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAHqFuoBYjYsx4cq4zQ6SBklTKN3Pd_juYs" rel="noopener noreferrer"&gt;AWS Community Builder program&lt;/a&gt;), I immediately thought of SFTP as the most obvious use case for my clients.&lt;/p&gt;

&lt;p&gt;If you're currently paying for AWS Transfer Family to give your partners SFTP access to S3, you should read this carefully. There's now a dramatically cheaper and more powerful alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is S3 Files?
&lt;/h2&gt;

&lt;p&gt;S3 Files creates a high-performance NFS file system backed by an S3 bucket. Think of it as an EFS-like layer that reads and writes directly to S3 objects, with automatic bidirectional synchronization. Any file written through the file system appears as an S3 object, and any object uploaded to S3 becomes visible through the file system.&lt;/p&gt;

&lt;p&gt;The key properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sub-millisecond latency&lt;/strong&gt; for file operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic sync&lt;/strong&gt; between file system and S3 bucket (powered by EventBridge under the hood)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mountable on ECS Fargate&lt;/strong&gt;, ECS Managed Instances, EKS, and EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard NFS protocol&lt;/strong&gt; — no special client needed on the compute side (ECS/EKS handle it natively)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access points&lt;/strong&gt; with POSIX user/group enforcement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Versioning&lt;/strong&gt; required and leveraged for consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem with AWS Transfer Family
&lt;/h2&gt;

&lt;p&gt;AWS Transfer Family has been the "official" way to expose SFTP endpoints backed by S3. It works, but it comes with serious pain points:&lt;/p&gt;

&lt;h3&gt;
  
  
  It's expensive
&lt;/h3&gt;

&lt;p&gt;Transfer Family charges &lt;strong&gt;$0.30/hour&lt;/strong&gt; just for the endpoint — that's &lt;strong&gt;~$216/month&lt;/strong&gt; before you transfer a single byte. Add data transfer costs on top. For a service that many teams use for a handful of daily file drops, this is hard to justify.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's a black box
&lt;/h3&gt;

&lt;p&gt;You get an SFTP endpoint, but you don't control the server. Custom authentication requires Lambda hooks. Logging is limited. You can't SSH in to debug. You can't customize the SFTP server behavior, add pre/post-processing scripts, or run anything alongside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Architecture: atmoz/sftp + S3 Files on ECS Fargate
&lt;/h2&gt;

&lt;p&gt;Here's what you could run instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l55rvat4vr4vxk97vbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l55rvat4vr4vxk97vbs.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket&lt;/strong&gt; with versioning enabled (required by S3 Files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files file system&lt;/strong&gt; pointed at the bucket, with mount targets in your VPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EFS volume&lt;/strong&gt; for persistent SSH host keys (stable fingerprint across restarts and scaling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS Fargate service&lt;/strong&gt; running &lt;a href="https://github.com/atmoz/sftp" rel="noopener noreferrer"&gt;atmoz/sftp&lt;/a&gt; with the S3 Files volume mounted at &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Load Balancer&lt;/strong&gt; exposing port 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS record&lt;/strong&gt; for &lt;code&gt;sftp.yourdomain.com&lt;/code&gt; (optional)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Files uploaded via SFTP land on the S3 Files mount → appear in S3 within seconds → trigger S3 event notifications for downstream processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Transfer Family&lt;/th&gt;
&lt;th&gt;S3 Files + Fargate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base cost&lt;/td&gt;
&lt;td&gt;$0.30/hr (~$216/mo)&lt;/td&gt;
&lt;td&gt;NLB: ~$16/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Fargate 0.25 vCPU / 512MB: ~$9/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;S3 pricing&lt;/td&gt;
&lt;td&gt;S3 pricing (same)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data transfer&lt;/td&gt;
&lt;td&gt;$0.04/GB over SFTP&lt;/td&gt;
&lt;td&gt;Standard NLB pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly minimum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$216&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly &lt;strong&gt;8x cheaper&lt;/strong&gt; at the base level. For low-to-medium traffic SFTP use cases (which is most of them), the savings are significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Better Than EFS-Backed SFTP
&lt;/h2&gt;

&lt;p&gt;Before S3 Files, the common DIY approach was to mount EFS on Fargate and run atmoz/sftp. We did exactly this. It worked, but had a fundamental limitation: &lt;strong&gt;your files lived in EFS, not S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No S3 event notifications when files arrived&lt;/li&gt;
&lt;li&gt;No S3 lifecycle policies&lt;/li&gt;
&lt;li&gt;No S3 cross-region replication&lt;/li&gt;
&lt;li&gt;No direct S3 API access to the files&lt;/li&gt;
&lt;li&gt;EFS pricing ($0.30/GB for Standard) vs S3 ($0.023/GB)&lt;/li&gt;
&lt;li&gt;Separate backup strategy needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With S3 Files, the data lives in S3. You get the full S3 feature set — notifications, lifecycle rules, replication, analytics, Glacier tiering — while still having a mountable file system for your SFTP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven File Processing
&lt;/h2&gt;

&lt;p&gt;Both Transfer Family and our S3 Files approach write to S3, so you get the same event-driven capabilities either way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Event Notifications → SQS/SNS/Lambda&lt;/strong&gt; for immediate processing when a file arrives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Event Notifications → EventBridge&lt;/strong&gt; for complex routing rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Inventory&lt;/strong&gt; for auditing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Object Lock&lt;/strong&gt; for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Replication&lt;/strong&gt; to replicate uploaded files to another region or account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference isn't in features — it's in cost. You get the exact same S3 event-driven pipeline for ~$25/mo instead of ~$216/mo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Terraform Implementation
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;aws_s3files_file_system&lt;/code&gt; isn't in the Terraform AWS provider yet (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; is open and prioritized), we manage S3 Files resources through &lt;code&gt;terraform_data&lt;/code&gt; with &lt;code&gt;local-exec&lt;/code&gt; provisioners calling the AWS CLI.&lt;/p&gt;

&lt;p&gt;The key resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# S3 Files file system — created via AWS CLI&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_file_system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-file-system \
        --bucket "$BUCKET_ARN" \
        --role-arn "$ROLE_ARN" \
        --accept-bucket-warning \
        --region "$REGION"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Mount targets in each private subnet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_mount_targets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-mount-target \
        --file-system-id "$FS_ID" \
        --subnet-id "${each.value}" \
        --security-groups "$SG_ID"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ECS task definition uses s3filesVolumeConfiguration&lt;/span&gt;
&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sftp-home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;s3files_volume_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;file_system_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3files_fs_arn&lt;/span&gt;
      &lt;span class="nx"&gt;root_directory&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full working Terraform code is available as a &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt;. The Terraform AWS provider doesn't support &lt;code&gt;aws_s3files_file_system&lt;/code&gt; yet (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; is open and prioritized), so S3 Files resources are currently managed via &lt;code&gt;terraform_data&lt;/code&gt; + AWS CLI. I pledge to update this module to use native Terraform resources as soon as the provider ships S3 Files support.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM Setup
&lt;/h2&gt;

&lt;p&gt;Two IAM roles are needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 Files service role&lt;/strong&gt; — assumed by &lt;code&gt;elasticfilesystem.amazonaws.com&lt;/code&gt; to sync between the file system and S3 bucket. Needs S3 read/write on the bucket + EventBridge permissions for change detection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECS task role&lt;/strong&gt; — needs &lt;code&gt;s3files:ClientMount&lt;/code&gt;, &lt;code&gt;s3files:ClientWrite&lt;/code&gt;, and &lt;code&gt;s3:GetObject&lt;/code&gt;/&lt;code&gt;s3:ListBucket&lt;/code&gt; on the backing bucket for optimized reads.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When Transfer Family Still Makes Sense
&lt;/h2&gt;

&lt;p&gt;To be fair, Transfer Family isn't dead for every use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed SFTP keys and user management&lt;/strong&gt; — Transfer Family has built-in identity provider integration (AD, Lambda custom auth). With atmoz/sftp, you manage users via config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AS2 protocol support&lt;/strong&gt; — if you need AS2, Transfer Family is still the only managed option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTPS&lt;/strong&gt; — Transfer Family supports FTPS natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero ops tolerance&lt;/strong&gt; — if you truly cannot manage a container, Transfer Family is fully managed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for the vast majority of SFTP use cases — partners dropping files that need processing — the S3 Files approach is cheaper, more flexible, and gives you better observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; The &lt;code&gt;aws s3files&lt;/code&gt; commands require AWS CLI v2.34.26 or later. You also need &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; installed (used by the Terraform provisioner scripts). Update the CLI with &lt;code&gt;brew upgrade awscli&lt;/code&gt; or see &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;AWS CLI install guide&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an S3 bucket with versioning enabled&lt;/li&gt;
&lt;li&gt;Create an IAM role for S3 Files with the required trust and permissions policies&lt;/li&gt;
&lt;li&gt;Create an S3 Files file system via the console or &lt;code&gt;aws s3files create-file-system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create mount targets in your VPC subnets&lt;/li&gt;
&lt;li&gt;Create an EFS for persistent SSH host keys&lt;/li&gt;
&lt;li&gt;Deploy an ECS Fargate service with atmoz/sftp, mounting S3 Files at &lt;code&gt;/home&lt;/code&gt; and EFS at &lt;code&gt;/etc/ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Put an NLB in front, point your DNS at it&lt;/li&gt;
&lt;li&gt;Set up S3 event notifications on the bucket for downstream processing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or just use the &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt; — the whole thing deploys in under 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it end-to-end
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;terraform apply&lt;/code&gt;, the SFTP server is ready in about 8 minutes (most of the time is S3 Files mount targets becoming available). Here's a quick test:&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;# Upload a file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from S3 Files SFTP!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test.txt
sshpass &lt;span class="nt"&gt;-p&lt;/span&gt; demo sftp &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-P&lt;/span&gt; 22 demo@&amp;lt;sftp_endpoint&amp;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;
cd upload
put test.txt
bye
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Verify it landed in S3 (wait ~30-60s for sync)&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://&amp;lt;sftp_bucket_name&amp;gt;/demo/upload/test.txt -
&lt;span class="c"&gt;# Output: Hello from S3 Files SFTP!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also verified that SSH host keys persist across task restarts — the server fingerprint stays the same after a forced redeployment, thanks to the EFS volume mounted at &lt;code&gt;/etc/ssh/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;S3 Files bridges the gap between file system and object storage in a way that makes a lot of expensive AWS services feel redundant. For SFTP specifically, the combination of atmoz/sftp + S3 Files on Fargate gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~8x lower cost&lt;/strong&gt; than Transfer Family&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control&lt;/strong&gt; over the SFTP server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native S3 event notifications&lt;/strong&gt; for event-driven processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 as the source of truth&lt;/strong&gt; — lifecycle rules, replication, analytics all work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; with Terraform (even before native provider support)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The days of paying $216/month minimum for a managed SFTP endpoint are over for most teams. S3 Files is the missing piece that makes DIY SFTP on AWS not just viable, but ~8x cheaper.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3files</category>
      <category>terraform</category>
      <category>sftp</category>
    </item>
    <item>
      <title>Agents Bedrock AgentCore en mode VPC : attention aux coûts de NAT Gateway !</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Sat, 04 Apr 2026 08:59:14 +0000</pubDate>
      <link>https://dev.to/aws-builders/agents-bedrock-agentcore-en-mode-vpc-attention-aux-couts-de-nat-gateway--3pp4</link>
      <guid>https://dev.to/aws-builders/agents-bedrock-agentcore-en-mode-vpc-attention-aux-couts-de-nat-gateway--3pp4</guid>
      <description>&lt;p&gt;La semaine dernière, j'ai reçu une alerte d'anomalie de coûts AWS. L'alerte pointait vers mon compte de formation (où je fais mes démos et aussi mes POCs), signalant une charge inattendue de 29 $ sous — étrangement — Amazon Elastic Block Store. Le type d'utilisation racontait cependant une tout autre histoire : &lt;code&gt;NatGateway-Bytes&lt;/code&gt;. 659 Go de données avaient transité par ma NAT Gateway en six jours.&lt;/p&gt;

&lt;p&gt;J'avais récemment déployé un agent vocal sur Bedrock AgentCore Runtime en mode VPC, utilisant une NAT Gateway pour l'accès internet sortant (nécessaire pour le relais TURN WebRTC) — voir &lt;a href="https://dev.to/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17"&gt;mon billet de blog ici&lt;/a&gt;. Le VPC avait été créé spécifiquement pour cet agent, donc le suspect était évident. Mais je voulais des preuves concrètes avant de tirer des conclusions. Était-ce le trafic WebRTC ? Autre chose ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Début de l'investigation
&lt;/h2&gt;

&lt;p&gt;Mon premier réflexe a été de consulter les métriques CloudWatch de la NAT Gateway. La métrique &lt;code&gt;BytesOutToDestination&lt;/code&gt; (trafic du conteneur vers internet) ne montrait que 2,1 Go au total sur les six jours. Négligeable. Mais &lt;code&gt;BytesInFromDestination&lt;/code&gt; (trafic d'internet vers le conteneur à travers la NAT) racontait une tout autre histoire :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Entrant via la NAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;26 mars&lt;/td&gt;
&lt;td&gt;6,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27 mars&lt;/td&gt;
&lt;td&gt;240,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28 mars&lt;/td&gt;
&lt;td&gt;149,1 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29 mars&lt;/td&gt;
&lt;td&gt;149,8 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 mars&lt;/td&gt;
&lt;td&gt;102,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 mars&lt;/td&gt;
&lt;td&gt;15,0 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1er avril&lt;/td&gt;
&lt;td&gt;5,4 Go (journée partielle)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ce déséquilibre entre les flux entrants et sortants plaidait contre WebRTC comme responsable du trafic.&lt;/p&gt;

&lt;p&gt;De plus, la métrique &lt;code&gt;ActiveConnectionCount&lt;/code&gt; montrait un nombre stable d'environ 90 connexions 24h/24, même quand personne n'utilisait l'agent. Le pattern horaire était remarquablement régulier — alternant entre ~850 Mo et ~430 Mo par heure, en continu.&lt;/p&gt;

&lt;p&gt;Pour en avoir le cœur net, j'ai vérifié CloudTrail pour les événements &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; entre le 28 et le 30 mars. Zéro. Aucune activité utilisateur pendant la période avec le trafic le plus intense. L'agent était complètement inactif.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" alt="Trafic NAT Gateway vs invocations AgentCore" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation des VPC Flow Logs
&lt;/h2&gt;

&lt;p&gt;J'avais besoin de voir d'où venait le trafic. J'ai activé les VPC Flow Logs (j'aurais dû le faire dès le premier jour ? Bah, c'était un POC !) sur le VPC, en les envoyant vers un groupe de logs CloudWatch, et j'ai lancé une requête Logs Insights pour identifier les plus gros consommateurs :&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="n"&gt;stats&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;srcAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstPort&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les résultats sur une fenêtre de deux heures montraient une poignée d'adresses IP responsables de tout le trafic lourd :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    52.216.58.42 -&amp;gt;       10.0.0.144: 31175     270.1 MB
    16.15.207.229 -&amp;gt;      10.0.0.144: 62935     263.7 MB
    16.15.191.63 -&amp;gt;       10.0.0.144: 25320     263.6 MB
    52.216.12.24 -&amp;gt;       10.0.0.144: 12542     115.8 MB
    3.5.16.209 -&amp;gt;         10.0.0.144: 30762     113.4 MB
    16.15.199.52 -&amp;gt;       10.0.0.144: 49632     113.3 MB
    54.231.160.154 -&amp;gt;     10.0.0.144: 55754      29.6 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'adresse &lt;code&gt;10.0.0.144&lt;/code&gt; est l'IP privée de la NAT Gateway. Tout le trafic transitait depuis des IP externes, à travers la NAT, vers les ENI du conteneur AgentCore dans les sous-réseaux privés.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identification de la source
&lt;/h2&gt;

&lt;p&gt;J'avais besoin de savoir à quel service appartenaient ces IP. J'ai utilisé mon outil &lt;a href="https://does-this-ip-belong-to-aws.terracloud.fr" rel="noopener noreferrer"&gt;does-this-ip-belong-to-aws&lt;/a&gt;, qui vérifie les IP par rapport aux plages IP officielles AWS publiées sur &lt;code&gt;https://ip-ranges.amazonaws.com/ip-ranges.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Chaque IP à fort trafic correspondait à Amazon S3 en us-east-1 !&lt;/p&gt;

&lt;p&gt;Tout le trafic — jusqu'au dernier gigaoctet — était des téléchargements S3 transitant par la NAT Gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le correctif : S3 Gateway Endpoint
&lt;/h2&gt;

&lt;p&gt;Le correctif est simple et gratuit. Un S3 Gateway VPC Endpoint route le trafic S3 directement via le réseau AWS, contournant entièrement la NAT Gateway. Contrairement aux interface endpoints, les gateway endpoints n'ont ni frais horaires ni frais de traitement de données.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.aws_region}.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un &lt;code&gt;terraform apply&lt;/code&gt; et les coûts de transfert de données de la NAT Gateway tombent à quasi zéro.&lt;/p&gt;

&lt;p&gt;Ce qui soulève une question plus large : pourquoi ne pas toujours avoir un S3 Gateway Endpoint dans un VPC ? C'est gratuit, ça se crée en une seule ressource, et ça prévient exactement ce genre de surprise. Si vous créez des VPC avec des sous-réseaux privés et des NAT Gateways, ajoutez un S3 Gateway Endpoint par défaut. Il n'y a aucun inconvénient. Les S3 Gateway endpoints sont bons pour votre portefeuille, sinon pour votre âme.&lt;/p&gt;

&lt;h2&gt;
  
  
  La cause racine : recyclage du warm pool
&lt;/h2&gt;

&lt;p&gt;Après avoir ouvert un ticket de support, l'équipe du service Bedrock AgentCore a identifié la cause racine.&lt;/p&gt;

&lt;p&gt;AgentCore Runtime maintient un warm pool de VM pour garantir des invocations à faible latence. Chaque VM du pool télécharge l'image du conteneur depuis ECR — et ECR stocke les couches d'images dans S3. Mon image de conteneur faisait environ 435 Mo compressée.&lt;/p&gt;

&lt;p&gt;Trois facteurs se sont combinés pour produire la facture de 659 Go :&lt;/p&gt;

&lt;p&gt;Premièrement, les 21 appels API &lt;code&gt;UpdateAgentRuntime&lt;/code&gt; que j'ai effectués le 27 mars (une journée de débogage et redéploiement intensifs) ont chacun déclenché un cycle asynchrone de re-provisionnement du warm pool. Plusieurs séries de provisionnement de 10 VM, chacune téléchargeant l'image de 435 Mo, ont produit le pic de ~240 Go observé ce jour-là.&lt;/p&gt;

&lt;p&gt;Deuxièmement, le warm pool a continué à recycler les VM les jours suivants pour les garder fraîches et prêtes. Avec 10 VM téléchargeant chacune l'image périodiquement, le trafic stable de ~150 Go/jour du 28 au 30 mars est cohérent avec un recyclage régulier.&lt;/p&gt;

&lt;p&gt;Troisièmement, après environ 72 heures sans invocations, le warm pool a automatiquement réduit sa taille de 10 VM à 1 VM. Cela explique la chute de ~150 Go/jour à ~15 Go/jour le 31 mars.&lt;/p&gt;

&lt;p&gt;Le recyclage du warm pool est un comportement attendu de la plateforme — c'est ce qui permet à AgentCore de servir les requêtes avec une faible latence. Le problème était que tous ces téléchargements S3 passaient par ma NAT Gateway à 0,045 $/Go au lieu de rester sur le réseau interne AWS.&lt;/p&gt;

&lt;p&gt;Lancer autant de VM pour si peu d'invocations me semble un peu comme tirer au bazooka pour tuer une mouche ; je me demande si c'est soutenable... Cela dit, AWS a un bon historique de gestion d'activités rentables à grande échelle : qui suis-je pour juger ?&lt;/p&gt;

&lt;p&gt;Quoi qu'il en soit, l'équipe du service a promis de mettre à jour la documentation pour que pas (trop) d'utilisateurs ne se retrouvent face à ces charges (franchement) indues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points à retenir
&lt;/h2&gt;

&lt;p&gt;Si vous utilisez Bedrock AgentCore Runtime en mode VPC, trois choses à garder en tête :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ajoutez un S3 Gateway Endpoint à votre VPC&lt;/strong&gt;. C'est gratuit et ça élimine ce qui s'est avéré être la source dominante de coûts de transfert de données de la NAT Gateway — les téléchargements d'images ECR par le warm pool. AWS a confirmé qu'ils mettent à jour leur documentation VPC pour recommander cela plus visiblement. Il n'y a véritablement aucune raison de ne pas en avoir un dans chaque VPC avec des sous-réseaux privés.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Soyez attentif à la taille de votre image de conteneur&lt;/strong&gt;. Mon image de 435 Mo, téléchargée par un warm pool de 10 VM avec recyclage régulier, a généré des centaines de gigaoctets de transfert. Réduire l'image (builds multi-étapes, moins de dépendances, base Alpine) réduit directement ce coût — même avec le endpoint S3 en place, des images plus petites signifient des démarrages à froid plus rapides.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Surveillez vos métriques NAT Gateway tôt&lt;/strong&gt;. Les métriques &lt;code&gt;BytesInFromDestination&lt;/code&gt; et &lt;code&gt;BytesOutToSource&lt;/code&gt; dans CloudWatch vous montreront si quelque chose d'inattendu se passe. Je ne m'en suis rendu compte que grâce à l'alerte d'anomalie de coûts — à ce moment-là, 29 $ avaient déjà été dépensés. Les VPC Flow Logs combinés avec CloudWatch Logs Insights ont rendu le diagnostic simple une fois que j'ai regardé.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Paul Santus est consultant cloud indépendant chez &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. Il accompagne les organisations dans la construction et le déploiement d'applications IA sur AWS. Retrouvez-le sur &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vpc</category>
      <category>bedrock</category>
      <category>ai</category>
    </item>
    <item>
      <title>VPC-connected Bedrock AgentCore Runtime-hosted agents: beware of NAT Gateway costs!</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 03 Apr 2026 14:05:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/vpc-connected-bedrock-agentcore-runtime-hosted-agents-beware-of-nat-gateway-costs-3ohh</link>
      <guid>https://dev.to/aws-builders/vpc-connected-bedrock-agentcore-runtime-hosted-agents-beware-of-nat-gateway-costs-3ohh</guid>
      <description>&lt;p&gt;Last week I received a cost anomaly alert from AWS. The alert pointed at my training account, flagging an unexpected $29 charge under — oddly enough — Amazon Elastic Block Store. The usage type, however, told a different story: &lt;code&gt;NatGateway-Bytes&lt;/code&gt;. 659 GB of data had flowed through my NAT Gateway in six days.&lt;/p&gt;

&lt;p&gt;I had recently deployed a voice agent on Bedrock AgentCore Runtime in VPC mode, using a NAT Gateway for outbound internet access (required for WebRTC TURN relay) - see &lt;a href="https://dev.to/aws-builders/switching-my-ai-voice-agent-from-websocket-to-webrtc-what-broke-and-what-i-learned-3dkn"&gt;my blog post here&lt;/a&gt;. The VPC had been created specifically for this agent, so the suspect was obvious. But I wanted ground truth before jumping to conclusions. Was it WebRTC traffic? Something else?&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting the investigation
&lt;/h2&gt;

&lt;p&gt;My first stop was CloudWatch metrics on the NAT Gateway. The &lt;code&gt;BytesOutToDestination&lt;/code&gt; metric (traffic from the container to the internet) showed only 2.1 GB total over the six days. Negligible. But &lt;code&gt;BytesInFromDestination&lt;/code&gt; (traffic from the internet into the container through the NAT) told a very different story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Inbound through NAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mar 26&lt;/td&gt;
&lt;td&gt;6.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 27&lt;/td&gt;
&lt;td&gt;240.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 28&lt;/td&gt;
&lt;td&gt;149.1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 29&lt;/td&gt;
&lt;td&gt;149.8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 30&lt;/td&gt;
&lt;td&gt;102.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 31&lt;/td&gt;
&lt;td&gt;15.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apr 01&lt;/td&gt;
&lt;td&gt;5.4 GB (partial)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This unbalanced metrics values between inbound and outbound flows pleaded against WebRTC as the traffic culprit. &lt;/p&gt;

&lt;p&gt;Moreover, the &lt;code&gt;ActiveConnectionCount&lt;/code&gt; metric showed a steady ~90 connections 24/7, even when nobody was using the agent. The hourly pattern was remarkably regular — alternating between ~850 MB and ~430 MB per hour, around the clock.&lt;/p&gt;

&lt;p&gt;Just to be sure, I checked CloudTrail for &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; events between March 28 and March 30. Zero. No user activity at all during the period with the heaviest traffic. The agent was completely idle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" alt="NAT Gateway traffic vs AgentCore invocations" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling VPC Flow Logs
&lt;/h2&gt;

&lt;p&gt;I needed to see where the traffic was coming from. I enabled VPC Flow Logs (shouldn't have it done on day 1? Nay, this was a POC workload!) on the VPC, sending them to a CloudWatch log group, and ran a Logs Insights query to identify the top talkers:&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="n"&gt;stats&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;srcAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstPort&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results over a two-hour window showed a handful of IP addresses responsible for all the heavy traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    52.216.58.42 -&amp;gt;       10.0.0.144: 31175     270.1 MB
    16.15.207.229 -&amp;gt;      10.0.0.144: 62935     263.7 MB
    16.15.191.63 -&amp;gt;       10.0.0.144: 25320     263.6 MB
    52.216.12.24 -&amp;gt;       10.0.0.144: 12542     115.8 MB
    3.5.16.209 -&amp;gt;         10.0.0.144: 30762     113.4 MB
    16.15.199.52 -&amp;gt;       10.0.0.144: 49632     113.3 MB
    54.231.160.154 -&amp;gt;     10.0.0.144: 55754      29.6 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;10.0.0.144&lt;/code&gt; address is the NAT Gateway's private IP. All the traffic was flowing from external IPs, through the NAT, to the AgentCore container ENIs in the private subnets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying the source
&lt;/h2&gt;

&lt;p&gt;I needed to know what service these IPs belonged to. I used my &lt;a href="https://does-this-ip-belong-to-aws.terracloud.fr" rel="noopener noreferrer"&gt;does-this-ip-belong-to-aws&lt;/a&gt; tool, which checks IPs against the official AWS IP ranges published at &lt;code&gt;https://ip-ranges.amazonaws.com/ip-ranges.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every single high-traffic IP resolved to Amazon S3 in us-east-1!&lt;/p&gt;

&lt;p&gt;All the traffic — every last gigabyte — was S3 pulls flowing through the NAT Gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: S3 Gateway Endpoint
&lt;/h2&gt;

&lt;p&gt;The fix is straightforward and free. An S3 Gateway VPC Endpoint routes S3 traffic directly through the AWS network, bypassing the NAT Gateway entirely. Unlike interface endpoints, gateway endpoints have no hourly charge and no data processing fee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.aws_region}.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;terraform apply&lt;/code&gt; and the NAT Gateway data transfer cost drops to near zero.&lt;/p&gt;

&lt;p&gt;This raises a broader question: why would you ever not have an S3 Gateway Endpoint in a VPC? It's free, takes one resource to create, and prevents exactly this kind of surprise. If you're creating VPCs with private subnets and NAT Gateways, add an S3 Gateway Endpoint as a default. There's no downside. S3 Gateway endpoints are good for you wallet, if not for your soul.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause: warm pool recycling
&lt;/h2&gt;

&lt;p&gt;After filing a support case, the Bedrock AgentCore service team identified the root cause.&lt;/p&gt;

&lt;p&gt;AgentCore Runtime maintains a warm pool of VMs to ensure low-latency invocations. Each VM in the pool pulls the container image from ECR — and ECR stores image layers in S3. My container image was ~435 MB compressed.&lt;/p&gt;

&lt;p&gt;Three things combined to produce the 659 GB bill:&lt;/p&gt;

&lt;p&gt;First, the 21 &lt;code&gt;UpdateAgentRuntime&lt;/code&gt; API calls I made on March 27 (a day of heavy debugging and redeployment) each triggered an asynchronous warm pool re-provisioning cycle. Multiple rounds of 10-VM provisioning, each pulling the 435 MB image, produced the ~240 GB spike that day.&lt;/p&gt;

&lt;p&gt;Second, the warm pool continued recycling VMs over the following days to keep them fresh and ready. With 10 VMs each pulling the image periodically, the steady ~150 GB/day on March 28-30 is consistent with regular recycling.&lt;/p&gt;

&lt;p&gt;Third, after approximately 72 hours with no invocations, the warm pool automatically downscaled from 10 VMs to 1 VM. This explains the drop from ~150 GB/day to ~15 GB/day on March 31.&lt;/p&gt;

&lt;p&gt;The warm pool recycling is expected platform behavior — it's what makes AgentCore able to serve requests with low latency. The problem was that all those S3 pulls were routing through my NAT Gateway at $0.045/GB instead of staying on the AWS internal network.&lt;/p&gt;

&lt;p&gt;Firing these many VMs for so few invocations seems to mme like shooting a bazooka to kill a fly; I wonder how sustainable that is.. yet AWS has a good track record at operating profitable business at scale: who am I to judge? &lt;/p&gt;

&lt;p&gt;Anyway, the service team promised they'll make an update to the documentation so that not (too) many users face these (frankly) undue charges. &lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you're running Bedrock AgentCore Runtime in VPC mode, three things to keep in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;add an S3 Gateway Endpoint to your VPC&lt;/strong&gt;. It's free and eliminates what turned out to be the dominant source of NAT Gateway data transfer costs — ECR image pulls from the warm pool. AWS has confirmed they are updating their VPC documentation to more prominently recommend this. There is genuinely no reason not to have one in every VPC with private subnets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;be mindful of container image size&lt;/strong&gt;. My 435 MB image, pulled across a 10-VM warm pool with regular recycling, generated hundreds of gigabytes of transfer. Slimming the image (multi-stage builds, fewer dependencies, Alpine base) directly reduces this cost — even with the S3 endpoint in place, smaller images mean faster cold starts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;monitor your NAT Gateway metrics early&lt;/strong&gt;. The &lt;code&gt;BytesInFromDestination&lt;/code&gt; and &lt;code&gt;BytesOutToSource&lt;/code&gt; metrics in CloudWatch will show you if something unexpected is happening. I only noticed because of the cost anomaly alert — by then, $29 had already been spent. VPC Flow Logs combined with CloudWatch Logs Insights made the diagnosis straightforward once I looked.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Paul Santus is an independent cloud consultant at &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. He helps organizations build and deploy AI-powered applications on AWS. Connect with him on &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>agentcore</category>
      <category>vpc</category>
    </item>
    <item>
      <title>Héberger un agent IA vocal sur AWS Bedrock AgentCore communiquant via WebRTC</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 27 Mar 2026 16:02:24 +0000</pubDate>
      <link>https://dev.to/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17</link>
      <guid>https://dev.to/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17</guid>
      <description>&lt;p&gt;Aujourd'hui j'ai migré agent vocal IA de WebSocket vers WebRTC — voici ce qui a cassé et ce que j'ai appris.&lt;/p&gt;

&lt;p&gt;Il y a quelques jours, je suis tombé sur le &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;billet de blog&lt;/a&gt; et le repo de Darryl Ruggles pour un agent vocal bidirectionnel construit avec Strands BidiAgent et Amazon Nova Sonic v2. Son travail est remarquablement bien ficelé — j'avais un assistant vocal fonctionnel sur mon laptop en une dizaine de minutes. L'agent écoute votre voix, cherche dans une base de recettes, programme des minuteurs de cuisson, consulte des données nutritionnelles et convertit des unités, le tout par conversation naturelle.&lt;/p&gt;

&lt;p&gt;La version de Darryl utilise WebSocket comme transport entre le navigateur et l'agent. Ça fonctionne bien, mais je voulais aller plus loin : passer le transport en WebRTC et déployer le tout sur Bedrock AgentCore Runtime. Ce billet couvre ce parcours — ce qui a changé, ce qui a cassé, et ce que j'en ai tiré.&lt;/p&gt;

&lt;p&gt;Vous avez rêvé de demander la recette des crêpes à un agent IA ? je l'ai fait :)&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/kxElgTLmIQ8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Le code source complet est disponible sur &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Le repo est entièrement géré par Terraform, mais vous pouvez toujours utiliser l'approche Makefile de Darryl si vous préférez garder Terraform pour l'infrastructure et le CLI pour le déploiement de l'agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi WebRTC pour un agent vocal ?
&lt;/h2&gt;

&lt;p&gt;La version WebSocket de l'agent fonctionne, alors pourquoi changer ? Plusieurs raisons m'ont poussé vers WebRTC.&lt;/p&gt;

&lt;p&gt;D'abord, la latence. WebSocket tourne sur TCP, ce qui signifie que chaque paquet est garanti d'arriver dans l'ordre. C'est parfait pour des messages de chat, mais pour de l'audio en temps réel, un seul paquet perdu bloque tout le flux pendant que TCP retransmet. WebRTC&lt;sup id="fnref1"&gt;1&lt;/sup&gt; utilise UDP — si un paquet est perdu, le flux continue. Pour une conversation vocale, un micro-glitch est bien préférable à une pause perceptible.&lt;/p&gt;

&lt;p&gt;Ensuite, le navigateur fait plus de travail. Avec WebSocket, je devais capturer l'audio du micro via &lt;code&gt;getUserMedia&lt;/code&gt;, le sous-échantillonner à 16kHz avec un &lt;code&gt;ScriptProcessorNode&lt;/code&gt;, l'encoder en base64 PCM et l'envoyer en messages JSON. Côté lecture, il fallait un &lt;code&gt;AudioWorklet&lt;/code&gt; avec un buffer circulaire pour gérer le flux audio entrant. Avec WebRTC, le navigateur gère nativement la capture audio, l'encodage (Opus) et la lecture via &lt;code&gt;RTCPeerConnection&lt;/code&gt;. Le code frontend s'en trouve considérablement simplifié.&lt;/p&gt;

&lt;p&gt;Enfin, WebRTC est prêt pour la vidéo. Les avatars IA arrivent à des latences acceptables, et WebRTC gère les pistes vidéo aussi naturellement que les pistes audio. Ajouter un flux vidéo plus tard revient simplement à ajouter une piste à la connexion existante — aucun changement d'architecture ne sera nécessaire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Petit tour d'horizon des architectures WebRTC
&lt;/h2&gt;

&lt;p&gt;Il existe deux façons fondamentalement différentes d'utiliser WebRTC, et le choix compte quand on construit un agent vocal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pair-à-pair (P2P)
&lt;/h3&gt;

&lt;p&gt;En WebRTC P2P, deux pairs se connectent directement l'un à l'autre. Pas de serveur média au milieu — l'audio circule directement du navigateur à l'agent et retour. Un serveur relais TURN&lt;sup id="fnref2"&gt;2&lt;/sup&gt; peut être nécessaire quand l'un ou les deux pairs sont derrière un NAT&lt;sup id="fnref3"&gt;3&lt;/sup&gt; (ce qui est quasi systématique en production : les clients sont derrière un routeur Internet et les agents doivent être dans un VPC privé pour accéder aux outils de l'entreprise), mais le serveur TURN ne fait que relayer les paquets sans les inspecter ni les traiter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" alt="P2P WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Basé sur des salles (SFU)
&lt;/h3&gt;

&lt;p&gt;Dans une architecture basée sur des salles de visio, un serveur média (appelé SFU&lt;sup id="fnref4"&gt;4&lt;/sup&gt; — Selective Forwarding Unit) se place au milieu. Les participants se connectent au serveur, pas entre eux. Le serveur reçoit les pistes audio/vidéo de chaque participant et les retransmet sélectivement aux autres. LiveKit, Amazon Chime SDK et Daily sont des exemples de plateformes SFU.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" alt="Media Server WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pour un agent vocal en 1:1, le P2P est plus simple et évite le coût et la complexité de faire tourner (ou de payer) un serveur média. J'ai opté pour le P2P avec Amazon Kinesis Video Streams (KVS) comme relais TURN managé — c'est l'approche documentée pour WebRTC sur AgentCore.&lt;/p&gt;

&lt;p&gt;J'ai envisagé les solutions basées sur des salles, mais chaque plateforme SFU nécessite son propre SDK — on ne peut pas simplement se connecter avec un &lt;code&gt;RTCPeerConnection&lt;/code&gt; standard. L'offre WebRTC d'AWS, Amazon Chime SDK, est riche en fonctionnalités (transcription, enregistrement, analytics) et nettement moins chère que les alternatives comme LiveKit ou Daily, mais elle n'offre pas encore de chemin balisé pour la communication agent-vers-salle côté serveur. C'est une fonctionnalité que j'aimerais beaucoup voir arriver, vu la qualité du reste du Chime SDK. Pour l'instant, le P2P avec KVS TURN était le chemin le plus direct. Je considérerai certainement le WebRTC en salle, mais c'est une histoire pour un autre billet.&lt;/p&gt;

&lt;h3&gt;
  
  
  La pile WebRTC : navigateur et serveur
&lt;/h3&gt;

&lt;p&gt;Côté navigateur, WebRTC est intégré nativement. L'API &lt;code&gt;RTCPeerConnection&lt;/code&gt; est disponible dans tous les navigateurs modernes — Chrome, Safari, Firefox, Edge. On crée une connexion pair, on ajoute une piste micro via &lt;code&gt;getUserMedia&lt;/code&gt;, et le navigateur gère l'encodage audio (Opus), la collecte des candidats ICE et le chiffrement DTLS. Aucune bibliothèque nécessaire.&lt;/p&gt;

&lt;p&gt;Côté serveur, c'est une autre histoire. WebRTC a été conçu pour les navigateurs, pas pour des backends Python. La bibliothèque de référence pour le WebRTC côté serveur en Python est &lt;a href="https://github.com/aiortc/aiortc" rel="noopener noreferrer"&gt;aiortc&lt;/a&gt; — une implémentation asyncio de WebRTC et ORTC. Elle gère les connexions pair, la négociation ICE et les pistes média, et utilise &lt;a href="https://github.com/PyAV-Org/PyAV" rel="noopener noreferrer"&gt;PyAV&lt;/a&gt; (bindings FFmpeg) pour le traitement des trames audio/vidéo. Elle n'est pas aussi éprouvée que le WebRTC des navigateurs, mais elle fonctionne bien et c'est ce qu'utilise aussi le code d'exemple AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture : développement local vs. déployé
&lt;/h2&gt;

&lt;p&gt;Un point que je voulais préserver du design original de Darryl est la possibilité de tout faire tourner localement pour le développement, sans aucune infrastructure cloud. La migration WebRTC maintient cela.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode local
&lt;/h3&gt;

&lt;p&gt;En mode local, l'agent tourne sur votre machine. Le navigateur et l'agent sont sur le même réseau (ou la même machine), donc WebRTC se connecte en pair-à-pair sans avoir besoin de relais TURN. La signalisation — l'échange d'offres/réponses SDP&lt;sup id="fnref5"&gt;5&lt;/sup&gt; et de candidats ICE&lt;sup id="fnref6"&gt;6&lt;/sup&gt; — passe par le proxy du serveur de développement Vite vers le serveur FastAPI local.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" alt="Local mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode hébergé sur Bedrock AgentCore
&lt;/h3&gt;

&lt;p&gt;En mode hébergé, l'agent tourne dans un conteneur Docker sur Bedrock AgentCore Runtime, attaché à un VPC via une interface réseau élastique (ENI) dans un sous-réseau privé. Le navigateur ne peut pas atteindre l'agent directement — tout le trafic média passe par un relais KVS TURN. La signalisation passe par le endpoint HTTP &lt;code&gt;/invocations&lt;/code&gt; d'AgentCore, authentifié en SigV4 via le SDK &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" alt="Voice Agent deployed on AgentCore"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le diagramme suivant, tiré de la &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-webrtc.html" rel="noopener noreferrer"&gt;documentation AWS&lt;/a&gt;, montre le réseau VPC en détail — la signalisation passe par le endpoint HTTP d'AgentCore tandis que le trafic média passe par la NAT gateway du VPC vers le relais KVS TURN :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" alt="AgentCore WebRTC Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le point important à noter est que le code de l'agent est quasi identique entre les modes local et déployé. Le &lt;code&gt;BidiAgent&lt;/code&gt;, le &lt;code&gt;BidiNovaSonicModel&lt;/code&gt; et les quatre outils (recherche de recettes, minuteur, recherche nutritionnelle, conversion d'unités) sont totalement inchangés. La seule différence est la couche transport : en local, aiortc se connecte en P2P ; en déployé, il se connecte via KVS TURN. L'agent détecte dans quel mode il se trouve via la variable d'environnement &lt;code&gt;CONTAINER_ENV&lt;/code&gt; et configure les serveurs ICE en conséquence.&lt;/p&gt;

&lt;p&gt;Cette séparation propre a été possible grâce au protocole &lt;code&gt;BidiInput&lt;/code&gt;/&lt;code&gt;BidiOutput&lt;/code&gt; de Strands. J'ai écrit deux petites classes adaptateurs — &lt;code&gt;WebRTCBidiInput&lt;/code&gt; et &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; — qui font le pont entre les pistes audio aiortc et le format d'événements attendu par BidiAgent. L'agent ne sait pas et ne se soucie pas de savoir si l'audio vient d'un WebSocket ou d'une piste WebRTC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'apporte le support WebRTC de Bedrock AgentCore
&lt;/h2&gt;

&lt;p&gt;Le 20 mars 2026, AWS a &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-webrtc/" rel="noopener noreferrer"&gt;annoncé&lt;/a&gt; le support WebRTC pour AgentCore Runtime. Je veux être honnête sur ce que cela signifie en pratique.&lt;/p&gt;

&lt;p&gt;Je n'en suis pas sûr à 100%, et je suis prêt à être corrigé, mais mon impression est que les briques de base — le mode réseau VPC, KVS TURN, le endpoint HTTP &lt;code&gt;/invocations&lt;/code&gt; — existaient tous déjà avant cette annonce. Le mode réseau VPC est disponible depuis la disponibilité générale d'AgentCore en octobre 2025. KVS TURN est une fonctionnalité de longue date de Kinesis Video Streams. Et &lt;code&gt;/invocations&lt;/code&gt; a toujours été le endpoint HTTP standard des runtimes AgentCore.&lt;/p&gt;

&lt;p&gt;Ce que la release du 20 mars ajoute, d'après ce que je comprends, c'est de la documentation officielle, du code d'exemple fonctionnel, et la déclaration explicite que WebRTC est un protocole supporté sur AgentCore Runtime. Avant cela, on pouvait techniquement assembler les mêmes pièces soi-même, mais on était seul — pas de docs, pas d'exemples, pas de garantie que ça continuerait à fonctionner.&lt;/p&gt;

&lt;p&gt;Ce qu'AgentCore apporte est réellement précieux : un hébergement de conteneurs managé avec auto-scaling, l'isolation des sessions entre utilisateurs concurrents, l'observabilité intégrée (logs CloudWatch, traces X-Ray), et aucune infrastructure à gérer au-delà du VPC. Je n'ai pas eu à configurer ECS, des load balancers ou de l'orchestration de conteneurs.&lt;/p&gt;

&lt;p&gt;Cela dit, il y a une bonne quantité de code custom. La signalisation WebRTC (échange SDP, gestion des candidats ICE), le cycle de vie de la connexion pair aiortc, le pont entre les pistes audio et BidiAgent, et la gestion des identifiants KVS TURN — tout cela est du code applicatif que j'ai écrit. AgentCore l'héberge et l'exécute, mais ne l'abstrait pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Défis et leçons apprises
&lt;/h2&gt;

&lt;p&gt;La migration de WebSocket vers WebRTC a commencé en douceur, puis ça s'est corsé. Voici ce qui m'a fait trébucher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibilité des zones de disponibilité du VPC
&lt;/h3&gt;

&lt;p&gt;AgentCore Runtime ne supporte que certaines zones de disponibilité. En us-east-1, seules &lt;code&gt;use1-az4&lt;/code&gt; (us-east-1a), &lt;code&gt;use1-az1&lt;/code&gt; (us-east-1c) et &lt;code&gt;use1-az2&lt;/code&gt; (us-east-1d) sont supportées. J'ai initialement laissé Terraform choisir les deux premières AZ automatiquement, ce qui m'a donné us-east-1a et us-east-1b. La mise à jour du runtime a échoué avec un statut cryptique &lt;code&gt;UPDATE_FAILED&lt;/code&gt;. Le vrai message d'erreur — mentionnant l'AZ non supportée — était enfoui dans le champ &lt;code&gt;failureReason&lt;/code&gt; de la réponse API, pas remonté dans l'erreur Terraform. J'ai fini par coder en dur les AZ supportées dans mon module VPC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Affinité de session
&lt;/h3&gt;

&lt;p&gt;Celui-ci m'a coûté des heures. La signalisation WebRTC est une poignée de main en plusieurs étapes — le navigateur et l'agent échangent plusieurs messages pour établir une connexion. L'agent doit se souvenir de l'état de la connexion du premier message quand il traite le deuxième et le troisième. Si ces messages atterrissent sur des instances serveur différentes, l'agent n'a aucune mémoire de la poignée de main en cours et la connexion échoue.&lt;/p&gt;

&lt;p&gt;J'ai d'abord utilisé des requêtes HTTP POST signées SigV4, en supposant qu'inclure l'identifiant de session comme paramètre de requête fournirait l'affinité de routage. Ce n'était pas le cas. Les candidats ICE atterrissaient sur une instance de conteneur différente de celle qui détenait la connexion pair.&lt;/p&gt;

&lt;p&gt;La solution a été d'utiliser le SDK &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt; avec &lt;code&gt;InvokeAgentRuntimeCommand&lt;/code&gt; et le paramètre &lt;code&gt;runtimeSessionId&lt;/code&gt;. C'est le seul moyen fiable de s'assurer que toutes les requêtes d'une session WebRTC atteignent la même instance de conteneur. Le code d'exemple AWS utilise ce pattern aussi — je ne l'avais simplement pas remarqué au début parce que j'étais concentré sur les parties WebRTC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtrage des candidats SDP
&lt;/h3&gt;

&lt;p&gt;Quand l'agent crée une connexion pair à l'intérieur du VPC, aiortc génère des candidats ICE pour toutes les interfaces réseau disponibles — y compris des IP internes au VPC comme &lt;code&gt;169.254.0.2&lt;/code&gt;. Ces candidats hôtes se retrouvent dans la réponse SDP envoyée au navigateur. Le navigateur essaie consciencieusement de s'y connecter, échoue (parce qu'ils sont injoignables depuis l'Internet public), et ne se rabat sur les candidats relais qu'ensuite. Cela ajoute plusieurs secondes au temps de connexion.&lt;/p&gt;

&lt;p&gt;La solution est simple : retirer les candidats non-relais de la réponse SDP avant de la renvoyer au navigateur. En mode déployé, les seuls candidats qui peuvent fonctionner sont les candidats relais TURN, donc il n'y a aucune raison d'inclure les autres.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode TURN uniquement
&lt;/h3&gt;

&lt;p&gt;Similaire au problème de filtrage SDP, l'instance aiortc de l'agent essaie les candidats hôtes avant les candidats relais par défaut. Comme les candidats hôtes utilisent des IP internes au VPC qui ne peuvent jamais fonctionner du point de vue du navigateur, c'est du temps perdu. Configurer aiortc pour n'utiliser que les candidats relais TURN (&lt;code&gt;turn_only=True&lt;/code&gt;) saute directement aux candidats qui fonctionnent réellement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialisation paresseuse de KVS
&lt;/h3&gt;

&lt;p&gt;J'appelais initialement &lt;code&gt;kvs.init()&lt;/code&gt; au moment de l'import du module, protégé par un &lt;code&gt;if IS_CONTAINER&lt;/code&gt;. Ça fonctionnait bien en local mais faisait crasher le conteneur sur AgentCore. L'appel API KVS pour trouver ou créer le canal de signalisation nécessite des identifiants AWS, et au démarrage du conteneur il peut y avoir un bref délai avant que les identifiants du rôle IAM soient disponibles. Déplacer l'initialisation à la première requête réelle (init paresseuse) a résolu le crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comportement au démarrage à froid
&lt;/h3&gt;

&lt;p&gt;Après que le conteneur est resté inactif un moment, la première tentative de connexion WebRTC échoue parfois. Les requêtes de signalisation réussissent (AgentCore renvoie 200), mais la connexion ICE ne s'établit jamais. Je soupçonne que c'est lié au fait qu'AgentCore démarre une nouvelle instance de conteneur — les premières requêtes peuvent être traitées par une instance qui n'est pas encore complètement prête. Côté agent, j'ai explicitement mis &lt;code&gt;--workers 1&lt;/code&gt; dans la commande uvicorn pour m'assurer que toutes les requêtes au sein d'un conteneur touchent le même processus (et donc le même état de connexion pair en mémoire). Côté frontend, j'ai ajouté un mécanisme de retry : attendre que ICE atteigne l'état "connected", et si ce n'est pas le cas dans les 10 secondes, tout démonter et réessayer avec un nouvel identifiant de session. Ensemble, ces deux mesures ont rendu la connexion fiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code clé
&lt;/h2&gt;

&lt;p&gt;Je ne vais pas parcourir chaque fichier, mais voici les pièces qui font fonctionner l'intégration WebRTC.&lt;/p&gt;

&lt;p&gt;L'adaptateur &lt;code&gt;WebRTCBidiInput&lt;/code&gt; lit les trames audio de la piste aiortc, les rééchantillonne à 16kHz, et les renvoie comme événements &lt;code&gt;bidi_audio_input&lt;/code&gt; que BidiAgent comprend :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&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;_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;track&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;__call__&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&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;_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MediaStreamError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopAsyncIteration&lt;/span&gt;
        &lt;span class="n"&gt;resampled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_resampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resampled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&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;L'adaptateur &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; fait l'inverse — il reçoit les événements de BidiAgent et pousse l'audio vers la piste de sortie aiortc :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_track&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;_output_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_track&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;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;audio_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&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;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_interruption&lt;/span&gt;&lt;span class="sh"&gt;"&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;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Côté frontend, le hook &lt;code&gt;useWebRTCSession&lt;/code&gt; utilise le SDK AgentCore pour la signalisation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeAgentRuntimeCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// assure l'affinité de session&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transformToByteArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le code source complet est dans le &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;repo&lt;/a&gt; — la branche &lt;code&gt;feat/webrtc&lt;/code&gt; contient la version locale uniquement, et &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; la version déployée complète avec Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outillage de développement
&lt;/h2&gt;

&lt;p&gt;J'ai construit ce projet avec &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;, l'assistant de développement IA d'Amazon. Il a géré la planification, la génération de code, le débogage et le déploiement itératif — y compris les nombreux allers-retours d'essais-erreurs avec la configuration WebRTC que ce billet décrit. Le va-et-vient entre écriture de code, déploiement, vérification des logs et correction des problèmes se prêtait naturellement à un workflow de pair-programming avec une IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essayez vous-même
&lt;/h2&gt;

&lt;p&gt;Pour lancer en local :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/psantus/strands-bidir-nova.git
&lt;span class="nb"&gt;cd &lt;/span&gt;strands-bidir-nova
git checkout feat/webrtc
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make install-frontend
&lt;span class="c"&gt;# Terminal 1 :&lt;/span&gt;
make serve
&lt;span class="c"&gt;# Terminal 2 :&lt;/span&gt;
make serve-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ouvrez &lt;code&gt;http://localhost:5173&lt;/code&gt;, cliquez sur le micro, et commencez à parler.&lt;/p&gt;

&lt;p&gt;Pour la version déployée sur AgentCore, passez sur la branche &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; et suivez le README. Vous aurez besoin d'une Knowledge Base Bedrock avec quelques recettes, d'un pool d'utilisateurs Cognito et de Docker pour construire l'image du conteneur. Un seul &lt;code&gt;terraform apply&lt;/code&gt; gère le reste.&lt;/p&gt;

&lt;p&gt;Si vous préférez commencer par la version WebSocket, le &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;billet original&lt;/a&gt; de Darryl Ruggles est le bon point de départ.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Paul Santus est consultant cloud indépendant chez &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. Il accompagne les organisations dans la construction et le déploiement d'applications IA sur AWS. Retrouvez-le sur &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;strong&gt;WebRTC&lt;/strong&gt; (Web Real-Time Communication) — Standard ouvert pour la communication audio, vidéo et données en temps réel directement entre navigateurs et appareils, utilisant un transport basé sur UDP.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;strong&gt;TURN&lt;/strong&gt; (Traversal Using Relays around NAT) — Serveur relais qui transfère le trafic média quand deux pairs ne peuvent pas se connecter directement. Les deux côtés envoient leur audio au serveur TURN, qui le relaie à l'autre côté.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;strong&gt;NAT&lt;/strong&gt; (Network Address Translation) — Mécanisme réseau qui fait correspondre des adresses IP privées à des adresses publiques. La plupart des routeurs domestiques et des VPC cloud utilisent le NAT, ce qui empêche les connexions entrantes directes.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;strong&gt;SFU&lt;/strong&gt; (Selective Forwarding Unit) — Serveur média qui reçoit les pistes audio/vidéo des participants et les retransmet sélectivement aux autres, sans mixage ni transcodage. Utilisé par LiveKit, Chime SDK, Daily, etc.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;strong&gt;SDP&lt;/strong&gt; (Session Description Protocol) — Format texte décrivant une session multimédia : codecs, adresses de transport et types de média. En WebRTC, les pairs échangent des « offres » et « réponses » SDP pour négocier la connexion.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;strong&gt;ICE&lt;/strong&gt; (Interactive Connectivity Establishment) — Protocole pour trouver le meilleur chemin réseau entre deux pairs. Il collecte des adresses candidates (locales, réflexives serveur, relais) et teste la connectivité entre elles.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>voice</category>
    </item>
  </channel>
</rss>
