<?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.us-east-2.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>Cloud Native Buildpack for AWS Lambda PHP (Bref) - technical detail</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 29 Jul 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/cloud-native-buildpack-for-aws-lambda-php-bref-technical-detail-4amp</link>
      <guid>https://dev.to/aws-builders/cloud-native-buildpack-for-aws-lambda-php-bref-technical-detail-4amp</guid>
      <description>&lt;p&gt;In first post of this series, I introduced my Cloud Native Buildpack for AWS Lambda PHP (Bref). &lt;/p&gt;

&lt;p&gt;Here I want to provide some technical details on how it works so far. &lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda's /var/task problem
&lt;/h2&gt;

&lt;p&gt;CNB places the application at &lt;code&gt;/workspace&lt;/code&gt;. Lambda requires it at &lt;code&gt;/var/task&lt;/code&gt;. And you can't override &lt;code&gt;LAMBDA_TASK_ROOT&lt;/code&gt;, as it's a reserved environment variable.&lt;/p&gt;

&lt;p&gt;The buildpack runs as a non-root user during certain CNB phases, so it can't write to &lt;code&gt;/var/task&lt;/code&gt; either. The solution is a thin flatten step after the CNB build that copies &lt;code&gt;/workspace&lt;/code&gt; to &lt;code&gt;/var/task&lt;/code&gt; and restores the Bref entrypoint. This is handled automatically by &lt;code&gt;make lambda&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Is it elegant? Not perfectly. But it works, and the developer never sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extension installation: three fallback strategies
&lt;/h2&gt;

&lt;p&gt;Bref's extra PHP extensions are published as Docker images (e.g., &lt;code&gt;bref/extra-gd-php-84:3&lt;/code&gt;). Each image is a FROM scratch layer containing just the &lt;code&gt;.so&lt;/code&gt; file and its dependencies under &lt;code&gt;/opt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The buildpack uses a three-tier strategy to install extensions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 1: crane&lt;/strong&gt;. It uses go-containerregistry to pull and extract the extension image directly from Docker Hub, no Docker daemon needed. Works great in CI. However, bref-extra only publishes x86_64 images (ARM64 is not supported by the bref-extra project).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 2: docker create + cp&lt;/strong&gt;. It falls back to the Docker daemon if crane fails. Same limitation: x86 only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 3: Compile from source&lt;/strong&gt;. When neither method works (ARM deployments), the buildpack compiles extensions from source using the Bref build image's toolchain. This is the same approach used in multi-stage Dockerfiles but automated via build recipes.&lt;/p&gt;

&lt;p&gt;The recipes file (&lt;code&gt;lib/extension-recipes.sh&lt;/code&gt;) defines how to build each extension: what system packages to install, what configure flags to pass, where to download external source. Currently supports gd, redis, imagick, amqp, soap, ftp, gmp, pgsql, uuid, yaml, mongodb, calendar, and exif.&lt;/p&gt;

&lt;h2&gt;
  
  
  How bref-extra extensions are built
&lt;/h2&gt;

&lt;p&gt;Looking at the bref-extra repo, each extension uses a two-stage Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;bref/build-php-$PHP_VERSION:$BREF_VERSION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ext&lt;/span&gt;
&lt;span class="c"&gt;# compile the extension...&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;php-config &lt;span class="nt"&gt;--extension-dir&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/redis.so"&lt;/span&gt; /tmp/redis.so
&lt;span class="k"&gt;RUN &lt;/span&gt;php /bref/lib-copy/copy-dependencies.php /tmp/redis.so /tmp/extension-libs

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ext /tmp/redis.so /opt/bref/extensions/redis.so&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ext /tmp/ext.ini /opt/bref/etc/php/conf.d/ext-redis.ini&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ext /tmp/extension-libs /opt/lib&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final published image is literally just &lt;code&gt;/opt&lt;/code&gt; with the compiled artifacts. That's what crane extracts during our build. And for ARM, our recipes replicate that first stage at build time.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>php</category>
      <category>lambda</category>
      <category>buildpack</category>
    </item>
    <item>
      <title>Cloud Native Buildpack for Bref: build your app for Lambda in one command</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Tue, 28 Jul 2026 13:29:23 +0000</pubDate>
      <link>https://dev.to/aws-builders/cloud-native-buildpack-for-bref-build-your-app-for-lambda-in-one-command-24gh</link>
      <guid>https://dev.to/aws-builders/cloud-native-buildpack-for-bref-build-your-app-for-lambda-in-one-command-24gh</guid>
      <description>&lt;p&gt;I've been deploying PHP applications to AWS Lambda with Bref for a while now. Since I'm a Terraform fan, I very quickly parted with &lt;a class="mentioned-user" href="https://dev.to/mnapoli"&gt;@mnapoli&lt;/a&gt;'s serverless.yml approach, in favour a Terraform + container-based Lambdas. &lt;/p&gt;

