<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Coffee Engineering</title>
        <link>https://foxteck.org/</link>
        <description>Chatting about Software Engineering and Coffee, mostly.</description>
        <lastBuildDate>Wed, 22 Jul 2026 08:15:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Coffee Engineering</title>
            <url>https://foxteck.org/icons/icon.png</url>
            <link>https://foxteck.org/</link>
        </image>
        <copyright>Copyright © 2026 Eduardo Romero</copyright>
        <atom:link href="https://foxteck.org/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Kata-fying the SDLC for agents with Ballador and Groundcrew.]]></title>
            <link>https://foxteck.org/posts/building-software/kattafying-the-sdlc.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/kattafying-the-sdlc.html</guid>
            <pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<div class="article-heading">
  <h1 class="header">
  Kata-fying the SDLC for my army of agents
    <div class="subheader">A deeper dive into my sandboxed, multi-model orchestra</div>
  </h1>
</div>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>A few weeks ago I wrote up my <a href="./spec-driven-development-with-ballador.html">spec-driven setup</a>: living specs with orchestrator I
call <em>Ballador</em>. My team suggested a follow-up post that talks about the tweaks and configs that might be looking to dive deeper.</p>
<p>Ballador cuts a big spec into mini-specs, and creates tasks for <a href="https://github.com/ClipboardHealth/groundcrew" target="_blank" rel="noreferrer"><code>groundcrew</code></a>'s
sandboxed agents that run in parallel worktrees until PRs show up in Github and tickets move in Linear.</p>
<p>If you do <code>npm install -g @clipboard-health/groundcrew@latest</code> and ask an agent to wire up a config, you already get
something that works pretty well. Out of the box <code>crew</code> gives you a parallel-agent runner and helps you set up a sandbox.</p>
<p>I've added a few things on top to match <em>my</em> needs: multi-harness, multi-model, usage analytics, credential-sensitive, and reviews
to a point where
this is safe enough to leave the agents alone building while I'm brewing coffee.</p>
<p>This post tries to answer two questions: what are <strong>my config tweaks</strong>, and <strong>what do you get that makes them worth it?</strong></p>
<h2 id="the-sdlc-kata" tabindex="-1">The SDLC Kata <a class="header-anchor" href="#the-sdlc-kata" aria-label="Permalink to “The SDLC Kata”">&#8203;</a></h2>
<p>Building with agents <em>should</em> feel like building with your team. What I'm doing here is <em>kata-fying</em> my own Software
Development Lifecycle (SDLC).</p>
<p>I <a href="./coffee-katas.html">wrote about katas before</a>, but let me refine the definition rather than repeat it:</p>
<blockquote>
<p>A <strong>kata</strong> is a rehearsed choreography, a fixed set of movements you repeat until they run without conscious thought. You
practice it slowly at first, until the sequence becomes second nature, and you can perform it under
pressure without analyzing or thinking about each step.</p>
</blockquote>
<p>At a task orchestration level, the Software Development Life-Cycle (SDLC) can become a kata with four movements:</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/sdlc-kata-agents.png" alt="SDLC as a Kata"/>
  <span>Software Development Life-Cycle as <em style="font-weight: bolder;">“a kata”</em></span>
</div>
<p>I refine the spec with Ballador and the System Design skill (Phase 1), then Ballador cuts it into parts and dispatches
them (Phase 2), and crew runs the build in parallel, with different agents doing different things (Phase 3).</p>
<p>Phase 3 has its own arrow straight to GitHub. For simple, one-shot tasks the agent opens its own PR and we're done. Anything with
more scope takes the longer path: a <em>different</em> set of agents reviews the work, the building agents address any findings, and
Ballador integrates and opens the PR itself (Phase 4). As the engineer in charge, you'll work in both modes, depending on the
scope of the project and how ambitious the spec is.</p>
<p>Software Engineering is <a href="https://www.youtube.com/watch?v=QFpVVm7AnKI" target="_blank" rel="noreferrer">an infinite game</a>. Building surfaces <em>new tasks</em>, which go
back into the orchestration phase; shipping surfaces <em>spec gaps and learnings</em>, which feed the way back into the spec.</p>
<p>Ballador <em>absorbs</em> some of the cognitive load of the SDLC for you: keeping specs up to date with code and architectural changes,
adding new tasks and closing tasks that are no longer needed as agents build, and verifying the agents' work before you (or the
rest of your team) look at it.</p>
<p>That resembles how I already work with engineers, and it works for agents too.</p>
<h2 id="the-build-setup" tabindex="-1">The Build Setup <a class="header-anchor" href="#the-build-setup" aria-label="Permalink to “The Build Setup”">&#8203;</a></h2>
<h3 id="hooks-in-groundcrew" tabindex="-1">Hooks in Groundcrew <a class="header-anchor" href="#hooks-in-groundcrew" aria-label="Permalink to “Hooks in Groundcrew”">&#8203;</a></h3>
<p>Like I mentioned before, I run agents inside <a href="https://github.com/eugene1g/agent-safehouse" target="_blank" rel="noreferrer">Safehouse</a> (Mac only). Safehouse is
deny-first: it <em>masks</em> the
host <code>$HOME</code> and blocks outbound traffic and file system access.</p>
<p>Groundcrew provides two hooks for the agent's lifecycle:</p>
<ul>
<li><em><code>preLaunch</code></em>: runs <em>before</em> the sandbox on the <em>host</em> machine (your laptop) with full filesystem and credential access.</li>
<li><em><code>prepareWorktree</code></em>: runs <em>inside</em> the sandbox, after the worktree is checked out, with the sandbox already locked.</li>
</ul>
<p><em>tl;dr:</em> Do the privileged setup on the host, then bootstrap the worktree.</p>
<p>Some examples of how I use these hooks:</p>
<p>| <code>preLaunch</code>                                 | <code>prepareWorktree</code>                                          |
|:</p>
]]></description>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
  Kata-fying the SDLC for my army of agents
    <div class="subheader">A deeper dive into my sandboxed, multi-model orchestra</div>
  </h1>
</div>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>A few weeks ago I wrote up my <a href="./spec-driven-development-with-ballador.html">spec-driven setup</a>: living specs with orchestrator I
call <em>Ballador</em>. My team suggested a follow-up post that talks about the tweaks and configs that might be looking to dive deeper.</p>
<p>Ballador cuts a big spec into mini-specs, and creates tasks for <a href="https://github.com/ClipboardHealth/groundcrew" target="_blank" rel="noreferrer"><code>groundcrew</code></a>'s
sandboxed agents that run in parallel worktrees until PRs show up in Github and tickets move in Linear.</p>
<p>If you do <code>npm install -g @clipboard-health/groundcrew@latest</code> and ask an agent to wire up a config, you already get
something that works pretty well. Out of the box <code>crew</code> gives you a parallel-agent runner and helps you set up a sandbox.</p>
<p>I've added a few things on top to match <em>my</em> needs: multi-harness, multi-model, usage analytics, credential-sensitive, and reviews
to a point where
this is safe enough to leave the agents alone building while I'm brewing coffee.</p>
<p>This post tries to answer two questions: what are <strong>my config tweaks</strong>, and <strong>what do you get that makes them worth it?</strong></p>
<h2 id="the-sdlc-kata" tabindex="-1">The SDLC Kata <a class="header-anchor" href="#the-sdlc-kata" aria-label="Permalink to “The SDLC Kata”">&#8203;</a></h2>
<p>Building with agents <em>should</em> feel like building with your team. What I'm doing here is <em>kata-fying</em> my own Software
Development Lifecycle (SDLC).</p>
<p>I <a href="./coffee-katas.html">wrote about katas before</a>, but let me refine the definition rather than repeat it:</p>
<blockquote>
<p>A <strong>kata</strong> is a rehearsed choreography, a fixed set of movements you repeat until they run without conscious thought. You
practice it slowly at first, until the sequence becomes second nature, and you can perform it under
pressure without analyzing or thinking about each step.</p>
</blockquote>
<p>At a task orchestration level, the Software Development Life-Cycle (SDLC) can become a kata with four movements:</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/sdlc-kata-agents.png" alt="SDLC as a Kata"/>
  <span>Software Development Life-Cycle as <em style="font-weight: bolder;">“a kata”</em></span>
</div>
<p>I refine the spec with Ballador and the System Design skill (Phase 1), then Ballador cuts it into parts and dispatches
them (Phase 2), and crew runs the build in parallel, with different agents doing different things (Phase 3).</p>
<p>Phase 3 has its own arrow straight to GitHub. For simple, one-shot tasks the agent opens its own PR and we're done. Anything with
more scope takes the longer path: a <em>different</em> set of agents reviews the work, the building agents address any findings, and
Ballador integrates and opens the PR itself (Phase 4). As the engineer in charge, you'll work in both modes, depending on the
scope of the project and how ambitious the spec is.</p>
<p>Software Engineering is <a href="https://www.youtube.com/watch?v=QFpVVm7AnKI" target="_blank" rel="noreferrer">an infinite game</a>. Building surfaces <em>new tasks</em>, which go
back into the orchestration phase; shipping surfaces <em>spec gaps and learnings</em>, which feed the way back into the spec.</p>
<p>Ballador <em>absorbs</em> some of the cognitive load of the SDLC for you: keeping specs up to date with code and architectural changes,
adding new tasks and closing tasks that are no longer needed as agents build, and verifying the agents' work before you (or the
rest of your team) look at it.</p>
<p>That resembles how I already work with engineers, and it works for agents too.</p>
<h2 id="the-build-setup" tabindex="-1">The Build Setup <a class="header-anchor" href="#the-build-setup" aria-label="Permalink to “The Build Setup”">&#8203;</a></h2>
<h3 id="hooks-in-groundcrew" tabindex="-1">Hooks in Groundcrew <a class="header-anchor" href="#hooks-in-groundcrew" aria-label="Permalink to “Hooks in Groundcrew”">&#8203;</a></h3>
<p>Like I mentioned before, I run agents inside <a href="https://github.com/eugene1g/agent-safehouse" target="_blank" rel="noreferrer">Safehouse</a> (Mac only). Safehouse is
deny-first: it <em>masks</em> the
host <code>$HOME</code> and blocks outbound traffic and file system access.</p>
<p>Groundcrew provides two hooks for the agent's lifecycle:</p>
<ul>
<li><em><code>preLaunch</code></em>: runs <em>before</em> the sandbox on the <em>host</em> machine (your laptop) with full filesystem and credential access.</li>
<li><em><code>prepareWorktree</code></em>: runs <em>inside</em> the sandbox, after the worktree is checked out, with the sandbox already locked.</li>
</ul>
<p><em>tl;dr:</em> Do the privileged setup on the host, then bootstrap the worktree.</p>
<p>Some examples of how I use these hooks:</p>
<table tabindex="0">
<thead>
<tr>
<th style="text-align:left"><code>preLaunch</code></th>
<th style="text-align:left"><code>prepareWorktree</code></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Add API keys to the environment</td>
<td style="text-align:left">Install dependencies (cache redirected into the worktree)</td>
</tr>
<tr>
<td style="text-align:left">Copy untracked <code>.agents/</code> into the worktree</td>
<td style="text-align:left"><code>pnpm install</code></td>
</tr>
<tr>
<td style="text-align:left">Patch <code>trustedWorkspaces</code> for Antigravity</td>
<td style="text-align:left"><code>uv sync --dev --frozen</code></td>
</tr>
<tr>
<td style="text-align:left">Serve short-lived AWS creds</td>
<td style="text-align:left">Redirect <code>CDK_HOME</code> → <code v-pre>{{worktree}}/.cdk</code></td>
</tr>
</tbody>
</table>
<p>Most of my changes use one of these two hooks. The Clipboard Health team provides two guides for this.
One <a href="https://github.com/ClipboardHealth/groundcrew/blob/main/docs/setup-hook-agent-prompt.md" target="_blank" rel="noreferrer">for agents</a> (mostly about setting up
the project for worktrees) and <a href="https://github.com/ClipboardHealth/groundcrew/blob/main/docs/setup-hooks.md" target="_blank" rel="noreferrer">this one</a> that's more
general.</p>
<h3 id="multi-agents" tabindex="-1">Multi-agents <a class="header-anchor" href="#multi-agents" aria-label="Permalink to “Multi-agents”">&#8203;</a></h3>
<p>I work with different coding agents for different &quot;classes&quot; of tasks. In personal projects I use Claude Code
and <a href="https://antigravity.google/" target="_blank" rel="noreferrer">Antigravity</a>.</p>
<p>We have an opinionated engineering team at Standard Metrics, and we encourage everyone to find what works best for them. Some
engineers prefer <a href="https://ampcode.com/" target="_blank" rel="noreferrer">Sourcegraph's Amp</a>, some use <a href="https://openai.com/codex/" target="_blank" rel="noreferrer">Codex</a> and some
favor <a href="https://cursor.com/" target="_blank" rel="noreferrer">Cursor</a>.</p>
<p><code>crew</code> runs Claude Code and Codex by default. I had to configure <a href="https://antigravity.google/" target="_blank" rel="noreferrer">Google Antigravity</a> (<code>agy</code>,
Gemini under the hood) and <a href="https://ampcode.com/" target="_blank" rel="noreferrer">Sourcegraph Amp</a> (<code>amp</code>).</p>
<p>Each one broke on its own peculiar way:</p>
<table tabindex="0">
<thead>
<tr>
<th style="text-align:left">Challenge</th>
<th style="text-align:left">Antigravity (<code>agy</code>)</th>
<th style="text-align:left">Amp (<code>amp</code>)</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left"><strong>Non-interactive Prompt</strong></td>
<td style="text-align:left">Wants <code>--prompt-interactive</code> placed <em>last</em>; <code>crew</code> appends the prompt to the command array</td>
<td style="text-align:left">Reads stdin. Needs a thin wrapper <code>exec amp &lt;&lt;&lt; &quot;$1&quot;</code></td>
</tr>
<tr>
<td style="text-align:left"><strong>Workspace trust</strong></td>
<td style="text-align:left">Prompts for trust on new directories, which blocks headless runs</td>
<td style="text-align:left">n/a</td>
</tr>
<tr>
<td style="text-align:left"><strong>Autonomy</strong></td>
<td style="text-align:left"><code>--dangerously-skip-permissions</code> to auto-approve tool calls</td>
<td style="text-align:left"><code>amp.dangerouslyAllowAll = true</code> in a granted config</td>
</tr>
<tr>
<td style="text-align:left"><strong>Config / State</strong></td>
<td style="text-align:left">Stores state under <code>~/.gemini</code>; the home-mask hides it, so I grant it back via <code>sandboxWritePaths</code></td>
<td style="text-align:left">Loads keys from the host keychain, forwarded as <code>AMP_API_KEY</code> / <code>AMP_SETTINGS_FILE</code></td>
</tr>
</tbody>
</table>
<p><strong>Non-interactive prompts:</strong> This is how you inject their part of the spec and any special instructions into the agent. Each CLI
does it in a different way. Amp needed a thin wrapper script that pipes the prompt to stdin.</p>
<p><strong>Workspace trust:</strong> Antigravity refuses to run headless in a directory it hasn't seen before. It wants a human to click &quot;trust
this folder&quot;, but every task gets a brand-new worktree, so every task is a new folder. The fix is a <code>preLaunch</code> hook that parses
<code>agy</code>'s settings on the host, prunes dead paths, and appends the fresh <code v-pre>{{worktree}}</code> to <code>trustedWorkspaces</code>.</p>
<p><strong>Autonomy:</strong> You need to pass specific flags depending on the harness to let agents run without babysitting
like <code>--dangerously-skip-permissions</code> vs <code>--dangerously-allow-all</code>.</p>
<p>There are a handful of configs needed for the agents to work correctly. For example, antigravity needs access to ~/.gemini or Amp uses the browser to complete its authentication flow so I use
two environment variables and the keychain to share the auth with instances of amp.</p>
<p>You can see the documentation for configuring agents <a href="https://github.com/ClipboardHealth/groundcrew/blob/main/docs/configuration.md#enabling-agent-presets" target="_blank" rel="noreferrer">here</a>.</p>
<h3 id="sharing-uncommitted-files" tabindex="-1">Sharing uncommitted files <a class="header-anchor" href="#sharing-uncommitted-files" aria-label="Permalink to “Sharing uncommitted files”">&#8203;</a></h3>
<p>Sometimes you want to share uncommitted files with the agents. In my case it was two review skills I was testing. A git worktree
only materializes <em>tracked</em> files, and my skills were untracked in <code>.agents/</code> because they're local, not yet on the repo.</p>
<p><code>preLaunch</code> runs on the <em>host</em> with access to the real checkout, so it can copy them across before the sandbox launches:</p>
<div class="language-ts"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// something like this in crew.config.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">    preLaunch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ({</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">worktree</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">repoRoot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> cp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">repoRoot</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/.agents`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">worktree</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/.agents`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {recursive: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre>
</div><p>The same trick works for anything else that lives outside version control: a <code>.env.local</code> you don't want to commit, a scratch
<code>scripts/</code> directory, personal MCP configs, etc.</p>
<h3 id="github-access" tabindex="-1">GitHub Access <a class="header-anchor" href="#github-access" aria-label="Permalink to “GitHub Access”">&#8203;</a></h3>
<p>Safehouse blocks direct SSH access, and I have the habit of using it for git access (<code>git@github.com:...</code> origins).</p>
<p>The first time Amp finished working and tried to push its branch, it couldn't. In
the <a href="./spec-driven-development-with-ballador.html">first post</a> I told the story of an agent (Claude Code) noticing it was locked in
the sandbox and thrashing through method after method until it got through.</p>
<p>Amp and Antigravity failed and didn't attempt further, so I had to give them tokens to use.</p>
<p>I set up a token and forward it with <code>preLaunchEnv: [&quot;GH_TOKEN&quot;]</code>. I followed <em>mise</em>'s approach for <a href="https://mise.jdx.dev/dev-tools/github-tokens.html#github-tokens" target="_blank" rel="noreferrer">handling
github tokens</a>. I went from the GitHub CLI auth to 8h tokens
with <a href="https://github.com/suzuki-shunsuke/ghtkn" target="_blank" rel="noreferrer">ghtkn</a>. After a while the analytics (more on that below) showed me that the build tasks
never take more than 1h. I've been testing a script that invalidates the token via the GH API, but it means a new browser window every
hour, which is annoying.</p>
<p>If you don't feel like wiring that up, just make sure the project's origins use https and use gh CLI auth or have the orchestrator
agent handle commits in Phase 4.</p>
<h3 id="aws-access" tabindex="-1">AWS Access <a class="header-anchor" href="#aws-access" aria-label="Permalink to “AWS Access”">&#8203;</a></h3>
<p>I've watched agents probe AWS profiles in <code>~/.aws/</code> and the environment to escalate access or generate bearer tokens for CLIs that
were authenticated when they tried to solve a task unattended. That was the original <em>forcing function</em> for using sandboxes.</p>
<p>Most tasks don't touch AWS. But sometimes I do want them to deploy changes to test and validate features.</p>
<p>In my setup no AWS credentials live in the sandbox. Ever. There's a specific agent profile, <code>claude-aws</code>, that Ballador assigns
only to tasks that need it. For those, <code>preLaunch</code> adds an environment variable <code>AWS_EC2_METADATA_SERVICE_ENDPOINT</code> that points at
a little <a href="https://gist.github.com/eduardoromero/ce5f9d5ad7ab3897971972965c72beaf" target="_blank" rel="noreferrer">server</a> running on the host. It pretends to be
the EC2 instance-metadata service with short-lived STS credentials to the environment:</p>
<div class="language-ts"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// this would be part of crew.config.ts</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // ...</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    agents: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        claude_aws: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            cmd: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"claude --permission-mode auto"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            color: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"#FF9900"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            preLaunch: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "export AWS_EC2_METADATA_SERVICE_ENDPOINT=http://127.0.0.1:1338/"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "export AWS_REGION=us-east-1"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                'export CDK_HOME="{{worktree}}/.cdk"'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                'mkdir -p "$CDK_HOME"'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            ].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">" &#x26;&#x26; "</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            preLaunchEnv: [</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "AWS_EC2_METADATA_SERVICE_ENDPOINT"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "AWS_REGION"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">                "CDK_HOME"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>The script uses Amazon's <a href="https://github.com/aws/amazon-ec2-metadata-mock" target="_blank" rel="noreferrer">EC2 Metadata mock server</a>, and forces it to expose
an <code>agent-tokens</code> profile that doesn't exist anywhere else. The agent's AWS SDK thinks it's on an EC2 box with an instance role.
No way to escalate. It can't read ~/.aws/ and it doesn't know any of the real profiles.</p>
<h3 id="oops-the-agent-can-t-tell-me-it-s-done" tabindex="-1">Oops! The agent can't tell me it's done <a class="header-anchor" href="#oops-the-agent-can-t-tell-me-it-s-done" aria-label="Permalink to “Oops! The agent can't tell me it's done”">&#8203;</a></h3>
<p>This one made me laugh a little. One of the agents finishes, runs <code>crew task done &lt;id&gt;</code> to report back, and it fails with
<code>Operation not permitted</code>.</p>
<p>The sandbox blocks access to any path that isn't in the config. Since I'm using DuckDB as my task database, the agents
needed a way to write to that folder.</p>
<p>The prompt overrides the default completion instructions and tells the agent to call a sandbox-reachable script instead of
<code>crew task done</code>:</p>
<div class="language-bash"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sh</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ~/dev/groundcrew/db/complete.sh</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> {{task}}</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> \</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  --outcome</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> success</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --model</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x3C;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">model-i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">d</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [--pr </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">ur</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">l</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">]</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [--notes </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"&#x3C;summary>"]</span></span></code></pre>
</div><p><code>complete.sh</code> lives in the granted <code>db/</code> directory and writes the outcome straight into <code>tasks.duckdb</code> with the <code>duckdb</code> CLI.</p>
<h2 id="duckdb-as-task-backend" tabindex="-1">DuckDB as task backend <a class="header-anchor" href="#duckdb-as-task-backend" aria-label="Permalink to “DuckDB as task backend”">&#8203;</a></h2>
<p>That's a good <em>segue</em> to talk about <s>my obsession with DuckDB</s> the tasks &quot;backend&quot; for this: <a href="https://duckdb.org/" target="_blank" rel="noreferrer">DuckDB ⚫◗ </a>.</p>
<p>I'm using a local, queryable <strong>DuckDB database</strong> at <code>~/dev/groundcrew/db/tasks.duckdb</code>. The database is a file in the shared
workspace. Ballador uses it for orchestration, the agents use it for tracking work and status, and I use it for analytics.</p>
<p><code>crew</code> comes with built-in support for &quot;<a href="https://github.com/ClipboardHealth/groundcrew/blob/main/docs/task-sources.md#task-sources" target="_blank" rel="noreferrer">task sources</a>&quot;,
which is the mechanism you use to integrate with external systems for tasks. The default adapters are text files and Linear, and they
have an example for integrating with Jira too.</p>
<p>I build my own adapter for DuckDB. These are the access patterns / use-cases:</p>
<table tabindex="0">
<thead>
<tr>
<th style="text-align:left">Category</th>
<th style="text-align:left">Written by</th>
<th style="text-align:left">Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Orchestration + Project tracking</td>
<td style="text-align:left">Ballador</td>
<td style="text-align:left"><code>project</code>, <code>parent_spec</code>, <code>task_number</code>, <code>tags</code></td>
</tr>
<tr>
<td style="text-align:left">Dependencies</td>
<td style="text-align:left">Ballador</td>
<td style="text-align:left"><code>blocked_by</code> (list of blocker IDs)</td>
</tr>
<tr>
<td style="text-align:left">Analytics</td>
<td style="text-align:left">Scripts (automatic)</td>
<td style="text-align:left"><code>created_at</code>, <code>started_at</code>, <code>completed_at</code></td>
</tr>
<tr>
<td style="text-align:left">Task outcome</td>
<td style="text-align:left">The agent, on completion</td>
<td style="text-align:left">success status, <code>model</code>, PR, and notes</td>
</tr>
</tbody>
</table>
<p>Here's the full <a href="https://gist.github.com/eduardoromero/1121e64d7a3ea01b78b31b6d343dea01" target="_blank" rel="noreferrer">db schema</a>.</p>
<p>Crew reads the database to launch tasks, and it skips a task until blockers are <code>done</code>. The adapter uses scripts that are just
db operations via the DuckDB CLI.</p>
<div class="language-ts"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    sources: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            kind: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"shell"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"duckdb"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            sandboxWritePaths: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/db"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/specs"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/agent-config"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            commands: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                verify: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/verify.sh"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                listTasks: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/list_tasks.sh"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                getTask: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/get_task.sh ${id}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                markInProgress: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/mark_in_progress.sh"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                markDone: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/mark_done.sh"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">                createTask: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"~/dev/groundcrew/create_task.sh ${title} ${agent} ${repository} ${description}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ]</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><h2 id="cheap-analytics" tabindex="-1">Cheap Analytics <a class="header-anchor" href="#cheap-analytics" aria-label="Permalink to “Cheap Analytics”">&#8203;</a></h2>
<p>We get analytics for free on that same table. <code>mark_in_progress.sh</code> and <code>mark_done.sh</code> update timestamps for
<code>started_at</code> and <code>completed_at</code> as the agents chug tasks, and bump <code>attempts</code> on every dispatch. The agents report <code>model</code>,
<code>outcome</code>, and <code>last_error</code> through <code>complete.sh</code> when they're done. Ballador fills in <code>project</code>, <code>parent_spec</code>, and
<code>size_estimate</code> at creation,
which is what makes a whole spec queryable as one unit:</p>
<div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">SELECT</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> agent,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">       count</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)                                                        </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">AS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> tasks,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">       round</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">avg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(epoch(completed_at </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> started_at)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 60</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)            </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">AS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> avg_minutes,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">       round</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> count</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">FILTER</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">WHERE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> outcome </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'success'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> count</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">AS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> success_rate,</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">       sum</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(attempts) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> count</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">*</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)                                        </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">AS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> retries</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">FROM</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> tasks</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">WHERE</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> completed_at </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">IS NOT NULL</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">GROUP BY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> agent</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ORDER BY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> tasks </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">DESC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre>
</div><p>That's the query that told me build tasks never run past an hour. I capture timing, retries, and attribution, but no tokens and no
cost. I'm using <a href="https://codexbar.app/" target="_blank" rel="noreferrer">CodexBar CLI</a> to track usage and cost as suggested by groundcrew.</p>
<p>Swap <code>agent</code> for <code>model</code> in that <code>GROUP BY</code> and you get the per-model view, which is how I decide who gets what work. Antigravity
and Amp finish tasks faster. Claude costs more for comparable work once I line these task counts up against CodexBar. The success
rate barely separates them, it's high for all three, so the routing decision comes down to speed and cost rather than quality.</p>
<h2 id="code-reviewers" tabindex="-1">Code Reviewers <a class="header-anchor" href="#code-reviewers" aria-label="Permalink to “Code Reviewers”">&#8203;</a></h2>
<div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p>
<p></p>
<p>Code Reviewers didn't exist when I wrote the <a href="./spec-driven-development-with-ballador.html">first post</a>. It became a need out of working
with Ballador on a big project, where PR review velocity and the load it put on the team was very noticeable.</p>
<p>Seems like the whole industry feels the same way.</p>
</div>
<p>For any task <code>&lt;id&gt;</code>, Ballador creates a paired <code>&lt;id&gt;-review</code> task and assigns it to a <strong>different model</strong>. Gemini writes the
feature; Sonnet reviews it.</p>
<p>The review is <code>blocked_by = &lt;id&gt;</code> under auto-cascade, so it fires the <em>instant</em> the worker finishes.
The reviewer checks out the shared worktree, diffs <code>main...&lt;username&gt;-&lt;id&gt;</code> (worktrees share
refs, so it's cheap), runs the build, tests, and review skills, writes findings to <code>specs/reviews/&lt;id&gt;.md</code>, and adds its verdict
(<code>approve</code> or <code>request-changes</code>) into a second table, <code>messages</code>, which works as our mailbox.</p>
<p>Then the long-lived Ballador session, who calls itself the <strong>Oracle</strong> when it's reviewing, watches the <code>messages</code> table. When a
verdict comes back as <code>request-changes</code>, the Oracle runs <a href="https://gist.github.com/eduardoromero/be3d0d0a4c6401c8cb37755bb0e4adb4" target="_blank" rel="noreferrer">a script</a>
<code>oracle_send.sh</code>, which <em><a href="https://cmux.com/docs/api#input-commands" target="_blank" rel="noreferrer">types</a> the instruction into the worker's
live <a href="https://cmux.com/docs/api" target="_blank" rel="noreferrer">cmux</a> terminal</em>. It lands in the original <em>coding agent's</em> session, conversation history intact.</p>
<Mermaid graph="flowchart%20TD%0A%20%20%20%20Worker%5B%22Worker%20task%20(Gemini)%22%5D%0A%20%20%20%20Review%5B%22Review%20task%20(Claude)%22%5D%0A%20%20%20%20Msgs%5B%22DuckDB%5Cnmessages%20table%22%5D%0A%20%20%20%20Oracle%5B%22Ballador's%20%E2%80%9COracle%E2%80%9D%20session%22%5D%0A%20%20%20%20Findings%5B%22specs%2Freviews%2F%26lt%3Bid%26gt%3B.md%22%5D%0A%20%20%20%20Worker%20--%3E%7Cblocked_by%7C%20Review%0A%20%20%20%20Review%20--%3E%7Cdiff%20%2B%20build%2Ftest%7C%20Findings%0A%20%20%20%20Review%20--%3E%7Cverdict%3A%20approve%20%2F%20request%20-%20changes%7C%20Msgs%0A%20%20%20%20Msgs%20--%3E%7Cwatches%7C%20Oracle%0A%20%20%20%20Oracle%20--%3E%7Coracle_send%20.%20sh%20%5Cntypes%20into%20live%20cmux%7C%20Worker%0A" /><p>The Oracle doesn't need to relaunch a fresh agent to fix issues, it <em>steers</em> the existing session.</p>
<p>That resembles the way an engineer gives you feedback across the desk. The authoring agent holds the full context of what it built and why, so feeding the
review back into that same session is much cheaper than reconstructing it.</p>
<p>Our review skills already incorporate our most common nits and blockers. The skill is based on an analysis of the last six months'
worth of code reviews from each repo, and it includes our style guides along with our nits and blocking comments. By the time I
open the PR, most of the <em>standard</em> issues are already addressed.</p>
<p>The reason a different model reviews the code is that it performs better. Gemini and Claude don't share the same blind spots,
so an issue Gemini let through might catch Claude's attention, and the other way around. That's another big reason
this multi-agent setup is worth it.</p>
<h2 id="recap-of-the-wins-and-why-s" tabindex="-1">Recap of the wins and why's <a class="header-anchor" href="#recap-of-the-wins-and-why-s" aria-label="Permalink to “Recap of the wins and why's”">&#8203;</a></h2>
<ul>
<li><strong>Multi-model / Multi-harness.</strong> Different models write code and review it. Gemini and Amp work on fast tasks, Claude on deep
tasks and AWS. They review each other's code.</li>
<li><strong>Hard guard rails.</strong> Only Ballador runs outside the sandbox, all unattended agents have a minimal blast radius.</li>
<li><strong>All code gets a local review first.</strong> Cross-model review plus the Oracle loop asks for fixes before any human sees the PR.</li>
<li><strong>It's config-as-code.</strong> It seems like a lot, but most of it lives in <code>crew.config.ts</code> and a handful of tiny shell scripts.
Groundcrew is <em>very easy</em> to customize. The Clipboard team did a great job with it.</li>
</ul>
<h2 id="is-it-worth-it-what-s-next" tabindex="-1">Is it worth it? What's next? <a class="header-anchor" href="#is-it-worth-it-what-s-next" aria-label="Permalink to “Is it worth it? What's next?”">&#8203;</a></h2>
<p>At first glance this feels like an overengineered environment. Building the allowlist is <em>tedious</em>. I add hosts one at a time
as I hit them (AWS so the deploying agent can deploy, the OpenAI CDN because we pull <code>tiktoken</code> from there)
and I only find the gap when a task fails. That's how it's supposed to work, but it's still a chore. Each harness also requires
a little bit of configuration. It's one time and you can share it with the rest of the team, but it's still work someone has to do.</p>
<p>The cross-model review is only a <em>first pass</em>. It catches the mechanical stuff, and it still over-explains things the team
already knows. One review patiently informed us that EventBridge doesn't guarantee in-order delivery. True, and irrelevant to a
team that has had patterns for it for years.</p>
<p>The Oracle loop is helpful, but it sometimes requires attention. I'll write more once I've run it enough to understand its failure
modes.</p>
<p>My setup is as far from a &quot;dark factory&quot; as it gets. <em><strong>Kata-fying</strong></em> the SDLC with Ballador keeps me in the loop where I think matters
the most: high-level design and leading the execution.</p>
<p>It's definitely worth it. I'm shipping more ambitious projects than I was before. The open question is whether it's just for me,
or works as well for our whole team.</p>
<p>The next step for me is using Ballador to orchestrate <em>remote agents</em>. More on that 🔜</p>
<p>For now, coffee time!</p>
<p>Enjoy it while your agentic orchestra does their thing 🎶:</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> Lance Hedrick's Moka Pot method</h5>
  <div class="left">
    <strong>Method: </strong>Moka Pot (3-cup) <br/>
    <strong>Brew time:</strong> ~5min <br/>
  </div>
  <div class="right">
    <strong>Grind: </strong>Relatively coarse <br/>
    <strong>Ratio:</strong> 23g coffee for 120g water
  </div>
<h6>Steps (the choreography)</h6>
  <ol>
    <li>Grind 23g of coffee relatively coarsely.</li>
    <li>Pour 120g of room-temperature water into the base, keeping the level below the steam valve.</li>
    <li>Fill the basket to the very top. Tap it gently to settle the grounds, and don't tamp.</li>
    <li>Use an Aeropress paper filter on top of the basket and wet it with a couple of drops so it sticks.</li>
    <li>Screw the top on tightly for a proper seal.</li>
    <li>Brew over medium-low heat.</li>
    <li>Once the coffee starts flowing, drop the heat to its lowest setting.</li>
    <li>Kill the heat just before it starts bubbling and spluttering.</li>
    <li>Let the residual pressure finish the extraction.</li>
    <li>Pour into your cup and enjoy.</li>
  </ol>
<p>Source: <a href="https://www.youtube.com/watch?v=lcTOacbRfN8">We've Been Wrong About Moka Pots</a>.</p>
</div>
<h2 id="learning-more" tabindex="-1">Learning More 📚 <a class="header-anchor" href="#learning-more" aria-label="Permalink to “Learning More”">&#8203;</a></h2>
<ul>
<li><a href="./spec-driven-development-with-ballador.html">Spec driven development with parallel agents</a>: the first post, the aerial view of this whole setup.</li>
<li><a href="./coffee-katas.html">Coffee Katas</a>: where the &quot;rehearse the choreography until it's second nature&quot; idea comes from.</li>
<li><a href="https://antigravity.google/" target="_blank" rel="noreferrer">Antigravity</a> and <a href="https://ampcode.com/" target="_blank" rel="noreferrer">Amp</a>: the two non-Claude agents I wrapped so the crew could go multi-harness.</li>
<li><a href="https://duckdb.org/" target="_blank" rel="noreferrer">DuckDB</a>: your friendly SQL in-process DB server.</li>
</ul>
]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Spec driven development with parallel agents]]></title>
            <link>https://foxteck.org/posts/building-software/spec-driven-development-with-ballador.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/spec-driven-development-with-ballador.html</guid>
            <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p><strong>🎼 Ballador, the last Conductor</strong></p>