&lt;p&gt;Here, the Dockerfile approach works, but it's verbose: a typical multi-stage Dockerfile for a Symfony app on Lambda is 100+ lines of repetitive infrastructure code.&lt;/p&gt;

&lt;p&gt;Recently, Cloud Native Buildpacks (CNB) caught my attention. Their promise is to turn source code into container images without a Dockerfile, with built-in layer caching and a standardized build/run separation. The question was: could this work for Lambda?&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;Lambda container images have specific requirements that don't align perfectly with the CNB model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda expects the app at &lt;code&gt;/var/task&lt;/code&gt; (a reserved, non-overridable path)&lt;/li&gt;
&lt;li&gt;Lambda needs a specific entrypoint (&lt;code&gt;/lambda-entrypoint.sh&lt;/code&gt; for Bref)&lt;/li&gt;
&lt;li&gt;The CNB lifecycle adds its own launcher binary that Lambda doesn't understand&lt;/li&gt;
&lt;li&gt;Bref base images use Amazon Linux with stripped-down package managers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are showstoppers, but they required creative solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What my buildpack does
&lt;/h2&gt;

&lt;p&gt;Given a PHP project with &lt;code&gt;bref/bref&lt;/code&gt; in its composer.json, the buildpack handles everything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects the app (composer.json + bref dependency)&lt;/li&gt;
&lt;li&gt;Installs PHP extensions (via crane download or from-source compilation)&lt;/li&gt;
&lt;li&gt;Runs composer install with production optimizations&lt;/li&gt;
&lt;li&gt;Configures OPcache for Lambda cold starts&lt;/li&gt;
&lt;li&gt;Detects Symfony and warms the cache&lt;/li&gt;
&lt;li&gt;Produces a Lambda-ready container image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire configuration is driven by environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make lambda &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;TEST_APP_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./my-symfony-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;LAMBDA_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BP_BREF_RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fpm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BP_BREF_EXTENSIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis,gd,soap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;BP_OPCACHE_JIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to 120 lines of multi-stage Dockerfile with extension compilation, vendor optimization, cache warmup, and OPcache tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it for real
&lt;/h2&gt;

&lt;p&gt;I deployed the Symfony Demo application to Lambda using the buildpack. Same app, same database, same environment variables as the existing layer-based deployment. Only change: it's just built with &lt;code&gt;pack build&lt;/code&gt; instead of &lt;code&gt;docker build&lt;/code&gt; and a long multi-stage Docker file.&lt;/p&gt;

&lt;p&gt;It works. The function URL serves the app correctly, Symfony cache is pre-warmed, and the second build reuses all cached layers (vendor, opcache config) making rebuilds near-instant when only application code changes.&lt;/p&gt;

&lt;p&gt;I also tested extension compilation: &lt;code&gt;ftp&lt;/code&gt; and &lt;code&gt;soap&lt;/code&gt; compile successfully from PHP source on ARM64 during the build step, with proper shared library dependency resolution via Bref's &lt;code&gt;copy-dependencies.php&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not handled by the buildpack
&lt;/h2&gt;

&lt;p&gt;The buildpack builds your app into a Lambda container image. It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up your Symfony app for Bref (you still need BrefKernel, trusted_proxies, etc.)&lt;/li&gt;
&lt;li&gt;Serve static assets (CSS/JS need CloudFront + S3, same as any Bref deployment)&lt;/li&gt;
&lt;li&gt;Replace your IaC (you still need Terraform/CDK/Serverless to create the Lambda function)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It replaces the Dockerfile and the build process, not the infrastructure or app configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;The buildpack is published and usable today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/psantus/bref-buildpack" rel="noopener noreferrer"&gt;https://github.com/psantus/bref-buildpack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Hub: &lt;code&gt;psantus/bref-buildpack:0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CNB Registry: &lt;code&gt;bref/php-lambda&lt;/code&gt; (registered at registry.buildpacks.io)&lt;/li&gt;
&lt;li&gt;CI: passing on GitHub Actions&lt;/li&gt;
&lt;li&gt;Tested: Symfony Demo running on Lambda ARM64 (Graviton) via function URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It supports PHP 8.2/8.3/8.4, ARM and x86 architectures, extension compilation from source, all 40+ bref-extra extensions (x86) or compile-from-source (ARM), Symfony auto-detection, composer caching, OPcache/JIT configuration, and vendor shrinking.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;buildpacks/tap/pack
git clone https://github.com/psantus/bref-buildpack
&lt;span class="nb"&gt;cd &lt;/span&gt;bref-buildpack
make lambda &lt;span class="nv"&gt;TEST_APP_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/your/php/app &lt;span class="nv"&gt;LAMBDA_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-lambda &lt;span class="nv"&gt;BP_BREF_EXTENSIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gd,redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting image is ready to push to ECR and deploy to Lambda.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>lambda</category>
      <category>buildpack</category>
      <category>php</category>
    </item>
    <item>
      <title>Serverless Lazy Generation: generate once, cache forever with CloudFront origin groups (+ S3 and Lambda)</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Sat, 25 Jul 2026 14:14:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-lazy-generation-generate-once-cache-forever-with-cloudfront-origin-groups-s3-and-4098</link>
      <guid>https://dev.to/aws-builders/serverless-lazy-generation-generate-once-cache-forever-with-cloudfront-origin-groups-s3-and-4098</guid>
      <description>&lt;p&gt;Sometimes, your business app may have to be ready to serve millions of assets  generated from your data (invoice PDFs, text-to-speech audio files, etc.), only a small fraction of which will actually be requested by users.&lt;/p&gt;

&lt;p&gt;When such assets generation is expensive, the naive approach - pre-generate every asset - leads to a lot of &lt;strong&gt;wasted compute&lt;/strong&gt;: you generate assets that may never be requested.&lt;/p&gt;

&lt;p&gt;Pre-generation may also cause &lt;strong&gt;bottlenecks&lt;/strong&gt; (adding 1,000 new articles means synthesizing 1,000 audio files before they're available), &lt;strong&gt;storage bloat&lt;/strong&gt; (you're paying to store assets that may never see a single request), and &lt;strong&gt;staleness&lt;/strong&gt; (when content changes, you need a pipeline to detect and regenerate affected assets).&lt;/p&gt;

&lt;p&gt;What if you could flip this model? Generate assets &lt;strong&gt;only when requested&lt;/strong&gt;, cache them permanently, and never generate the same asset twice.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk through a complete serverless architecture that uses CloudFront's native failover mechanism to implement lazy audio generation with Amazon Polly. The same pattern applies to image thumbnails, PDF rendering, video transcoding, or any expensive asset generation.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Architecture Overview: The CloudFront Origin Group Pattern
&lt;/h2&gt;

&lt;p&gt;The key insight is that CloudFront Origin Groups support &lt;strong&gt;automatic failover&lt;/strong&gt;. You configure a primary origin (S3) and a failover origin (API Gateway + Lambda). When the primary returns a 403 or 404, CloudFront transparently routes the request to the failover origin: no client-side retry, no custom logic.&lt;/p&gt;