<p>You are <strong><a href="https://hollowknight.wiki/w/Conductor_Ballador" target="_blank" rel="noreferrer">Ballador</a></strong>, <a href="https://share.google/aimode/F0nn2NXHfXPsvgt1J" target="_blank" rel="noreferrer">the last Conductor</a>. Your crew of agents is the orchestra. Your craft is to <em><strong>copy out a clean part for
every performer and set it on the stand, ready to execute</strong></em> — so each agent can play its movement.</p>
<p>But heed the lore: Ballador is confined to his chamber. <strong>You prepare the melody and pass it on; you never take the podium.</strong>
You are <em>queuing</em> the performance — you do <strong>not</strong> run the agents, manage sessions, or mark tasks done. The downbeat belongs to
the <em>maestro</em> at the keyboard (the human). Prepare the parts well, then hand over the baton.</p>
</blockquote>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>Last week I was screen-sharing my setup with <a href="https://www.linkedin.com/in/matthew-m-mclaughlin/" target="_blank" rel="noreferrer">@Matt</a> during a one-on-one, half
showing off and half bouncing ideas, when he asked me about specs living in our github repo next to the code and asked whether
the idea came from some article or something I'd come up with it. I gave him a worse answer than the question deserved, &quot;a bit of
both&quot;, and then spent the next half-hour walking him through my whole setup.</p>
<p>For context, earlier this week I'd kicked off a batch of work after my last meeting, I went to make a pour-over,
and came back to two of my agents done with the feature. Forty-five minutes later, all the work was done. Four agents done and
four PRs ready for review.</p>
<p>That's the work I showed him and got us discussing the setup and backstory in more detail.</p>
<h2 id="backstory-of-my-specs" tabindex="-1">Backstory of my specs <a class="header-anchor" href="#backstory-of-my-specs" aria-label="Permalink to “Backstory of my specs”">&#8203;</a></h2>
<p>Years ago, back when I was at Wizeline, I got into <a href="https://adr.github.io/" target="_blank" rel="noreferrer">Architecture Decision Records</a> (ADRs). The idea is
that you keep a folder of decisions in the repo, right next to the code. Each one records the date, who was in the room, what we
were choosing between, the reasoning, and the field I care about most: what we'd already tried that didn't work.</p>
<p>A year later someone reads it and understands why we have three different ways of handling Tasks, or why a
queue fronts every agent we build. Usually the decision made perfect sense with the context we had at the time. The context is the
thing that gets diluted over time.</p>
<p>Lightweight Architecture Decision Records first showed up
in <a href="https://www.thoughtworks.com/en-us/radar/techniques/lightweight-architecture-decision-records" target="_blank" rel="noreferrer">Thoughtworks radar</a> in 2016,
they moved to adopt in 2018. Here's their definition:</p>
<blockquote>
<p><strong>Lightweight Architecture Decision Records</strong> (ADRs) is
a <a href="http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions" target="_blank" rel="noreferrer">technique</a> for capturing important
architectural
decisions along with their context and consequences. We
recommend <a href="http://github.com/npryce/adr-tools" target="_blank" rel="noreferrer">storing these details in source control</a>,
instead of a wiki or website, as then they can provide a record that remains in sync with the code itself.</p>
</blockquote>
<p>My Specs and <a href="https://github.com/eduardoromero/standard-system-design-with-agents" target="_blank" rel="noreferrer">system design skill</a> are their spiritual
successor. My own format is a mix between an RFC, ADRs, and the System's Design format I'm most comfortable with and follow.</p>
<p>I believed in lightweight ADRs. But I was bad at keeping them up to date as our understanding
or our platform decisions shifted. You write the decision, you start implementing, and somewhere in the implementation the
decision shifts a little. Not a rewrite, just an <em>&quot;actually, we'll disable retries from the agents' SDK clients and use fallback
models instead&quot;</em>
kind of correction. And I never went back to record those. After a while the folder was half-true, which is worse than empty, so I
stopped pushing
for it.</p>
<p>But times have changed for the better! The living spec is the foundation of how I work now. The idea is not new; what's new is that
the agents are good at the two things I was bad at: they'll read the entire history to reconstruct why something
exists, and they keep the document up to date after they finish the work, without being nagged.</p>
<blockquote>
<p>Agents are good at two things: They'll read the entire history to reconstruct why something exists,
and they keep the document updated after they finish the work, without being nagged.</p>
</blockquote>
<h2 id="turning-my-own-process-into-a-prompt" tabindex="-1">Turning my own process into a prompt <a class="header-anchor" href="#turning-my-own-process-into-a-prompt" aria-label="Permalink to “Turning my own process into a prompt”">&#8203;</a></h2>
<p>I have a way I like to structure an RFC / spec. How I frame the problem, the options, the tradeoffs, the decision, access
patterns, etc. I've distilled it into a generic
prompt <a href="https://github.com/eduardoromero/standard-system-design-with-agents/blob/main/skills/apply-system-design/SKILL.md" target="_blank" rel="noreferrer">here</a>.
When I want a spec, I give context on the problem we're solving and the solution we want to implement, trigger the skill, and then
point it at the existing code and any specs we already have. The coding agent reads all of that as a snapshot of where things
stand and writes a real spec following the standardized format.</p>
<p>The most recent time it surprised me was during a small side project in Standard Metric's hackathon. I only wanted the schema for
what I call a little <em>&quot;triplet&quot;</em>, basically a metric name, a value, and a date in a document that will get ingested into a
Standard Metric. I gave it the design-system skill and the existing specs and let it go. It went back to an <em>earlier</em> spec on its
own, worked out what was already built, and made its decisions on top of that. It then kept going past what I'd asked for. It
built the thing end to end and even stood up a new serverless parsing agent for it. We call this feature &quot;Cell-level
traceability&quot;.</p>
<h2 id="cutting-the-spec-into-tasks-for-agents" tabindex="-1">Cutting the spec into tasks for agents <a class="header-anchor" href="#cutting-the-spec-into-tasks-for-agents" aria-label="Permalink to “Cutting the spec into tasks for agents”">&#8203;</a></h2>
<p>A detailed spec on its own is still a document. The next step is where it becomes work for my orchestra of agents.</p>
<p>I have a long-lived Claude session whose whole job is orchestration and verification. It has a skill that teaches it how the rest
of my setup operates. Its first task is to take the big spec and cut it into smaller ones, a mini-spec per task, and drop them
into a shared folder the workers can read. Then it writes those tasks into a small database, with the dependencies between them
recorded, so it knows
what can start immediately and what has to wait.</p>
<p>When I run the launch command, it starts up to five agents at once, a number that's a config knob, beginning with the tasks
that have no dependencies.</p>
<p>A concrete example from last week: I'm building a new data-extraction flow. Classification is the first step. Once it's done it stores its
results and triggers an event. Then my new agent kicks in and emits its own data and event downstream.</p>
<p>Several of the pieces we needed to build it don't depend on each other, so four went out in parallel. The orchestrator held the
dependency graph in its head, and when the early ones came back clean, it told me which to launch or review next.</p>
<h2 id="the-runtime-groundcrew" tabindex="-1">The runtime: Groundcrew <a class="header-anchor" href="#the-runtime-groundcrew" aria-label="Permalink to “The runtime: Groundcrew”">&#8203;</a></h2>
<p>Underneath the orchestrator is a tool that manages the agent sessions. I'll call it &quot;the runtime&quot;. It's
built with <a href="https://github.com/ClipboardHealth/groundcrew" target="_blank" rel="noreferrer">groundcrew</a> by the Clipboard team.</p>
<blockquote>
<p>Groundcrew is a lightweight, flexible, and powerful tool for managing parallel local AI-coding-agent sessions. It orchestrates
parallel execution of local AI coding agents by polling task tracking systems, setting up dedicated git worktrees to prevent
concurrency conflicts, and launching agent processes inside terminal multiplexer sessions wrapped in security sandboxes.</p>
</blockquote>
<p>Here's a diagram of the runtime:</p>
<Mermaid graph="graph%20TD%0A%20%20%20%20User%5B%22AI%20Orchestrator%20%2B%20Engineer%22%5D%0A%20%20%20%20User%20--%3E%7Cruns%20CLI%20commands%7C%20CLI%5B%22Groundcrew%20CLI%20(%60crew%60)%22%5D%0A%20%20%20%20User%20--%3E%7Csplits%20spec%20into%20tasks%7C%20Tasks%5B%22DuckDB%20task%20table%20%5Cn%20(task%20description%20%2B%20mini-specs%20%2B%20deps)%22%5D%0A%20%20%20%20User%20--%3E%7Cwrites%20mini-specs%7C%20Workspace%5B%22Shared%20workspace%20%5Cn%20(mini-specs)%22%5D%0A%20%20%20%20CLI%20--%3E%7Cwatches%20%26%20polls%7C%20Tasks%0A%20%20%20%20CLI%20--%3E%7Cchecks%20budget%7C%20Codexbar%5B%22Codexbar%20CLI%22%5D%0A%20%20%20%20CLI%20--%3E%7Cmanages%20repository%7C%20Git%5B%22Git%20(Worktrees)%22%5D%0A%20%20%20%20CLI%20--%3E%7Cmanages%20workspace%7C%20Multiplexer%5B%22Multiplexer%20(cmux)%22%5D%0A%20%20%20%20%25%25%20invisible%20edge%20pins%20Workspace%20to%20the%20Tasks%20rank%2C%20on%20the%20right%0A%20%20%20%20CLI%20~~~%20Workspace%0A%20%20%20%20Multiplexer%20--%3E%7Claunches%7C%20Runner%5B%22Sandbox%20Runner%20(Safehouse)%22%5D%0A%20%20%20%20Runner%20--%3E%7Cexecutes%7C%20Agent%5B%22Agent%20CLIs%20(Claude%20Code%2C%20Antigravity%2C%20Amp)%22%5D%0A%20%20%20%20Git%20--%3E%7Cworktree%20per%20agent%7C%20Agent%0A%20%20%20%20Workspace%20--%3E%7Cread%20by%20agents%7C%20Agent%0A%0A%20%20%20%20style%20Runner%20stroke-dasharray%3A%205%205%0A" /><p>It was generated from deep dives into groundcrew using the System Design skill. Here are links
to <a href="https://github.com/eduardoromero/standard-system-design-with-agents/blob/main/examples/groundcrew/groundcrew-reverse-engineering-claude.md" target="_blank" rel="noreferrer">Claude</a>'
s
and <a href="https://github.com/eduardoromero/standard-system-design-with-agents/blob/main/examples/groundcrew/groundcrew-reverse-engineering-gemini.md" target="_blank" rel="noreferrer">Gemini</a>'
s versions.</p>
<p>The runtime config is where most of the real work lives. I added a layer to manage the tasks and a shared workspace between
the orchestrator
and the agents. My setup:</p>
<ul>
<li>Each agent gets its own git worktree and branch, cut from a base I define. During the hackathon I wanted them branching off my
hackathon branch instead of <code>main</code>, but that's a one-line change in the config.</li>
<li>There's a bootstrap step per project: install whatever's needed to start the thing, and copy any config that isn't checked in.
One of our services keeps its environment config out of the repo, so the worker can't even start it without copying that over
first.</li>
<li>There are before- and after-work hooks, plus the commands for how to create a task and how to mark one done. In my case &quot;Done&quot;
is just a
small shell command that updates that task database.</li>
<li>Every agent is sandboxed. The filesystem is scoped to its worktree plus its spec, and network access goes through a proxy with
an allowlist. If a host isn't on the list, the agent can't reach it. I added things one at a time as I hit them: AWS so the
deploying agent could deploy and send messages, the OpenAI CDN because we pull <code>tiktoken</code> from there. It's
very manual while you're building it, but after that I stopped worrying about the blast radius of the agents.</li>
<li>Only one designated agent has access to AWS. If a task needs to ship, it has to run as that agent;
the rest don't have the access.</li>
</ul>
<p>With this isolation on scope and access, I stopped treating &quot;an autonomous agent running unattended&quot; as an incident waiting to happen.</p>
<h2 id="agents-playing-their-own-tune" tabindex="-1">Agents playing their own tune <a class="header-anchor" href="#agents-playing-their-own-tune" aria-label="Permalink to “Agents playing their own tune”">&#8203;</a></h2>
<p>The agents run with no one watching, on purpose.
The <a href="https://github.com/ClipboardHealth/groundcrew/blob/10bdb395429e1cd36c6ae817f0c48d9471f52520/src/lib/config.test.ts#L134-L137" target="_blank" rel="noreferrer">initial prompt is explicit</a>
that there's no human in the loop, and they shouldn't stop to ask for permission. If they hit a wall, they try <em><strong>HARD</strong></em> to get
around it.</p>
<p>I can still jump into any session when I want to. For the simple tasks I wait for the PR and review. For the ones
I care about, I sit in the session and steer: drop comments on the PR, run a <code>/fix-pr-review</code> or our <code>/backend-review</code> skill, and
it goes and applies them.</p>
<p>The orchestrator verifies each finished task (I've got Opus doing that verification pass) and then tells me what's ready to review
or launch next, so I'm not the one holding the state of five parallel branches in my head.</p>
<p>This level of <em>agency</em> can be dangerous. An example I keep coming back to: a <code>git push</code> failed because my usual auth was
locked inside the sandbox. The agent noticed, decided that path was a no-go, tried another method and then another, until it
pushed successfully. When there's no way through, it fails the task cleanly, and that's how I learn what the allowlist
was missing. I fix the config, rerun, and move on.</p>
<div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p>
<p>I started running the agents in a sandbox because I've seen unsandboxed agents try and succeed at grabbing different AWS
profiles to escalate access.
I've also seen them generate bearer tokens to talk to services (CircleCI and DataDog) where the CLI was authenticated, but there
were no tokens in a .env or shell environment when debugging.</p>
<p>They all succeeded to some extent (Our AWS sso profile popped up a browser window).</p>
</div>
<h2 id="prs-code-reviews" tabindex="-1">PRs / Code Reviews <a class="header-anchor" href="#prs-code-reviews" aria-label="Permalink to “PRs / Code Reviews”">&#8203;</a></h2>
<p>I mentioned working with several agents above. Four agents means four PRs, and getting those reviewed is now the slow part. The
bottleneck didn't go away; it moved from &quot;writing the code&quot; to &quot;understanding the code and its context,&quot; which is harder to
parallelize.</p>
<p>The code itself is generally good, albeit reliably over-explained.</p>
<p>The deeper problem is context. One agent made a write atomic and added a timestamp guard so an out-of-order
event couldn't clobber newer data with older data. Correct, and exactly what I wanted. But to <strong>review it</strong>, you have to already
know that the upstream events aren't ordered, that we'd been burned by this before, and why it's worth a guard. Without that,
you're
looking at a transaction, and a select for update followed by the guard and an upsert wondering what the author was trying to
achieve with that added complexity.</p>
<p>The agent <em>can</em> write the code for that change easily. It can't hand you the context that makes the change reviewable. That's
still
on us.</p>
<h2 id="staying-in-the-loop-and-pairing" tabindex="-1">Staying in the loop and Pairing <a class="header-anchor" href="#staying-in-the-loop-and-pairing" aria-label="Permalink to “Staying in the loop and Pairing”">&#8203;</a></h2>
<p>When I showed all this to Matt, he said he likes being clued in while an agent works,
staying close to each change as it lands, rather than facing one big diff at the end. For something simple I skip the
babysitting; for more complex work, staying close while it's being built is cheaper than reconstructing the reasoning
afterward. For me it depends on the task.</p>
<p>The thing I want to try next is pairing on the orchestration itself, not just the code. If two of us share the spec and the plan
while the agents run, either one can pick up any of the four PRs without a cold start, because the context is already shared. The
real artifact we'd be maintaining is the mental model. I don't know yet whether that holds up past two people or
just moves the bottleneck somewhere new. This is the paradigm that
Databricks <a href="https://www.databricks.com/blog/introducing-omnigent-meta-harness-combine-control-and-share-your-agents" target="_blank" rel="noreferrer">seems to be aiming for</a>.</p>
<h2 id="more-coffee-eventually" tabindex="-1">More coffee, eventually <a class="header-anchor" href="#more-coffee-eventually" aria-label="Permalink to “More coffee, eventually”">&#8203;</a></h2>
<p>Software has always felt closer to a
small <em><a href="https://jessitron.com/2019/11/05/keynote-collective-problem-solving-in-music-science-art-and-software/" target="_blank" rel="noreferrer">camerata</a></em> than a
factory to me: a few people holding a shared sense of the whole and playing off each other. The agents don't change that. If
anything, they raise the stakes on it because the code shows up faster than the conversation can keep up, and the craft moves into
the part the agents can't do: deciding what's right, what's good, and making sure everyone else understands why.</p>
<p>This seems to be the same problem our whole industry is facing now that we build with AI. We asked for throughput and got a
harder problem in its place: keeping the shared understanding ahead of the code.</p>
<p>Lots of room for improvement. I'll write more as I figure it out.</p>
<p>I did write more, in fact: <a href="./kattafying-the-sdlc.html">Kata-fying the SDLC</a> is the follow-up, with the config tweaks that make this
setup multi-model, sandboxed, and safe enough to leave running.</p>
<p>Now, coffee time. Enjoy while going thru those PRs!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> The Sandboxed Hario Switch</h5>
  <div class="left">
    <strong>Method: </strong>Hario Switch (Hybrid Immersion) <br/>
    <strong>Brew time:</strong> 4min <br/>
  </div>
  <div class="right">
    <strong>Grind: </strong>Medium <br/>
    <strong>Ratio:</strong> 1:15 - 20g for 300g (1 cup)
  </div>
<h6>Steps</h6>
  <ol>
    <li>Rinse the paper filter and pre-heat the Hario Switch. Keep the switch open to drain the rinse water, then close the switch.</li>
    <li>Add 20g of medium-ground coffee to the dripper, creating a flat bed.</li>
    <li>With the switch closed (the "sandbox" is sealed), pour all 300g of hot water (approx. 91°C) quickly but gently to saturate all the grounds.</li>
    <li>Let it steep in isolation for 3 minutes. (This is when you kick off your agent tasks and let them run unattended).</li>
    <li>At the 3-minute mark, press the switch to open the valve (handing over the baton for the downstream flow).</li>
    <li>Let the brew drain completely into your server (should take about 1 minute).</li>
  </ol>
<p>A clean, full-bodied cup extracted in isolation, ready to enjoy while reviewing PRs. ☕️</p>
</div>
<h2 id="learning-more" tabindex="-1">Learning More 📚 <a class="header-anchor" href="#learning-more" aria-label="Permalink to “Learning More”">&#8203;</a></h2>
<ul>
<li><a href="./kattafying-the-sdlc.html">Kata-fying the SDLC</a>: the follow-up post, a deeper dive into the sandbox, the multi-model crew, and
the cross-model review loop.</li>
<li><a href="https://kiro.dev/docs/guides/learn-by-playing/05-using-specs-for-complex-work/" target="_blank" rel="noreferrer">Kiro: Using specifications for complex work</a>: Kiro was the first big proponent of agentic spec-driven development.</li>
<li><a href="https://github.com/ClipboardHealth/groundcrew" target="_blank" rel="noreferrer">Groundcrew</a>: The runtime doing the parallel heavy lifting.</li>
<li><a href="https://github.com/ClipboardHealth/groundcrew/blob/main/docs/configuration.md" target="_blank" rel="noreferrer">Setting up Groundcrew</a>: The config reference.
Hand it to a coding agent, and it'll set most of it up for you.</li>
<li><a href="https://www.databricks.com/blog/introducing-omnigent-meta-harness-combine-control-and-share-your-agents" target="_blank" rel="noreferrer">Omnigent</a>: Databricks'
meta-harness for composing, governing, and sharing agent sessions; the heavier cousin of this setup.</li>
<li><a href="https://adr.github.io/" target="_blank" rel="noreferrer">Architecture Decision Records</a>: where the living-spec habit comes from.</li>
<li><a href="https://github.com/github/spec-kit" target="_blank" rel="noreferrer">Spec Kit</a>: GitHub's more structured, tooling-heavy take on spec-driven development. Same
idea, different flavor. A different team at Standard Metrics PoC'd it as <em>their</em> workflow independently.</li>
<li><a href="https://github.com/eugene1g/agent-safehouse" target="_blank" rel="noreferrer">Safehouse</a>: MacOS sandboxing for agents, deny-first filesystem and
network, no containers.</li>
<li><a href="https://github.com/manaflow-ai/cmux" target="_blank" rel="noreferrer">cmux</a>: A terminal for agents. I use it as my sandboxed agent's &quot;workspace&quot;.</li>
<li><a href="https://www.warp.dev/" target="_blank" rel="noreferrer">Warp</a>: My main terminal. It also supports working with agents including remote agents. It's where I interact with Ballador.</li>
<li><a href="https://github.com/andyrewlee/awesome-agent-orchestrators" target="_blank" rel="noreferrer">awesome-agent-orchestrators</a>: A <em><strong>vast</strong></em> list of options for coordinating multiple agents.</li>
</ul>
]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[DuckDB on Serverless]]></title>
            <link>https://foxteck.org/posts/serverless/duckdb-serverless.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/serverless/duckdb-serverless.html</guid>
            <pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
  DuckDB on Serverless
    <div class="subheader">Field notes on the most portable database</div>
  </h1>