&lt;p&gt;This gives us a clean separation:&lt;/p&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;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;S3&lt;/strong&gt; (primary origin)&lt;/td&gt;
&lt;td&gt;Serves cached audio files. Acts as a permanent cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;API Gateway + Lambda&lt;/strong&gt; (failover origin)&lt;/td&gt;
&lt;td&gt;Generates audio on-demand when S3 returns 404.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon Polly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Converts text to speech (the expensive operation).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudFront&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Edge caching + origin failover orchestration.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The beauty is that once Lambda generates an audio file and stores it in S3, all subsequent requests are served directly from S3 (or CloudFront's edge cache). Lambda never executes again for that asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;(full-disclosure: from this point onwards, the post is AI-generated)&lt;/p&gt;

&lt;h3&gt;
  
  
  First Request (Cache Miss)
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; requests &lt;code&gt;https://d1234.cloudfront.net/audio/welcome.mp3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; checks its edge cache — miss. Routes to the Origin Group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; (primary origin) returns &lt;strong&gt;404&lt;/strong&gt; — the file doesn't exist yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; detects the 404 and fails over to the secondary origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; receives the request and invokes &lt;strong&gt;Lambda&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; reads the source text from &lt;code&gt;s3://bucket/texts/welcome.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; calls &lt;strong&gt;Amazon Polly&lt;/strong&gt; to synthesize speech from the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; stores the generated MP3 at &lt;code&gt;s3://bucket/audio/welcome.mp3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; returns the MP3 binary (base64-encoded) with &lt;code&gt;Cache-Control: public, max-age=31536000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; caches the response at the edge and returns it to the client.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Second Request (Cache Hit)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; requests &lt;code&gt;https://d1234.cloudfront.net/audio/welcome.mp3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; checks its edge cache — &lt;strong&gt;hit&lt;/strong&gt;. Returns immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the edge cache has expired:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; routes to the Origin Group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; (primary origin) returns &lt;strong&gt;200&lt;/strong&gt; — the file exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; caches and returns the response. Lambda is never invoked.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Components
&lt;/h2&gt;

&lt;p&gt;Below I detail the components of the solution I share in &lt;a href="https://github.com/psantus/serverless-lazy-generation" rel="noopener noreferrer"&gt;my sample GitHub repo&lt;/a&gt;. My use case is for Text-to-Speech generation with Polly, but you can adapt it to your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudFront Origin Group
&lt;/h3&gt;

&lt;p&gt;The Origin Group is the orchestration layer. It defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;primary origin&lt;/strong&gt; (S3 with OAC) for serving cached content&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;failover origin&lt;/strong&gt; (API Gateway) for on-demand generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover criteria&lt;/strong&gt;: HTTP 403 and 404 status codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is infrastructure-level routing — no application code needed to handle caching logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 as a Permanent Cache
&lt;/h3&gt;

&lt;p&gt;S3 serves dual purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source storage&lt;/strong&gt;: Text files live at &lt;code&gt;texts/{filename}.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache storage&lt;/strong&gt;: Generated audio lives at &lt;code&gt;audio/{filename}.mp3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The S3 bucket is not publicly accessible. CloudFront accesses it via Origin Access Control (OAC), and Lambda accesses it via IAM role permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda as the Generator
&lt;/h3&gt;

&lt;p&gt;The Lambda function is lean — its only job is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the source text from S3&lt;/li&gt;
&lt;li&gt;Call Polly to synthesize audio&lt;/li&gt;
&lt;li&gt;Store the result back in S3&lt;/li&gt;
&lt;li&gt;Return the audio to the caller&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It includes a belt-and-suspenders check: before calling Polly, it verifies the audio doesn't already exist in S3 (handling race conditions where multiple simultaneous first-requests hit Lambda).&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon Polly for TTS
&lt;/h3&gt;

&lt;p&gt;Polly provides neural and standard voices for text-to-speech synthesis. In this architecture, we use the standard engine with the Joanna voice and MP3 output format. Polly charges per character synthesized — another reason lazy generation saves money.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Zero cold starts for cached content.&lt;/strong&gt; Once generated, audio is served from S3 or CloudFront's edge — sub-10ms latency worldwide. Lambda cold starts only affect the very first request for each asset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infinite scalability.&lt;/strong&gt; S3 and CloudFront scale to any traffic level without configuration. The generation layer (Lambda) only handles the small fraction of requests that are cache misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay only for generation once.&lt;/strong&gt; Each audio file is synthesized exactly once. After that, you're paying only S3 storage ($0.023/GB/month) and CloudFront data transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudFront edge caching.&lt;/strong&gt; Popular content is served from 400+ edge locations worldwide. Your users get sub-50ms response times regardless of where the origin bucket lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero maintenance.&lt;/strong&gt; No cron jobs, no batch pipelines, no cache invalidation logic. The system is self-healing — if you delete a cached file, it regenerates on next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Analysis
&lt;/h2&gt;

&lt;p&gt;Let's compare pre-generation vs. lazy generation for a platform with 10,000 text articles:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Generation Approach
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Polly: 10,000 articles × 2,000 chars avg&lt;/td&gt;
&lt;td&gt;$16.00 (standard voice)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 storage: 10,000 × 500KB avg&lt;/td&gt;
&lt;td&gt;$0.12/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda compute: 10,000 invocations × 5s avg&lt;/td&gt;
&lt;td&gt;$0.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total upfront&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$16.95&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You pay this every time content changes and you re-run the batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy Generation Approach (assuming 20% of content is ever requested)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Polly: 2,000 articles × 2,000 chars avg&lt;/td&gt;
&lt;td&gt;$3.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 storage: 2,000 × 500KB avg&lt;/td&gt;
&lt;td&gt;$0.02/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda compute: 2,000 invocations × 5s avg&lt;/td&gt;
&lt;td&gt;$0.17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudFront requests (all 10K articles, cached)&lt;/td&gt;
&lt;td&gt;$0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.40&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;80% cost reduction&lt;/strong&gt; — and you never pay for content nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lambda Handler Highlights
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;polly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;polly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&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;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&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;path&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/audio/(.+)\.mp3$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&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;match&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;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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;Invalid path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&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="c1"&gt;# Belt-and-suspenders: check if already generated (race condition guard)
&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.mp3&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;existing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoSuchKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Read source text
&lt;/span&gt;        &lt;span class="n"&gt;text_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;texts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&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="c1"&gt;# Synthesize with Polly
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;polly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synthesize_speech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OutputFormat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;VoiceId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Joanna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard&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;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AudioStream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Cache to S3 (non-fatal if this fails)
&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&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="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio/mpeg&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# Audio still returned even if caching fails
&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;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&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;Content-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;audio/mpeg&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;Cache-Control&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;public, max-age=31536000&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;body&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;audio_bytes&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;isBase64Encoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Terraform Highlights: Origin Group Configuration
&lt;/h3&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_cloudfront_distribution"&lt;/span&gt; &lt;span class="s2"&gt;"cdn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# Primary origin: S3 with OAC&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_regional_domain_name&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-assets"&lt;/span&gt;
    &lt;span class="nx"&gt;origin_access_control_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_origin_access_control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oac&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="c1"&gt;# Failover origin: API Gateway&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_api_gateway_rest_api.api.id}.execute-api.us-east-1.amazonaws.com"&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-fallback"&lt;/span&gt;
    &lt;span class="nx"&gt;origin_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/live"&lt;/span&gt;
    &lt;span class="nx"&gt;custom_origin_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;http_port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
      &lt;span class="nx"&gt;https_port&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
      &lt;span class="nx"&gt;origin_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https-only"&lt;/span&gt;
      &lt;span class="nx"&gt;origin_ssl_protocols&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TLSv1.2"&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;# Origin Group: failover on 403/404&lt;/span&gt;
  &lt;span class="nx"&gt;origin_group&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-with-fallback"&lt;/span&gt;
    &lt;span class="nx"&gt;failover_criteria&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;status_codes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;origin_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-assets"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;origin_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-fallback"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Route /audio/* through the origin group&lt;/span&gt;
  &lt;span class="nx"&gt;ordered_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path_pattern&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/audio/*"&lt;/span&gt;
    &lt;span class="nx"&gt;target_origin_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-with-fallback"&lt;/span&gt;
    &lt;span class="c1"&gt;# Uses CachingOptimized managed policy&lt;/span&gt;
    &lt;span class="nx"&gt;cache_policy_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"658327ea-f89d-4fab-a63d-7e88639e58f6"&lt;/span&gt;
    &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cached_methods&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&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 key is the &lt;code&gt;origin_group&lt;/code&gt; block — CloudFront handles all failover logic natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;serverless-lazy-generation

&lt;span class="c"&gt;# Add your text files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Welcome to our platform."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sample-texts/welcome.txt
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This is a demo of lazy generation."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sample-texts/demo.txt

&lt;span class="c"&gt;# Deploy with Terraform&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform
terraform init
terraform plan
terraform apply

&lt;span class="c"&gt;# Note the outputs&lt;/span&gt;
&lt;span class="c"&gt;# cloudfront_url = "https://d1234abcdef.cloudfront.net"&lt;/span&gt;
&lt;span class="c"&gt;# sample_audio_url = "https://d1234abcdef.cloudfront.net/audio/welcome.mp3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get the CloudFront URL from Terraform output&lt;/span&gt;
&lt;span class="nv"&gt;CF_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; cloudfront_url&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# First request — triggers generation (slower, ~2-5 seconds)&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; welcome.mp3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CF_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/audio/welcome.mp3"&lt;/span&gt;

&lt;span class="c"&gt;# Second request — served from cache (fast, &amp;lt;100ms)&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; welcome2.mp3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CF_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/audio/welcome.mp3"&lt;/span&gt;

&lt;span class="c"&gt;# Verify the files are identical&lt;/span&gt;
md5 welcome.mp3 welcome2.mp3

&lt;span class="c"&gt;# Check response headers for cache status&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CF_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/audio/welcome.mp3"&lt;/span&gt;
&lt;span class="c"&gt;# Look for: X-Cache: Hit from cloudfront&lt;/span&gt;

&lt;span class="c"&gt;# Request a non-existent text (should return 404)&lt;/span&gt;
curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CF_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/audio/nonexistent.mp3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extensions: Other Use Cases
&lt;/h2&gt;

&lt;p&gt;The lazy generation pattern isn't limited to audio. Any expensive, deterministic transformation works:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Generator&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image thumbnails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Original images in S3&lt;/td&gt;
&lt;td&gt;Lambda + Sharp&lt;/td&gt;
&lt;td&gt;Resized images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF generation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTML templates + data&lt;/td&gt;
&lt;td&gt;Lambda + Puppeteer&lt;/td&gt;
&lt;td&gt;PDF documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Video transcoding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source video in S3&lt;/td&gt;
&lt;td&gt;Lambda + FFmpeg&lt;/td&gt;
&lt;td&gt;Transcoded video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Graph images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Article metadata&lt;/td&gt;
&lt;td&gt;Lambda + Canvas&lt;/td&gt;
&lt;td&gt;Social preview images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map tiles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vector data&lt;/td&gt;
&lt;td&gt;Lambda + Mapbox GL&lt;/td&gt;
&lt;td&gt;Raster tiles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each case, the pattern is identical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;S3 stores the output (acts as cache)&lt;/li&gt;
&lt;li&gt;CloudFront Origin Group fails over to Lambda on 404&lt;/li&gt;
&lt;li&gt;Lambda generates, caches to S3, and returns&lt;/li&gt;
&lt;li&gt;All subsequent requests bypass Lambda entirely&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The serverless lazy generation pattern gives you the best of both worlds: the simplicity of static file serving with the flexibility of on-demand generation. CloudFront Origin Groups handle all the routing logic at the infrastructure level — no caching libraries, no invalidation jobs, no state management.&lt;/p&gt;

&lt;p&gt;The architecture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing&lt;/strong&gt;: Delete a cached file, and it regenerates on next request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-efficient&lt;/strong&gt;: You only pay to generate content that's actually consumed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Globally fast&lt;/strong&gt;: CloudFront edge caching provides sub-50ms responses worldwide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-ops&lt;/strong&gt;: No servers to manage, no batch jobs to monitor, no cache to warm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're building a content platform, an accessibility layer, or a media processing pipeline, this pattern scales from zero to millions of assets with no code changes.&lt;/p&gt;

&lt;p&gt;The complete source code — Lambda function, Terraform infrastructure, and sample texts — is available in &lt;a href="https://github.com/psantus/serverless-lazy-generation" rel="noopener noreferrer"&gt;the repository&lt;/a&gt;. Deploy it in under 5 minutes and start generating audio on demand.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>Zipping 15Gb of S3 files in 6s. How the power of community made it possible.</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Thu, 25 Jun 2026 18:23:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/zipping-15gb-of-s3-files-in-11s-how-the-power-of-community-made-it-possible-5fgg</link>
      <guid>https://dev.to/aws-builders/zipping-15gb-of-s3-files-in-11s-how-the-power-of-community-made-it-possible-5fgg</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/aws-builders/s3-zipper-challenge-a-parallel-zip-assembly-that-beats-the-single-lambda-approach-37gf"&gt;first article&lt;/a&gt;, I showed how parallelizing zip assembly across multiple Lambdas can beat the single-Lambda bandwidth ceiling. I zipped 6.9GB in 35 seconds with just 5 workers.&lt;/p&gt;

&lt;p&gt;Since then, Jérémie published a &lt;a href="https://rustysl.com/fr/blog/beyond-s3-archive-streaming" rel="noopener noreferrer"&gt;follow-up article&lt;/a&gt; where a contributor (&lt;a href="https://github.com/FigmentEngine/demo-s3-archiving/tree/main/contenders/rust/figment-engine" rel="noopener noreferrer"&gt;Fitz&lt;/a&gt;) introduced a brilliant optimization: &lt;code&gt;UploadPartCopy&lt;/code&gt;. Instead of downloading (or even streaming) big files through Lambda just to upload them back into the zip, you can tell S3 to copy them server-side. This halves the bandwidth requirement and brought his single-Lambda solution down to &lt;strong&gt;106 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I took Fitz's &lt;code&gt;UploadPartCopy&lt;/code&gt; idea and combined it with my parallel approach. Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from Jérémie and Fitz
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;UploadPartCopy&lt;/code&gt; insight is elegant: since ZIP STORE mode has deterministic offsets, we know exactly where each file's data lands in the final archive. For big files (≥5MB), we can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write just the &lt;strong&gt;local file header&lt;/strong&gt; (50 bytes) in an &lt;code&gt;UploadPart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Have S3 copy the &lt;strong&gt;file data&lt;/strong&gt; directly via &lt;code&gt;UploadPartCopy&lt;/code&gt; — no download, no upload, instant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means workers barely use any memory or bandwidth for big files. &lt;/p&gt;

&lt;p&gt;Only issue is that S3 multipart upload API requires all segments (except the last one) to be bigger than 5MB. So the local file header needs to be appended to an another file (or group of files). &lt;/p&gt;

&lt;p&gt;My planner Lambda groups small files together until they reach 5MB, appends the LOC header of the next big file, then the worker fires an &lt;code&gt;UploadPartCopy&lt;/code&gt; for that big file's data. &lt;/p&gt;

&lt;p&gt;When we run out of small files, we stream the smallest remaining big file and pair it with (the LOC header then) a copy of the largest remaining one.&lt;/p&gt;

&lt;p&gt;For CRC32 (required in zip headers): files uploaded with modern AWS SDKs already have CRC32 stored as object metadata. A simple &lt;code&gt;HeadObject&lt;/code&gt; call retrieves it — no need to read the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step Functions: three limitations
&lt;/h2&gt;

&lt;p&gt;My original architecture used Step Functions to orchestrate workers. Here's what I hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inline Map caps at ~40 concurrent iterations
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-map.html#concepts-map-process-modes" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt; says the Inline Map state supports "up to 40 concurrent iterations." In practice I saw up to 55, but never more. With 1500 duos to process, Step Functions queued them in batches of 55. &lt;/p&gt;

&lt;p&gt;I switched to &lt;strong&gt;Distributed Map&lt;/strong&gt; which launches Express child workflow executions. All 1120 iterations started within 2 seconds. Problem solved? Not quite.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Distributed Map: fast to dispatch, slow to collect
&lt;/h3&gt;

&lt;p&gt;With Distributed Map, all workers started within 2 seconds. Every single one finished in under 1 second (mostly &lt;code&gt;UploadPartCopy&lt;/code&gt; calls). Total Lambda compute: ~500ms average.&lt;/p&gt;

&lt;p&gt;Yet the Map state took &lt;strong&gt;38 seconds&lt;/strong&gt; to complete.&lt;/p&gt;

&lt;p&gt;The bottleneck? Step Functions' internal machinery for collecting and aggregating results from 1120 Express child executions. I confirmed: all workers started at 10:06:52-53, all finished by 10:06:54, but the Map state didn't report success until 10:07:28. &lt;strong&gt;35 seconds of pure orchestration overhead&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The 256KB payload limit
&lt;/h3&gt;

&lt;p&gt;Step Functions states can pass at most 256KB between them. With 3000 files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The planner's assignment list exceeds 256KB → had to write to S3&lt;/li&gt;
&lt;li&gt;The aggregated worker results exceed 256KB → had to write CRC32s to S3, read them back in the finalizer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This added complexity and latency (the finalizer reading 1500 small S3 files — 29 seconds sequentially, until I parallelized it down to 1.5s).&lt;/p&gt;

&lt;p&gt;After all these fixes, the Step Functions version ran in &lt;strong&gt;41 seconds&lt;/strong&gt; for 3000 × 5MB files. Respectable — 2.5× faster than Jérémie's 106s — but I knew most of that time was Step Functions overhead, not actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final version: direct Lambda invocation
&lt;/h2&gt;

&lt;p&gt;I stripped out Step Functions entirely and wrote a single &lt;strong&gt;orchestrator Lambda&lt;/strong&gt; that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lists files, computes zip layout (the job of the "planner" Lambda in my StepFunction architecture), and initiates multipart upload (~0.5s)&lt;/li&gt;
&lt;li&gt;Invokes all worker Lambdas &lt;strong&gt;synchronously in parallel&lt;/strong&gt; using goroutines + the Lambda SDK (~0.5s to dispatch)&lt;/li&gt;
&lt;li&gt;Collects results (workers return inline, no S3 round-trip for parts)&lt;/li&gt;
&lt;li&gt;Reads CRC32 files from S3 in parallel, builds central directory, completes multipart upload (~1s)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orchestrator Lambda (15min timeout, 1024MB)
    │
    ├─── goroutine → Invoke Worker 1 (sync) → return {parts}
    ├─── goroutine → Invoke Worker 2 (sync) → return {parts}
    ├─── ...
    └─── goroutine → Invoke Worker N (sync) → return {parts}
    │
    └─── All done → Build CD → CompleteMultipartUpload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda SDK's synchronous &lt;code&gt;Invoke&lt;/code&gt; blocks until the worker returns. With 200 concurrent goroutines, all workers are dispatched instantly. No orchestration overhead, no state size limits for the parts (only CRC32s go to S3), no 35-second result aggregation.&lt;/p&gt;

&lt;p&gt;Now the theoretical time is: &lt;code&gt;orchestration time&lt;/code&gt; + &lt;code&gt;time to upload the smallest large file that stays orphan after we pair all large files with groups of small files or single large files&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: 3000 × 5MB benchmark
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jérémie Gen1 (Rust, streaming)&lt;/td&gt;
&lt;td&gt;212s&lt;/td&gt;
&lt;td&gt;Single Lambda, 512MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jérémie Gen2 (Rust, UploadPartCopy)&lt;/td&gt;
&lt;td&gt;106s&lt;/td&gt;
&lt;td&gt;Single Lambda, 640MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My Step Functions version&lt;/td&gt;
&lt;td&gt;41s&lt;/td&gt;
&lt;td&gt;Distributed Map, 1120 workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;My orchestrator Lambda&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct invoke, ~1500 workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;6 seconds&lt;/strong&gt; to zip 15GB into a single valid ZIP64 archive. That's a 18× improvement over the optimized single-Lambda approach, and 35× over the original.&lt;/p&gt;

&lt;p&gt;Worker stats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max memory: &lt;strong&gt;85 MB&lt;/strong&gt; (I initially allocated 3008MB — massively over-provisioned thanks to &lt;code&gt;UploadPartCopy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Average duration: &lt;strong&gt;516ms&lt;/strong&gt; per worker&lt;/li&gt;
&lt;li&gt;Max duration: &lt;strong&gt;1035ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned (round 2)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step Functions Parallel Map adds seconds, not milliseconds.&lt;/strong&gt; For latency-sensitive fan-out/fan-in, direct Lambda invocation is faster. Step Functions shines when you need retries, visual debugging, long-running workflows, or error handling, or lightning fast step transition speed. This outstanding performance lasts only until you need more than 40 parallel processes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UploadPartCopy is the killer optimization.&lt;/strong&gt; When most files are ≥5MB, workers barely do any work — they just tell S3 to copy data server-side. Memory stays under 100MB regardless of file sizes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The orchestrator pattern is underrated.&lt;/strong&gt; A single Lambda with goroutines can invoke hundreds of child Lambdas synchronously, collect results, and finalize — all within one execution context. No state machine, no payload limits between states, no aggregation overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over-parallelization can hurt.&lt;/strong&gt; 1500 separate assignments created more Step Functions overhead than the actual compute. Grouping into fewer, larger batches would have been better for the SFN approach.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Code: &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;The repo has both approaches: Step Functions (&lt;code&gt;cmd/planner&lt;/code&gt; + &lt;code&gt;cmd/worker&lt;/code&gt; + &lt;code&gt;cmd/finalizer&lt;/code&gt;) and the orchestrator Lambda (&lt;code&gt;cmd/orchestrator&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Jérémie's challenge repo: &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;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;The theoretical minimum is bounded by Lambda cold start time (~200ms) plus the slowest &lt;code&gt;UploadPart&lt;/code&gt; call (if we lack small files, we may need to upload a large file manually to append another file's LOC to it) plus orchestrator overhead (~500ms). &lt;/p&gt;

&lt;p&gt;Your move, Jérémie 😏&lt;/p&gt;

&lt;p&gt;Edit: with 73.2Gb (15,000 files), my solutions gives quite acceptable performance. Just 20s (probably due to my 1000 account default concurrency, would likely be faster on an unbounded account :D) &lt;/p&gt;

&lt;p&gt;Paul out. &lt;/p&gt;

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

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>s3</category>
      <category>zip</category>
    </item>
    <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>
  </channel>
</rss>