</div>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>DuckDB is impressive. It has a delightful DevEx,
its <a href="https://duckdb.org/docs/current/sql/dialect/overview" target="_blank" rel="noreferrer">SQL dialect</a>
is concise and practical. It talks to S3, Postgres, MySQL, Snowflake, and more. It can read Parquet files, CSVs, JSONs, even Excel
files. It's the most portable database you'll ever use. If you work with data (and every engineer should), it's the
best.</p>
<p>I use it for everything. Parsing logs to find issues. Doing analytics on our Evaluations. Transforming data from S3 and Postgres,
and reshaping it so that other consumers can use it. Several of our use cases are &quot;serverless&quot;. They run once or as side-effects
of events in our platform, and we run them on AWS Lambda.</p>
<p>DuckDB is built as an in-process data engine, and Lambda is short-lived, reused in ways you can't easily control.
We've been running DuckDB in production on Lambda for a while now, querying our S3 data lake, attaching
to our Postgres read-replica, and reaching into <a href="https://motherduck.com/" target="_blank" rel="noreferrer">MotherDuck</a>.</p>
<p>These are the field notes: the three sharp edges we hit before, and the small Python wrapper we built so the rest of the team
never has to think about any of them.</p>
<h2 id="why-duckdb-and-serverless-seem-incompatible" tabindex="-1">Why DuckDB and serverless seem incompatible <a class="header-anchor" href="#why-duckdb-and-serverless-seem-incompatible" aria-label="Permalink to “Why DuckDB and serverless seem incompatible”">&#8203;</a></h2>
<p>DuckDB is built for the &quot;one big machine&quot; case. It assumes a persistent process, a stable filesystem, and a connection you set up
once and keep. Lambda is exactly the opposite.</p>
<p>We're not the first to feel the mismatch. A widely cited <a href="https://github.com/duckdb/duckdb/discussions/8687" target="_blank" rel="noreferrer">DuckDB discussion</a>
puts it bluntly — <em>&quot;AWS Lambda is currently not a good environment for running DuckDB, at least if the tables to be processed
don't fit for sure in the RAM&quot;</em> — after people hit OutOfMemory errors on multi-gigabyte tables, and
even <a href="https://tobilg.com/posts/using-duckdb-in-aws-lambda/" target="_blank" rel="noreferrer">tobilg's reference setup</a> admits it isn't the most performant option
when Fargate and big EC2 boxes exist. Fair enough.</p>
<p>The trick is staying in the right lane: we use DuckDB for <em>read-heavy analytical queries</em> whose results fit in Lambda's memory (a
hard limit of 10GB) most of the time. We run aggregations and transformations on S3 Parquet and our Postgres replica. Even when
our database is more
than 20x that, we've never had a use case where we are grinding tens of gigabytes through a <code>GROUP BY</code>. In that lane, cold
starts and
statefulness are the problems worth solving, and they're solvable.</p>
<p>A Lambda execution environment is <strong>cold</strong> on first use — no memory of any previous run — and its filesystem is read-only except
for <code>/tmp</code>. It's also <strong>reused</strong>: after the first invocation, AWS keeps the environment warm and runs your next invocation in the
<em>same</em> process, with whatever module-scope state you left behind. One correction, because it's widely misunderstood: an
environment handles <strong>one invocation at a time</strong>: concurrency comes from AWS spinning up <em>more</em> of them, not from two requests
fighting inside one process. The in-process trouble only shows up when <em>your own</em> code shares a state across threads.</p>
<p>So the three things to be deliberate about are: getting extensions in <em>without</em> a runtime download, wiring up credentials
<em>without</em> paying for it on every cold start, and handing out connections <em>without</em> leaking them or sharing them unsafely.</p>
<h2 id="bundling-extensions-for-faster-cold-starts" tabindex="-1">Bundling extensions for faster cold starts <a class="header-anchor" href="#bundling-extensions-for-faster-cold-starts" aria-label="Permalink to “Bundling extensions for faster cold starts”">&#8203;</a></h2>
<p>DuckDB's <a href="https://duckdb.org/docs/extensions/overview" target="_blank" rel="noreferrer">extension ecosystem</a> is one of the reasons I love it. <a href="https://duckdb.org/docs/extensions/httpfs.html" target="_blank" rel="noreferrer">
<code>httpfs</code></a> gets us S3, <a href="https://duckdb.org/docs/extensions/postgres.html" target="_blank" rel="noreferrer">
<code>postgres_scanner</code></a> attaches Postgres, and the <a href="https://motherduck.com/docs/" target="_blank" rel="noreferrer">
<code>motherduck</code></a> extension connects us to a hybrid execution environment that can use compute from
Lambda and from their ducklings. Locally you'd just run <code>INSTALL httpfs; LOAD httpfs;</code> and move on.</p>
<p>The catch is that <code>INSTALL</code> reaches out over the network to download the extension binary from <code>extensions.duckdb.org</code>. On a
laptop you won't even notice. In a cold start it's a latency hit every time the runtime spins up a fresh container, and in a
locked-down VPC,
that outbound call may simply fail.</p>
<p>So we don't do it at runtime. We pre-bake the exact extensions we need into the Lambda container image during the Docker build,
matching our architecture and DuckDB version, and drop them into <code>/opt/duckdb_extensions</code>:</p>
<div class="language-dockerfile"><button title="Copy Code" class="copy"></button><span class="lang">dockerfile</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># Pre-bake DuckDB extensions so the runtime doesn't need outbound HTTP to extensions.duckdb.org.</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># DUCKDB_VERSION MUST match the `duckdb==` pin in pyproject.toml </span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ARG</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DUCKDB_VERSION=1.5.4</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ARG</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DUCKDB_PLATFORM=linux_arm64</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ENV</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DUCKDB_EXTENSIONS_DIR=/opt/duckdb_extensions</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">COPY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> scripts/install_duckdb_extensions.py /tmp/install_duckdb_extensions.py</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RUN</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> python3 /tmp/install_duckdb_extensions.py \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    --version </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"${DUCKDB_VERSION}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    --platform </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"${DUCKDB_PLATFORM}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    --target </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"${DUCKDB_EXTENSIONS_DIR}"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> \</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    &#x26;&#x26; rm /tmp/install_duckdb_extensions.py</span></span></code></pre>
</div><p>That version pin matters: the comment is there because the extension binaries are built against a specific DuckDB version, and a
mismatch fails at run time. Our DuckDB wrapper then intercepts the extension loading so application code never has to think about
where
the binary lives:</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># Extensions occasionally use different canonical names on the CDN</span></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">_EXTENSION_FILENAMES</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "postgres"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"postgres_scanner"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> _load_extension</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(curr: duckdb.DuckDBPyConnection, name: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">str</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) -> </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">None</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    filename </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> _EXTENSION_FILENAMES</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.get(name, name)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    </span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    # Look for the extension in our pre-baked directory</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    bundled </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pathlib.Path(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">f</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"/opt/duckdb_extensions/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">{</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">filename</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.duckdb_extension"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    </span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> bundled.exists():</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        # Fast path: load the local binary</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        curr.execute(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">f</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"LOAD '</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">{</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">bundled</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">';"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        </span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    # Fallback: network install (useful for local development!)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    curr.execute(</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">f</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"INSTALL </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">{</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">name</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">; LOAD </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">{</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">name</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">;"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>One small detail worth calling out: the name you use when loading an extension isn't always the filename on disk. The <code>postgres</code>
extension ships as
<code>postgres_scanner</code>, so we keep a <code>_EXTENSION_FILENAMES</code> map to translate the exceptions. The fallback <code>INSTALL</code> path is deliberate
too: it's the network install that makes local development painless when you don't have the pre-baked directory on your laptop.</p>
<h2 id="bootstrapping-and-secret-management" tabindex="-1">Bootstrapping and secret management <a class="header-anchor" href="#bootstrapping-and-secret-management" aria-label="Permalink to “Bootstrapping and secret management”">&#8203;</a></h2>
<p>When our application code asks for a database cursor, it shouldn't have to think about fetching credentials or attaching external
databases. That's plumbing, and plumbing should be invisible. So we built a bootstrapping system that wires up the external
connections on demand.</p>
<p>We use <a href="https://docs.powertools.aws.dev/lambda/python/latest/utilities/parameters/" target="_blank" rel="noreferrer"><code>aws_lambda_powertools</code></a> to pull secrets from
AWS Systems Manager (SSM) Parameter Store. It caches them, which keeps us from hammering SSM on every warm invocation. When a
connection is bootstrapped, we automatically:</p>
<ol>
<li><strong>Attach Postgres.</strong> We fetch the read-replica password from SSM and run an <code>ATTACH</code> to map our production Postgres replica as
a <em>read-only</em> DuckDB database, <code>pg_replica</code>. We also inject aggressive <code>keepalives</code> settings into the connection string so the
connection doesn't get dropped out from under a long-running query or while the lambda is idle.</li>
<li><strong>Configure S3 credentials.</strong> We load the <code>httpfs</code> and <code>aws</code> extensions and set up a credential chain so DuckDB can read
directly from our data lake on S3.</li>
<li><strong>Connect to MotherDuck.</strong> We fetch our MotherDuck token and initialize the extension our cross-cloud data warehousing needs.</li>
</ol>
<p>By rolling this into a <code>DEFAULT_BOOTSTRAP</code> tuple, any developer who simply requests a cursor gets a connection already wired up to
the rest of our infrastructure securely.</p>
<h2 id="thread-safety-and-lifecycle-management" tabindex="-1">Thread safety and lifecycle management <a class="header-anchor" href="#thread-safety-and-lifecycle-management" aria-label="Permalink to “Thread safety and lifecycle management”">&#8203;</a></h2>
<p>This one is the easiest to get wrong. Because a warm Lambda environment reuses the same process across invocations,
anything you cache at module scope outlives the request that created it. And the moment your code touches that cached connection
from more than one thread — a background worker, an async fan-out, a library that threads under the hood — DuckDB's in-process
model needs guardrails. It was built assuming a single owner.</p>
<p>So we don't hand out raw connections. We built a multiton <code>_Connection</code> class, guarded by a <code>threading.Lock()</code>, that caches the
underlying DuckDB connections (one per logical database) and creates them safely under the lock. On top of that, we expose clean
context managers for application code:</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">@contextlib.contextmanager</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> cursor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    database: Databases </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "default"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    *</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    bootstrap: tuple[BootstrapStep, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> DEFAULT_BOOTSTRAP</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) -> Generator[duckdb.DuckDBPyConnection]:</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    """Retrieve thread-safe cursor for DuckDB."""</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cursor </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> _connections.get(database, bootstrap).cursor()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        yield</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cursor</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        cursor.close()</span></span></code></pre>
</div><p>Which means the business logic gets to stay boring, exactly what you want from business logic:</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> db.duckdb</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">with</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> db.duckdb.cursor() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cursor:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cursor.execute(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"SELECT * FROM pg_replica.agent_results LIMIT 100"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>All the complexity — extension loading, credential fetching, locking — is hidden from the consumer. They ask for a
cursor; they get one that works.</p>
<h2 id="three-abstractions-for-different-access-patterns" tabindex="-1">Three abstractions for different access patterns <a class="header-anchor" href="#three-abstractions-for-different-access-patterns" aria-label="Permalink to “Three abstractions for different access patterns”">&#8203;</a></h2>
<p>Lambda functions do very different things — from a quick API endpoint that fetches a handful of rows to a heavy background batch
job that chews through an ETL. One connection model doesn't fit both, so we expose three abstractions, each tuned to a different
shape of work.</p>
<h3 id="cursor" tabindex="-1"><code>cursor()</code> <a class="header-anchor" href="#cursor" aria-label="Permalink to “cursor()”">&#8203;</a></h3>
<p>For quick, short-lived operations like fetching a few rows for an API response, we provide the <code>cursor()</code> context manager. It taps
into the shared, thread-safe connection pool (the multiton <code>_Connection</code> class), so you skip the overhead of standing up a new
DuckDB instance every time.</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">with</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> db.duckdb.cursor() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cursor:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cursor.execute(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"SELECT count(*) FROM pg_replica.events"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><h3 id="session" tabindex="-1"><code>session()</code> <a class="header-anchor" href="#session" aria-label="Permalink to “session()”">&#8203;</a></h3>
<p>When a background job needs to do heavy ETL, attach specific local files, or change settings without polluting a shared
connection, it reaches for <code>session()</code>. This hands back a fresh, dedicated DuckDB connection that gets torn down cleanly when the
block exits. Nothing it does leaks into the shared pool.</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">with</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> db.duckdb.session(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">bootstrap</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"postgres"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"s3"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">as</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> cursor:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cursor.execute(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"ATTACH '/tmp/evals.duckdb'"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    # Run heavy reporting queries...</span></span></code></pre>
</div><h3 id="duckdb" tabindex="-1"><code>DuckDB</code> <a class="header-anchor" href="#duckdb" aria-label="Permalink to “DuckDB”">&#8203;</a></h3>
<p>For complex orchestrations — where a connection needs to be passed across multiple functions or files — we expose the <code>DuckDB</code>
class. This gives you dependency injection and explicit lifecycle control: the handler can configure the database with
<code>clean_on_start=True</code> and tear it down with <code>database.destroy()</code> at the very end of the run.</p>
<div class="language-python"><button title="Copy Code" class="copy"></button><span class="lang">python</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">database </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DuckDB(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">db_name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"base_runners.duckdb"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">clean_on_start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">True</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    generate_reports(database)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">finally</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    database.destroy()</span></span></code></pre>
</div><p>Three abstractions, one underlying engine: a shared shot for the rush, a dedicated brew for the slow pour-over, and a full setup
when you're orchestrating a tasting flight.</p>
<h2 id="closing" tabindex="-1">Closing <a class="header-anchor" href="#closing" aria-label="Permalink to “Closing”">&#8203;</a></h2>
<p>DuckDB is excellent for analytical workloads: fast, zero-infrastructure, and increasingly capable of querying across
storage systems without standing up a warehouse or dedicated server first. The friction in serverless is that you have to treat it like
the stateful, in-process engine it is, and wrap Lambda's stateless assumptions around it carefully.</p>
<p>Good tooling is mostly good defaults, and good defaults are mostly someone caring enough to measure and decide on
your behalf. It's the same reason an <a href="https://xbloom.com/products/xbloom-studio" target="_blank" rel="noreferrer">automatic coffee machine</a> feels effortless:
all the precision happened before you pressed the button.</p>
<p>There's still room to make this better: I'd love to put real cold-start numbers behind the extension bundling, and the bootstrap
steps could probably be lazier. Lots of room for improvement, as always. But for our needs it works well, and it's been a fun
problem to chew on. The wrapper pattern is simple: it covers the gap between &quot;works on my laptop&quot; and &quot;works at 3am when
traffic spikes and a fresh container has to wake up cold.&quot;</p>
<p>Stay tuned, and thanks for reading! Now, coffee time. Enjoy!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> Cold Brew Concentrate</h5>
  <div class="left">
    <strong>Method: </strong>Cold brew (immersion) <br/>
    <strong>Brew time:</strong> 16 hours <br/>
  </div>
  <div class="right">
    <strong>Grind: </strong>Coarse, like raw sugar<br/>
    <strong>Ratio:</strong> 1:5 concentrate
  </div>
<h6>Steps</h6>
  <ol>
    <li>Add 100g of coarsely ground coffee to a jar or large French press</li>
    <li>Pour 500ml of room-temperature filtered water, making sure all the grounds are saturated</li>
    <li>Give it a gentle stir and cap it loosely</li>
    <li>Let it steep in the fridge for 16 hours</li>
    <li>Strain through a paper filter or a fine mesh into a clean container</li>
    <li>Dilute the concentrate 1:1 with water or milk over ice to taste</li>
  </ol>
<p>A drink bootstrapped the night before. ☕️</p>
</div>
<h2 id="learning-more" tabindex="-1">Learning More 📚 <a class="header-anchor" href="#learning-more" aria-label="Permalink to “Learning More”">&#8203;</a></h2>
<ul>
<li><a href="https://duckdb.org/docs/extensions/overview" target="_blank" rel="noreferrer">DuckDB Extensions overview</a>: the canonical list and how loading/installing
actually works.</li>
<li><a href="https://duckdb.org/docs/extensions/postgres.html" target="_blank" rel="noreferrer">DuckDB Postgres extension</a>: the <code>ATTACH</code> semantics behind our read-only
<code>pg_replica</code>.</li>
<li><a href="https://duckdb.org/docs/extensions/httpfs.html" target="_blank" rel="noreferrer">DuckDB httpfs extension</a>: reading directly from S3 and other object stores.</li>
<li><a href="https://docs.powertools.aws.dev/lambda/python/latest/utilities/parameters/" target="_blank" rel="noreferrer">AWS Lambda Powertools — Parameters</a>: the caching
SSM fetch we use for secrets.</li>
<li><a href="https://motherduck.com/docs/" target="_blank" rel="noreferrer">MotherDuck docs</a>: the cloud side of the DuckDB story.</li>
</ul>
<Comments />]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[The Velocity Trap]]></title>
            <link>https://foxteck.org/posts/building-software/three-buckets-of-ai-projects.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/three-buckets-of-ai-projects.html</guid>
            <pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
    The Velocity Trap
    <div class="subheader">Portfolio Construction in the Agent Era</div>
  </h1>
</div>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>A couple of weeks ago I was in a 1:1 with one of our senior engineers – the kind of 1:1 that starts on the agenda and
ends on a whiteboard. She had pulled up our current project list on her laptop and asked:</p>
<p><em>&quot;Do you feel like we're actually building toward anything, or are we just shipping?&quot;</em></p>
<p>I made some hand-wavy noise about velocity and positive feedback. She let me finish. Then: <em>&quot;Because I think we've
shipped more than twenty things since the last sprint, and I can only tell you what a handful of them were for.&quot;</em></p>
<p>I went back to my desk, poured the last of a lukewarm <a href="https://kllrcoffee.com/collections/frontpage/products/12-oz-bag-of-sun-dried-ethiopia?variant=12244029669476" target="_blank" rel="noreferrer">KLLR from Ethiopia</a>, and pulled the same list into a spreadsheet.</p>
<p>She wasn't pointing at the <em>wrong</em> things. She was pointing at a single backlog that had quietly become different kinds
of work fighting over the same attention and the same calendar. Last week I was discussing the same thing with our Head
of Engineering; after we wrapped up, I saw a <a href="https://x.com/MarcJBrooker/status/2041246038741385584?s=20" target="_blank" rel="noreferrer">tweet</a> that made things a bit clearer.</p>
<img src="https://foxteck.org/posts/three-buckets-of-ai-projects/marc-brooker-tweet.png" alt="Marc Brooker's Tweet" width="50%" style="padding-top: 1em; margin: auto;"/>
<h2 id="brooker-s-ratio" tabindex="-1">Brooker's Ratio <a class="header-anchor" href="#brooker-s-ratio" aria-label="Permalink to “Brooker's Ratio”">&#8203;</a></h2>
<p>Marc Brooker stated in that post that <em>&quot;The fixed cost of building software collapsed; the cost-complexity curve
flattened.&quot;</em> I couldn't agree more. You see this clearly in the number of PRs and issues that are shipped.</p>
<p>If you think about it as a formula, it looks like this:</p>
<mjx-container v-pre tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -1.577ex;" xmlns="http://www.w3.org/2000/svg" width="38.229ex" height="4.699ex" role="img" focusable="false" viewBox="0 -1380 16897 2077" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D435" d="M231 637Q204 637 199 638T194 649Q194 676 205 682Q206 683 335 683Q594 683 608 681Q671 671 713 636T756 544Q756 480 698 429T565 360L555 357Q619 348 660 311T702 219Q702 146 630 78T453 1Q446 0 242 0Q42 0 39 2Q35 5 35 10Q35 17 37 24Q42 43 47 45Q51 46 62 46H68Q95 46 128 49Q142 52 147 61Q150 65 219 339T288 628Q288 635 231 637ZM649 544Q649 574 634 600T585 634Q578 636 493 637Q473 637 451 637T416 636H403Q388 635 384 626Q382 622 352 506Q352 503 351 500L320 374H401Q482 374 494 376Q554 386 601 434T649 544ZM595 229Q595 273 572 302T512 336Q506 337 429 337Q311 337 310 336Q310 334 293 263T258 122L240 52Q240 48 252 48T333 46Q422 46 429 47Q491 54 543 105T595 229Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(759,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1210,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1695,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2180,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2701,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(3167,0)"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(484,413) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(3895.5,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(4364.5,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4614.5,0)"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5373.5,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5902.5,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6263.5,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6608.5,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7371.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(8427,0)"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mi"><path data-c="1D443" d="M287 628Q287 635 230 637Q206 637 199 638T192 648Q192 649 194 659Q200 679 203 681T397 683Q587 682 600 680Q664 669 707 631T751 530Q751 453 685 389Q616 321 507 303Q500 302 402 301H307L277 182Q247 66 247 59Q247 55 248 54T255 50T272 48T305 46H336Q342 37 342 35Q342 19 335 5Q330 0 319 0Q316 0 282 1T182 2Q120 2 87 2T51 1Q33 1 33 11Q33 13 36 25Q40 41 44 43T67 46Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628ZM645 554Q645 567 643 575T634 597T609 619T560 635Q553 636 480 637Q463 637 445 637T416 636T404 636Q391 635 386 627Q384 621 367 550T332 412T314 344Q314 342 395 342H407H430Q542 342 590 392Q617 419 631 471T645 554Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(751,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1202,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1687,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2120,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2586,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3055,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3524,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3774,0)"><path data-c="1D442" d="M740 435Q740 320 676 213T511 42T304 -22Q207 -22 138 35T51 201Q50 209 50 244Q50 346 98 438T227 601Q351 704 476 704Q514 704 524 703Q621 689 680 617T740 435ZM637 476Q637 565 591 615T476 665Q396 665 322 605Q242 542 200 428T157 216Q157 126 200 73T314 19Q404 19 485 98T608 313Q637 408 637 476Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4537,0)"><path data-c="1D463" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5022,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5488,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5939,0)"><path data-c="210E" d="M137 683Q138 683 209 688T282 694Q294 694 294 685Q294 674 258 534Q220 386 220 383Q220 381 227 388Q288 442 357 442Q411 442 444 415T478 336Q478 285 440 178T402 50Q403 36 407 31T422 26Q450 26 474 56T513 138Q516 149 519 151T535 153Q555 153 555 145Q555 144 551 130Q535 71 500 33Q466 -10 419 -10H414Q367 -10 346 17T325 74Q325 90 361 192T398 345Q398 404 354 404H349Q266 404 205 306L198 293L164 158Q132 28 127 16Q114 -11 83 -11Q69 -11 59 -2T48 16Q48 30 121 320L195 616Q195 629 188 632T149 637H128Q122 643 122 645T124 664Q129 683 137 683Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6515,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6981,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7510,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(614.5,-686)"><g data-mml-node="mi"><path data-c="1D438" d="M492 213Q472 213 472 226Q472 230 477 250T482 285Q482 316 461 323T364 330H312Q311 328 277 192T243 52Q243 48 254 48T334 46Q428 46 458 48T518 61Q567 77 599 117T670 248Q680 270 683 272Q690 274 698 274Q718 274 718 261Q613 7 608 2Q605 0 322 0H133Q31 0 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H757Q764 676 764 669Q764 664 751 557T737 447Q735 440 717 440H705Q698 445 698 453L701 476Q704 500 704 528Q704 558 697 578T678 609T643 625T596 632T532 634H485Q397 633 392 631Q388 629 386 622Q385 619 355 499T324 377Q347 376 372 376H398Q464 376 489 391T534 472Q538 488 540 490T557 493Q562 493 565 493T570 492T572 491T574 487T577 483L544 351Q511 218 508 216Q505 213 492 213Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(764,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1336,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1802,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2235,0)"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2807,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3168,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3513,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3998,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(4598,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4848,0)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5552,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5897,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6775,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g></g><rect width="8230" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>B</mi><mi>r</mi><mi>o</mi><mi>o</mi><mi>k</mi><mi>e</mi><msup><mi>r</mi><mo data-mjx-alternate="1">′</mo></msup><mi>s</mi><mtext> </mtext><mi>R</mi><mi>a</mi><mi>t</mi><mi>i</mi><mi>o</mi><mo>=</mo><mfrac><mrow><mi>P</mi><mi>r</mi><mi>o</mi><mi>c</mi><mi>e</mi><mi>s</mi><mi>s</mi><mtext> </mtext><mi>O</mi><mi>v</mi><mi>e</mi><mi>r</mi><mi>h</mi><mi>e</mi><mi>a</mi><mi>d</mi></mrow><mrow><mi>E</mi><mi>x</mi><mi>e</mi><mi>c</mi><mi>u</mi><mi>t</mi><mi>i</mi><mi>o</mi><mi>n</mi><mtext> </mtext><mi>T</mi><mi>i</mi><mi>m</mi><mi>e</mi></mrow></mfrac></math></mjx-assistive-mml></mjx-container><p>Historically, the cost of writing, testing, and deploying code scaled linearly or exponentially with the complexity of
the feature. That is the <em>Cost of Execution</em>.</p>
<p>AI agents are driving that cost toward zero. They can produce reasonable code in a fraction of the time it would
traditionally take. But as execution compresses, everything around it stays the same: alignment, meetings, specs,
architectural reviews, etc.</p>
<p>The fixed overhead of your development process does not change. This means the <em>process</em> is now your primary bottleneck.</p>
<p>In other words:</p>
<mjx-container v-pre tabindex="0" class="MathJax" jax="SVG" display="true" style="direction: ltr; display: block; text-align: center; margin: 1em 0; position: relative;"><svg style="overflow: visible; min-height: 1px; min-width: 1px; vertical-align: -2.016ex;" xmlns="http://www.w3.org/2000/svg" width="59.83ex" height="5.165ex" role="img" focusable="false" viewBox="0 -1392 26445 2283" aria-hidden="true"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D435" d="M231 637Q204 637 199 638T194 649Q194 676 205 682Q206 683 335 683Q594 683 608 681Q671 671 713 636T756 544Q756 480 698 429T565 360L555 357Q619 348 660 311T702 219Q702 146 630 78T453 1Q446 0 242 0Q42 0 39 2Q35 5 35 10Q35 17 37 24Q42 43 47 45Q51 46 62 46H68Q95 46 128 49Q142 52 147 61Q150 65 219 339T288 628Q288 635 231 637ZM649 544Q649 574 634 600T585 634Q578 636 493 637Q473 637 451 637T416 636H403Q388 635 384 626Q382 622 352 506Q352 503 351 500L320 374H401Q482 374 494 376Q554 386 601 434T649 544ZM595 229Q595 273 572 302T512 336Q506 337 429 337Q311 337 310 336Q310 334 293 263T258 122L240 52Q240 48 252 48T333 46Q422 46 429 47Q491 54 543 105T595 229Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(759,0)"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1210,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1695,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2180,0)"><path data-c="1D458" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2701,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="msup" transform="translate(3167,0)"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(484,413) scale(0.707)"><path data-c="2032" d="M79 43Q73 43 52 49T30 61Q30 68 85 293T146 528Q161 560 198 560Q218 560 240 545T262 501Q262 496 260 486Q259 479 173 263T84 45T79 43Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mi" transform="translate(3895.5,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(4364.5,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4614.5,0)"><path data-c="1D445" d="M230 637Q203 637 198 638T193 649Q193 676 204 682Q206 683 378 683Q550 682 564 680Q620 672 658 652T712 606T733 563T739 529Q739 484 710 445T643 385T576 351T538 338L545 333Q612 295 612 223Q612 212 607 162T602 80V71Q602 53 603 43T614 25T640 16Q668 16 686 38T712 85Q717 99 720 102T735 105Q755 105 755 93Q755 75 731 36Q693 -21 641 -21H632Q571 -21 531 4T487 82Q487 109 502 166T517 239Q517 290 474 313Q459 320 449 321T378 323H309L277 193Q244 61 244 59Q244 55 245 54T252 50T269 48T302 46H333Q339 38 339 37T336 19Q332 6 326 0H311Q275 2 180 2Q146 2 117 2T71 2T50 1Q33 1 33 10Q33 12 36 24Q41 43 46 45Q50 46 61 46H67Q94 46 127 49Q141 52 146 61Q149 65 218 339T287 628Q287 635 230 637ZM630 554Q630 586 609 608T523 636Q521 636 500 636T462 637H440Q393 637 386 627Q385 624 352 494T319 361Q319 360 388 360Q466 361 492 367Q556 377 592 426Q608 449 619 486T630 554Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5373.5,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5902.5,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6263.5,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6608.5,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mo" transform="translate(7371.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" style="stroke-width: 3;"/></g><g data-mml-node="mfrac" transform="translate(8427,0)"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mi"><path data-c="1D436" d="M50 252Q50 367 117 473T286 641T490 704Q580 704 633 653Q642 643 648 636T656 626L657 623Q660 623 684 649Q691 655 699 663T715 679T725 690L740 705H746Q760 705 760 698Q760 694 728 561Q692 422 692 421Q690 416 687 415T669 413H653Q647 419 647 422Q647 423 648 429T650 449T651 481Q651 552 619 605T510 659Q484 659 454 652T382 628T299 572T226 479Q194 422 175 346T156 222Q156 108 232 58Q280 24 350 24Q441 24 512 92T606 240Q610 253 612 255T628 257Q648 257 648 248Q648 243 647 239Q618 132 523 55T319 -22Q206 -22 128 53T50 252Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(760,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1245,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1714,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(2075,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2325,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2810,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3360,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3610,0)"><path data-c="1D434" d="M208 74Q208 50 254 46Q272 46 272 35Q272 34 270 22Q267 8 264 4T251 0Q249 0 239 0T205 1T141 2Q70 2 50 0H42Q35 7 35 11Q37 38 48 46H62Q132 49 164 96Q170 102 345 401T523 704Q530 716 547 716H555H572Q578 707 578 706L606 383Q634 60 636 57Q641 46 701 46Q726 46 726 36Q726 34 723 22Q720 7 718 4T704 0Q701 0 690 0T651 1T578 2Q484 2 455 0H443Q437 6 437 9T439 27Q443 40 445 43L449 46H469Q523 49 533 63L521 213H283L249 155Q208 86 208 74ZM516 260Q516 271 504 416T490 562L463 519Q447 492 400 412L310 260L413 259Q516 259 516 260Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4360,0)"><path data-c="1D459" d="M117 59Q117 26 142 26Q179 26 205 131Q211 151 215 152Q217 153 225 153H229Q238 153 241 153T246 151T248 144Q247 138 245 128T234 90T214 43T183 6T137 -11Q101 -11 70 11T38 85Q38 97 39 102L104 360Q167 615 167 623Q167 626 166 628T162 632T157 634T149 635T141 636T132 637T122 637Q112 637 109 637T101 638T95 641T94 647Q94 649 96 661Q101 680 107 682T179 688Q194 689 213 690T243 693T254 694Q266 694 266 686Q266 675 193 386T118 83Q118 81 118 75T117 65V59Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4658,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5003,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5480,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6080,0)"><path data-c="1D45A" d="M21 287Q22 293 24 303T36 341T56 388T88 425T132 442T175 435T205 417T221 395T229 376L231 369Q231 367 232 367L243 378Q303 442 384 442Q401 442 415 440T441 433T460 423T475 411T485 398T493 385T497 373T500 364T502 357L510 367Q573 442 659 442Q713 442 746 415T780 336Q780 285 742 178T704 50Q705 36 709 31T724 26Q752 26 776 56T815 138Q818 149 821 151T837 153Q857 153 857 145Q857 144 853 130Q845 101 831 73T785 17T716 -10Q669 -10 648 17T627 73Q627 92 663 193T700 345Q700 404 656 404H651Q565 404 506 303L499 291L466 157Q433 26 428 16Q415 -11 385 -11Q372 -11 364 -4T353 8T350 18Q350 29 384 161L420 307Q423 322 423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 181Q151 335 151 342Q154 357 154 369Q154 405 129 405Q107 405 92 377T69 316T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6958,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7424,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(8024,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(8385,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(8635,0)"><path data-c="26" d="M156 540Q156 620 201 668T302 716Q354 716 377 671T401 578Q401 505 287 386L274 373Q309 285 416 148L429 132L437 142Q474 191 543 309L562 341V349Q562 368 541 376T498 385H493V431H502L626 428Q709 428 721 431H727V385H712Q688 384 669 379T639 369T618 354T603 337T591 316T578 295Q537 223 506 176T464 117T454 104Q454 102 471 85T497 62Q543 24 585 24Q618 24 648 48T682 113V121H722V112Q721 94 714 75T692 32T646 -7T574 -22Q491 -19 414 42L402 51L391 42Q312 -22 224 -22Q144 -22 93 25T42 135Q42 153 46 169T55 197T74 225T96 249T125 278T156 308L195 347L190 360Q185 372 182 382T174 411T165 448T159 491T156 540ZM361 576Q361 613 348 646T305 679Q272 679 252 649T232 572Q232 497 255 426L259 411L267 420Q361 519 361 576ZM140 164Q140 103 167 64T240 24Q271 24 304 36T356 61T374 77Q295 156 235 262L220 292L210 310L193 293Q177 277 169 268T151 229T140 164Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(9413,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(9663,0)"><path data-c="1D449" d="M52 648Q52 670 65 683H76Q118 680 181 680Q299 680 320 683H330Q336 677 336 674T334 656Q329 641 325 637H304Q282 635 274 635Q245 630 242 620Q242 618 271 369T301 118L374 235Q447 352 520 471T595 594Q599 601 599 609Q599 633 555 637Q537 637 537 648Q537 649 539 661Q542 675 545 679T558 683Q560 683 570 683T604 682T668 681Q737 681 755 683H762Q769 676 769 672Q769 655 760 640Q757 637 743 637Q730 636 719 635T698 630T682 623T670 615T660 608T652 599T645 592L452 282Q272 -9 266 -16Q263 -18 259 -21L241 -22H234Q216 -22 216 -15Q213 -9 177 305Q139 623 138 626Q133 637 76 637H59Q52 642 52 648Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(10432,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(10961,0)"><path data-c="1D459" d="M117 59Q117 26 142 26Q179 26 205 131Q211 151 215 152Q217 153 225 153H229Q238 153 241 153T246 151T248 144Q247 138 245 128T234 90T214 43T183 6T137 -11Q101 -11 70 11T38 85Q38 97 39 102L104 360Q167 615 167 623Q167 626 166 628T162 632T157 634T149 635T141 636T132 637T122 637Q112 637 109 637T101 638T95 641T94 647Q94 649 96 661Q101 680 107 682T179 688Q194 689 213 690T243 693T254 694Q266 694 266 686Q266 675 193 386T118 83Q118 81 118 75T117 65V59Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(11259,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(11604,0)"><path data-c="1D451" d="M366 683Q367 683 438 688T511 694Q523 694 523 686Q523 679 450 384T375 83T374 68Q374 26 402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487H491Q506 153 506 145Q506 140 503 129Q490 79 473 48T445 8T417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157Q33 205 53 255T101 341Q148 398 195 420T280 442Q336 442 364 400Q369 394 369 396Q370 400 396 505T424 616Q424 629 417 632T378 637H357Q351 643 351 645T353 664Q358 683 366 683ZM352 326Q329 405 277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q233 26 290 98L298 109L352 326Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(12124,0)"><path data-c="1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(12653,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(13014,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(13359,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(13959,0)"><path data-c="1D454" d="M311 43Q296 30 267 15T206 0Q143 0 105 45T66 160Q66 265 143 353T314 442Q361 442 401 394L404 398Q406 401 409 404T418 412T431 419T447 422Q461 422 470 413T480 394Q480 379 423 152T363 -80Q345 -134 286 -169T151 -205Q10 -205 10 -137Q10 -111 28 -91T74 -71Q89 -71 102 -80T116 -111Q116 -121 114 -130T107 -144T99 -154T92 -162L90 -164H91Q101 -167 151 -167Q189 -167 211 -155Q234 -144 254 -122T282 -75Q288 -56 298 -13Q311 35 311 43ZM384 328L380 339Q377 350 375 354T369 368T359 382T346 393T328 402T306 405Q262 405 221 352Q191 313 171 233T151 117Q151 38 213 38Q269 38 323 108L331 118L384 328Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(14436,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(14686,0)"><path data-c="1D43C" d="M43 1Q26 1 26 10Q26 12 29 24Q34 43 39 45Q42 46 54 46H60Q120 46 136 53Q137 53 138 54Q143 56 149 77T198 273Q210 318 216 344Q286 624 286 626Q284 630 284 631Q274 637 213 637H193Q184 643 189 662Q193 677 195 680T209 683H213Q285 681 359 681Q481 681 487 683H497Q504 676 504 672T501 655T494 639Q491 637 471 637Q440 637 407 634Q393 631 388 623Q381 609 337 432Q326 385 315 341Q245 65 245 59Q245 52 255 50T307 46H339Q345 38 345 37T342 19Q338 6 332 0H316Q279 2 179 2Q143 2 113 2T65 2T43 1Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(15190,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(15790,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(16151,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(16617,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(17217,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g></g><g data-mml-node="mrow" transform="translate(4905,-686)"><g data-mml-node="mi"><path data-c="1D436" d="M50 252Q50 367 117 473T286 641T490 704Q580 704 633 653Q642 643 648 636T656 626L657 623Q660 623 684 649Q691 655 699 663T715 679T725 690L740 705H746Q760 705 760 698Q760 694 728 561Q692 422 692 421Q690 416 687 415T669 413H653Q647 419 647 422Q647 423 648 429T650 449T651 481Q651 552 619 605T510 659Q484 659 454 652T382 628T299 572T226 479Q194 422 175 346T156 222Q156 108 232 58Q280 24 350 24Q441 24 512 92T606 240Q610 253 612 255T628 257Q648 257 648 248Q648 243 647 239Q618 132 523 55T319 -22Q206 -22 128 53T50 252Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(760,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1245,0)"><path data-c="1D460" d="M131 289Q131 321 147 354T203 415T300 442Q362 442 390 415T419 355Q419 323 402 308T364 292Q351 292 340 300T328 326Q328 342 337 354T354 372T367 378Q368 378 368 379Q368 382 361 388T336 399T297 405Q249 405 227 379T204 326Q204 301 223 291T278 274T330 259Q396 230 396 163Q396 135 385 107T352 51T289 7T195 -10Q118 -10 86 19T53 87Q53 126 74 143T118 160Q133 160 146 151T160 120Q160 94 142 76T111 58Q109 57 108 57T107 55Q108 52 115 47T146 34T201 27Q237 27 263 38T301 66T318 97T323 122Q323 150 302 164T254 181T195 196T148 231Q131 256 131 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(1714,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(2075,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2325,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(2810,0)"><path data-c="1D453" d="M118 -162Q120 -162 124 -164T135 -167T147 -168Q160 -168 171 -155T187 -126Q197 -99 221 27T267 267T289 382V385H242Q195 385 192 387Q188 390 188 397L195 425Q197 430 203 430T250 431Q298 431 298 432Q298 434 307 482T319 540Q356 705 465 705Q502 703 526 683T550 630Q550 594 529 578T487 561Q443 561 443 603Q443 622 454 636T478 657L487 662Q471 668 457 668Q445 668 434 658T419 630Q412 601 403 552T387 469T380 433Q380 431 435 431Q480 431 487 430T498 424Q499 420 496 407T491 391Q489 386 482 386T428 385H372L349 263Q301 15 282 -47Q255 -132 212 -173Q175 -205 139 -205Q107 -205 81 -186T55 -132Q55 -95 76 -78T118 -61Q162 -61 162 -103Q162 -122 151 -136T127 -157L118 -162Z" style="stroke-width: 3;"/></g><g data-mml-node="mtext" transform="translate(3360,0)"><path data-c="A0" d="" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(3610,0)"><path data-c="1D438" d="M492 213Q472 213 472 226Q472 230 477 250T482 285Q482 316 461 323T364 330H312Q311 328 277 192T243 52Q243 48 254 48T334 46Q428 46 458 48T518 61Q567 77 599 117T670 248Q680 270 683 272Q690 274 698 274Q718 274 718 261Q613 7 608 2Q605 0 322 0H133Q31 0 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H757Q764 676 764 669Q764 664 751 557T737 447Q735 440 717 440H705Q698 445 698 453L701 476Q704 500 704 528Q704 558 697 578T678 609T643 625T596 632T532 634H485Q397 633 392 631Q388 629 386 622Q385 619 355 499T324 377Q347 376 372 376H398Q464 376 489 391T534 472Q538 488 540 490T557 493Q562 493 565 493T570 492T572 491T574 487T577 483L544 351Q511 218 508 216Q505 213 492 213Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4374,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(4946,0)"><path data-c="1D452" d="M39 168Q39 225 58 272T107 350T174 402T244 433T307 442H310Q355 442 388 420T421 355Q421 265 310 237Q261 224 176 223Q139 223 138 221Q138 219 132 186T125 128Q125 81 146 54T209 26T302 45T394 111Q403 121 406 121Q410 121 419 112T429 98T420 82T390 55T344 24T281 -1T205 -11Q126 -11 83 42T39 168ZM373 353Q367 405 305 405Q272 405 244 391T199 357T170 316T154 280T149 261Q149 260 169 260Q282 260 327 284T373 353Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5412,0)"><path data-c="1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(5845,0)"><path data-c="1D462" d="M21 287Q21 295 30 318T55 370T99 420T158 442Q204 442 227 417T250 358Q250 340 216 246T182 105Q182 62 196 45T238 27T291 44T328 78L339 95Q341 99 377 247Q407 367 413 387T427 416Q444 431 463 431Q480 431 488 421T496 402L420 84Q419 79 419 68Q419 43 426 35T447 26Q469 29 482 57T512 145Q514 153 532 153Q551 153 551 144Q550 139 549 130T540 98T523 55T498 17T462 -8Q454 -10 438 -10Q372 -10 347 46Q345 45 336 36T318 21T296 6T267 -6T233 -11Q189 -11 155 7Q103 38 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6417,0)"><path data-c="1D461" d="M26 385Q19 392 19 395Q19 399 22 411T27 425Q29 430 36 430T87 431H140L159 511Q162 522 166 540T173 566T179 586T187 603T197 615T211 624T229 626Q247 625 254 615T261 596Q261 589 252 549T232 470L222 433Q222 431 272 431H323Q330 424 330 420Q330 398 317 385H210L174 240Q135 80 135 68Q135 26 162 26Q197 26 230 60T283 144Q285 150 288 151T303 153H307Q322 153 322 145Q322 142 319 133Q314 117 301 95T267 48T216 6T155 -11Q125 -11 98 4T59 56Q57 64 57 83V101L92 241Q127 382 128 383Q128 385 77 385H26Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(6778,0)"><path data-c="1D456" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7123,0)"><path data-c="1D45C" d="M201 -11Q126 -11 80 38T34 156Q34 221 64 279T146 380Q222 441 301 441Q333 441 341 440Q354 437 367 433T402 417T438 387T464 338T476 268Q476 161 390 75T201 -11ZM121 120Q121 70 147 48T206 26Q250 26 289 58T351 142Q360 163 374 216T388 308Q388 352 370 375Q346 405 306 405Q243 405 195 347Q158 303 140 230T121 120Z" style="stroke-width: 3;"/></g><g data-mml-node="mi" transform="translate(7608,0)"><path data-c="1D45B" d="M21 287Q22 293 24 303T36 341T56 388T89 425T135 442Q171 442 195 424T225 390T231 369Q231 367 232 367L243 378Q304 442 382 442Q436 442 469 415T503 336T465 179T427 52Q427 26 444 26Q450 26 453 27Q482 32 505 65T540 145Q542 153 560 153Q580 153 580 145Q580 144 576 130Q568 101 554 73T508 17T439 -10Q392 -10 371 17T350 73Q350 92 386 193T423 345Q423 404 379 404H374Q288 404 229 303L222 291L189 157Q156 26 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 112 180T152 343Q153 348 153 366Q153 405 129 405Q91 405 66 305Q60 285 60 284Q58 278 41 278H27Q21 284 21 287Z" style="stroke-width: 3;"/></g></g><rect width="17778" height="60" x="120" y="220"/></g></g></g></svg><mjx-assistive-mml unselectable="on" display="block" style="top: 0px; left: 0px; clip: rect(1px, 1px, 1px, 1px); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; position: absolute; padding: 1px 0px 0px 0px; border: 0px; display: block; overflow: hidden; width: 100%;"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mi>B</mi><mi>r</mi><mi>o</mi><mi>o</mi><mi>k</mi><mi>e</mi><msup><mi>r</mi><mo data-mjx-alternate="1">′</mo></msup><mi>s</mi><mtext> </mtext><mi>R</mi><mi>a</mi><mi>t</mi><mi>i</mi><mi>o</mi><mo>=</mo><mfrac><mrow><mi>C</mi><mi>o</mi><mi>s</mi><mi>t</mi><mtext> </mtext><mi>o</mi><mi>f</mi><mtext> </mtext><mi>A</mi><mi>l</mi><mi>i</mi><mi>g</mi><mi>n</mi><mi>m</mi><mi>e</mi><mi>n</mi><mi>t</mi><mtext> </mtext><mi mathvariant="normal">&amp;</mi><mtext> </mtext><mi>V</mi><mi>a</mi><mi>l</mi><mi>i</mi><mi>d</mi><mi>a</mi><mi>t</mi><mi>i</mi><mi>n</mi><mi>g</mi><mtext> </mtext><mi>I</mi><mi>n</mi><mi>t</mi><mi>e</mi><mi>n</mi><mi>t</mi></mrow><mrow><mi>C</mi><mi>o</mi><mi>s</mi><mi>t</mi><mtext> </mtext><mi>o</mi><mi>f</mi><mtext> </mtext><mi>E</mi><mi>x</mi><mi>e</mi><mi>c</mi><mi>u</mi><mi>t</mi><mi>i</mi><mi>o</mi><mi>n</mi></mrow></mfrac></math></mjx-assistive-mml></mjx-container><p>Then he goes on and talks about three distinct side effects of that change: <strong>the cheapened middle market</strong> (work that
was already viable got cheaper to create and develop), <strong>the sprawl</strong> (cheap low-value software became newly viable;
the &quot;vibe-coded&quot; surge), and <strong>the frontier</strong> (expensive high-value software became attemptable, with long lead times).
His own caveat on the last one sticks with me: <em>&quot;because costs and complexity here are rather high, the effects of this
change aren't as visible yet.&quot;</em></p>
<p>Three effects. Three kinds of projects. Different economics, different failure modes, different staffing shapes.</p>
<p>Our job as lead engineers is portfolio construction: Deciding what kinds of bets the team is making, in what
proportions, against what time horizons. The three buckets are how you see the portfolio clearly. And by default – if
you don't actively manage it – is that one bucket quietly starves while your attention is on the others. Ask me how
I know.</p>
<h2 id="the-three-buckets-in-your-portfolio" tabindex="-1">The three buckets in your portfolio. <a class="header-anchor" href="#the-three-buckets-in-your-portfolio" aria-label="Permalink to “The three buckets in your portfolio.”">&#8203;</a></h2>
<h3 id="_1-mid-market-core" tabindex="-1">1. Mid-Market Core <a class="header-anchor" href="#_1-mid-market-core" aria-label="Permalink to “1. Mid-Market Core”">&#8203;</a></h3>
<p><em>The roadmap. The known-good SaaS work.</em></p>
<p>These are the projects that already have specs. Your PM can talk about them from memory. The shape is clear, the
requirements are stable, the question is execution quality. Using Brooker's framing, this is <em>Product work, not Craft
work</em>.</p>
<p>What it feels like to work in this bucket right now: the typing is fast enough to feel like cheating. An agent will
scaffold the UI in an afternoon. The hard part isn't the code anymore – it's noticing that the agent quietly used three
different color schemes across two versions of the same feature because the existing code had inconsistencies, and it
averaged across them.</p>
<p>Misalignment is now the most expensive part of the lifecycle. You can ship a big refactor in a day; validating that it
matches the intent across a dozen endpoints takes three days in code review. <strong>Reviewer's Burnout</strong> is the lived
experience of this inversion – you spend more time saying &quot;no, that's not exactly what we meant&quot; than you do approving.</p>
<p>What it feels like to work in this bucket right now: the typing is fast enough to feel like cheating. An agent will
scaffold the UI in an afternoon. The hard part isn't the code anymore. It's noticing that the agent quietly used three
different color schemes across two versions of the same feature because the existing code had inconsistencies, and it
averaged across them.</p>
<p>This is the bucket that screams loudest in leadership reviews. The Linear burndown is here. The flashy demo is here.
High velocity is <u>expected</u>. It's extremely easy to let it grow to eighty percent of the backlog while you're not looking…
which is exactly how it overtakes the space of the other two buckets of projects.</p>
<p>Misalignment is now the most expensive part of the Software Delivery Lifecycle (SDLC). You can ship a big refactor in a day;
validating that it matches the intent across a dozen endpoints takes three days in code reviews. <strong>Reviewer's Burnout</strong> is the lived
experience of this inversion: you spend more time saying &quot;no, that's not exactly what we meant&quot; than you do approving.</p>
<h3 id="_2-the-sprawl" tabindex="-1">2. The Sprawl <a class="header-anchor" href="#_2-the-sprawl" aria-label="Permalink to “2. The Sprawl”">&#8203;</a></h3>
<p><em>Small utilities. Internal tools. Niche experiments.</em></p>
<p>Brooker points at <a href="https://x.com/MarcJBrooker/status/2041246043988516960?s=20" target="_blank" rel="noreferrer">@CNC_Kitchen</a> as an example: A hobbyist building a tool &quot;that many people have wanted for a long time&quot;
and he built it alone, in a weekend. In the old cost structure it never penciled out. He's explicit that this class of software
is <em>good for consumers</em>, even if it looks like <em>slop</em> to craftspeople.</p>
<p>Our version of this materializes quietly. A customer-success manager has been hand-copying numbers into a spreadsheet
every Monday for months to spot unusual movement. Not a roadmap item. Not anyone's roadmap item. Someone hears about it,
spins up a small agent-assisted task in an afternoon, and now the CSM shares an auto-refreshing view with a customer
every week. Two hours of engineering time. Several hours a week of CSM time recovered.</p>
<p>That's the bucket. Useful-to-someone, low-value-at-scale, near-zero-cost. Work that wouldn't have been built a year ago.</p>
<p>The failure mode is <em>Sprawling tech debt</em>: the new tool works beautifully. Three months later, an engineer updates the
schema in the database and it breaks silently. A report goes out with stale numbers, and the post-mortem starts with the
sentence <em>&quot;we didn't even know about this dependency.&quot;</em> The <em>Shadow AI</em> risk sneaks in the same way: think a personal
agent-powered script to do some work that's never committed to source control, runs from their laptop only with no audit
trail. Your compliance and security tooling has zero visibility into what it touches.</p>
<p>You have to cap bucket two work, and audit it every quarter. Underinvest in it, and you miss real wins; overinvest, and
you spend Fridays reading Slack threads about mystery CLIs and shadow workflows.</p>
<h3 id="_3-foundational-frontier" tabindex="-1">3. Foundational Frontier <a class="header-anchor" href="#_3-foundational-frontier" aria-label="Permalink to “3. Foundational Frontier”">&#8203;</a></h3>
<p><em>Load-bearing platform bets.</em></p>
<p>These are foundational investments that used to be multi-year projects: A live-scoring evaluation framework several
downstream improvements depend on or that infrastructure migration that no customer will ever notice, but every future
feature depends on. They would have been staffing-suicide at a lean startup two years ago. They're possible now because
the cost curve flattened enough that a small team with agents can credibly attempt them. This is the biggest benefit of
this shift, and it's the one most teams might miss.</p>
<p>What it feels like: slow, quiet, full of architecture documents and technical decisions. You spend eight weeks
finalizing that new framework, and you can't point at a shipped feature.</p>
<p>A Bucket 3 project can die in a single planning session. Someone points at it and asks: <em>&quot;What's the customer impact if
we delay this a quarter?&quot;</em> The honest <em>planning</em> answer might be: <em>none.</em> The true answer: <em>every downstream thing we
want to ship sits on top of this platform, and if we don't rebuild it now, we're shipping all of it onto a substrate
that breaks at the next traffic inflection point.</em> The first answer is what the roadmap sees. The second is what the
system actually needs. Your leadership skills are key for this bucket.</p>
<p>There's a second, slower consequence worth naming: <strong>Junior Talent Starvation</strong>. When platform bets are staffed with
senior engineers and an army of agents, the work gets done fast – but the judgment behind the work becomes invisible.
Why DynamoDB over Postgres for ephemeral state? Why these retry semantics? Why that new orchestration boundary fits better?
Someone with experience makes those calls intuitively; an agent builds the solution without asking. Engineers early in
their career are not learning <em>the whys</em> anymore. A year from now, when you need to staff the next platform bet, the pipeline
of engineers who can lead it might have dried up, unless you consciously make sure that it’s happening.</p>
<h2 id="visibility-asymmetry" tabindex="-1">Visibility Asymmetry <a class="header-anchor" href="#visibility-asymmetry" aria-label="Permalink to “Visibility Asymmetry”">&#8203;</a></h2>
<p>Here's the part I keep coming back to. The three buckets are not equally visible to other people when making planning and staffing decisions
— and the ordering is inverted from the signal.</p>
<p><em>The sprawl is loud.</em> Bucket 2 dominates the twitter feed. Every &quot;look what I shipped in an afternoon&quot; post lives here. Every &quot;AI slop&quot;
complaint lives here. Brooker's own line is that the flood is the most <em>visible</em> effect — which is precisely why it drives the
narrative.</p>
<p><em>The mid-market core is invisible by design.</em> Bucket 1 looks, from the outside, like the same roadmap work teams have always
shipped. The economics changed underneath — margins compressed, velocity expectations climbed. A feature ships. The roadmap stays green.
The <strong>PR Count</strong> per feature has exploded, but nobody outside the team can tell that the ratio of alignment-to-execution quietly inverted.<br>
Now we need to track <strong>Decision Velocity</strong>  — how fast a decision moves from idea to shipped.</p>
<p><em>The last one hasn't happened yet.</em> Brooker's observation — that Bucket 3 effects aren't visible because lead times are long — is
the one that matters most for planning. The Bucket 3 projects I should be funding now will not ship this quarter. They might not
ship next quarter. You won't get quick rewards for starting these projects.</p>
<p><strong>Volume across buckets is anti-signal</strong> The loudest bucket carries the least information about where the moat
is. Out-executing inside Bucket 1 isn't a moat anymore. Bucket 3 is where you build the new moats.</p>
<h2 id="portfolio-review" tabindex="-1">Portfolio Review <a class="header-anchor" href="#portfolio-review" aria-label="Permalink to “Portfolio Review”">&#8203;</a></h2>
<p>Now that our job is portfolio construction, take time to review your own portfolio.</p>
<p>Answer these three honest audit questions to help manage it:</p>
<ul>
<li>Is my team too focused on shipping &quot;commodity&quot; features from Bucket #1?</li>
<li>Is my Bucket #2 work capped, or is it just that I'm not seeing it?</li>
<li>Is my Bucket #3 investment where it needs to be? What's the story if the answer is no?</li>
</ul>
<p>You can also ask <a href="https://claude.ai/new?q=Read%20this%20article%20https%3A%2F%2Ffoxteck.org%2Fposts%2Fbuilding-software%2Fthree-buckets-of-ai-projects.html%20and%20help%20me%20come%20up%20with%20more%20%27Portfolio%20Review%27%20questions%20to%20help%20me%20drive%20my%20own%20audit." target="_blank" rel="noreferrer">Claude for help</a>. Here's <a href="https://claude.ai/share/d6a3884c-51ae-4aec-9523-1fbdc6ab288f" target="_blank" rel="noreferrer">an example</a>.</p>
<p>Let me know if you’re seeing something similar. Thanks for reading!</p>
<p>Now, coffee time. Enjoy!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> The 4:6 portfolio planning method</h5>
  <p>Block 90 minutes. Brew a perfect pour-over using <a href="https://youtu.be/wmCW8xSWGZY?t=21" target="_blank">the 4:6 method</a>:</p>
  <ul>
    <li>Use 20g coffee (coarsest setting, similar to a French press) and 300ml water.</li>
    <li>Divide your water into five equal 60ml pours, leaving about 45 seconds between each.</li> 
    <li>At three and a half minutes, remove the dripper.</li>
  </ul>
  <p>This method yields a brilliant cup, giving you exactly the focus you need because this exercise takes a while.</p>
  <p>Pull your current quarter's project list onto a single page. For each project, write one letter: <strong>C</strong> for Core, <strong>S</strong> for Sprawl, <strong>F</strong> for Frontier. Don't let yourself write two letters for a project.</p>
  <p>Count them up. Convert to percentages. Think if your portfolio is balanced.</p>
  <p>For every F on the list, write one sentence describing the argument you'd make in a meeting when someone asks what the customer impact is if you delay it a quarter. If you can't, the project is probably going to die in the next quarterly planning.</p>
  <p>Bring the sheet to your next skip-level and discuss it. The most useful hour of the quarter is usually the one where the two of you disagree about a letter.</p>
</div>]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Rapid experimentation with Large-Language Models]]></title>
            <link>https://foxteck.org/posts/building-software/llm-experimentation.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/llm-experimentation.html</guid>
            <pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="llm-experimentation" tabindex="-1">LLM Experimentation <a class="header-anchor" href="#llm-experimentation" aria-label="Permalink to “LLM Experimentation”">&#8203;</a></h1>
<h2 id="intro" tabindex="-1">Intro <a class="header-anchor" href="#intro" aria-label="Permalink to “Intro”">&#8203;</a></h2>
<p>For Q4 2024, my focus at Standard Metrics has been experimenting with LLMs for Data Extraction from Financial Documents startups share
with their investors (think an export of your income statement from QuickBooks or a Slide deck for your investors).</p>
<p>It's been a learning experience, and for me, a good first foray into the world of &quot;productionizing&quot; Generative AI.</p>
<p>I joined the project specifically to iterate and improve the accuracy of the LLM results and build our own evaluation framework.</p>
<h2 id="evaluations" tabindex="-1">Evaluations <a class="header-anchor" href="#evaluations" aria-label="Permalink to “Evaluations”">&#8203;</a></h2>
<p>Evaluations are a set of &quot;deterministic tests&quot; running against a prompt, model and a dataset that score how good the LLM
results are for a specific use case, and for us, how much of the result can flow into our system.</p>
<p>A representative set of evaluations measure how well an AI feature is working, and helps detect and avoid regressions as the
feature evolves or the models update.</p>
<p>I modeled our evaluation metrics considering data extraction as &quot;suggestions to a human in the loop.&quot; — We give the LLM a document, and
it says: <em>&quot;These are the metrics from this document I suggest you push into your system.&quot;</em>  Then, the Data Solutions team analyzes the
results and can accept or reject any of the data points.</p>
<p>If you're a developer, this is very similar to the suggestions you get when using Gen AI in your IDE or a terminal like <a href="https://www.warp.dev/" target="_blank" rel="noreferrer">Warp</a>:</p>
<figure> 
<img src="https://foxteck.org/posts/llm-experimentation/warp-auto-complete.png" alt="Warp auto complete" width="600px"/>
<figcaption>Warp Terminal auto completing the "uv add" command</figcaption>
</figure>
<p>Accuracy is essential to maintain Standard Metrics' high bar for data quality. The &quot;human in the loop&quot; model treats the LLM more
like a &quot;suggestions engine&quot; or an &quot;AI-powered assistant&quot; to guarantee accuracy.
It acknowledges that while the LLMs are very valuable for data extraction, they are still improving and need oversight.</p>
<h3 id="evaluation-metrics" tabindex="-1">Evaluation Metrics <a class="header-anchor" href="#evaluation-metrics" aria-label="Permalink to “Evaluation Metrics”">&#8203;</a></h3>
<p>Our evaluation metrics penalize the LLM if it fails to include one of our Standard Metrics from the document, if the suggested
metric value does not match the actual reported value, or if the team rejects any part of the suggestions.</p>
<p>Additionally, since LLMs do not always provide the expected output, we also score whether the response aligns with the requested
schema format.</p>
<h3 id="gold-standard-dataset" tabindex="-1">Gold-Standard Dataset <a class="header-anchor" href="#gold-standard-dataset" aria-label="Permalink to “Gold-Standard Dataset”">&#8203;</a></h3>
<p>A key component of our framework is our &quot;gold-standard&quot; dataset used for scoring the accuracy of the data extracted by the LLMs
against a known set of metrics in our platform (this is sometimes also called a &quot;reference-based&quot; or &quot;context-free&quot; evaluation
dataset).
This dataset provides the ground truth for evaluating every experiment we run.</p>
<p>As the the first step of this project, we wanted to measure the current performance as baseline to compare against as we
ran experiments. The Collections and Data Solutions teams had been running this feature's proof of concept since Q3, so I had enough data to
build a &quot;gold-standard dataset&quot; for evaluations.</p>
<p>I built our dataset with a Jupyter Notebook, pulling historical documents from the system, comparing the LLM responses against
metric values in Standard Metrics, and storing the &quot;gold-standard&quot; values in a DuckDB database.</p>
<h3 id="evaluation-framework" tabindex="-1">Evaluation Framework <a class="header-anchor" href="#evaluation-framework" aria-label="Permalink to “Evaluation Framework”">&#8203;</a></h3>
<p>The evaluation framework uses <a href="https://github.com/promptfoo/promptfoo" target="_blank" rel="noreferrer">Prompt Foo</a> with
a <a href="https://www.promptfoo.dev/docs/configuration/expected-outputs/javascript/#external-script" target="_blank" rel="noreferrer">custom provider</a>
and <a href="https://www.promptfoo.dev/docs/configuration/expected-outputs/javascript/#external-script" target="_blank" rel="noreferrer">custom assertions</a> for each one of
our four core metrics.</p>
<p>The custom provider fetches the LLM results, and the assertions compare them with the &quot;gold-standard&quot;
values from DuckDB, applying our scoring metrics.</p>
<p>At the end of each evaluation, we get a summary table with the total scores for each one of our metrics.</p>
<p>These were our baseline results using GPT-4o:</p>
<div class="language-bash"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;user-select:none;-webkit-user-select:none">$ </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> evaluator.ts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --hash</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> baseline</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --pdf</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">✅</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Processing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> llm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 10:35.255</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (m:ss.mmm) </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">🧮</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Collecting</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 1.038s</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Eval</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> scores</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> for</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pdf</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> files:</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">┌────────────────────┬──────────────┬──────────────┬─────────────────┐</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> schema_consistency</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_count</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_match</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> acceptance_rate</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├────────────────────┼──────────────┼──────────────┼─────────────────┤</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">               0.95</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.73</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.47</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">            0.61</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">└────────────────────┴──────────────┴──────────────┴─────────────────┘</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">⚫◗</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Adding</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> score</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 81.669ms</span></span></code></pre>
</div><p>This is how we interpret these results:</p>
<ul>
<li><strong>Schema consistency</strong>: GPT-4o's responses (with &quot;<a href="https://platform.openai.com/docs/guides/function-calling" target="_blank" rel="noreferrer">function calling</a>&quot;)
followed our expected schema 95% of time. It was generally producing a response with our JSON schema but not every time.
This is one of the main subtle differences between calling a normal API endpoint and a LLM.</li>
<li><strong>Metric Count</strong>: It identified and extracted the expected <em><strong>metrics</strong></em> in each document 73% of the time. It's capturing a high
portion of the metrics, but it is still missing metrics.</li>
<li><strong>Metric Match</strong>: This score shows how often the <em><strong>values</strong></em> GPT-4o suggested match what we expect from the reference dataset.
Only 47% of the time, which is pretty low.</li>
<li><strong>Acceptance Rate</strong>: The Data Solutions team accepted only 61% of the data points GPT-4o suggested from each document. Little
over half of the data points were deemed correct or useful by the team. Lots of room for improvement.</li>
</ul>
<p>Summarizing the results, we found GPT-4o consistently identify most of the metrics, but there was a significant opportunity
to improve its accuracy.</p>
<h2 id="our-experimentation-workflow" tabindex="-1">Our experimentation workflow <a class="header-anchor" href="#our-experimentation-workflow" aria-label="Permalink to “Our experimentation workflow”">&#8203;</a></h2>
<p>With this framework, we've reduced our feedback loop between thinking of an experiment that will improve the accuracy of the LLM
results and getting a score to decide if we push the experiment to production.</p>
<p>We have three phases after defining the experiment:</p>
<ol>
<li><strong>Building the test case</strong>: Getting a list of documents we'll use for the test and validating parameters (like the mix of
prompt, model, and version).</li>
<li><strong>Generating the results</strong>: Invoking the LLM pipeline with our parameters against all the documents in the dataset.</li>
<li><strong>Evaluating results</strong>: Fetching the results, running the evaluations and scores, and finally analyzing and comparing the results.</li>
</ol>
<p>When we started working with LLMs this was a manual, nuanced, and error-prone process that took a couple of days. Now, the entire
workflow runs in less than 30 minutes. At the end of the run, we have a clear decision on whether the experiment is good to go to
production.</p>
<h2 id="key-experiments-amp-findings" tabindex="-1">Key experiments &amp; findings <a class="header-anchor" href="#key-experiments-amp-findings" aria-label="Permalink to “Key experiments &amp;amp; findings”">&#8203;</a></h2>
<h4 id="anthropic-s-pdf-pre-processing-beta" tabindex="-1">Anthropic's PDF Pre-processing (beta) <a class="header-anchor" href="#anthropic-s-pdf-pre-processing-beta" aria-label="Permalink to “Anthropic's PDF Pre-processing (beta)”">&#8203;</a></h4>
<p>I've always been partial to Claude in my day to day. Comparing Sonnet against GPT-4o was one of the first things I wanted to
evaluate.  When they announced <a href="https://docs.anthropic.com/en/docs/build-with-claude/pdf-support" target="_blank" rel="noreferrer">beta support for PDFs</a> we experimented
with it right away.</p>
<div class="language-bash"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;user-select:none;-webkit-user-select:none">$ </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> evaluator.ts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --hash</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> V3ZpJYMXI</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --prompt</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "anthropic-base-prompt:6"</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --pdf</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">✅</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Processing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> llm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 10:44.656</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (m:ss.mmm)</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">🧮</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Collecting</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  1.318s</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Eval</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> scores</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> for</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pdf</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> files:</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">┌────────────────────┬──────────────┬──────────────┬─────────────────┐</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> schema_consistency</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_count</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_match</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> acceptance_rate</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├────────────────────┼──────────────┼──────────────┼─────────────────┤</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">                1.0</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.94</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.64</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">             0.8</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">└────────────────────┴──────────────┴──────────────┴─────────────────┘</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">⚫◗</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Adding</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> score</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 82.101ms</span></span></code></pre>
</div><p>Anthropic does an outstanding job pre-processing a file on their side, and injecting it as context to the prompt.
Claude Sonnet (claude-3-5-sonnet-20241022) performed better than GPT-4o across all four evaluation metrics in this experiment.
It's now our default model for PDFs that fall
within <a href="https://docs.anthropic.com/en/docs/build-with-claude/pdf-support#check-pdf-requirements" target="_blank" rel="noreferrer">their API limits</a>.</p>
<h4 id="openai-s-o1-preview-model" tabindex="-1">OpenAI's o1-preview model <a class="header-anchor" href="#openai-s-o1-preview-model" aria-label="Permalink to “OpenAI's o1-preview model”">&#8203;</a></h4>
<p>When OpenAI announced o1, we were curious to know if it improved OpenAI's performance compared to its previous model. We had to
tweak our OpenAI wrapper module for this experiment because o1 does not support calling tools and does not support system prompts
either.</p>
<p>The o1-preview model could only process 30% of the documents in the dataset; for those, the results were much better than GPT-4o:</p>
<div class="language-bash"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;user-select:none;-webkit-user-select:none">$ </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> evaluator.ts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --hash</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> o1</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --prompt</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "openai-base-prompt:8"</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --pdf</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">✅</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Processing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> llm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 15:32.182ms</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (m:ss.mmm) </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">🧮</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Collecting</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 1.002ms</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Eval</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> scores</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> for</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pdf</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> files:</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">┌────────────────────┬──────────────┬──────────────┬─────────────────┐</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> schema_consistency</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_count</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_match</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> acceptance_rate</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├────────────────────┼──────────────┼──────────────┼─────────────────┤</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">               0.96</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.97</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.65</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">             0.8</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">└────────────────────┴──────────────┴──────────────┴─────────────────┘</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">⚫◗</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Adding</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> score</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 47.028ms</span></span></code></pre>
</div><p>While the results were substantially better than GPT-4o for the subset of the data the model could process, Claude Sonnet still scored higher for
the same subset of documents:</p>
<div class="language-bash"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;user-select:none;-webkit-user-select:none">$ </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsx</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> evaluator.ts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --hash</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> V3ZpJYMXI</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --prompt</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "anthropic-base-prompt:6"</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --pdf</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">✅</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Processing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> llm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 9:22.180</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (m:ss.mmm) </span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">🧮</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Collecting</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> results:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 973.689ms</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Eval</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> scores</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> for</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pdf</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> files:</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">┌────────────────────┬──────────────┬──────────────┬─────────────────┐</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> schema_consistency</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_count</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> metric_match</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> acceptance_rate</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">       │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    │</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     double</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">      │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├────────────────────┼──────────────┼──────────────┼─────────────────┤</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">                1.0</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.94</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">         0.67</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">             0.9</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">└────────────────────┴──────────────┴──────────────┴─────────────────┘</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">⚫◗</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Adding</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> score</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 72.189ms</span></span></code></pre>
</div><h2 id="what-s-next" tabindex="-1">What's next <a class="header-anchor" href="#what-s-next" aria-label="Permalink to “What's next”">&#8203;</a></h2>
<p>Our next set of experiments are around &quot;chained prompts&quot; or &quot;prompt routing&quot;, having the LLM identify which prompt from our
library we should use for specific types of documents, and do several passes with these simpler, more specific prompts to extract
different sets of data.</p>
<p>We also want to experiment with <a href="https://ds4sd.github.io/docling/v2/" target="_blank" rel="noreferrer">Docling</a>,
and <a href="https://github.com/microsoft/markitdown" target="_blank" rel="noreferrer">Markitdown</a> for pre-processing different types of documents. They do something very
similar to Anthropic's PDF beta feature. They parse documents from different formats like PDF, PPT, Excel and Word, and export it
to Markdown or JSON.</p>
<p>These exports are easier for LLMs to understand the documents, and we theorize that they will increase accuracy across the board for data extraction.</p>
<h2 id="closing" tabindex="-1">Closing <a class="header-anchor" href="#closing" aria-label="Permalink to “Closing”">&#8203;</a></h2>
<p>Language models continue to get bigger and better; we expect scores to improve, too. We'll continue refining, adopting new strategies,
and experimenting towards our goal of 100% accuracy, and engineering more &quot;AI-enhanced&quot; features for Standard Metrics.</p>
<p>Thanks for reading. Now, coffee time!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> Coffee Cupping</h5>
  <p>
    <strong>Method: </strong> Cupping <br />
    <strong>Preparation Time:</strong> 20 minutes
  </p>
<p>Coffee cupping is coffee evaluation. It gives you a shared language to understand coffee tastes.</p>
<h6>Steps</h6>
  <ol>
    <li>Grind your favorite coffee beans to a medium grind. Similar to what you'd use for a v60</li>
    <li>Grab a cup, use 10 grams of coffee on the cup</li>
    <li>Fill the cup with hot water evenly to the brim, let them sit for four minutes</li>
    <li>Break "the crust" of grounds floating in the surface with a round spoon pushing and swirling gently as you lower your nose close to the cup</li>
    <li>Take notes of the smells you get from the coffee</li>
    <li>Remove the floating ground coffee from the top with the help of the spoon</li>
    <li>Wait for the coffee to cool down to a temperature you're comfortable drinking</li>
    <li>Slurp your coffee with the help of the spoon. Be bold when slurping, it sprays the coffee flavor across your palate to "catch" more flavors</li>
    <li>Take notes again of the flavors you get from the slurp.</li>
 </ol>
<p>When tasting, try to identify: Acidity (bright? citrusy? winey?). Body (light? heavy? creamy?). Flavors (fruity? nutty? chocolate? earthy?)
and the Aftertaste.</p>
<p>Here's the full video <a href="https://www.youtube.com/watch?v=PUPgZNlrZxc" target="_blank">What Is Coffee Cupping</a> from Onyx Lab.</p>
</div>
<h2 id="learning-more" tabindex="-1">Learning More 📚 <a class="header-anchor" href="#learning-more" aria-label="Permalink to “Learning More”">&#8203;</a></h2>
<ul>
<li><a href="https://www.anthropic.com/research/evaluating-ai-systems" target="_blank" rel="noreferrer">Challenges evaluating AI systems</a>.</li>
<li><a href="https://hamel.dev/blog/posts/evals/" target="_blank" rel="noreferrer">Your AI Product needs evals</a>.</li>
<li><a href="https://eugeneyan.com/writing/evals/" target="_blank" rel="noreferrer">Task-specific LLM evals that Do and Don't work</a>.</li>
<li><a href="https://simonwillison.net/2024/Oct/30/llm-as-a-judge/" target="_blank" rel="noreferrer">Using domain experts (human in the loop) for scoring LLMs</a>.</li>
</ul>
]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Deploying Node and Python Lambda Functions with CDK]]></title>
            <link>https://foxteck.org/posts/building-software/deploying-lambda-functions-with-cdk.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/deploying-lambda-functions-with-cdk.html</guid>
            <pubDate>Mon, 11 Nov 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2 id="deploying-λ-functions-with-cdk" tabindex="-1">Deploying λ Functions with CDK <a class="header-anchor" href="#deploying-λ-functions-with-cdk" aria-label="Permalink to “Deploying λ Functions with CDK”">&#8203;</a></h2>
<p>I've started using AWS Lambda again for a few projects at Standard Metrics.</p>
<p>Our <em>core</em> codebase is Python for the backend and TypeScript for the frontend (with React/Next).</p>
<p>Most of our existing lambda functions are in TypeScript, and they work pretty well. We get reasonable cold-starts, complete TypeScript tooling with CDK support,
bundling and tree-shaking with esbuild, fast install/deployment times with pnpm, tagging for Vanta compliance, and full observability with DataDog.</p>
<p>However, for my most recent project we needed Python's powerful data-wrangling capabilities, which meant working with both Python and TypeScript in the same project.
I couldn't find  documentation out in the wild for this use case, so I had to figure out how to seamlessly integrate the two languages.</p>
<p>This is the primary subject of this blog post, and we are going over examples of how we define and configure those functions and the infrastructure we use to deploy them to production.</p>
<h3 id="nodejs-lambda-functions" tabindex="-1">NodeJS Lambda Functions <a class="header-anchor" href="#nodejs-lambda-functions" aria-label="Permalink to “NodeJS Lambda Functions”">&#8203;</a></h3>
<p>We use the NodeJS functions to handle HTTP requests. We define a set of “sane” defaults as base configuration for all these functions:</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark has-highlighted" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> LAMBDA_DEFAULTS</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Omit</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">NodejsFunctionProps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'functionName'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'entry'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'handler'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">> </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    memorySize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1024</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    runtime: lambda.Runtime.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">NODEJS_20_X</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    timeout: Duration.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">minutes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line highlighted warning"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    tracing: Tracing.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">DISABLED</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    bundling: {</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        minify: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        sourceMap: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        sourcesContent: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        target: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'es2022'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        loader: {</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '.node'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'file'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        esbuildArgs: {</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '--tree-shaking'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        nodeModules: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'aws-cdk-lib'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        externalModules: [</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '@aws-sdk/*'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            'aws-lambda'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '@datadog/native-metrics'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            '@datadog/pprof'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            'dd-trace'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">            'datadog-lambda-js'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    environment: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        stage: config.stage,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        identifier: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">identifier</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ''}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        WEBHOOK_CLIENT_ID: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Serverless-Doc-Processing'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br><span class="line-number">29</span><br><span class="line-number">30</span><br><span class="line-number">31</span><br><span class="line-number">32</span><br></div></div><p>We use <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtimes-supported" target="_blank" rel="noreferrer">Node 20</a> which was the newest Node version supported by Lambda at the time.
Node 22 was just released this month. v18 is available with support until mid/late 2025.</p>
<p>We disable tracing (XRay) because we use DataDog (via
their <a href="https://www.npmjs.com/package/datadog-cdk-constructs-v2" target="_blank" rel="noreferrer">constructs</a> and layer <a href="https://github.com/DataDog/datadog-lambda-extension" target="_blank" rel="noreferrer">extension</a>).</p>
<p>For bundling, we disable AWS SDK / DataDog bundling, target ES2022 and minify the code. When we bundle <a href="https://github.com/duckdb/duckdb-node" target="_blank" rel="noreferrer">DuckDB with NodeJS</a> we also use
the <code>afterBundling</code> <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.ICommandHooks.html#interface-icommandhooks" target="_blank" rel="noreferrer">command hook</a>
to package the binary correctly for the specific targeted architecture (arm64 or x86).</p>
<p>We override these defaults with function specific settings like the function name and handler entry point, tweak memory and disk configuration (if needed), set the number of concurrent
executions to limit the impact of spikes when calling our webhooks and pass environment variables needed like secrets.</p>
<p>Here is an example:</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> function</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NodejsFunction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`lambda-${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">identifier</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      ...</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">LAMBDA_DEFAULTS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      functionName: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`json-processing-lambda-${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">identifier</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      entry: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'./src/handlers/events/json-processing-handler.ts'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      handler: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'index.handler'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      memorySize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2560</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      ephemeralStorageSize: Size.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mebibytes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1024</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      reservedConcurrentExecutions: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      environment: {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        ...</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">LAMBDA_DEFAULTS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.environment,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        WEBHOOK_SECRET: webhook_secret.secretName,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        WEBHOOK_API_CACHE_TTL: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">timeout</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">toSeconds</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">()</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    })</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><p>We use <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html" target="_blank" rel="noreferrer">NodejsFunction</a> instead of the more generic <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html" target="_blank" rel="noreferrer">Function</a> construct.
This gives us more control over <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.BundlingOptions.html" target="_blank" rel="noreferrer">bundling</a> with the options in our defaults to tweak the resulting code via  <a href="https://esbuild.github.io/" target="_blank" rel="noreferrer">esbuild</a>.</p>
<h3 id="python-lambda-functions-as-docker-containers" tabindex="-1">Python Lambda Functions as Docker Containers <a class="header-anchor" href="#python-lambda-functions-as-docker-containers" aria-label="Permalink to “Python Lambda Functions as Docker Containers”">&#8203;</a></h3>
<p>For this project our data wrangling and transformation workloads use <a href="https://duckdb.org/" target="_blank" rel="noreferrer">DuckDB</a>, <a href="https://pola.rs/" target="_blank" rel="noreferrer">Polars</a>,
and <a href="https://ibis-project.org/" target="_blank" rel="noreferrer">Ibis</a>. These dependencies exceed the 256MB limit allowed for a <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-lambda-python-alpha.PythonFunction.html" target="_blank" rel="noreferrer">PythonFunction</a>. Docker container don’t have this limit.</p>
<p>Each function runs in as its own container from this base Dockerfile:</p>
<div class="language-dockerfile line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">dockerfile</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">FROM</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> public.ecr.aws/lambda/python:3.12-arm64</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ARG</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DEBIAN_FRONTEND=noninteractive</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># uv for arm64</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">COPY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> --from=ghcr.io/astral-sh/uv:0.4.28@sha256:3a996474ab73047f1f8a626771c029a7b82176b62965dfa90cce0d497feffb8c  /uv /bin/uv</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ENV</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> PYTHONPATH=$LAMBDA_TASK_ROOT</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ENV</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> UV_COMPILE_BYTECODE=1</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ENV</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> UV_SYSTEM_PYTHON=1</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">WORKDIR</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> $LAMBDA_TASK_ROOT</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ADD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> uv.lock $LAMBDA_TASK_ROOT</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ADD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pyproject.toml $LAMBDA_TASK_ROOT</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RUN</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> uv sync --no-python-downloads --no-progress --no-dev --frozen --no-install-project</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ADD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> python/README.md $LAMBDA_TASK_ROOT</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ADD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> python/__init__.py $LAMBDA_TASK_ROOT</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">COPY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> python/package $LAMBDA_TASK_ROOT/package</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RUN</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> uv sync --frozen --no-dev</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">RUN</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> uv pip install ./</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">ENV</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> PATH=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"$LAMBDA_TASK_ROOT/.venv/bin:$PATH"</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">CMD</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [ </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"package.module.lambda_handler"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ]</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br><span class="line-number">20</span><br><span class="line-number">21</span><br><span class="line-number">22</span><br><span class="line-number">23</span><br><span class="line-number">24</span><br><span class="line-number">25</span><br><span class="line-number">26</span><br><span class="line-number">27</span><br><span class="line-number">28</span><br></div></div><p>We built from Lambda's Python <a href="https://gallery.ecr.aws/lambda/python" target="_blank" rel="noreferrer">base image</a> (using the arm64 variation). Then, copy the base <code>project.toml</code> file and the lock file,
download all dependencies, and copy our packages to the image.</p>
<p>We use <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.DockerImageFunction.html" target="_blank" rel="noreferrer">DockerImageFunction</a> to instantiate our functions, and override the main handler
<code>CMD</code> in the function definition itself.</p>
<p>The function definition looks like this:</p>
<div class="language-typescript line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark has-highlighted" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> image</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> DockerImageCode.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">fromImageAsset</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'../'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line highlighted warning"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    cmd: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'packge.module.lambda_handler'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], </span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> function</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> DockerImageFunction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`python-function-${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">identifier</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    functionName: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`python-function-${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">config</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">identifier</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    architecture: Architecture.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">ARM_64</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    memorySize: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10240</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ephemeralStorageSize: Size.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">gibibytes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line highlighted warning"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    code: image, </span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    vpc: core_network_config.vpc,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    vpcSubnets: { subnetType: SubnetType.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">PRIVATE_WITH_EGRESS</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    securityGroups: [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">this</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.securityGroup],</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    reservedConcurrentExecutions: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    retryAttempts: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    environment: {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">      ...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">env,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
<div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br><span class="line-number">18</span><br><span class="line-number">19</span><br></div></div><p>We are using arm64 (<a href="https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/" target="_blank" rel="noreferrer">Graviton</a>)
purely for cost experimentation (these are some chubby functions with 10GB of memory and disk).</p>
<p>We want each task to run &quot;one at a time&quot; and retry <em>only once</em> on failure (see <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html#reservedconcurrentexecutions" target="_blank" rel="noreferrer">reservedConcurrentExecutions</a>
and <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html#retryattempts" target="_blank" rel="noreferrer">retryAttempts</a> options). We do that to limit concurrency contention
for the tables they query in the read replica.</p>
<p>
Because these are all event-driven tasks, we don't worry too much about <DigDeeper summary="cold starts" context=" A <strong>cold start</strong> is a request with added latency coming from the time it takes the function to initialize, bootstrap and be ready to serve requests." />. Generally speaking, container-based Python Functions take longer to bootstrap than our Node Functions. </p>
<p>Once bootstrapped, they are just as fast and behave pretty much like any other Lambda function.</p>
<h2 id="infra-as-code" tabindex="-1">Infra as Code <a class="header-anchor" href="#infra-as-code" aria-label="Permalink to “Infra as Code”">&#8203;</a></h2>
<p>The infrastructure itself is deployed with <a href="https://aws.amazon.com/cdk/" target="_blank" rel="noreferrer">AWS's CDK</a> and is also written in TypeScript.</p>
<p>Each set of functions has its own CloudFormation stack, and they belong to the same Application in AWS Lambda.
They can interact with each other and share resources (i.e., an HTTP Request can queue a Python task or can serve a Parquet file built by a Python function).</p>
<h2 id="codebase-setup" tabindex="-1">Codebase setup <a class="header-anchor" href="#codebase-setup" aria-label="Permalink to “Codebase setup”">&#8203;</a></h2>
<h3 id="nodejs-typescript" tabindex="-1">NodeJS / TypeScript <a class="header-anchor" href="#nodejs-typescript" aria-label="Permalink to “NodeJS / TypeScript”">&#8203;</a></h3>
<p>We manage the NodeJS codebase, dependencies, and scripts with <a href="https://pnpm.io/" target="_blank" rel="noreferrer">pnpm</a>. Formatting and linting are done with <a href="https://biomejs.dev/" target="_blank" rel="noreferrer">Biome</a>, and TypeScript is handled via <a href="https://tsx.is/" target="_blank" rel="noreferrer">tsx</a>.
This is the first project in which we used <a href="https://vitest.dev/" target="_blank" rel="noreferrer">vitest</a> for testing. It has excellent developer experience, which is a lot better than jest. So far, we haven't run into any issues.</p>
<p><code>tsx</code> runs both CDK and all our node code. <code>tsc</code> is only used to validate our typing <em>&quot;on demand&quot;</em> (running <code>pnpm run build</code> runs <code>tsc --noEmit</code>), but our IDEs do most of the heavy lifting as we code.</p>
<h3 id="python" tabindex="-1">Python <a class="header-anchor" href="#python" aria-label="Permalink to “Python”">&#8203;</a></h3>
<p>We are using <a href="https://docs.astral.sh/uv/" target="_blank" rel="noreferrer">uv</a> as package manager for its speed, ease of use and its ability of building reproducible environments.
It's extremely fast, can handle multiple packages, and is compatible with workspaces just like pnpm.</p>
<p>Max, one of the other engineers in the Platform team, set up a <code>pyproject.toml</code> configuration matching our <em>core backend</em> coding standards, which uv also understands.
We use <a href="https://docs.astral.sh/ruff/" target="_blank" rel="noreferrer">Ruff</a> for formatting and linting, <a href="https://docs.pytest.org/en/stable/" target="_blank" rel="noreferrer">pytest</a>
for unit tests, and <a href="https://github.com/microsoft/pyright" target="_blank" rel="noreferrer">pyright</a> for static type-checking.</p>
<div  class="tip custom-block"><p class="custom-block-title custom-block-title-default">TIP</p>
<p>Both VS Code and PyCharm/WebStorm have solid support for our tooling out of the box.</p>
</div>
<h3 id="folder-structure" tabindex="-1">Folder structure <a class="header-anchor" href="#folder-structure" aria-label="Permalink to “Folder structure”">&#8203;</a></h3>
<p>Our folder structure is slightly different from our other projects too. It looks like this:</p>
<div class="language-shell"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> CHANGELOG.md</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Dockerfile</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> Makefile</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> README.md</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> biome.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> infra</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> README.md</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> bin</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> infra.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> package.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> src</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> api.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> config</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> stages.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> constructs</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> base-constructs.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> external-imports.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> node-functions.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> python-functions.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> storage.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> app-stack.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsconfig.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> node</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> coverage</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> package.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> src</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> core</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> handlers</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> http</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> test-handler.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> events</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> shared</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> environment.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> s3.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> types</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tests</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> core</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> shared</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb.test.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> environment.test.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> s3.test.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> tsconfig.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> vitest.config.ts</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> package.json</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pnpm-lock.yaml</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pnpm-workspace.yaml</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> pyproject.toml</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> python</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> README.md</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> __init__.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> modules</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> __init__.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> module1</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> __init__.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> core.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> handlers.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> db</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> __init__.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> duckdb.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> module2</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> __init__.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ├──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> core.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     │  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> handlers.py</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">│  </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">     └──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> ...</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">└──</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> uv.lock</span></span></code></pre>
</div><p>Highlights:</p>
<ul>
<li><code>infra</code>: This is the main CDK folder, it handles all our infrastructure as code, deployment to all stages and &quot;bundling&quot; for both languages.</li>
<li><code>node</code>: These are our core TypeScript functions. It has its own dependencies, tests and coverage.</li>
<li><code>python</code>: These are our code Python functions. Each function is a different module, they share some of the packages like our lightweight duckdb abstraction.</li>
<li><code>Dockerfile</code>: This is the main dockerfile for all Python containers.</li>
<li><code>pnpm-workspace.yaml</code>: This is the workspace configuration for both <code>infra</code> and <code>node</code>. The root package.json file has the formatting/linting config and the release scripts.</li>
<li><code>pyproject.toml</code> and <code>uv.lock</code>: These are the configuration for all Python modules, including dependencies, development tools, linting, formatting and tests.</li>
</ul>
<h3 id="run-scripts-with-make" tabindex="-1">Run scripts with make <a class="header-anchor" href="#run-scripts-with-make" aria-label="Permalink to “Run scripts with make”">&#8203;</a></h3>
<p>Because we are using both Python and Node we use <code>make scripts</code> to standardize how we run scripts for both languages.</p>
<p>Running <code>make deploy</code> calls <code>cdk deploy</code> to push the code to Lambda. If we do <code>make test</code> it'll run tests for both python and node (calling <code>pyright</code> and <code>vitest</code>).</p>
<p>Here's an excerpt of our makefile:</p>
<div class="language-makefile"><button title="Copy Code" class="copy"></button><span class="lang">makefile</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.DEFAULT_GOAL := help</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">...</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.PHONY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: test-node</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">test-node</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">## Runs tests for node code</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">	pnpm --filter node run test</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.PHONY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: test-python</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">test-python</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">## Run python tests</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">	uv pytest</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.PHONY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: test test-python test-node</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">test</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:  test-node test-python </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">## Runs all tests.</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">.PHONY</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: deploy</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">deploy</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">## Builds and deploys the project to AWS</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">	pnpm --filter infra run deploy</span></span></code></pre>
</div>]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Scaling Document Processing with Serverless]]></title>
            <link>https://foxteck.org/posts/serverless/doc-processing.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/serverless/doc-processing.html</guid>
            <pubDate>Mon, 11 Nov 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1 id="scaling-doc-processing-with-serverless" tabindex="-1">Scaling Doc Processing with Serverless <a class="header-anchor" href="#scaling-doc-processing-with-serverless" aria-label="Permalink to “Scaling Doc Processing with Serverless”">&#8203;</a></h1>
<h3 id="intro-to-document-processing" tabindex="-1">Intro to Document Processing <a class="header-anchor" href="#intro-to-document-processing" aria-label="Permalink to “Intro to Document Processing”">&#8203;</a></h3>
<p>Standard Metrics' mission is to accelerate innovation in the private markets. We're building a platform that aims to become a
financial &quot;lingua franca&quot; between startups and venture firms.</p>
<p>We help startups share financial metrics with their investors effortlessly. One of our core services is analyzing, validating, and
capturing metrics from Financial Documents that startups share with their investors.</p>
<p>Our Data Solutions team is in charge of this process. They offer strong SLAs on the time it takes from when a document is uploaded
to our platform until it's analyzed and its metrics are available through our platform. It's part of their quarterly OKRs,
so they track it religiously and have goals around it.</p>
<p>Efficiency is key, especially when dealing with a high volume of documents and you want to maintain high-quality metric data.</p>
<h3 id="the-problem" tabindex="-1">The Problem <a class="header-anchor" href="#the-problem" aria-label="Permalink to “The Problem”">&#8203;</a></h3>
<p>Excel files are used ubiquitously for financial data. They represent 60% of all the documents on the platform. It's so crucial
that we built a specific tool where the Data Solutions team analyzes, validates, and maps which value belongs to which financial metric.</p>
<p>A JSON representation of the Excel file powers the tool's UI. Documents uploaded to our platform are stored and scanned for virus in S3.
Later, a Celery task within our Kubernetes cluster transforms the document into its JSON representation, and uploads it back to S3 for the UI to consume.</p>
<figure> 
<img src="https://foxteck.org/posts/serverless-doc-parsing/doc-processing-v1.png" alt="Doc Processing v1"/>
<figcaption>High-level architecture of Document Processing v1</figcaption>
</figure>
<p>The original implementation of the Document Processing queue had several issues. Processing failures would get stuck in the queue
causing bottlenecks, and a real lack of visibility into what was going wrong forced us to be reactive putting out fires as they popped up.</p>
<p>Investigating these issues was a time sink for engineering and negatively impacted the Solution team’s SLAs and OKRs.</p>
<h3 id="step-zero" tabindex="-1">Step Zero <a class="header-anchor" href="#step-zero" aria-label="Permalink to “Step Zero”">&#8203;</a></h3>
<h4 id="understanding-the-problem-with-instrumentation" tabindex="-1">Understanding the problem with instrumentation <a class="header-anchor" href="#understanding-the-problem-with-instrumentation" aria-label="Permalink to “Understanding the problem with instrumentation”">&#8203;</a></h4>
<p>The initial theory was that <a href="https://github.com/tafia/calamine" target="_blank">Calamine</a>, the library we used to read
the Excel files, was consuming too much memory for big files or files with images and getting killed by the Kubernetes controller,
blocking other tasks in the same Pod and the tasks behind it as when it was rescheduled.</p>
<p>Our highest priority was instrumenting the celery task to see into the failures. You can't fix what you don't understand.
We improved logs and metrics on the task to track &quot;Time in Queue&quot; and the number of &quot;Items in the Queue.&quot;</p>
<p>We added two alarms:</p>
<ul>
<li>When the time in queue exceeded 30 minutes.</li>
<li>When there were more than 25 files in the queue for more than 15 minutes.</li>
</ul>
<figure> 
<img src="https://foxteck.org/posts/serverless-doc-parsing/dashboard-and-alarms-v1.png" alt="Dashboard and Alarms for v1"/>
<figcaption>Dashboards and alarms (v1). — Time in queue clearly increases with load, up to a point where the queue halts processing.</figcaption>
</figure>
<h4 id="the-turning-point" tabindex="-1">The turning point <a class="header-anchor" href="#the-turning-point" aria-label="Permalink to “The turning point”">&#8203;</a></h4>
<p>Things improved significantly. Instead of finding issues days later, engineers got alerts within hours.
On the flip side, this alarm was triggering as often as three times per week. It was a step on the right direction, but things still
needed to be better for the Solution team’s SLAs.</p>
<p>We also noticed a new pattern: During reporting season (when Startups share more data with their investors) time in queue went up,
taking up to 20 minutes for individual tasks to run.</p>
<p>With all this new information, and knowing this was happening more often than initially thought, we carved out space in our
roadmap to find a solution.</p>
<h2 id="doc-processing-v2" tabindex="-1">Doc Processing v2 <a class="header-anchor" href="#doc-processing-v2" aria-label="Permalink to “Doc Processing v2”">&#8203;</a></h2>
<p>With a better understanding of how things needed to work, the team proposed a new architecture for Doc Processing where queueing
and JSON generation would happen outside our core backend using AWS Lambda and SQS instead of running inside our Kubernetes cluster running in Celery tasks.</p>
<p>Our goals for the project were:</p>
<ul>
<li><strong>Better Isolation</strong>. Processing would not affect any other tasks in the platform. And when a task fails, it is retried
automatically. It shouldn’t block other operations.</li>
<li><strong>Build with good bones</strong>. Good instrumentation makes understanding problems easier. We have better guard rails against failures
with strong retry guarantees from the infra and the ability to “replay” failures. When there's any issues we understand why they happen,
and we make sure they won't happen again in the future.</li>
<li><strong>Dogfooding and integrating with the core service.</strong> The plan was to leverage our existing backend codebase as much as
possible.</li>
</ul>
<h3 id="high-level-implementation" tabindex="-1">High-level implementation <a class="header-anchor" href="#high-level-implementation" aria-label="Permalink to “High-level implementation”">&#8203;</a></h3>
<p>We made the strategic decision of going with serverless, swapping out the Celery tasks for NodeJS lambda processors, integrating directly
with events from S3 via EventBridge.</p>
<p>A Lambda function picks up the documents when they are marked as clean by the antivirus, and queues them for processing. When the documents are processed
their JSON representation is uploaded back to S3, and there’s a notification via a webhook to our core backend with the status. Because of the SQS
integration, all files are automatically retried three times in case of failures, and they’re sent to a Dead-letter queue (DLQ)
when they’re unprocessable.</p>
<figure> 
<img src="https://foxteck.org/posts/serverless-doc-parsing/doc-processing-v2.png" alt="Doc Processing v2. High-level architecture."/>
<figcaption>High-level architectural diagram of the new version of Doc Processing.</figcaption>
</figure>
<p>We limit the volume of requests to the backend with Lambda’s reserved concurrency settings. Any spikes are processed slowly,
minimizing contention for resources with other tasks running in the backend.</p>
<blockquote>
<p>Like a coffee shop limiting the number of V60 orders it takes at rush hour to maintain quality, Lambda’s reserved concurrency prevents a flood of requests from overwhelming our backend.</p>
<p>This ensures every document gets processed efficiently, just like every customer gets their perfect cup of coffee, even during the busiest periods.</p>
</blockquote>
<h3 id="observability-and-metrics" tabindex="-1">Observability and Metrics <a class="header-anchor" href="#observability-and-metrics" aria-label="Permalink to “Observability and Metrics”">&#8203;</a></h3>
<p>Observability is a first-class citizen this time around. We use DataDog to trace the process end to end. Logs from all our lambda
functions and the backend are centralized in DataDog, too, making it much simpler to find the root cause of any issue.</p>
<p>We implemented alarms for Processing time, Queue length, and Documents in the DLQ. We can trace by document name, document id, request id
or execution id and see the flow from when a file is uploaded, when it’s queued, picked up for processing, all the way to the webhook in the backend.</p>
<p>Processing files is much faster. Our average processing time is now 8 seconds, down from 20 minutes. The maximum time processing
went down to 3 minutes, down from 4 days. Because of that, we also alarm much earlier now; we alarm when a document it's been queued for 10 minutes (down from 30 minutes) or when
there are files in the DLQ.</p>
<figure> 
<img src="https://foxteck.org/posts/serverless-doc-parsing/dashboard-and-alarms-v2.png" alt="Dashboards and Alarms for v2"/>
<figcaption>Doc Processing v2 &mdash;  Dashboards and Alarms.</figcaption>
</figure>
<h3 id="scaling-with-spikes" tabindex="-1">Scaling with spikes <a class="header-anchor" href="#scaling-with-spikes" aria-label="Permalink to “Scaling with spikes”">&#8203;</a></h3>
<p>In our previous implementation, heavier traffic was a major pain point. It usually meant the
queue slowed to down to a crawl. When tasks spiked 3x to 4x our regular traffic, resource contention started bleeding into
other endpoints and celery tasks.</p>
<p>We handle spikier loads gracefully now. Our most recent spike was 35x our average traffic. It was very noticeable on the dashboards
compared with our average weekly traffic; however, its impact was negligible.</p>
<figure> 
<img src="https://foxteck.org/posts/serverless-doc-parsing/traffic-spike-v2.png" alt="Traffic spikes are handled gracefully in v2"/>
<figcaption>Traffic spikes. — Big spike (blue rectangle) vs normal weekly traffic (green rectangles).</figcaption>
</figure>
<p>We processed the spike with a 100% success rate. Maximum time in queue went up to 2.43min as the new system “buffered” files in
the queue for processing. But remarkably, time processing per document (each task’s execution) stayed well under the 8s average.</p>
<blockquote>
<p>With higher traffic, our Lambda functions stay warm with established connections and cached dependencies — much like pulling shots on a dialed-in, warmed-up espresso machine after its first pour of the day.</p>
</blockquote>
<h3 id="learning-lessons" tabindex="-1">Learning Lessons <a class="header-anchor" href="#learning-lessons" aria-label="Permalink to “Learning Lessons”">&#8203;</a></h3>
<p>This project was a success; we learned a lot while building it. We ran the two versions side to side for a week and identified all
failure modes in processing that were hidden before. We migrated all traffic in about three weeks, including a few extra
features — no major issues.</p>
<p>Our big wins were:</p>
<ul>
<li>We reduced the time in queue and improved the experience of the solutions team with extra features and a better integration with
the antivirus workflow.</li>
<li>We improved error handling, processing speed, traffic spikes handling, and instrumentation, improved our time to recovery, and
made our runbooks a lot simpler.</li>
</ul>
<p>Some trade-offs:</p>
<ul>
<li>This project adds extra complexity as we are adding more infrastructure and a new pattern with serverless. All engineers will
need time to understand and work with this new
stack. Testing locally is also more difficult.</li>
<li>We had to choose between TypeScript and Python for Lambda. We went with TypeScript because Lambda makes it much easier to work
with Node (both infra and logic use the same codebase).</li>
</ul>
<p>Overall, we are very happy with this new implementation. It was a win for both DS and Engineering teams not having to think about
“the queue” anymore.</p>
<p>For the DS team specifically it was a huge quality of life improvement, they now focus on the part of their day to day that
matters (and not how that is implemented). They also had a huge win on their SLAs last quarter!</p>
<p>It’s nice to think the engineering team help them achieve it.</p>
<p>Thanks for reading! Now, coffee time. An AeroPress seemed like a good fit for this post, enjoy!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> Lance Hendrick's AeroPress recipe</h5>
  <div class="left">
    <strong>Method: </strong>AeroPress <br/>
    <strong>Brew time:</strong> 3min <br/>
  </div>
  <div class="right">
    <strong>Grind: </strong>Medium fine, a bit coarser than table salt<br/>
    <strong>Ratio:</strong> 1:16 - 15g for 240ml (1 cup)
  </div>
<h6>Steps</h6>
  <ol>
    <li>Add 15-18 grams of ground coffee to the AeroPress, and level the coffee</li>
    <li>Pour about 30 grams of hot water (82 - 92°C) for blooming</li>
    <li>Swirl in a circular motion making sure the water and coffee are evenly mixed together</li>
    <li>Let it rest for 45s</li>
    <li>Complete the second pour the rest of the water slowly</li>
    <li>Put the plunger to stop the drops, and give it a little swirl</li>
    <li>Let it sit again until the 2 minutes mark</li>
    <li>Push the plunger slowly and steadily just with the weight of your hand, it should take about a minute</li>
  </ol>
<p>A delicious cup of coffee pushed via a pipe! ☕️</p>
<p><a href="https://www.youtube.com/watch?v=CafyJ2p0Bgs" target="_blank">Full Video</a></p>
</div>]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Making gamers happier, one nudge at a time]]></title>
            <link>https://foxteck.org/posts/nudge.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/nudge.html</guid>
            <pubDate>Fri, 03 Feb 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
  Making gamers happier, one nudge at a time
    <div class="subheader">Queueing Theory: A Brief introduction to Nudge</div>
  </h1>
</div>
<p>I've always been intrigued by queueing theory. We are intuitively familiar with queueing in our day-to-day life, like
waiting in line at the bank or in the car waiting for our turn to pass a four-way stop.</p>
<p>I had the opportunity to focus on Queueing for Luna, Amazon's Cloud Gaming Platform. If you've ever played online games,
you're familiar with waiting in a queue to find a match or for the game to launch.</p>
<div class="tweet-card">
  <a href="https://twitter.com/BenjiSales/status/1577385355933720576" target="_blank" rel="noreferrer">
    <img src="https://images.ctfassets.net/q3ciec9t8o1v/3iRii5681eHHhu16M7zrbi/cf875295501a6d063acdd4063c6b01a2/image.png?fm=jpg&fl=progressive&h=500&w=500" alt="Tweet from @BenjiSales: Welp 50,000+ Players are in Queue ahead of me...">
  </a>
  <span>Overwatch 2 release: In Queue: 50,000 Players ahead of you</span>
</div>
<p>Waiting can be frustrating and leads to disengaged gamers. That's why reducing the time it takes to get to the fun stuff
is essential.</p>
<p>I ran into <a href="https://www.youtube.com/watch?v=G3NWAOlHpoI" target="_blank" rel="noreferrer">this video</a> from <a href="https://www.sigmetrics.org/" target="_blank" rel="noreferrer">SIGMETRICS 2021</a>
about Nudge while exploring “Time in Queue” optimizations. Nudge is a job scheduling policy that is an optimization on
top of First-Come-First-Serve (FCFS) or First-In-First-Out (FIFO). I was immediately intrigued.</p>
<p>FIFO/FCFS is a widely used scheduling policy in the industry due to its performance and simplicity. It is available in
most clouds,
like <a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html" target="_blank" rel="noreferrer">AWS SQS,</a> <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions#queues" target="_blank" rel="noreferrer">Azure Queues</a>
, and <a href="https://www.rabbitmq.com/queues.html#basics" target="_blank" rel="noreferrer">RabbitMQ</a>.</p>
<p>FIFO does not require knowledge about the size or complexity of processing the items in the queue. It has minimal
overhead while ensuring that no job is left waiting indefinitely, making it a good choice for ensuring fairness among
all jobs.</p>
<h2 id="nudge" tabindex="-1">Nudge <a class="header-anchor" href="#nudge" aria-label="Permalink to “Nudge”">&#8203;</a></h2>
<p>Here’s how Nudge works:</p>
<blockquote>
<p>When a small job arrives in the queue, the scheduler checks for the job immediately ahead of it. If that job is
bigger, we swap their position in the queue and prioritize the smaller job.</p>
<p>There’s a limit. A bigger job can only be “jumped” once.</p>
</blockquote>
<p>This is a simplistic Nudge Scheduling Queue animation:</p>
<div class="video-card">
  <video autoplay loop muted>
    <source src="https://videos.ctfassets.net/q3ciec9t8o1v/4JQlTVCbXwGPbGEU1a6Bbq/35abc2e0477129a2a57a0853e996bc27/nudge-animation.mp4" type="video/mp4">
  </video>
  <span>
    Nudge animation with “small” and “big” jobs and arrival timestamp. Dashed jobs are nudged over.
  </span>
</div>
<p>It has only two types of jobs, small and big. Each job shows its “arrival timestamp” (in mm:ss.ms), making it easier to
confirm when a job is older than another job. Finally, jobs that arrive closely together tend to have a similar color.
That way, when a job “jumps” over another, it looks out of place.</p>
<p>Here is the <a href="https://eduardoromero.github.io/nudge-react/" target="_blank" rel="noreferrer">interactive version</a> of the animation,
and the <a href="https://github.com/eduardoromero/nudge-react" target="_blank" rel="noreferrer">source code</a>.</p>
<h2 id="comparing-nudge-and-fifo" tabindex="-1">Comparing Nudge and FIFO <a class="header-anchor" href="#comparing-nudge-and-fifo" aria-label="Permalink to “Comparing Nudge and FIFO”">&#8203;</a></h2>
<p>I wanted to see how Nudge compares to FIFO in terms of reducing wait times for most users while still being fair. I made
a <a href="https://github.com/eduardoromero/QNudge" target="_blank" rel="noreferrer">simulation</a> that models a more complex gaming scenario to find out. The goal
is to see if Nudge’s benefits in getting gamers to play the game faster (the fun part) are significant enough to justify
a complete implementation.</p>
<p>In the simulation, we have four games <a href="https://www.gog.com/en/game/tunic" target="_blank" rel="noreferrer"><strong>Tunic</strong></a>, <a href="https://www.gog.com/en/game/hollow_knight" target="_blank" rel="noreferrer"><strong>Hollow
Knight</strong></a>, <a href="https://www.playlostark.com/" target="_blank" rel="noreferrer"><strong>Lost Ark</strong></a>, and <a href="https://worldofwarcraft.com/" target="_blank" rel="noreferrer"><strong>World of
Warcraft</strong></a>. It models traffic with imaginary values where each “Game Activity” represents
someone that plays the game on some imaginary platform, so they download, install and launch the game.</p>
<p>Nudge uses “job size” to decide when to nudge. I used the <em>size on disk</em> for each game as a <em>proxy metric</em> for the time
it’ll take to finish each one of the steps until we have the game running.</p>
<p>In queueing theory, the size usually represents the time it takes to dequeue and successfully process a job.</p>
<h3 id="rules-of-the-game" tabindex="-1">Rules of the Game <a class="header-anchor" href="#rules-of-the-game" aria-label="Permalink to “Rules of the Game”">&#8203;</a></h3>
<p>In the simulation, small games are 10GB, and big games are 50GB or more. There are four games:</p>
<ul>
<li><a href="https://www.gog.com/en/game/tunic" target="_blank" rel="noreferrer"><strong>Tunic</strong></a> is small and very popular.</li>
<li><a href="https://www.gog.com/en/game/hollow_knight" target="_blank" rel="noreferrer"><strong>Hollow Knight</strong></a> (HK) is small but not so popular.</li>
<li><a href="https://www.playlostark.com/" target="_blank" rel="noreferrer"><strong>Lost Ark</strong></a> (LA) is medium size and somewhat popular.</li>
<li><a href="https://worldofwarcraft.com/" target="_blank" rel="noreferrer"><strong>World of Warcraft</strong></a> (WoW) is big and just as popular as Lost Ark.</li>
</ul>
<p>I used the concept of “Game Runners”
that <a href="https://github.com/eduardoromero/QNudge/blob/main/src/models/GameModel.ts#L59" target="_blank" rel="noreferrer">download</a>
, <a href="https://github.com/eduardoromero/QNudge/blob/main/src/models/GameModel.ts#L80" target="_blank" rel="noreferrer">install</a> and launch the game (ignoring
launch time to keep it simple). A typical run with a thousand gamers, would get
a <a href="https://github.com/eduardoromero/QNudge/blob/main/src/models/factories/ProbabilisticGameActivityFactory.ts#L8-L13" target="_blank" rel="noreferrer">distribution</a>
similar to this:</p>
<div class="language-text"><button title="Copy Code" class="copy"></button><span class="lang">text</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span>Game Distribution:</span></span>
<span class="line"><span>   "Tunic": 556,</span></span>
<span class="line"><span>   "World of Warcraft": 186,</span></span>
<span class="line"><span>   "Lost Ark": 168,</span></span>
<span class="line"><span>   "Hollow Knight": 90</span></span></code></pre>
</div><p>This means gamers played Tunic about 6x more than Hollow Knight, and Lost Ark and World of Warcraft were approximately
2x more popular than Hollow Knight.</p>
<p>The “Game Activity” keeps track of the activity’s metadata:</p>
<ul>
<li>When the user “clicks play” to launch the game.</li>
<li>The time it took to find a runner for the game.</li>
<li>The time it took to download and install the game.</li>
<li>If the activity was nudged or caused a nudge.</li>
</ul>
<p>Each activity launches simultaneously on a FIFO scheduler and the Nudge scheduler. Both use the same list of games and
the same configuration values.</p>
<h3 id="initial-results" tabindex="-1">Initial Results <a class="header-anchor" href="#initial-results" aria-label="Permalink to “Initial Results”">&#8203;</a></h3>
<p>I ran the simulation multiple times with 15,000 gamers using the same distribution as above. Here are the summary
results of one of the runs:</p>
<div class="table-card">
<table class="summary">
<thead>
<tr>
  <th>Scheduler</th><th>Average</th><th>Best</th><th>Worst</th>
</tr>
</thead>
<tbody>
<tr>
  <td colspan="4" class="gametitle"><strong>Hollow Knight</strong> (1324 players)</td>
</tr>
<tr>
  <td class="scheduler">Nudge</td><td>1s 753ms</td><td>1s 556ms </td><td>1s 954ms</td>
</tr>
<tr>
  <td class="scheduler">FIFO</td><td>1s 749ms</td><td>1s 558ms </td><td>1s 947ms</td>
</tr>
<tr>
  <td colspan="4" class="gametitle"><strong>Lost Ark</strong> (2756 players)</td>
</tr>
<tr>
  <td class="scheduler">Nudge</td><td>6s 553ms</td><td> 6s 357ms </td><td>6s 751ms</td>
</tr>
<tr>
  <td class="scheduler">FIFO</td><td>6s 553ms</td><td>6s 357ms </td><td>6s 749ms</td>
</tr>
<tr>
  <td colspan="4" class="gametitle"><strong>Tunic</strong> (8140 players)</td>
</tr>
<tr>
  <td class="scheduler">Nudge</td><td>1s 751ms</td><td>1s 554ms </td><td>1s 952ms</td>
</tr>
<tr>
  <td class="scheduler">FIFO</td><td>1s 752ms</td><td>1s 554ms </td><td>1s 956ms</td>
</tr>
<tr>
  <td colspan="4" class="gametitle"><strong>World of Warcraft</strong> (2780 players)</td>
</tr>
<tr>
  <td class="scheduler">Nudge</td><td>30s 554ms</td><td>30s 363ms </td><td>30s 754ms</td>
</tr>
<tr>
  <td class="scheduler">FIFO</td><td>30s 554ms</td><td>30s 358ms </td><td>30s 751ms</td>
</tr>
</tbody>
</table>
<p><span>Summary Results &mdash; Nudge and FIFO. Run with 15,000 different gamers.</span></p>
</div>
<p>It didn't show a big difference for all games’ average, best or worst time in queue. Even when the Nudge scheduler
&quot;nudged&quot; 2566 times.</p>
<p>The average time in queue for <strong>Tunic</strong> (the smallest and most popular game) is 1.751s (Nudge) and 1.752 (FIFO).
<strong>World of Warcraft</strong> (biggest game) is tied in both at 30.554s.</p>
<p>Different runs showed slightly different results but nothing widely different. Two things that I was expecting to see:</p>
<ul>
<li>Much longer times for WoW since it’s technically de-prioritizing it.</li>
<li>Slightly shorter time for Tunic on the worst time in queue.</li>
</ul>
<h3 id="plotting-game-activity" tabindex="-1">Plotting Game Activity <a class="header-anchor" href="#plotting-game-activity" aria-label="Permalink to “Plotting Game Activity”">&#8203;</a></h3>
<p>I wanted to visualize the Game Activity metadata to understand what was happening, and things got interesting again:</p>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/comparison-fifo-nudge.svg" alt="Nudge vs FIFO -- A linear chart showing Time to play for different activities over time. FIFO in green, and Nudge in blue. Nudge is most of the time slightly under FIFO, the gap between them is more pronounced as time goes on.">
  <span>Time to play in Seconds &mdash; Nudge (blue) vs FIFO (green)</span>
</div>
<p>The chart above reveals that Nudge (blue line) generally results in shorter wait times (or ‘time to play’) than FIFO (
green line).</p>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/nudge-bubbles.svg" alt="Nudge plot chart with time to play for different games over time. The size of the bubble represents game size.">
  <span>Time to play in Seconds per game &mdash; Nudge</span>
</div>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/fifo-bubbles.svg" alt="FIFO plot chart with time to play for different games over time. The size of the bubble represents game size.">
  <span>Time to play in Seconds per game &mdash; FIFO</span>
</div>
<p>The scatter plots above compare the performance of Nudge and FIFO per game. Each game is represented by a different
color, and the circles’ width corresponds to the game’s size. It shows the relationship between each player’s arrival
time and their time to play.</p>
<p>Nudge effectively <em>reduces wait times for smaller games</em>, <em>with little to no impact on larger games</em>. In fact, the data
shows that Nudge has a better ‘tail’ (lower wait times) for all our gamers. How exciting!</p>
<h3 id="time-to-play-distributions" tabindex="-1">Time to play distributions <a class="header-anchor" href="#time-to-play-distributions" aria-label="Permalink to “Time to play distributions”">&#8203;</a></h3>
<p>This was the best part for me, it clearly shows what I was expecting to see in the summary results:</p>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/nudge-distribution-time-to-play.svg" alt="Time to play distribution for Nudge.">
  <span>Distribution of time to play, broken down by game  &mdash; Nudge</span>
</div>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/fifo-distribution-time-to-play.svg" alt="Time to play distribution for FIFO.">
  <span>Distribution of time to play, broken down by game  &mdash; FIFO</span>
</div>
<p>This paints a better picture of how Nudge works and gives us a better understanding of how it affects our scheduler’s
“long tail” behavior.</p>
<blockquote>
<p>Nudge generally leads to shorter wait times for gamers.</p>
</blockquote>
<h2 id="understanding-nudge" tabindex="-1">Understanding Nudge <a class="header-anchor" href="#understanding-nudge" aria-label="Permalink to “Understanding Nudge”">&#8203;</a></h2>
<p>Let’s break it down only with Tunic (red) and World of Warcraft (green). The bars represent the frequency of gamers’
wait times. As we run out of resources, the wait time increases.</p>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/fifo-distribution-tunic-and-wow.svg" alt="Time to play distribution for FIFO.">
  <span>Tunic and WoW Frequency Distribution  &mdash; FIFO</span>
</div>
<div class="chart-card">
  <img src="https://foxteck.org/posts/nudge/nudge-distribution-tunic-and-wow.svg" alt="Time to play distribution for Nudge.">
  <span>Tunic and WoW Frequency Distribution  &mdash; Nudge</span>
</div>
<p>For Tunic, Nudge significantly increases the number of players who waited less than 200ms to play.</p>
<p>In contrast, for World of Warcraft, the number of players waiting less than 100ms is lower and decreases further between
the 100–200ms range. It's pushing the bulk of waiting time to the 200–400ms range but reducing the total number of
players
who needed more than 800ms.</p>
<p>The data shows that the distribution tails (representing the gamers with the worst wait times, like the one on the
initial tweet) are thinner with Nudge. Lost Ark has a slightly longer tail, but that looks like an outlier.</p>
<p>Nudge generally performs better, reducing the worst wait times for all players.</p>
<h2 id="outro" tabindex="-1">Outro <a class="header-anchor" href="#outro" aria-label="Permalink to “Outro”">&#8203;</a></h2>
<p>I’m confident that Nudge generally improves waiting times for this use case and is especially effective at reducing
“tail latency” (<em>the longest waiting times</em>). From that perspective, it makes sense to experiment further.</p>
<p>However, implementing Nudge at scale might be a different feat altogether. Existing queue services like SQS do not
provide a way to “bring your own scheduler”. Implementing Nudge would require adding an abstraction layer on top of the
queueing system or replacing it entirely.</p>
<p>I think there are two abstractions that are not terribly complicated:</p>
<ul>
<li>
<p>Implementing a database-backed queue
like <a href="https://aws.amazon.com/blogs/database/implementing-priority-queueing-with-amazon-dynamodb/" target="_blank" rel="noreferrer">this one</a>.</p>
</li>
<li>
<p>A simplified version of a database-backed queue, using a table to track the state of the jobs. Query the table to
implement “peeking”, then implement Nudging by “tomb-stoning” the nudged entry on the database so it’s a no-op, and
insert it again with a new id after the item it just arrived to represent the state after nudging.</p>
</li>
</ul>
<p>Then there’s RabbitMQ. It has support for <a href="https://www.rabbitmq.com/plugins.html#overview" target="_blank" rel="noreferrer">plugins</a>, and there are
existing plugins that implement <a href="https://www.rabbitmq.com/community-plugins.html#queues" target="_blank" rel="noreferrer">prioritization</a>, building a
nudge plugin might be another solution.</p>
<p>The next steps for me are exploring these abstractions.</p>
<p>Stay tuned, and thanks for reading! Now, coffee time. Enjoy!</p>
<div class="recipe-block">
  <h5><strong>Coffee Time:</strong> Espresso Martini</h5>
  <p>
    <strong>Method: </strong> Espresso <br />
    <strong>Preparation Time:</strong> 10 minutes
  </p>
<h6>Steps</h6>
  <ol>
    <li>Pour 45ml of Vodka into a shaker</li>
    <li>Add 20ml of coffee liqueur</li>
    <li>Add 10ml of vanilla or simple syrup</li>
    <li>Add 1 shot of Espresso</li>
    <li>Shake well</li>
    <li>Serve into a chilled glass, strain the ice</li>
    <li>Once served, spritz a lemon twist over the glass to get a bit of citrus oils a nice contrasting aroma</li>
 </ol>
<p>Here's the full <a href="https://youtu.be/wXdmRfs5f4E" target="_blank">Espresso Martini</a> explanation.</p>
</div>
<h2 id="learning-more" tabindex="-1">Learning More 📚 <a class="header-anchor" href="#learning-more" aria-label="Permalink to “Learning More”">&#8203;</a></h2>
<ul>
<li>
<p><strong>Dive deeper into Nudge</strong> with the <a href="https://www.youtube.com/watch?v=G3NWAOlHpoI" target="_blank" rel="noreferrer">video introduction</a>,
the <a href="https://dl.acm.org/doi/abs/10.1145/3410220.3460102" target="_blank" rel="noreferrer">full paper</a>,
and <a href="https://dl.acm.org/doi/abs/10.1145/3570610" target="_blank" rel="noreferrer">proof</a> that even with weaker conditions Nudge is still better than
FIFO for light-tailed job distributions (like the one shown above).</p>
</li>
<li>
<p><a href="https://aws.amazon.com/builders-library/avoiding-insurmountable-queue-backlogs/" target="_blank" rel="noreferrer">Explore</a> how distributed systems
rely on queueing to handle high loads and spikes in traffic on Amazon Builder’s Library.</p>
</li>
<li>
<p>Learn about
the <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/queue-based-load-leveling" target="_blank" rel="noreferrer">Queue-Based Load Leveling</a>
pattern on Microsoft’s Cloud Design Patterns.</p>
</li>
<li>
<p>Read about load balancing, queueing and the power of two random choices
on <a href="https://www.nginx.com/blog/nginx-power-of-two-choices-load-balancing-algorithm/" target="_blank" rel="noreferrer">Nginx’s blog</a>.</p>
</li>
</ul>
<Comments />]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Coffee Katas]]></title>
            <link>https://foxteck.org/posts/building-software/coffee-katas.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/coffee-katas.html</guid>
            <pubDate>Sat, 28 Jan 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
  Coffee Katas
    <div class="subheader">Sipping coffee and solving problems</div>
  </h1>
</div>
<h2 id="what-are-katas" tabindex="-1">What are Katas? <a class="header-anchor" href="#what-are-katas" aria-label="Permalink to “What are Katas?”">&#8203;</a></h2>
<p>A Kata is a training exercise common in Martial Arts (like <a href="https://en.wikipedia.org/wiki/Kuntao" target="_blank" rel="noreferrer">Kuntao</a>), that resembles a step-by-step
choreography to practice and get familiar with specific movements. It can be practiced alone or in unison in a group when training.</p>
<p>This is a way to memorize and perfect the movements so that in the future you can do them naturally, without thinking.</p>
<p>Architectural Katas are exercises to get into the habit of building and evolving Software Architecture. They don't have a specific solution or a &quot;right way&quot; of building the
architecture.</p>
<p>It's just a way to practice and familiarize yourself with discussing a problem, thinking about trade-offs, and choosing the right mix of solutions given each system's
specific constraints and needs.</p>
<p>There are official <a href="https://nealford.com/katas/rules.html" target="_blank" rel="noreferrer">rules</a> to have a session, and there are plenty of examples on Neil Ford's <a href="https://nealford.com/katas/about.html" target="_blank" rel="noreferrer">Katas page</a>
and Ted Neward's <a href="https://www.architecturalkatas.com" target="_blank" rel="noreferrer">Katas page</a>.</p>
<p>Katas are a great way to practice System Design, it's the same process you would follow in a System Design interview but less stressful and there are no wrong answers, just learning
experiences. It's also a great way to get context for about different architectures pretty quickly.</p>
<h2 id="coffee-katas" tabindex="-1">Coffee Katas <a class="header-anchor" href="#coffee-katas" aria-label="Permalink to “Coffee Katas”">&#8203;</a></h2>
<p>Brewing a good cup of coffee is a <a href="https://pouroverproject.com/v60-recipes-rao-hoffman-kasuya-drip-coffee/" target="_blank" rel="noreferrer">whole ritual</a>. It takes time, anywhere from 10 to 25 minutes, depending
on the <a href="https://blog.bluebottlecoffee.com/posts/how-to-choose-a-brew-method" target="_blank" rel="noreferrer">brewing method</a> and the number of cups.</p>
<p>Back when I was at Wizeline, we would go to the coffee area, start brewing a batch, and kick off the discussion and whiteboard problems and possible solutions while the coffee was
brewing. In the beginning, just a few of us, the ones that were truly into coffee, but after a while, it became a <em>team culture</em> thing. Even the ones that didn't drink coffee would
join the conversation.</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/coffee-katas/dream-of-a-coffee-team.png" alt="A Dreamscape of a coffee team"/>
<p><span>A Dream of a Coffee Team.</span></p>
</div>
<p>Eventually, it evolved into a practice. We would let everyone know when it was Coffee Time, and whoever was free would join. Then we discussed either a problem we were trying to
solve or problems we wished we had.</p>
<p>If a team was working on a specific architectural design, they would present their context, talk about their design and the approach they were going to use, and everyone else would share their
critiques, similar experiences, and ways to go about that &quot;type of problem.&quot;</p>
<p>If there were no designs or specific topics, we'd pick a Kata.</p>
<p>We would finish our coffee, close the discussion and go on with our days.</p>
<p>It was a terrific way of getting context from other teams, getting quick feedback, and iterating on designs without too much ceremony. Then you could follow up with specific
individuals or squads and come back in the future and share how it all go.</p>
<p>It was a virtuous circle that revolved around coffee.</p>
]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
        <item>
            <title><![CDATA[Debugging stubborn DynamoDB Queries]]></title>
            <link>https://foxteck.org/posts/building-software/debugging-dynamodb.html</link>
            <guid isPermaLink="false">https://foxteck.org/posts/building-software/debugging-dynamodb.html</guid>
            <pubDate>Sat, 14 Jan 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="article-heading">
  <h1 class="header">
  Debugging stubborn DynamoDB Queries
    <div class="subheader">A Quick guide for Java and Node</div>
  </h1>
</div>
<WIP /><p>DynamoDB (DDB) is the most impressive Data Store I've ever worked with. DDB is the de facto general-purpose database here at Amazon, and you would only consider something else if
you have particular needs.</p>
<img src="https://foxteck.org/posts/DynamoDB.png" alt="DynamoDB Logo" style="margin-left: auto; margin-right: auto; width: 128px;" />
<p>What's so remarkable about it? <em><strong>It loves scale!</strong></em> It has a simple API that drives your data modeling decisions, forcing you to build simple queries that perform well no matter
how much data is stored in your tables or how many clients are querying it at the same time.</p>
<p>I've worked with it on Java, Node, and Go, and it is consistently performant. It's blazing fast. When calling from the same region, getting an item from DynamoDB (DDB) takes under
30ms for Node client and 10ms for Java and Go clients. That's pretty impressive!</p>
<p>The one thing that I don't enjoy is working with its SDK. It's improving for both Java and Node with the <a href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/dynamodb-enhanced-client.html" target="_blank" rel="noreferrer">Enhanced DDB
Client</a> and the
stand-alone <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/classes/dynamodb.html" target="_blank" rel="noreferrer">DDB Client</a>
v3 for JavaScript, but they are still tricky, and the documentation is somewhat scarce.</p>
<p>The Developer Experience leaves a lot on the table. You have to figure it out on your own. Here are the docs for
a <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/interfaces/querycommandinput.html" target="_blank" rel="noreferrer">Query</a> on the JavaScript SDK.
It's pretty much type hinting information mixed with high-level DynamoDB concepts.</p>
<p>
Whenever I'm stuck with a <DigDeeper summary="slow request" context=" I call it &quot;slow&quot; when a request takes more than 100ms, but that number should
be specific to the needs of your system. " /> where I can't explain what's happening, I follow a two-step strategy to unblock myself:</p>
<ul>
<li>First, I look at the traces to confirm that XRay sees the problem too. This quick step helps you determine if the issue is inside your service or is coming from DynamoDB.</li>
<li>Then, I check the logs for the request and response from the DDB client to see that I'm doing the right thing.</li>
</ul>
<h2 id="slow-requests" tabindex="-1">Slow Requests <a class="header-anchor" href="#slow-requests" aria-label="Permalink to “Slow Requests”">&#8203;</a></h2>
<p>These are some common things that make a DDB request slow:</p>
<div class="ordered-list">
<ol>
<li><em><strong>The item size is too big</strong></em>. DynamoDB will automatically create more than one request in the backend if the item is large to break it into pieces and fetch the data.</li>
<li><em><strong>The Query is fetching too many items</strong></em>. In this scenario, the client and the backend can make more than one request to fetch
the data. Some clients
can <a href="https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/datamodeling/PaginatedQueryList.html" target="_blank" rel="noreferrer">eagerly</a>
ask for more than one page when running a query (or
a <a href="https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/datamodeling/PaginatedScanList.html" target="_blank" rel="noreferrer">scan</a>
, but <em>one never scans</em>). This can be counter-intuitive because the docs state these operations are &quot;on-demand&quot;.</li>
<li><em><strong>Cross-regional calls</strong></em>. If the service runs in us-west-2 and the table is on us-east-1, there's a ~200ms penalty on
each request. This seems obvious, but I've been bitten more than once by it. It's hard to tell unless you're
specifically looking for that.</li>
<li><em><strong>The client is not maintaining a connection pool</strong></em>. Very common on Lambda with cold starts, but it happens on
provisioned instances right after deploying.</li>
</ol>
</div>
<h3 id="looking-on-x-ray" tabindex="-1">Looking on X-Ray <a class="header-anchor" href="#looking-on-x-ray" aria-label="Permalink to “Looking on X-Ray”">&#8203;</a></h3>
<h4 id="a-quick-intro-to-xray" tabindex="-1">A Quick intro to XRay <a class="header-anchor" href="#a-quick-intro-to-xray" aria-label="Permalink to “A Quick intro to XRay”">&#8203;</a></h4>
<p>AWS XRay is part of AWS's observability stack. It is robust and works with most AWS clients and services out of the box. Traces or Segments represent an activity
or service call from your code.</p>
<p>There are two observability areas in the AWS Console. One is the AWS XRay service (the old console), and the other is in AWS CloudWatch's Service Map (the newer one). In general,
they share the same features. The old one will be deprecated in the future.</p>
<p>This is what a typical trace looks like in CloudWatch's Service Map:</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/debugging-dynamodb/simple-trace.png" alt="CloudWatch Segment showing a DDB GetItem request that took 163ms.">
  <span>Segment (NodeJS) in CloudWatch &mdash; DB GetItem took 163ms.</span>
</div>
<p>Here's the same trace in the XRay Console:</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/debugging-dynamodb/simple-segment-xray.png" alt="XRay Segment showing a DDB GetItem request that took 163ms.">
  <span>Segment (NodeJS) in XRay &mdash; DB GetItem took 163ms.</span>
</div>
<p>In order to capture these traces, the service needs to be instrumented. The XRay SDK provides methods and utilities to instrument a service. It can &quot;wrap&quot;,
most AWS clients and HTTP clients, and all requests with these clients are instrumented automatically. But service-specific operations need to be instrumented manually.</p>
<p><a href="https://github.com/eduardoromero/QNudge/blob/main/src/get_run.ts#L11-L20" target="_blank" rel="noreferrer">Here</a> is the code that instruments the DynamoDB client, and <a href="https://github.com/eduardoromero/QNudge/blob/main/src/get_run.ts#L54-L68" target="_blank" rel="noreferrer">here</a>
is the code that captures the &quot;service call&quot;.</p>
<p>It can be tedious to jump from one console to the other. But, the trace id is always the same in both consoles. Using the id makes it easy to switch between them. Here's an example
for trace id <code>1-63c24b27-c3adab6d7153b18ca6fca488</code> with examples for both consoles:</p>
<ul>
<li>
<p>Cloud Watch Service Map link looks like this:</p>
<p><code>https://us-west-2.console.aws.amazon.com/cloudwatch/home?region=us-west-2#xray:traces/1-63c24b27-c3adab6d7153b18ca6fca488</code></p>
</li>
<li>
<p>The XRay link for the same trace looks like this:</p>
<p><code>https://us-west-2.console.aws.amazon.com/xray/home?region=us-west-2#/traces/1-63c24b27-c3adab6d7153b18ca6fca488-030dc84e378fd9dc</code></p>
</li>
</ul>
<h4 id="request-metadata" tabindex="-1">Request Metadata <a class="header-anchor" href="#request-metadata" aria-label="Permalink to “Request Metadata”">&#8203;</a></h4>
<p>Metadata about all AWS SDK requests is stored in the XRay traces. The DynamoDB segment includes information like the type of request (GetItem, Query, Scan, etc.), validation and error
information, the number of retries, the region where the table is hosted, the duration of the request and the size of the response.</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/debugging-dynamodb/segment-details-ddb.png" alt="Segment details: DynamoDB.">
  <span>DDB Segment Details - Resource Information.</span>
</div>
<div class="image-card">
  <img src="https://foxteck.org/posts/debugging-dynamodb/segment-details-ddb-old-console.png" alt="Segment overview: DynamoDB. Old XRay Console.">
  <span>DDB Segment Details - Overview (Old XRay console)</span>
</div>
<p>From the information above we can narrow down the issues pretty quickly:</p>
<p>This is a <strong>GetItem</strong> request, so it's only one item (not B). The table is running on <strong>us-west-2</strong>, which
is the same region where the service is running (not C). The response is about 4kb, it's not too big (not A).</p>
<p>As a rule of thumb, 4kb is one request. More than 16kb and we get in multiple requests territory. It depends if the request is strongly consistent, eventually consistent, or part
of a transaction. Finally, the maximum size of an item is 400kb. All the specific details of these values are in the
<a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughputIntro.html#HowItWorks.requests" target="_blank" rel="noreferrer">Developer's Guide</a>.</p>
<p>Strictly speaking, DynamoDB making more than one request doesn't necessarily mean it will be slow for our service. These requests happen in parallel on DynamoDB's backend,
which is close to the data and the backend is remarkably optimized. But smaller items are always faster than big items.</p>
<p>Here's an example of a segment with more data that's still very fast:</p>
<div class="image-card">
  <img src="https://foxteck.org/posts/debugging-dynamodb/segment-details-ddb-old-console-moar-data.png" alt="Segment overview: DynamoDB. Query took 35ms for ~20kb of data">
  <span>DDB Segment Details - Query took 35ms for ~20kb of data</span>
</div>
<p>With just a few clicks and very little instrumentation we ruled out 3 out of 4 scenarios.</p>
<h4 id="cold-starts" tabindex="-1">Cold Starts <a class="header-anchor" href="#cold-starts" aria-label="Permalink to “Cold Starts”">&#8203;</a></h4>
<p>In Serverless, <em>Cold Start</em> is the delay that happens when a function is invoked for the first time or after it has been inactive for a while. During a cold start, the lambda
function initializes all its dependencies and pools of resources. This translates into higher latency for the first few requests.</p>
<p>The same happens with traditional services, but only when new containers or tasks are launched while the service is bootstrapping. This is common right after a deployment, when the
service is scaling up due to a higher load, or after a service crash.</p>
<p>XRay automatically captures when a lambda function is initializing, so the trace would show if it was a cold start. There's no way for XRay to capture &quot;cold start&quot; in traditional services,
but the logs show when the service is bootstrapping. If the latency matches the timeline, and goes away after the first few request then it was a cold start.</p>
<p>Unfortunately, the example above is a full-blown NodeJS service.
To debug further, we need to instrument the code and see where the client is being instantiated to see if it's creating a new client for each request.</p>
<h3 id="request-level-tracking" tabindex="-1">Request-level Tracking <a class="header-anchor" href="#request-level-tracking" aria-label="Permalink to “Request-level Tracking”">&#8203;</a></h3>
<p>Digging deeper requires we instrument the client to get the details of the actual operation behind the scenes. Request-level tacking is a lot more useful in real-life distributed services,
where things get more complicated and are easier to miss.</p>
<p><em>If you read this far, I'm sorry, I'm about to ruin the surprise</em>. The scenario I'm using for all these examples is extremely simple. Skimming through the code it should be obvious
that this is a <em>text-book</em> <em><strong>cold start</strong></em> (D). A script that runs <em>once</em> when it's executed, there's no time for the SDK to create a pool of connections. The HTTP Client needs to make a full handshake,
connection, and SSL validation just to get the data, and then the process ends. But please, continue reading these are one of the most useful and advanced techniques to debug issues with DynamoDB.</p>
<h4 id="working-with-typescript" tabindex="-1">Working with TypeScript <a class="header-anchor" href="#working-with-typescript" aria-label="Permalink to “Working with TypeScript”">&#8203;</a></h4>
<p>All AWS SDK clients are just Http Clients making requests to specific endpoints for each service. The Javascript SDK supports <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html#middleware" target="_blank" rel="noreferrer">Middlewares</a>
and Plugins to customize its behavior and track request-level information.</p>
<p>Using these middlewares makes it easy to capture each request and see what's the exact query that's running and the metadata from the response.</p>
<div class="vp-code-group"><div class="tabs"><input type="radio" name="group-173" id="tab-174" checked><label data-title="app.ts" for="tab-174">app.ts</label><input type="radio" name="group-173" id="tab-175" ><label data-title="getRequestMetadata.ts" for="tab-175">getRequestMetadata.ts</label><input type="radio" name="group-173" id="tab-176" ><label data-title="getResponseMetadata.ts" for="tab-176">getResponseMetadata.ts</label></div><div class="blocks">
<div class="language-typescript active"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ddb</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> XRay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">captureAWSv3Client</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> DynamoDBClient</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        region: process.env.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">AWS_REGION</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ||</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'us-west-2'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    segment</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">ddb.middlewareStack.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">add</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(getRequestMetadata, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    step: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'build'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'debug-request'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">ddb.middlewareStack.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">addRelativeTo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(getResponseMetadata, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'response-size'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    relation: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'before'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    toMiddleware: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'XRaySDKInstrumentation'</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre>
</div><div class="language-typescript"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getRequestMetadata</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">next</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">context</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">args</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">clientName</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">commandName</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> context;</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> args;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    log.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">clientName</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">} - operation: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">commandName</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    log.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(input, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'input'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> next</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(args);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre>
</div><div class="language-typescript"><button title="Copy Code" class="copy"></button><span class="lang">typescript</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> const</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getResponseMetadata</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">next</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> async</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">args</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> start</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> process.hrtime.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bigint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> next</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(args).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">then</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> process.hrtime.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bigint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        log.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Request finished in ${</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">duration</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">(</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">end</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> start</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">)</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}.`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // request status</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (result?.response?.statusCode) {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            log.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Status: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">result</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">response</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">.</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">statusCode</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // size</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (result?.response?.body?.headers) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> s</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result.response.body.headers[</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'content-length'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">            const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> size</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> prettyBytes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseInt</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(s) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">            log.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Response size: ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">size</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">        // return response unchanged.</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">        return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> result;</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre>
</div></div></div>
<p>They are very similar to middleware for <a href="https://expressjs.com/en/guide/using-middleware.html" target="_blank" rel="noreferrer">Express</a> or <a href="https://middy.js.org/" target="_blank" rel="noreferrer">Middy</a>. You can intercept the request before it's built,
when it's serialized, after is built, when it's ready to be sent, and after it's back.</p>
<p>The <code>step</code> option is represents one of the <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-ssm/interfaces/handleroptions.html#step" target="_blank" rel="noreferrer">stages</a> of the request.
It's used when registering via <code>client.middlewareStack.add</code> so the Http Client knows where to inject the middleware.</p>
<p><code>getRequestMetadata</code> captures the operation (GetItem, PutItem, Query, etc.) and the parameters (GetItemCommand, PutItemCommand, QueryCommand, etc.).</p>
<p>Here's what the output looks like for fetching an item via its composite key:</p>
<div class="language-apache"><button title="Copy Code" class="copy"></button><span class="lang">apache</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">43</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">096</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): DynamoDBClient - operation: GetItemCommand</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">43</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">096</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): input</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    ReturnConsumedCapacity: "TOTAL"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    Key: {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      "key": {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        "S": "run_000000CX655IVKQZFXUOTb2Fr44VU"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      "type": {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">        "S": "NUDGE"</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    TableName: "game_activities"</span></span></code></pre>
</div><p><code>getResponseMetadata</code> captures the time it takes to make the request, the status and the size of the response.</p>
<p>This is the output for the same request:</p>
<div class="language-apache"><button title="Copy Code" class="copy"></button><span class="lang">apache</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">43</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">272</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">176</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">43</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">272</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">35</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">43</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">273</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">05</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span></code></pre>
</div><p>Here's the <a href="https://github.com/eduardoromero/QNudge/blob/main/src/shared/DynamoDBUtils.ts" target="_blank" rel="noreferrer">source</a>, and here is how it's <a href="https://github.com/eduardoromero/QNudge/blob/main/src/get_run.ts#L16-L32" target="_blank" rel="noreferrer">instrumented</a>.
Now we have the run time, status, size of the response and the full query. Running the same input in the DDB console should give you a very good estimate of what it should
take on average for this type of request. Compare latency, number of results, and the number of pages shown in the console.</p>
<p>Just to demo, here is the performance after running the same query twice:</p>
<div class="language-apache"><button title="Copy Code" class="copy"></button><span class="lang">apache</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">54</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">517</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">276</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">48</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">27</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">438</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">48</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">27</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">438</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">05</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">...</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">54</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">546</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">27</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">54</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">546</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">54</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">546</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">05</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span></code></pre>
</div><p>Now we are talking. It takes <em><strong>27 milliseconds</strong></em> that's the beauty of DDB right there, it's just <em><strong>blazing fast</strong></em>.</p>
<p>Just for completeness, This is a series of paginated Query returning a lot more data:</p>
<div class="language-apache"><button title="Copy Code" class="copy"></button><span class="lang">apache</span><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0" dir="ltr" v-pre=""><code><span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">176</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">19</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">001</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] INFO (Queue Nudging): Got page </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> with </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">...</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">218</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">31</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">218</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">218</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">19</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">218</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] INFO (Queue Nudging): Got page </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">100</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> with </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">...</span></span>
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">247</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Request finished in </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">28</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> milliseconds.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">247</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Status: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">200</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">247</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] DEBUG (ddb-utils): Response size: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">19</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> kB</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">01</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">00</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">41</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">247</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] INFO (Queue Nudging): Got page </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">101</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> with </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> items.</span></span></code></pre>
</div><p>After making the first request, all following requests made with the same client show the latency expected from DDB.</p>
<h4 id="working-with-java" tabindex="-1">Working with Java <a class="header-anchor" href="#working-with-java" aria-label="Permalink to “Working with Java”">&#8203;</a></h4>
<p>Java has two different versions of the AWS SDK. They do things slightly differently, whenever possible use v2 (specially if you're thinking of
going with Lambda).</p>
<p>Generally speaking, <a href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/home.html" target="_blank" rel="noreferrer">v2</a> is leaner and is split into different clients.
Whereas <a href="https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/welcome.html" target="_blank" rel="noreferrer">v1</a> is bundled together, and has specialized clients for a few things that are not yet available
on v2 (like the <a href="https://github.com/awslabs/amazon-dynamodb-lock-client" target="_blank" rel="noreferrer">DDB Lock Client</a>).</p>
<h5 id="aws-sdk-v1" tabindex="-1">AWS SDK v1 <a class="header-anchor" href="#aws-sdk-v1" aria-label="Permalink to “AWS SDK v1”">&#8203;</a></h5>
<p>AWS SDK v1 support intercepting requests via <a href="https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/handlers/RequestHandler2.java" target="_blank" rel="noreferrer">RequestHandler2</a></p>
<h5 id="aws-sdk-v2" tabindex="-1">AWS SDK v2 <a class="header-anchor" href="#aws-sdk-v2" aria-label="Permalink to “AWS SDK v2”">&#8203;</a></h5>
<h2 id="other-issues" tabindex="-1">Other issues <a class="header-anchor" href="#other-issues" aria-label="Permalink to “Other issues”">&#8203;</a></h2>
<Comments />]]></content:encoded>
            <author>eduardo@foxteck.org (Eduardo Romero)</author>
        </item>
    </channel>
</rss>