<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Divy Yadav</title>
    <description>The latest articles on DEV Community by Divy Yadav (@divy_ai).</description>
    <link>https://dev.to/divy_ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845890%2Fc910b35f-87b4-4888-b116-ad2cde414360.png</url>
      <title>DEV Community: Divy Yadav</title>
      <link>https://dev.to/divy_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/divy_ai"/>
    <language>en</language>
    <item>
      <title>OpenCode Is Powerful. That's Exactly the Problem.</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Tue, 21 Jul 2026 12:31:40 +0000</pubDate>
      <link>https://dev.to/divy_ai/opencode-is-powerful-thats-exactly-the-problem-3po0</link>
      <guid>https://dev.to/divy_ai/opencode-is-powerful-thats-exactly-the-problem-3po0</guid>
      <description>&lt;p&gt;&lt;em&gt;OpenCode is the free, open source alternative to Claude Code, with full shell access to match. Here's how I ran it safely using a sandbox instead of my laptop.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;OpenCode is an open-source coding agent with a workflow similar to Claude Code.&lt;/p&gt;

&lt;p&gt;You type what you want, it reads your codebase, edits files, runs shell commands on its own. Same basic idea as Claude Code. Free though, open source, and you're not locked into one model.&lt;/p&gt;

&lt;p&gt;Which is great, until you actually sit with what "runs shell commands on its own" means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first time you hand a terminal agent that kind of access, there's one question you can't quite shake:&lt;/strong&gt; what happens the moment it runs something you didn't expect?&lt;/p&gt;

&lt;p&gt;Most people pick a folder they don't care about and hope for the best. Or they don't think about it at all until something breaks.&lt;/p&gt;

&lt;p&gt;I didn't love either option, so before I let OpenCode near anything that mattered, I went looking for a third one.&lt;/p&gt;




&lt;p&gt;That's how I ended up moving the agent's shell into a disposable sandbox from &lt;a href="https://www.tensorlake.ai/" rel="noopener noreferrer"&gt;Tensorlake&lt;/a&gt;, that runs these as a cloud service, while leaving everything else right where it was on my laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Laptop
      │
      ▼
   OpenCode
      │
      ▼
Tensorlake Plugin
      │
      ▼
 Disposable Linux Sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what that looked like, and what actually surprised me once I had it running.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Kept Picturing Before I Typed Anything
&lt;/h2&gt;

&lt;p&gt;A coding agent's real risk has nothing to do with intelligence. Most of the time it gets things right. What it doesn't do is pause. It doesn't stop to double-check whether the command it's about to fire off is the one you actually meant to approve.&lt;/p&gt;

&lt;p&gt;One scenario kept nagging at me before I typed anything. I ask it to clean up build artifacts, and it runs &lt;code&gt;rm -rf ./build&lt;/code&gt;. Except &lt;code&gt;./build&lt;/code&gt; turns out to be a symlink into somewhere I still needed, the kind of thing an agent skims right past, and honestly, the kind of thing I don't always catch either when I'm moving fast.&lt;/p&gt;

&lt;p&gt;Or I ask it to install a dependency. &lt;code&gt;npm install&lt;/code&gt; fires a postinstall script. That script rewrites a config file I never agreed to touch. I've seen postinstall scripts do weirder things than that with a human sitting right there watching.&lt;/p&gt;

&lt;p&gt;Neither one is a bug to be honest. &lt;/p&gt;

&lt;p&gt;That's just what shell access does when nothing stands between a command and your machine.&lt;/p&gt;

&lt;p&gt;Then I thought bigger than my own laptop. Same agent, but now it's refactoring a production monorepo that ten other engineers are actively pushing commits to. Same misread symlink. Same rogue postinstall script. Except now it's not just my afternoon on the line.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;. These aren't agent-specific features. They're shell and filesystem access, handed to a process making its own calls about what to run next.&lt;/p&gt;

&lt;p&gt;The instinct is to fix this with more caution. Review every diff. Approve every command. That works right up until it doesn't, because the entire point of an autonomous agent is that you eventually stop reviewing every single step.&lt;/p&gt;

&lt;p&gt;The real fix turned out to be a different blast radius. &lt;/p&gt;

&lt;p&gt;Put the agent's commands somewhere disposable instead of on my machine, and now a bad command won't harm my system.&lt;/p&gt;

&lt;p&gt;Running locally isn't wrong, to be clear. For a throwaway project, or a workflow you already trust, it's exactly what you want. What changes the math is pointing autonomous shell access at something a mistake would actually cost you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brain Local, Hands in a Sandbox
&lt;/h2&gt;

&lt;p&gt;One table covers the whole shift. The wiring behind it takes a little longer to explain.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Local OpenCode&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;OpenCode + Tensorlake&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commands run&lt;/td&gt;
&lt;td&gt;On your laptop&lt;/td&gt;
&lt;td&gt;Remotely, in a sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem&lt;/td&gt;
&lt;td&gt;Your actual filesystem&lt;/td&gt;
&lt;td&gt;Isolated, disposable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Whatever's on your machine&lt;/td&gt;
&lt;td&gt;Disposable environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bad command affects&lt;/td&gt;
&lt;td&gt;Your machine&lt;/td&gt;
&lt;td&gt;The sandbox, not you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the outcome. The wiring is simpler than it sounds.&lt;/p&gt;

&lt;p&gt;Tensorlake ships a plugin called &lt;a href="https://www.npmjs.com/package/tensorlake-opencode" rel="noopener noreferrer"&gt;&lt;code&gt;tensorlake-opencode&lt;/code&gt;&lt;/a&gt;. The plugin doesn't replace OpenCode.It intercepts specific tool calls and reroutes them. Once I actually understood that distinction, the idea turned out simpler than I expected.&lt;/p&gt;

&lt;p&gt;OpenCode on your laptop, tools in a sandbox, that's the actual shape of it. The OpenCode harness itself, the interface, your session, all of that keeps running on your machine exactly like before.&lt;/p&gt;

&lt;p&gt;The model call still goes out to whichever provider you've configured, Anthropic, OpenAI, whoever, the same as it always did; that part was never local to begin with, and this plugin doesn't change it.&lt;/p&gt;

&lt;p&gt;What changed was where each individual tool call landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Your Laptop
                     │
                     ▼
          OpenCode Harness
                     │
      ┌──────────────┴──────────────┐
      ▼                             ▼
Model Provider              Tensorlake Sandbox
(Claude/OpenAI/etc.)      Shell + Filesystem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The model call still goes wherever you configured your provider. Only the tool calls, the hands, move into the sandbox.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;webfetch&lt;/code&gt; and &lt;code&gt;websearch&lt;/code&gt; are the two exceptions that stay local, since neither touches a filesystem. &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;glob&lt;/code&gt;, and &lt;code&gt;grep&lt;/code&gt; are the ones that get rerouted.&lt;/p&gt;

&lt;p&gt;Every intercepted command now makes a network round trip instead of running instantly on my machine. Tensorlake's documentation says a sandbox starts up in a few seconds, with the underlying VM image itself booting in hundreds of milliseconds. Their GitHub page and product site separately claim resume from a suspended state also lands under a second. My own first sandbox, the one the plugin spun up automatically on that &lt;code&gt;uname -a&lt;/code&gt; call, took 2.3 seconds end to end, per the timestamp the plugin logged, which fits comfortably inside what the docs describe. That's likely the plugin's own provisioning and connection overhead stacked on top of the raw VM boot, not just the VM starting up. Either way, it's not something you sit around waiting on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting It Up
&lt;/h2&gt;

&lt;p&gt;The whole setup turned out to be one config entry and one environment variable, typed in this order.&lt;/p&gt;

&lt;p&gt;First, the plugin, added to &lt;code&gt;~/.config/opencode/opencode.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://opencode.ai/config.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tensorlake-opencode"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenCode installs it automatically, no separate &lt;code&gt;npm install&lt;/code&gt; needed. Then the API key, exported in the same shell I was about to launch OpenCode from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_api_key_here
opencode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing happened. No sandbox spun up on launch, which threw me for a second. First instinct was that I'd messed up the config somehow, and I almost went back to check the JSON for a typo before actually rereading what I'd just set up: lazy creation, not eager. Tailing &lt;code&gt;~/.local/share/opencode/log/tensorlake.log&lt;/code&gt; just confirmed the plugin had loaded, nothing more, until I actually asked the agent to do something that needed a sandbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Real Test
&lt;/h2&gt;

&lt;p&gt;Sandbox creation in this plugin is lazy. It waits for the first tool call that actually needs the filesystem or a shell, not the moment you launch the session.&lt;/p&gt;

&lt;p&gt;I didn't want to point it at anything that mattered yet, so the first command I actually gave it was about as low-stakes as they come:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run: uname -a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;bash&lt;/code&gt; call was what triggered everything. A "Sandbox created" toast showed up in the terminal, and I had the log tailing in a second window the whole time. It read almost exactly what the docs describe: a line saying a new sandbox was being created for the session, then a second line confirming it was live, at 2.3 seconds.&lt;/p&gt;

&lt;p&gt;The output said Linux. My laptop runs macOS. Not going to lie, that one word convinced me faster than any architecture diagram would have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next I asked it to prove the filesystem side too:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write "Hello Tensorlake" to /tmp/workspace/test.txt, then read it back.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write and read both happened entirely inside the sandbox, confirming that filesystem operations were isolated from my machine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/tmp/workspace&lt;/code&gt; was the agent's working directory inside the sandbox, not a folder anywhere on my disk. Nothing in either test would have cost me anything if it had gone wrong.&lt;/p&gt;

&lt;p&gt;With the sandbox working, I wanted to move beyond smoke tests and verify the full developer workflow on a real repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clone https://github.com/benjaminp/six.git, find the ensure_str function, 
improve its TypeError message so it names the function it came from, 
then run the test suite and tell me if anything broke.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was intentionally a tiny change—the goal wasn't to contribute a feature, but to verify that OpenCode could complete the same edit–test loop I'd expect during normal development.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;six&lt;/code&gt; is a small, well-known Python compatibility library: one source file, a straightforward test suite, and just enough structure to exercise a realistic edit–test workflow. &lt;/p&gt;

&lt;p&gt;OpenCode cloned the repository, located &lt;code&gt;ensure_str&lt;/code&gt;, modified the function, and ran the test suite entirely inside the sandbox.&lt;/p&gt;

&lt;p&gt;The original implementation raised &lt;code&gt;TypeError("not expecting type '%s'" % type(s))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The edit made it &lt;code&gt;TypeError("ensure_str: not expecting type '%s'" % type(s))&lt;/code&gt;—a deliberately small edit that was easy to verify with the existing test suite. Because the tests only verify that a &lt;code&gt;TypeError&lt;/code&gt; is raised rather than checking the exact message text, this made for a safe, minimal edit, and the run confirmed it: 184 passed, 16 skipped, 0 failed. &lt;/p&gt;

&lt;p&gt;Same as before the change.&lt;/p&gt;

&lt;p&gt;That's the part that mattered to me. Not the specific repo or the specific one-line fix, but that a clone, a real edit, and a full test run all happened inside the sandbox, on an actual project, without me once worrying about what would happen to my own machine if something in that chain went sideways.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things That Clicked Once It Was Running
&lt;/h2&gt;

&lt;p&gt;I expected the isolation. The other two took me by surprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; A bad command genuinely has nowhere real to land. A runaway install, an &lt;code&gt;rm&lt;/code&gt; aimed at the wrong path, a dependency that half-installs and leaves things broken. All of it happens in a sandbox I can throw away, not my actual working tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt; mattered more once I pictured someone else on the team running this same setup. Whatever's actually installed on my laptop stops being relevant, because the agent isn't running on my laptop anymore. Register one image with the right toolchain baked in, point every future session at it, and the "works on my machine" conversation just stops happening.&lt;/p&gt;

&lt;p&gt;Then there's &lt;strong&gt;persistence&lt;/strong&gt;, the one I hadn't planned for. A named sandbox doesn't vanish when OpenCode restarts. It just sits there, parked. Come back later and the same working directory, the same installed packages, the same warm caches are all still exactly where I left them. Nothing rebuilt, nothing reinstalled.&lt;/p&gt;

&lt;p&gt;A disposable CI container vanishes the second a run finishes. This one was still there the next day, same as I'd left it.&lt;/p&gt;

&lt;p&gt;Took me an hour of lost installed state before that actually registered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring the Sandbox for Real Work
&lt;/h2&gt;

&lt;p&gt;Once I trusted it with something more than a test file, I went back and actually sized it properly. The plugin reads a small set of environment variables once, at the moment the first sandbox gets created, so they need to be set before you launch OpenCode, not after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_CPUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_MEMORY_MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8192
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_DISK_MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20480
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-custom-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Variable&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Default&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Controls&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TENSORLAKE_CPUS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;vCPUs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TENSORLAKE_MEMORY_MB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4096&lt;/td&gt;
&lt;td&gt;RAM in MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TENSORLAKE_DISK_MB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10240&lt;/td&gt;
&lt;td&gt;Disk in MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TENSORLAKE_IMAGE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;platform default&lt;/td&gt;
&lt;td&gt;The image the sandbox boots from&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're using a Personal Access Token instead of a project-scoped key, you'll also need &lt;code&gt;TENSORLAKE_ORGANIZATION_ID&lt;/code&gt; and &lt;code&gt;TENSORLAKE_PROJECT_ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TENSORLAKE_IMAGE&lt;/code&gt; is the one actually worth using. Bake your language runtime, system packages, and project dependencies into a registered image once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tl sbx image create Dockerfile &lt;span class="nt"&gt;--registered-name&lt;/span&gt; my-custom-image
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-custom-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every session after that starts already warm. I stopped watching the agent reinstall my stack from scratch every time I opened a new session.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;export&lt;/code&gt; only lasts for the current shell, so I added these to my shell profile once I knew I'd be using this setup regularly.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Operational Questions I Actually Had
&lt;/h2&gt;

&lt;p&gt;Before trusting this with anything real, I wanted answers to a few things that don't come up in a quick demo. Worth being upfront about one thing here: most of what follows is documented at the Tensorlake SDK and platform level. I haven't confirmed that the OpenCode plugin specifically exposes or manages each of these the same way, only that the underlying sandboxes it creates support them. Where that distinction matters, I've called it out below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I just walk away mid-session?&lt;/strong&gt; Named sandboxes auto-suspend after their idle timeout rather than terminating, at the platform level. Tensorlake's product site says the meter stops the moment it suspends, and their GitHub page puts resume at under a second, with filesystem, memory, and running processes exactly where you left them, not rebuilt. I haven't independently timed a resume myself, and I haven't confirmed whether the OpenCode plugin surfaces any control over this behavior or just inherits the platform default, so take the specific number as Tensorlake's claim about the platform, not a tested claim about this plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the agent kicks off something long-running, like a build or a test suite?&lt;/strong&gt; The underlying Tensorlake SDK supports starting background processes inside a sandbox that outlive the single command that launched them. I haven't tested whether the OpenCode plugin's &lt;code&gt;bash&lt;/code&gt; interception uses this pattern specifically, or whether a long-running command inside OpenCode just holds the tool call open for the duration. Either way, the sandbox itself isn't the bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my connection drops mid-command?&lt;/strong&gt; Tensorlake has written publicly about this exact failure mode at the platform level: when the transport hiccups mid-run, they retry and reap orphaned sandboxes so a flaky connection doesn't cost you the whole task. Again, this is a platform-level guarantee. I haven't tested how the plugin itself behaves if your local connection drops mid-tool-call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the sandbox itself crashes outright, not just an idle timeout?&lt;/strong&gt; Here's where I'll be straight with you: I didn't find documentation covering a hard crash mid-command specifically, at either the platform or plugin level, and I didn't manage to force one during testing either. If you're planning to run this against something you really can't afford to lose, that's worth confirming directly with Tensorlake rather than taking my word for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I'd Actually Use This
&lt;/h2&gt;

&lt;p&gt;By the end I had a rough rule for myself. Picture a coding agent refactoring a production monorepo while another engineer is pushing commits at the same time. The question was never whether the model was smart enough. It was whether I wanted its shell commands landing on my own workstation.&lt;/p&gt;

&lt;p&gt;This isn't the right setup for every OpenCode session, and I don't pretend otherwise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;My situation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What I'd do&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Personal throwaway project&lt;/td&gt;
&lt;td&gt;Run locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client repository&lt;/td&gt;
&lt;td&gt;Use Tensorlake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production codebase&lt;/td&gt;
&lt;td&gt;Use Tensorlake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letting the agent experiment freely with shell commands&lt;/td&gt;
&lt;td&gt;Use Tensorlake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared engineering environment&lt;/td&gt;
&lt;td&gt;Use Tensorlake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sandbox earns its place anywhere the blast radius of a wrong command actually matters. If I'm the only person who'll ever touch the repo and I'd shrug off losing it, I'm solving a problem I don't have yet.&lt;/p&gt;

&lt;p&gt;Worth a quick word on why this isn't just Docker or a Codespace with extra steps. A Docker container shares your host's kernel, which is fine for packaging an app but a thinner isolation boundary than a full VM if you're worried about what an autonomous agent might run. &lt;/p&gt;

&lt;p&gt;A local VM gives you the stronger boundary but is heavy and slow to spin up and tear down for something you might want to throw away every few minutes. GitHub Codespaces and Dev Containers solve a different problem: a consistent, persistent dev environment tied to a specific repo, not a fast, disposable environment built to be created and discarded on every session. &lt;/p&gt;

&lt;p&gt;The MicroVM approach here is closer to a VM's isolation with something closer to a container's boot speed, and it's built specifically to be thrown away.&lt;/p&gt;




&lt;h2&gt;
  
  
  If You Want to Try This Yourself
&lt;/h2&gt;

&lt;p&gt;Here's the order I'd do it in, knowing what I know now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Install OpenCode, add &lt;code&gt;tensorlake-opencode&lt;/code&gt; to &lt;code&gt;opencode.json&lt;/code&gt;. This just gets the plugin loaded, nothing else happens yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Export &lt;code&gt;TENSORLAKE_API_KEY&lt;/code&gt;, launch OpenCode, confirm the plugin loaded via the log file. This is the "is it actually on" check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Ask it to run something trivial, like &lt;code&gt;uname -a&lt;/code&gt;. This is what triggers sandbox creation and gives you visible proof the command ran remotely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Set &lt;code&gt;TENSORLAKE_CPUS&lt;/code&gt;, &lt;code&gt;TENSORLAKE_MEMORY_MB&lt;/code&gt;, and &lt;code&gt;TENSORLAKE_DISK_MB&lt;/code&gt; to match your real workload, before your next session starts, not mid-session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Later:&lt;/strong&gt; Build a custom image with your actual toolchain baked in, once the default image starts feeling like it's missing things you keep reinstalling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I went looking for a third option: an agent with full autonomy, running somewhere that wasn't my laptop. Got that part working fast. What actually surprised me was how little I had to think about afterward.&lt;/p&gt;

&lt;p&gt;I stopped reading every command before approving it once this was running. Wasn't carelessness. Just nothing left on my machine for a bad command to reach.&lt;/p&gt;

&lt;p&gt;Turns out the part that mattered was never whether the agent could run commands. It was where they landed. Not stopping mistakes. Just making sure they happen somewhere disposable instead of somewhere that costs me.&lt;/p&gt;

&lt;p&gt;If you've started letting an autonomous coding agent anywhere near your shell, moving that shell execution into a disposable sandbox is one of the highest-leverage changes you can make, and Tensorlake's own numbers put the setup at a few minutes, not an afternoon. The config entry and the environment variable up above are the whole thing.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/opencode" rel="noopener noreferrer"&gt;Tensorlake OpenCode Integration&lt;/a&gt;: Full setup, configuration, and lazy sandbox creation model&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;: The open source, terminal-first coding agent, MIT-licensed and model-agnostic&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/tensorlake-opencode" rel="noopener noreferrer"&gt;tensorlake-opencode on npm&lt;/a&gt;: The plugin package referenced in this article&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tensorlakeai/opencode-tensorlake-plugin" rel="noopener noreferrer"&gt;Plugin source on GitHub&lt;/a&gt;: Tool interceptors, session manager, lifecycle handling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/lifecycle" rel="noopener noreferrer"&gt;Tensorlake Sandbox Lifecycle&lt;/a&gt;: The suspend, resume, and snapshot model underneath this integration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/images" rel="noopener noreferrer"&gt;Tensorlake Sandbox Images&lt;/a&gt;: Building and registering a custom image&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>programming</category>
      <category>beginners</category>
      <category>python</category>
    </item>
    <item>
      <title>Master These 6 AI Concepts to Become an AI Engineer ( A Visual Explanation)</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Wed, 15 Jul 2026 09:27:36 +0000</pubDate>
      <link>https://dev.to/divy_ai/master-these-6-ai-concepts-to-become-an-ai-engineer-a-visual-explanation-3589</link>
      <guid>https://dev.to/divy_ai/master-these-6-ai-concepts-to-become-an-ai-engineer-a-visual-explanation-3589</guid>
      <description>&lt;p&gt;Job postings asking for AI skills are up 143% in a single year. The people filling those roles didn't spend years on advanced math. They learned six ideas, in the right order, and started building.&lt;/p&gt;

&lt;p&gt;Most people think you need a math degree, or years of computer science training, to become a serious AI developer. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gap between someone who can barely get a chatbot to behave and someone building real, production AI systems isn't intelligence. It isn't years of study. It's six specific concepts, learned in an order that actually makes sense, instead of scattered across a hundred confusing tutorials.&lt;/p&gt;

&lt;p&gt;Here they are, explained assuming you know nothing yet.&lt;/p&gt;




&lt;h3&gt;
  
  
  A quick honest note before we start
&lt;/h3&gt;

&lt;p&gt;This roadmap covers how to build AI-powered applications, chatbots, assistants, and tools that use existing AI models well. That's different from becoming a machine learning researcher who builds new models from scratch, which genuinely does need heavy math and years of study.&lt;/p&gt;

&lt;p&gt;Most people who say "I want to become an AI developer" mean the first path. That's also where almost all the current job demand is. AI-related job postings are up 143% year over year, and the engineers filling those roles are, overwhelmingly, application builders, not research scientists.&lt;/p&gt;

&lt;p&gt;This is that path.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Level 1: Beginner
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  1. The Context Window
&lt;/h3&gt;

&lt;p&gt;Here's the concept that trips up almost every beginner, because it isn't obvious until something breaks.&lt;/p&gt;

&lt;p&gt;An AI model can only "see" a limited amount of text at once. That limit is called the &lt;strong&gt;context window&lt;/strong&gt;. Picture a whiteboard in a meeting room. You can write a lot on it, but once it's full, anything written past the edge simply isn't there anymore. It's not that the AI forgot. It never saw it.&lt;/p&gt;

&lt;p&gt;This matters the moment you try to have a long conversation, or feed the AI a huge document. Past a certain point, older parts of the conversation quietly fall off the edge of the whiteboard, and the AI starts responding as if they never happened.&lt;/p&gt;

&lt;p&gt;Understanding this one limit explains most of the "why did the AI suddenly get confused" moments beginners run into.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 2: Intermediate
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  2. RAG (Retrieval-Augmented Generation)
&lt;/h3&gt;

&lt;p&gt;An AI model only knows what it learned during training. Ask it about your company's internal policies, and it has nothing, because it never read them. &lt;strong&gt;RAG fixes this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think of it as the difference between a closed-book exam and an open-book one. Without RAG, the AI answers purely from memory. With RAG, it's allowed to flip open a specific book—your documents, your database, your knowledge base—and check before answering.&lt;/p&gt;

&lt;p&gt;In practice, this happens in three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chunking:&lt;/strong&gt; Your documents get broken into smaller pieces and stored in a searchable format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; The AI searches that storage for the pieces most relevant to the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Augmentation and Generation:&lt;/strong&gt; It answers using both what it already knew and what it just found.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Virtually every AI chatbot that answers questions about a specific company's internal information is running RAG under the hood. It is arguably the single most valuable skill on this entire list, because it is the difference between a generic AI and one that actually knows your business.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fine-Tuning
&lt;/h3&gt;

&lt;p&gt;Prompting hands the AI a fresh set of instructions every single time. Fine-tuning is different: it actually reshapes how the model behaves, permanently, by training it further on a narrow, specific set of examples.&lt;/p&gt;

&lt;p&gt;Think of the difference between handing someone a manual before every task versus sending them through months of specialized training. The manual works for most jobs. Specialized training changes how someone thinks about the job itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Most developers should reach for RAG first. Fine-tuning costs more, takes longer, and is usually only worth it when you need a very specific style, format, or behavior that no amount of careful instruction alone can reliably produce.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. AI Agents
&lt;/h3&gt;

&lt;p&gt;Everything so far has been about the AI answering questions. &lt;strong&gt;An agent is about the AI actually doing things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Picture the difference between a consultant who gives you advice and an assistant who actually goes and books the flight. A consultant-style AI just answers. An agent actually takes real action instead of just describing what action you should take: searching the web, running code, sending an email, or updating a database.&lt;/p&gt;

&lt;p&gt;The AI does this by deciding, on its own, which step to take next based on what it just learned. Ask it to fix a bug, and it can read the error, try a fix, check if that fix worked, and try something else if it didn't—all without you typing a new instruction after every single step.&lt;/p&gt;

&lt;p&gt;This is where AI development starts feeling genuinely powerful, and also where it starts requiring real care, which is exactly why the next two concepts exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Level 3: Advanced
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  5. MCP (Model Context Protocol)
&lt;/h3&gt;

&lt;p&gt;Imagine every country having a different shape of electrical socket, and every single appliance needing its own custom adapter just to work when you travel. That was the old way AI agents connected to outside tools: every connection was custom-built, one at a time, for every tool and every AI model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP works like a universal socket standard.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once a tool speaks MCP, any AI agent that understands MCP can plug into it directly. No custom adapter needed. Anthropic introduced this standard, and it has quickly become the common way agents connect to databases, files, and other services, regardless of which AI model is actually doing the work. If you're building an agent that needs to reach outside tools, MCP is very likely how that connection gets made.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Harness Engineering
&lt;/h3&gt;

&lt;p&gt;This is one of the most critical operational concepts for production engineering. Picture a stunt performer on a film set. They're genuinely skilled. But nobody lets them attempt a dangerous stunt without a safety harness, a spotter, a hard limit on how many takes they get, and a director watching every single shot.&lt;/p&gt;

&lt;p&gt;The performer's skill isn't what keeps them safe on set. The equipment and process wrapped around them is.&lt;/p&gt;

&lt;p&gt;An AI agent works the same way. The model is the skilled performer. &lt;strong&gt;The harness is everything wrapped around it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A hard limit on how much it's allowed to spend before it has to stop.&lt;/li&gt;
&lt;li&gt;Checkpoints that save progress so a crash doesn't waste hours of work.&lt;/li&gt;
&lt;li&gt;Guardrails on which actions it's actually allowed to execute.&lt;/li&gt;
&lt;li&gt;Logs so a person can audit exactly what happened if something goes wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This concept exists because of one striking number: &lt;strong&gt;88% of AI agent projects never make it into real production use.&lt;/strong&gt; Not because the models were bad, but because nobody built the harness around them.&lt;/p&gt;




&lt;h3&gt;
  
  
  How these six AI concepts fit together
&lt;/h3&gt;

&lt;p&gt;None of these exist alone in a real system. A production AI assistant, the kind companies actually pay for, typically uses several of these at once: &lt;strong&gt;RAG&lt;/strong&gt; to pull in company-specific knowledge, an &lt;strong&gt;agent&lt;/strong&gt; to actually take action, &lt;strong&gt;MCP&lt;/strong&gt; so that agent can reach real tools, and a &lt;strong&gt;harness&lt;/strong&gt; watching the whole thing to make sure nothing goes wrong.&lt;/p&gt;

&lt;p&gt;Learning them one at a time makes sense. Using only one at a time, in a real product, almost never does.&lt;/p&gt;




&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The context window&lt;/strong&gt; is the AI's limited working memory. Understanding its limit explains most confusing AI mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt; lets an AI answer from your own documents instead of just what it learned in training. It is the most valuable skill on this list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning&lt;/strong&gt; permanently reshapes a model's behavior. Reach for it only when instructions genuinely can't get you there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI Agent&lt;/strong&gt; takes real action instead of just answering, deciding its own next step based on what it just learned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP&lt;/strong&gt; is the universal standard that lets agents plug into outside tools without a custom connection built for every single one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness engineering&lt;/strong&gt; is what turns a smart model into a reliable system. The missing harness is usually why agent projects fail to reach production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The part worth remembering
&lt;/h3&gt;

&lt;p&gt;Nobody starts as an advanced AI developer. Every person building serious production AI systems today started exactly where you might be starting now—watching a conversation quietly lose track of what was said three messages ago and wondering why.&lt;/p&gt;

&lt;p&gt;What separates a beginner from someone advanced isn't talent. It's this list, learned in order, applied to something real. The math and the deep model architecture that everyone assumes is required? Most working AI developers never touch it. They learned six ideas, built things with them, and kept going.&lt;/p&gt;

&lt;p&gt;You can start with the context window today. Nothing on this list requires anything you don't already have.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hackernoon.com/5-must-have-ai-skills-for-developers-in-2026-and-how-to-learn-them" rel="noopener noreferrer"&gt;5 Must-Have AI Skills for Developers (HackerNoon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doit.software/blog/ai-developer-skills" rel="noopener noreferrer"&gt;Top 13 AI Developer Skills (DOIT Software)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://uploadarticle.com/ai-developer-roadmap-2026/" rel="noopener noreferrer"&gt;From Beginner to AI Developer: A Practical Roadmap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@adnanmasood/agent-harness-engineering-the-rise-of-the-ai-control-plane-938ead884b1d" rel="noopener noreferrer"&gt;Agent Harness Engineering: The Rise of the AI Control Plane (Medium)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Fri, 10 Jul 2026 12:39:27 +0000</pubDate>
      <link>https://dev.to/divy_ai/-2g20</link>
      <guid>https://dev.to/divy_ai/-2g20</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo" class="crayons-story__hidden-navigation-link"&gt;Why Your AI Experiments Keep Starting From Scratch (And How Tensorlake Fixes It)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/divy_ai" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845890%2Fc910b35f-87b4-4888-b116-ad2cde414360.png" alt="divy_ai profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/divy_ai" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Divy Yadav
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Divy Yadav
                
              
              &lt;div id="story-author-preview-content-4111884" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/divy_ai" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845890%2Fc910b35f-87b4-4888-b116-ad2cde414360.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Divy Yadav&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo" id="article-link-4111884"&gt;
          Why Your AI Experiments Keep Starting From Scratch (And How Tensorlake Fixes It)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            12 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why Your AI Experiments Keep Starting From Scratch (And How Tensorlake Fixes It)</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Fri, 10 Jul 2026 08:58:57 +0000</pubDate>
      <link>https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo</link>
      <guid>https://dev.to/divy_ai/why-your-ai-experiments-keep-starting-from-scratch-and-how-tensorlake-fixes-it-4gbo</guid>
      <description>&lt;p&gt;&lt;em&gt;How memory checkpoints and sandbox forking let you build once, checkpoint the warm state, and run as many parallel workers as you need. No reinstalls. No reloads.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Running eight parallel ML experiments sounds efficient. Watching each one reinstall numpy from scratch does not.&lt;/p&gt;

&lt;p&gt;The logs told the story: eight sandbox workers, each spending its first 40 seconds on &lt;code&gt;pip install numpy&lt;/code&gt; and loading the dataset before a single line of training code ran. The training scripts were different. &lt;/p&gt;

&lt;p&gt;The setup wasn't. &lt;strong&gt;I had paid for the same 40 seconds of work eight times.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The training scripts were the candidates. The setup was not.&lt;/p&gt;

&lt;p&gt;That is not a performance bug. It is a mental model problem. &lt;/p&gt;

&lt;p&gt;This article is the fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Starting Fresh" Is Not the Same as "Starting Clean"
&lt;/h2&gt;

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

&lt;p&gt;Most experiment setups quietly make an assumption: real worker isolation means booting from a clean image every time. No shared state. No residue from previous runs.&lt;/p&gt;

&lt;p&gt;That assumption is not wrong. It is just more expensive than it needs to be.&lt;/p&gt;

&lt;p&gt;Workers running parallel experiments need independent filesystems and independent process trees. They need a known starting state. What they do not need is to reinstall numpy, reload the same dataset from disk, or re-seed a Python environment that was identical across all of them at the start of the iteration.&lt;/p&gt;

&lt;p&gt;"Starting fresh" became a proxy for isolation. &lt;/p&gt;

&lt;p&gt;But the actual requirement is narrower: get every worker to the same state, then let them diverge. That is a different primitive than booting from a base image.&lt;/p&gt;




&lt;h2&gt;
  
  
  Filesystem Snapshots vs Memory Snapshots
&lt;/h2&gt;

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

&lt;p&gt;Before I discovered how &lt;strong&gt;Tensorlake&lt;/strong&gt; handles this, I assumed all snapshots worked the same way Docker images do.&lt;/p&gt;

&lt;p&gt;A filesystem snapshot captures disk state only. When you restore from one, the sandbox cold boots: the VM initializes from scratch, the OS comes up, Python starts, your process begins again. The installed packages are there on disk, so you skip the &lt;code&gt;apt install&lt;/code&gt; and &lt;code&gt;pip install&lt;/code&gt; steps. But you still pay for boot time and for every piece of process initialization that was not baked into the image. &lt;/p&gt;

&lt;p&gt;The interpreter loads. Modules get imported. Data gets pulled into RAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I learned this the hard way.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;My first implementation used filesystem snapshots because they were the default. I launched eight workers from the same filesystem snapshot and watched every one of them cold boot, import NumPy, load the dataset, and rebuild the execution environment. &lt;/p&gt;

&lt;p&gt;I had skipped the package installation step, but I still paid for everything that came afterward.&lt;/p&gt;

&lt;p&gt;Then I read the docs again and saw &lt;code&gt;CheckpointType.MEMORY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A memory snapshot captures the filesystem plus the entire VM memory state, including all running processes at the exact moment of capture. Restore from one and the sandbox resumes warm. The Python interpreter is already running. Modules imported before the checkpoint are already loaded. Variables that were in memory are still there.&lt;/p&gt;

&lt;p&gt;It is closer to Unix &lt;code&gt;fork()&lt;/code&gt; than to a Docker image. You are not restoring a filesystem. You are branching from a specific execution point.&lt;/p&gt;

&lt;p&gt;Tensorlake exposes filesystem and memory checkpoints as distinct primitives, making it possible to choose between cold restores and warm restores depending on the workload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;

&lt;span class="c1"&gt;# Cold-boot restore: captures disk state only
&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FILESYSTEM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Warm restore: captures disk + VM memory + running processes
&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default when you call &lt;code&gt;sandbox.checkpoint()&lt;/code&gt; with no arguments is &lt;code&gt;filesystem&lt;/code&gt;. For the fork pattern described in this article, you want &lt;code&gt;memory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One tradeoff to understand upfront: memory snapshots lock the resource configuration at capture time.Image, CPU count, memory limit, entrypoint, and secrets all come from the snapshot when you restore. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot change them at restore time.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;If you need different resource allocations per worker, use filesystem snapshots and accept the cold boot. For most parallel experiment setups, the locked resources are fine since you sized the base environment for the workers when you created it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets are the exception.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;They survive the checkpoint but are not locked to it. Secrets are passed as environment variables to &lt;code&gt;run()&lt;/code&gt;, so you can pass a new value at restore time and overwrite whatever was captured in the snapshot. Rotating an API key across forked workers does not require a new snapshot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fork Pattern
&lt;/h2&gt;

&lt;p&gt;Once you have a memory snapshot, the fork pattern is straightforward. Here is what it looks like end to end:&lt;/p&gt;

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

&lt;p&gt;Every worker starts from the same warm execution point. They share nothing at runtime. The base sandbox can be terminated right after the checkpoint. The &lt;code&gt;snapshot_id&lt;/code&gt; persists independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create one base sandbox and do all shared setup inside it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake/ubuntu-minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Install shared dependencies once
&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numpy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--break-system-packages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Verify the environment is ready
&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import numpy; import pandas; print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environment ready.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Capture the warm state.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snapshot captured: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The base sandbox can be terminated. The snapshot persists independently.
&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Restore N workers in parallel from the same checkpoint.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Each worker restores a fresh copy of the warm environment
&lt;/span&gt;    &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;

&lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;script_v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script_v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script_v3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script_v4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script_v5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_experiment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;Sandbox.create(snapshot_id=...)&lt;/code&gt; call restores an independent copy of the warm environment. Workers share no filesystem and no runtime state. They just started from the same point.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;snapshot.snapshot_id&lt;/code&gt; is a persistent string. Save it to a file. Use it in the next session, in the next iteration of your loop, in a different process entirely. The warm state survives until you explicitly delete the snapshot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Actually Matters
&lt;/h2&gt;

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




&lt;h3&gt;
  
  
  ML Experiment Racing
&lt;/h3&gt;

&lt;p&gt;Andrej Karpathy published the autoresearch repo in early 2026. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core idea:&lt;/strong&gt; an LLM reads your current best training script, proposes N code modifications, you race all N in parallel sandboxes, keep the winner, and loop. The loop runs overnight. Each accepted modification becomes the new baseline for the next iteration.&lt;/p&gt;

&lt;p&gt;The naive implementation pays the setup cost N times per iteration. If the training script needs numpy, scipy, and a small dataset loaded into memory, that is real time per candidate, per loop. With 8 iterations and 3 candidates each, you have paid identical setup cost 24 times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Tensorlake's Snapshot Fork Pattern&lt;/strong&gt;, you do the setup once. &lt;/p&gt;

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

&lt;p&gt;Take a memory checkpoint. Fork all candidates from that checkpoint. &lt;/p&gt;

&lt;p&gt;They start warm and only diverge where the proposed modification changes behavior.&lt;/p&gt;

&lt;p&gt;The structure of one iteration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Build the warm baseline once per iteration
&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake/ubuntu-minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numpy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scipy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--break-system-packages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import dataset; dataset.load_into_memory()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Checkpoint once: all candidates share this starting point
&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Race candidates from the warm snapshot in parallel
# Each worker starts where the base left off, not from a fresh image
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;run_in_sandbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Keep the winner, discard the rest, repeat
&lt;/span&gt;&lt;span class="n"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;val_loss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;snapshot.snapshot_id&lt;/code&gt; is reused across all candidates in the same iteration. Setup cost is paid once per loop, not once per candidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  RL Rollouts with Reproducibility
&lt;/h3&gt;

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

&lt;p&gt;RL rollouts have a hard requirement: same seed, same action sequence, same trajectory. Every time, without exception. That guarantee breaks the moment workers share any state. A shared pip cache can introduce version skew between runs. A shared &lt;code&gt;/tmp&lt;/code&gt; carries residual files from previous episodes. Even calling &lt;code&gt;env.reset()&lt;/code&gt; correctly does not help when state outside the environment object persists between episodes.&lt;/p&gt;

&lt;p&gt;With per-rollout sandboxes forked from a common snapshot, the isolation is structural rather than enforced by convention. There is no shared filesystem between workers.&lt;/p&gt;

&lt;p&gt;The seed goes directly into the Python script that runs inside the sandbox, not into the host process. Keeping it there means the host's random state stays completely out of the episode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gym_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Script runs inside the sandbox, not the host
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
import gymnasium as gym, json

env = gym.make(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CartPole-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
obs, _ = env.reset(seed=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)
env.action_space.seed(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)

trajectory, total_reward = [], 0.0
for _ in range(200):
    action = env.action_space.sample()
    next_obs, reward, terminated, truncated, _ = env.step(action)
    trajectory.append((obs.tolist(), int(action), float(reward), bool(terminated)))
    total_reward += reward
    obs = next_obs
    if terminated or truncated:
        break

print(json.dumps({{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_reward&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: total_reward, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: len(trajectory)}}))
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_rollout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;gym_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One specific gotcha here: &lt;code&gt;env.reset(seed=seed)&lt;/code&gt; only seeds the observation and transition RNG. The action space has its own separate RNG that requires &lt;code&gt;env.action_space.seed(seed)&lt;/code&gt; independently. Miss the second call and trajectories vary across runs with no obvious error. The mismatch is silent and will look like flaky results until you trace it to the gymnasium source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Browser Agents
&lt;/h3&gt;

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

&lt;p&gt;Browser warmup is slow: login flows, OAuth, session cookies, waiting for JavaScript-heavy pages to stabilize. If you are running 20 browser workers in parallel, you do not want each one repeating that from scratch.&lt;/p&gt;

&lt;p&gt;Tensorlake's &lt;code&gt;ubuntu-vnc&lt;/code&gt; sandbox image makes this practical because the authenticated browser itself becomes part of the checkpoint.&lt;/p&gt;

&lt;p&gt;Complete the auth flow once, checkpoint the authenticated browser state, then fork your workers from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;

&lt;span class="c1"&gt;# Authenticate once in a single browser sandbox
&lt;/span&gt;&lt;span class="n"&gt;browser_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake/ubuntu-vnc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ... drive Chrome via CDP, complete login, reach stable page state ...
&lt;/span&gt;
&lt;span class="c1"&gt;# Checkpoint the authenticated browser in memory
&lt;/span&gt;&lt;span class="n"&gt;auth_snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser_base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;browser_base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# All 10 workers start with the logged-in browser state already in memory
&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth_snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern that eliminates repeated &lt;code&gt;pip install&lt;/code&gt; across ML workers eliminates repeated OAuth flows across browser workers. The primitive is the same. Only the warmup content changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Suspend vs. Snapshot: Two Different Things
&lt;/h2&gt;

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

&lt;p&gt;Suspend and snapshot both preserve sandbox state. They solve different problems and it is worth being clear about which one you reach for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspend&lt;/strong&gt; pauses this specific sandbox and holds its state for later resumption under the same sandbox ID. It uses no compute while suspended. When you resume, the sandbox comes back in under a second with the same process IDs, filesystem, and in-memory state intact. Named sandboxes auto-suspend on timeout instead of terminating, which means you do not lose a long-running agent session because a task ran slightly over its time budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot&lt;/strong&gt; captures a reusable artifact you can restore into new sandboxes. The artifact persists after the source sandbox is terminated. Restore from it once or many times.&lt;/p&gt;

&lt;p&gt;Suspend is for pausing and resuming a single workstream. Snapshot is for branching from a known state into N parallel workers.&lt;/p&gt;

&lt;p&gt;The sandbox keeps running while the snapshot is being captured. The snapshot artifact persists regardless of what happens to the source sandbox afterward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Matrix
&lt;/h2&gt;

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




&lt;h2&gt;
  
  
  Why This Works: The Underlying Principle
&lt;/h2&gt;

&lt;p&gt;Unix &lt;code&gt;fork()&lt;/code&gt; exists for the same reason. Process creation is expensive, so instead of spawning a child from scratch, you copy the parent's entire memory state and let the child diverge from there.&lt;/p&gt;

&lt;p&gt;Git branches for the same reason: you checkpoint a known state and branch from it because re-deriving full history for every branch would be wasteful.&lt;/p&gt;

&lt;p&gt;AI experiment infrastructure got filesystem snapshots first. But filesystem snapshots only moved the cost boundary to disk. The running process, loaded modules, in-memory data, and interpreter state all had to rebuild on every new worker anyway.&lt;/p&gt;

&lt;p&gt;Memory checkpointing moves that boundary further. The running process is part of what gets captured. Load a 500MB dataset into a pandas DataFrame before the checkpoint, and every forked worker starts with that DataFrame already in RAM. Pre-compile JIT functions and the cache is there too. The interpreter does not restart. Nothing reloads.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The expensive part of an experiment is rarely the experiment. It is everything that had to be true before the experiment could start.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That cost is not fixed. It just looked fixed because the tooling treated it that way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Platform Handles
&lt;/h2&gt;

&lt;p&gt;Three numbers that matter for the fork pattern specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; The &lt;code&gt;tensorlake/ubuntu-minimal&lt;/code&gt; image starts up in a few hundred milliseconds; &lt;code&gt;tensorlake/ubuntu-systemd&lt;/code&gt; (full init system) takes around one second. Forked workers restore from a memory snapshot warm, skipping boot and process initialization entirely. Suspended named sandboxes resume in under a second without losing any memory or filesystem state. The &lt;a href="https://github.com/tensorlakeai/sandbox-sqlite-bench" rel="noopener noreferrer"&gt;published SQLite benchmark&lt;/a&gt; (100k inserts, 2 vCPU / 4GB) shows Tensorlake at 2.45s against Vercel at 3.00s, E2B at 3.92s, Modal at 4.66s, and Daytona at 5.51s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale.&lt;/strong&gt; Tensorlake supports fanning out to thousands concurrent sandbox environments — the number the Harbor integration targets for RL rollouts and eval pipelines running from a single snapshot. The overall project limit is 5 million sandboxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; Sandboxes run on MicroVMs backed by Firecracker and CloudHypervisor. LLM-generated code never shares a kernel with other tenants or with your host process. Tensorlake is SOC 2 Type II and HIPAA compliant, with EU data residency and zero data retention options available.&lt;/p&gt;




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

&lt;p&gt;Install the SDK and grab an API key from &lt;a href="https://cloud.tensorlake.ai" rel="noopener noreferrer"&gt;cloud.tensorlake.ai&lt;/a&gt; (free tier available):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tensorlake
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_api_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal working fork pattern you can run right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="c1"&gt;# Step 1: Warm the base environment
&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;numpy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--break-system-packages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Checkpoint the warm state
&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Step 3: Fork workers from the snapshot
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="n"&gt;scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three workers start warm. &lt;code&gt;snapshot.snapshot_id&lt;/code&gt; is a persistent string you can store and reuse across sessions.&lt;/p&gt;




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

&lt;p&gt;Those eight &lt;code&gt;pip install numpy&lt;/code&gt; calls were not a performance bug.They were a modeling mistake. I had built my experiment loop around "start fresh" when what I actually needed was "start identical."&lt;/p&gt;

&lt;p&gt;The difference is real. Starting fresh means paying full setup cost per worker. Starting identical means paying it once and forking from there.&lt;/p&gt;

&lt;p&gt;Tensorlake's memory checkpoints provide one implementation of this pattern: build the environment once, capture the execution state, and fork as many isolated workers as you need from the same warm baseline.&lt;/p&gt;

&lt;p&gt;You do the expensive shared work once, freeze the execution state, and branch as many workers as you need from that exact point. Each worker gets its own isolated environment. The setup cost is paid once.&lt;/p&gt;

&lt;p&gt;This applies to most parallel AI workflows: ML candidate racing, RL rollouts, browser agent pools, CI evaluation pipelines. The pattern is the same across all of them.&lt;/p&gt;

&lt;p&gt;If I could go back to those eight workers all stuck on &lt;code&gt;pip install numpy&lt;/code&gt;, the fix would have been four lines. Create the base. Warm it. Call &lt;code&gt;checkpoint(MEMORY)&lt;/code&gt;. Restore from there. The 320 seconds of identical setup would have happened once. The other seven copies would never have existed.&lt;/p&gt;

&lt;p&gt;That is what the pattern is for.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/introduction.md" rel="noopener noreferrer"&gt;Tensorlake Sandboxes: Introduction&lt;/a&gt;: Overview of how MicroVM sandboxes work, boot times, and supported images&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/snapshots.md" rel="noopener noreferrer"&gt;Tensorlake Snapshots Documentation&lt;/a&gt;: Full reference for &lt;code&gt;checkpoint()&lt;/code&gt;, snapshot types, restore, clone, and delete operations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/lifecycle.md" rel="noopener noreferrer"&gt;Tensorlake Sandbox Lifecycle&lt;/a&gt;: State machine for ephemeral vs named sandboxes, suspend/resume, and timeout behavior&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/agentic-autoresearch.md" rel="noopener noreferrer"&gt;Tensorlake Agentic Autoresearch Loop&lt;/a&gt;: Full working example of the Karpathy-inspired autoresearch pattern using Tensorlake sandboxes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/agentic-rl-reproducible-env.md" rel="noopener noreferrer"&gt;Tensorlake RL Reproducible Environments&lt;/a&gt;: Deterministic rollout guarantees with per-rollout sandbox isolation and seed embedding&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/agentic-swarm-intelligence.md" rel="noopener noreferrer"&gt;Tensorlake Agentic Swarm Intelligence&lt;/a&gt;: Map-reduce pattern for parallel agent swarms in isolated sandboxes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/chrome-cdp.md" rel="noopener noreferrer"&gt;Tensorlake Drive Chrome over CDP&lt;/a&gt;: Running Chrome inside &lt;code&gt;ubuntu-vnc&lt;/code&gt; sandboxes and driving them via the Chrome DevTools Protocol&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/harbor.md" rel="noopener noreferrer"&gt;Tensorlake Harbor Integration&lt;/a&gt;: Running Harbor evaluations and RL rollouts with 10k+ concurrent sandbox environments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tensorlakeai/sandbox-sqlite-bench" rel="noopener noreferrer"&gt;Tensorlake SQLite Benchmark&lt;/a&gt;: Public benchmark comparing sandbox filesystem performance across Tensorlake, Vercel, E2B, Modal, and Daytona&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/karpathy/autoresearch" rel="noopener noreferrer"&gt;karpathy/autoresearch&lt;/a&gt;: The original autoresearch repo by Andrej Karpathy that inspired the ML experiment racing pattern&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.tensorlake.ai/sandboxes/sdk-reference.md" rel="noopener noreferrer"&gt;Tensorlake SDK Reference&lt;/a&gt;: Complete Sandbox handle method reference for Python and TypeScript&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI Document Processing: What Production Systems Actually Need</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Wed, 08 Jul 2026 11:44:47 +0000</pubDate>
      <link>https://dev.to/divy_ai/ai-document-processing-what-production-systems-actually-need-121d</link>
      <guid>https://dev.to/divy_ai/ai-document-processing-what-production-systems-actually-need-121d</guid>
      <description>&lt;p&gt;Getting extraction working on test documents is the easy part. Here is what breaks before you hit month two.&lt;/p&gt;

&lt;p&gt;Three weeks, two engineers, one prompt. They tested it on twenty invoices from their biggest vendor: 95% field accuracy, clean JSON, ready to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So they shipped it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Six weeks later, the pipeline was silently dropping line items from any vendor whose invoice didn't match the layout it had been tested on. No crashes. No errors. Just missing data, quietly piling up into a four-month reconciliation mess before anyone noticed.&lt;/p&gt;

&lt;p&gt;The team hadn't gotten extraction wrong. They'd gotten the problem wrong. The real issues were layout variation across vendors, no validation layer to catch silent failures, and nowhere for exceptions to go.&lt;br&gt;
**&lt;br&gt;
That's where most AI document processing projects actually die. **&lt;/p&gt;

&lt;p&gt;Not in the demo. In month two, once documents start arriving from vendors nobody tested against.&lt;/p&gt;

&lt;p&gt;This piece covers what production-ready AI document processing actually requires, and the four failure modes I keep watching teams hit once they're past the pilot.&lt;/p&gt;



&lt;p&gt;If you want more such information about AI, consider subscribing to my newsletter, where you will get noise-free AI information every week&lt;br&gt;
Link for the newsletter: &lt;a href="https://aiengsimplified.beehiiv.com/" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What AI Document Processing Actually Is
&lt;/h2&gt;

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

&lt;p&gt;AI document processing means using machine learning models and LLMs to pull structured data from unstructured documents, without manually configuring templates for each document format.&lt;/p&gt;

&lt;p&gt;Inputs vary widely: native PDFs, scanned images, photographs of physical forms, multi-page mixed files. The output is structured JSON or database-ready records that downstream systems can consume directly.&lt;/p&gt;

&lt;p&gt;The processing chain has four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion.&lt;/strong&gt; Document enters the pipeline, type gets identified, file is prepared for parsing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout understanding.&lt;/strong&gt; The system maps spatial structure: columns, rows, headers, key-value pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction.&lt;/strong&gt; The LLM reads the structured layout and identifies target fields based on context and semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output.&lt;/strong&gt; Structured JSON flows into a database, ERP, data warehouse, or API endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OCR and LLM-based extraction are not competing approaches. They're two separate layers in the same pipeline. OCR converts pixels to text. The LLM then operates on that text to understand meaning and pull specific fields. The critical dependency is that the OCR layer has to preserve layout for the LLM layer to work correctly.&lt;/p&gt;

&lt;p&gt;This is the first thing most teams miss, and it's where a lot of production failures originate.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Layout Problem Nobody Explains Well
&lt;/h2&gt;

&lt;p&gt;Here is what happens when you run a standard OCR tool on a bank statement with multi-column transactions:&lt;/p&gt;

&lt;p&gt;The tool reads the page left to right, top to bottom, and produces a flat text stream. A transaction table with date, description, debit, credit, and running balance columns gets compressed into something like: &lt;code&gt;07/01/2019 Deposit = 131 $209.54 $654.82&lt;/code&gt;. Which value is the debit? Which is the balance? The spatial relationship that made the table readable is gone.&lt;/p&gt;

&lt;p&gt;An LLM trying to extract structured data from that flattened output isn't working with a document. It's working with noise. It may produce plausible-looking results, and it might even get most fields right on the documents you tested. But accuracy will be inconsistent across different banks, different statement periods, and different vendor layouts. You won't know how inconsistent until something downstream breaks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Layout is where meaning lives in documents. Flatten the layout at the parsing stage and you have destroyed the document's semantics before extraction even begins.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix is &lt;a href="https://unstract.com/llmwhisperer/" rel="noopener noreferrer"&gt;layout-aware parsing&lt;/a&gt;: an OCR layer that preserves spatial structure rather than discarding it. When transaction rows stay aligned, column relationships stay intact, and headers remain separated from body content, the LLM gets input it can reason over correctly. The extraction layer becomes reliable because the parsing layer did its job.&lt;/p&gt;

&lt;p&gt;This distinction matters operationally. A pipeline that gets layout right will generalize to new document variants from the same class. One that doesn't will fail silently, and the failures will look random because the actual root cause is buried upstream.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Traditional Pipelines Break at Scale
&lt;/h2&gt;

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

&lt;p&gt;Most document processing systems start with templates. You define where each field lives on the page, and the system reads that coordinate.&lt;/p&gt;

&lt;p&gt;It works exactly as long as nothing changes.&lt;/p&gt;

&lt;p&gt;The moment a vendor updates their invoice layout, moves the PO number two columns right, or switches accounting software, the template breaks. Extraction fails silently or returns garbage, and someone has to rebuild the template before processing can resume. Then again next quarter.&lt;/p&gt;

&lt;p&gt;At enterprise scale, this becomes a maintenance treadmill. Organizations processing documents from hundreds of vendors end up maintaining hundreds of templates. Every new vendor requires a new one. Every layout change requires a manual update. The engineering overhead compounds continuously, and the system gets more fragile, not more capable, as document variety grows.&lt;/p&gt;

&lt;p&gt;The two approaches diverge early, and the divergence is the whole story:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEMPLATE-BASED                       AI-BASED

Document                             Document
   │                                    │
   ▼                                    ▼
Fixed rules                          Layout-aware parsing
   │                                    │
   ▼                                    ▼
Works until layout changes           LLM extraction
   │                                    │
   ▼                                    ▼
Breaks silently                      Accuracy validation
                                         │
                                         ▼
                                      Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One path has a dead end built in. The other has a checkpoint built in.&lt;/p&gt;

&lt;p&gt;A few specific patterns show up over and over with the template path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variability at the margins.&lt;/strong&gt; Templates work on documents you designed them for. Edge cases fall outside the template and fail quietly. Handwritten annotations on printed forms, scans from older equipment, PDFs generated by different accounting platforms. All of these are edge cases in production, not theoretical ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule explosion.&lt;/strong&gt; Teams try to handle variability by adding more rules. The rule set grows. Rules start conflicting. Testing becomes a manual slog, and regressions appear in unexpected places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accuracy rot.&lt;/strong&gt; Template accuracy is static. It doesn't improve as you process more documents. It only degrades as document variety increases. There is no feedback loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why AI-Native Pipelines Also Fail in Production
&lt;/h2&gt;

&lt;p&gt;Replace the template with an LLM and you've solved the variability problem. LLMs can read documents they've never seen and infer field locations from context. But a new set of problems shows up in its place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent accuracy drift.&lt;/strong&gt; An LLM might extract a date in one format from one vendor and a different format from another. Both look like dates. Both are technically correct. But your downstream database expects ISO 8601, and the inconsistency breaks a batch job on a night when nobody is watching. You find out when someone files a support ticket about bad data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence without calibration.&lt;/strong&gt; LLMs don't know what they don't know. A model will extract a field with the same apparent confidence whether it found it clearly labeled or inferred something plausible from surrounding context. Without a validation layer, you can't distinguish reliable extractions from reasonable guesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No prompt version control.&lt;/strong&gt; A prompt change that fixes extraction for one document type breaks it for another. Without a baseline to compare against and a way to measure impact across your full document set, you're making changes blind and discovering regressions through downstream complaints.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An extraction system with no accuracy measurement is not a production system. It's a demo that happens to be running in your infrastructure.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These aren't edge cases. They are the failure modes that show up in almost every AI document processing project that gets far enough along to encounter real production traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Production-Ready Actually Requires
&lt;/h2&gt;

&lt;p&gt;Most demos skip the same four things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layout-preserving parsing.&lt;/strong&gt; The OCR layer needs to output text the LLM can reason over correctly. For complex documents (multi-column tables, nested forms, scanned images), that means preserving spatial structure, not just converting pixels to characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation with measurable accuracy.&lt;/strong&gt; Every extraction should produce a confidence score. Low-confidence extractions should route to review rather than propagate downstream. Accuracy should be measured against verified ground truth so you have a number you can track over time, not a subjective sense that things look fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-in-the-loop routing.&lt;/strong&gt; Not every document should process automatically. Documents that fail validation or produce low-confidence extractions need a clear path to human review, and corrections from that review need to feed back into the system. Exception handling is not an afterthought; it's a core part of the architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt management with version control.&lt;/strong&gt; Prompts evolve. Changes need to be tracked, their impact measured across the full document set, and rollback available when a change causes regression. A prompt is not a one-time configuration. It's a versioned artifact.&lt;/p&gt;

&lt;p&gt;None of these are model problems. They're systems problems. And they're the ones that determine whether a document processing pipeline survives contact with production.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Unstract Handles the Production Problem
&lt;/h2&gt;

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

&lt;p&gt;Most platforms optimize for the demo. They make extraction easy to set up on a clean document. What happens next, with real documents, real volume, real variability, gets left to your engineering team to figure out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://unstract.com" rel="noopener noreferrer"&gt;Unstract&lt;/a&gt; is an open-source intelligent document processing platform built around the opposite assumption: that extraction is the easy part, and the hard parts are everything required to keep it working.&lt;/p&gt;

&lt;p&gt;The platform has two distinct layers. Here's how a document actually moves through the system, from PDF to structured output:&lt;/p&gt;

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

&lt;p&gt;Two things matter about this shape. The OCR layer and the extraction layer are separate concerns, handled by separate components, so a failure in one doesn't get misdiagnosed as a failure in the other. And low-confidence output never reaches a downstream system unreviewed. It loops back through a human first.&lt;/p&gt;

&lt;p&gt;Try for Free - &lt;a href="https://playground.unstract.com/" rel="noopener noreferrer"&gt;Unstract Playground &lt;/a&gt;(no signup required)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Parsing Foundation: LLMWhisperer
&lt;/h3&gt;

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

&lt;p&gt;LLMWhisperer is a document parsing engine built specifically for LLM-based extraction pipelines. Its job is the step before extraction, converting PDFs, scanned images, and complex documents into text output that preserves enough of the original structure for a language model to reason over correctly.&lt;/p&gt;

&lt;p&gt;The practical difference from standard OCR shows up immediately. A conventional tool flattens a multi-column bank statement into a single text stream, destroying the spatial relationships that give the data its meaning. LLMWhisperer solves this through layout-aware parsing. Rather than flattening the page, it analyses the spatial structure and reconstructs the text in a way that preserves the relationships between elements. Table rows stay intact. Multi-column sections are handled correctly. Headers, footers, and summary blocks are separated from transaction data.&lt;/p&gt;

&lt;p&gt;LLMWhisperer also includes auto-compaction, which removes low-value tokens like repetitive headers and footers before text reaches the LLM. These techniques can reduce token usage by up to 7x, which matters once you're processing thousands of documents instead of twenty.&lt;/p&gt;

&lt;p&gt;Try for Free - &lt;a href="https://pg.llmwhisperer.unstract.com/" rel="noopener noreferrer"&gt;LLMWhisperer Playground &lt;/a&gt;(no signup required)&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Pipelines Get Built: Agentic Prompt Studio
&lt;/h3&gt;

&lt;p&gt;This is the part worth understanding in detail.&lt;/p&gt;

&lt;p&gt;The traditional way to build an extraction project: define a schema manually, write prompts per field, run extraction, eyeball results, adjust prompts, repeat. There was no automated accuracy scoring, no mismatch matrix, no field-level comparison against ground truth. You eyeballed the output, spot-checked values, and made judgment calls about whether the extraction was good enough to ship.&lt;/p&gt;

&lt;p&gt;The bottleneck wasn't capability. LLMs could extract accurately when given the right prompts. The bottleneck was everything around the LLM: the human time required to define what to extract, write how to extract it, and verify that it was actually working across the full range of documents the pipeline would encounter in production.&lt;/p&gt;

&lt;p&gt;The Agentic Prompt Studio replaces that manual loop with an AI agent-driven pipeline. You bring the documents. The agents handle the rest: analyzing structure, inferring schema, generating extraction prompts, running extractions, and scoring accuracy against verified outputs, automatically, in sequence, without manual input at each step.&lt;/p&gt;

&lt;p&gt;Six agents, two stages, run one after the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Documents
    │
    ▼
Summarizer Agent      reads each document, finds the fields
    │
    ▼
Uniformer Agent        merges duplicate fields across documents
    │
    ▼
Finalizer Agent         outputs a clean JSON Schema
    │
    ▼
  Schema
    │
    ▼
Pattern Miner Agent     finds the labels and patterns per field
    │
    ▼
Prompt Architect Agent  writes the extraction prompt
    │
    ▼
Critic Dry-Runner Agent stress-tests it before you ever run it
    │
    ▼
Validated Prompt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the shape. Now the detail behind each stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema generation&lt;/strong&gt; runs through the first three agents. The Summarizer Agent analyzes each document on its own. It identifies field names, data types, descriptions, and example values. Because it processes each variant on its own, no field quietly gets dropped. The Uniformer Agent takes those summaries and finds commonalities, recognizing that similarly-named fields are the same, merging duplicates, picking consistent names. The Finalizer Agent converts everything into a standard-compliant JSON schema with proper data types, required fields, nested structures, and validation rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt generation&lt;/strong&gt; runs through the next three. The Pattern Miner Agent digs through your samples to find extraction clues: the labels that precede fields, the formatting patterns, where fields tend to sit in each layout. The Prompt Architect Agent constructs a detailed extraction prompt with structured instructions, field-level guidance, disambiguation rules, edge case handling, and output format. The Critic Dry-Runner Agent stress-tests the prompt before you ever run it. It simulates an extraction, validates the output against the schema, and identifies potential failure points.&lt;/p&gt;

&lt;p&gt;The result is a validated extraction prompt you didn't have to write, tested against the document variants you actually need to handle in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accuracy tracking&lt;/strong&gt; closes the loop that most platforms leave open. The Verification Set compares new results with baselines and shows what improved, regressed, and by how much. Every prompt version provides an instant document accuracy score. Track trends over time and identify which edit caused a regression.&lt;/p&gt;

&lt;p&gt;The Mismatch Identification feature lets you see which fields matched and which didn't for each document, find the source location of any value, and get an overview of extraction quality with a Project Accuracy score. So when you fix a prompt, you're not guessing whether it helped. You can see exactly which document types improved and which ones quietly got worse.&lt;/p&gt;

&lt;p&gt;Once a project reaches your accuracy threshold, it exports as a Tool. This packages the schema, prompts, and configuration into a deployable unit that can be connected to a workflow or exposed directly as an API endpoint. You're not rebuilding anything for deployment. You're shipping exactly what you tested.&lt;/p&gt;




&lt;h2&gt;
  
  
  An Invoice Pipeline, End to End
&lt;/h2&gt;

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

&lt;p&gt;To make this concrete, here is what processing invoices from 50 different vendors looks like with this pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingestion:&lt;/strong&gt; Documents arrive via S3, Google Drive, or a direct API call. LLMWhisperer parses each one and preserves layout regardless of whether it's a native PDF or a scanned image from older equipment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema + Prompts:&lt;/strong&gt; The Agentic Prompt Studio runs across your sample invoices. Six agents, in two sequential pipelines, build a schema covering vendor name, invoice number, line items, totals, payment terms, and due date, then generate and validate extraction prompts across all 50 layout variants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation and Routing:&lt;/strong&gt; Every extraction runs against the schema. Low-confidence fields route to Human-in-the-Loop review rather than passing through automatically, and corrections from human reviewers feed back into the accuracy baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; Validated JSON flows into destinations like Snowflake, PostgreSQL, or your ERP via native connectors. The pipeline runs 24/7 without manual intervention.&lt;/p&gt;

&lt;p&gt;When a new vendor appears with a novel layout, you add sample documents to the project, re-run the agents, and redeploy. The entire iteration cycle takes minutes, not days.&lt;/p&gt;

&lt;p&gt;That asymmetry matters. Template-based systems front-load simplicity and back-load cost: every new vendor is a new maintenance burden. AI-based systems with proper tooling invert that: the setup cost is fixed and the marginal cost of each new document type drops over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  When NOT to Build This
&lt;/h2&gt;

&lt;p&gt;Worth saying directly because most articles about AI tooling skip it: not every document processing problem needs this level of infrastructure.&lt;/p&gt;

&lt;p&gt;If you are processing documents from a single vendor with a fixed, predictable layout and low volume, a well-built template or simple rule-based extractor is cheaper and faster to maintain. AI-based extraction earns its cost when document variety is high, your vendor footprint is large, or layouts change frequently enough that template maintenance becomes expensive.&lt;/p&gt;

&lt;p&gt;Here is how to make the call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single vendor, fixed layout, low volume&lt;/td&gt;
&lt;td&gt;Build a template or simple extractor. Don't over-engineer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple vendors, recognizable document class&lt;/td&gt;
&lt;td&gt;Start with Agentic Prompt Studio on one class first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High volume, high variety across document types&lt;/td&gt;
&lt;td&gt;Full AI pipeline with validation and HITL routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanned documents, inconsistent quality&lt;/td&gt;
&lt;td&gt;Fix the OCR/parsing layer before touching anything else.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated environment (HIPAA, GDPR, SOC2)&lt;/td&gt;
&lt;td&gt;Require on-premises or private cloud deployment explicitly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document layouts change frequently&lt;/td&gt;
&lt;td&gt;Template maintenance won't scale. AI approach required.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with one document type, one vendor class. Get the accuracy baseline established. Then expand. The teams that start with the most complex use case and expect immediate results are the same ones who end up with six months of bad data and an emergency cleanup project.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming
&lt;/h2&gt;

&lt;p&gt;Multi-modal models are changing the extraction layer in a way that's worth tracking. Current pipelines convert documents to text before the LLM sees them. Multi-modal models can reason directly over the visual representation: tables are processed as tables, signature blocks as signature blocks, stamps as stamps. This removes a translation step and with it a class of errors that stem from OCR misrepresenting visual structure.&lt;/p&gt;

&lt;p&gt;Unstract is also building self-improving prompts: multi-agent pipelines that analyze extraction accuracy as feedback and fine-tune prompts automatically, removing the one step that still requires manual input.&lt;/p&gt;

&lt;p&gt;Both of these raise the accuracy ceiling without requiring more engineering effort per document type. Organizations with operational capability built now will absorb those improvements without rebuilding their pipelines from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Diagnostic
&lt;/h2&gt;

&lt;p&gt;Before you conclude your extraction model is failing, check these four things in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is your OCR output preserving layout?&lt;/strong&gt; Inspect the raw text your LLM is actually working with. If table columns are flattened into a single stream, the problem starts at the parsing layer, not the prompt. Fix that first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you have field-level accuracy scores against verified ground truth?&lt;/strong&gt; If you're eyeballing outputs and calling it good, you don't know what your actual accuracy is. Build a verification set before you make any other changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What happens to documents that fail validation?&lt;/strong&gt; If you don't have a defined exception path, errors are propagating downstream. Find them before your finance team does.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Have you tested across the full range of layouts you'll encounter in production?&lt;/strong&gt; Twenty clean test documents from your primary vendor is not a production test. Add the edge cases deliberately, because your vendors will not warn you when they update their invoice template.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams find the problem in step one.&lt;/p&gt;

&lt;p&gt;Extraction accuracy is easier to measure than most engineers think. The hard part is not getting the model to extract. It's knowing whether it extracted correctly, across every document variant your pipeline will encounter at 2 AM when nobody is watching.&lt;/p&gt;

&lt;p&gt;That's the gap between a demo and a production system. Production document processing isn't won by picking a smarter model. It's won by building a system that knows when the model is right, and catches it when it isn't.&lt;/p&gt;




&lt;p&gt;*Unstract is available as &lt;a href="https://github.com/Zipstack/unstract" rel="noopener noreferrer"&gt;open-source &lt;/a&gt;(AGPL-3.0), managed cloud, and on-premises. The Agentic Prompt Studio is available in beta on all Unstract Cloud and on-premises plans.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Multi-Aspect E-Commerce Semantic Engine Using Qdrant Multivectors</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:33:24 +0000</pubDate>
      <link>https://dev.to/divy_ai/multi-aspect-e-commerce-semantic-engine-using-qdrant-multivectors-3jpp</link>
      <guid>https://dev.to/divy_ai/multi-aspect-e-commerce-semantic-engine-using-qdrant-multivectors-3jpp</guid>
      <description>&lt;p&gt;How I built a multi-vector semantic search engine that splits user intent before touching the database, using ColBERT, SigLIP, and BGE within a single Qdrant point.&lt;/p&gt;

&lt;p&gt;Six months ago I was building a semantic search engine for a small e-commerce catalog. Everything looked fine. I was using a good embedding model, cosine similarity, and the search results seemed reasonable.&lt;/p&gt;

&lt;p&gt;Then I searched for:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Waterproof black hiking boots with good arch support."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first result was perfect.&lt;/p&gt;

&lt;p&gt;The second was a waterproof jacket.&lt;/p&gt;

&lt;p&gt;The third was a pair of black Chelsea boots.&lt;/p&gt;

&lt;p&gt;Technically, the search wasn't wrong. Every result was semantically similar to the query.&lt;/p&gt;

&lt;p&gt;But if I were a customer, I wouldn't care. I wanted hiking boots with arch support, not products that happened to share a few similar words.&lt;/p&gt;

&lt;p&gt;I tried a better embedding model. I changed the chunking strategy. I added more metadata. The results improved a little, but the real problem never went away.&lt;/p&gt;

&lt;p&gt;The problem wasn't the model.&lt;/p&gt;

&lt;p&gt;It was the assumption that one embedding could represent an entire product.&lt;/p&gt;

&lt;p&gt;A product has technical specifications, images, and customer reviews. Those are different kinds of information, yet I was compressing all of them into a single vector.&lt;/p&gt;

&lt;p&gt;So I rebuilt the system.&lt;/p&gt;

&lt;p&gt;Instead of one embedding per product, I stored separate vector spaces for specifications, images, and review findings. I split each query into different types of intent before embedding it and added a lightweight personalization layer on top.&lt;/p&gt;

&lt;p&gt;This article walks through how I built it, why I chose this architecture, and when this extra complexity is actually worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental model: before you read a single line of code
&lt;/h2&gt;

&lt;p&gt;Here's the full pipeline in one view. Read this once, keep it in mind, and the rest of the article will click much faster.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How to read this:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think of the system as three specialists evaluating the same product.&lt;/p&gt;

&lt;p&gt;When a user searches for "Waterproof black hiking boots with good arch support", the query is split into three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it waterproof? → Specs expert (ColBERT)&lt;/li&gt;
&lt;li&gt;Does it look like black hiking boots? → Visual expert (SigLIP)&lt;/li&gt;
&lt;li&gt;Do customers praise the arch support? → Review expert (BGE)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each expert searches a different vector field stored inside the same Qdrant point. Text and review retrieval nominate candidates first, the visual channel performs final scoring, and personalization reranks the results.&lt;/p&gt;

&lt;p&gt;Instead of forcing one embedding to represent everything, the system evaluates independent pieces of evidence and combines them at ranking time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core problem: one vector can't represent everything
&lt;/h2&gt;

&lt;p&gt;When someone searches "waterproof black hiking boots with good arch support," they're not asking one question. They're asking three at the same time:&lt;/p&gt;

&lt;p&gt;The first is a spec question: "Is this waterproof?" That's answered by product descriptions and technical data sheets.&lt;/p&gt;

&lt;p&gt;The second is a visual question: "Does it look like black hiking boots?" That's answered by images. Not text descriptions of images. Actual images.&lt;/p&gt;

&lt;p&gt;The third is a social question: "Do people say the arch support is good?" That's answered by customer reviews.&lt;/p&gt;

&lt;p&gt;No single embedding model can capture all three signals with equal fidelity. When you pool them together, you get a vector that's roughly in the neighborhood of all three but isn't precise about any of them.&lt;/p&gt;

&lt;p&gt;It's why you get Chelsea boots in your hiking boot results. Both are black footwear. The visual signal is doing fine. The arch-support signal is completely lost.&lt;/p&gt;

&lt;p&gt;The fix is to stop treating a product as one thing and start treating it as a collection of distinct signals, each searchable independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I chose Qdrant for this
&lt;/h2&gt;

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

&lt;p&gt;I evaluated Pinecone, Weaviate, and Qdrant before committing. The deciding factor was named multivector fields.&lt;/p&gt;

&lt;p&gt;I needed to store multiple different types of vectors per document, each with its own dimensionality and comparison function. Pinecone doesn't support this natively. Weaviate's multivector story was still evolving when I started.&lt;/p&gt;

&lt;p&gt;Qdrant's API is clean: you define named vector configs at collection creation time, each with different dimensions, distance metrics, and HNSW settings.&lt;/p&gt;

&lt;p&gt;The other thing that sold me was &lt;code&gt;update_vectors&lt;/code&gt;. In a live catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Products get new images. The visual matrix needs to grow without a collection rebuild.&lt;/li&gt;
&lt;li&gt;Reviews arrive daily. Each new review adds rows to the review matrix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Qdrant handles both by letting you retrieve an existing vector field, concatenate new vectors, and push the updated matrix back. The collection stays up. Running queries keep working.&lt;/p&gt;

&lt;p&gt;Versions matter here. I'm on Qdrant &lt;code&gt;v1.15.3&lt;/code&gt; with &lt;code&gt;qdrant-client&amp;gt;=1.15.0&lt;/code&gt;. Multivectors have been available since &lt;code&gt;v1.10&lt;/code&gt;, but the &lt;code&gt;query_points&lt;/code&gt; prefetch API that makes multi-stage search work cleanly became stable in &lt;code&gt;v1.14&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the system stores: one point, three matrices
&lt;/h2&gt;

&lt;p&gt;Every product becomes a single Qdrant point. That point has three named vector fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/qdrant_store.py
&lt;/span&gt;
&lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visual_vectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# 768-d SigLIP image embedding
&lt;/span&gt;&lt;span class="n"&gt;TEXT_VECTOR&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_vectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 96-d ColBERT token matrix
&lt;/span&gt;&lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review_vectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# 384-d BGE per-finding embeddings
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three use &lt;code&gt;MAX_SIM&lt;/code&gt; as the comparator. Here is the full collection setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vector_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hnsw_m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hnsw_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hnsw_m&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hnsw_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HnswConfigDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hnsw_m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;multivector_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MultiVectorConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;comparator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiVectorComparator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAX_SIM&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;hnsw_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hnsw_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recreate_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;disable_text_hnsw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;vector_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VISION_DIM&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                                       &lt;span class="c1"&gt;# 768
&lt;/span&gt;            &lt;span class="n"&gt;TEXT_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;vector_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT_DIM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hnsw_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;disable_text_hnsw&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="c1"&gt;# 96
&lt;/span&gt;            &lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;vector_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REVIEW_DIM&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                                       &lt;span class="c1"&gt;# 384
&lt;/span&gt;        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;quantization_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;quantization_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;create_payload_indexes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;hnsw_m=0&lt;/code&gt; on &lt;code&gt;TEXT_VECTOR&lt;/code&gt; is intentional. Setting &lt;code&gt;m=0&lt;/code&gt; disables HNSW graph indexing entirely for that field.&lt;/p&gt;

&lt;p&gt;Normally that would be a performance disaster. Here it isn't, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text vectors are not used for first-stage retrieval.&lt;/strong&gt; They're used as a ColBERT reranker over the small candidate set that the prefetch stages already returned. At 20–50 candidates, brute-force matrix comparison beats HNSW because the graph traversal overhead costs more than it saves at that N.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It saves index memory.&lt;/strong&gt; Token-level matrices are larger than pooled vectors. Skipping the HNSW graph for &lt;code&gt;text_vectors&lt;/code&gt; reduces memory footprint, which compounds when you're storing 96-d matrices across thousands of tokens per product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payload also gets indexed separately so Qdrant can filter before scoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_payload_indexes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Keyword fields
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;color&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_payload_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;field_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadSchemaType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KEYWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Bool field
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_payload_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;availability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;field_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadSchemaType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Float fields
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eco_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_payload_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;field_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadSchemaType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLOAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters more than it looks. When indexes are present, Qdrant runs payload filtering before vector scoring. Filtering &lt;code&gt;availability=True&lt;/code&gt; or &lt;code&gt;price &amp;lt;= 150.0&lt;/code&gt; eliminates candidates before any embedding comparison runs. Without indexes you get post-filtering, which is slower and produces inconsistent result counts. Index your filterable fields before you query.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three embedding pipelines
&lt;/h2&gt;

&lt;p&gt;Each vector field in the point comes from a different embedding pipeline. The same product goes through all three before ingestion.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Photo from AI]&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual pipeline: SigLIP
&lt;/h3&gt;

&lt;p&gt;Product images go through &lt;code&gt;google/siglip-base-patch16-224&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;SigLIP is a vision-language model with aligned image and text embeddings. Aligned means you can encode a text query like "black hiking boots" and compare it directly against an image embedding. No bridge model. No separate alignment step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/embeddings.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_patches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returns shape [1, 768]: normalized SigLIP pooled image embedding.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_load_siglip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;siglip_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_siglip_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;siglip_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;siglip_inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;vision_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_siglip_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vision_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;siglip_inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pooled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vision_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pooler_output&lt;/span&gt;        &lt;span class="c1"&gt;# shape: [1, 768]
&lt;/span&gt;        &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pooled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the corresponding text-side query encoding:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returns shape [1, 768]: normalized SigLIP text tower output.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_load_siglip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;text_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_siglip_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;text_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;text_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_siglip_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;text_inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pooled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pooler_output&lt;/span&gt;          &lt;span class="c1"&gt;# shape: [1, 768]
&lt;/span&gt;        &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pooled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both outputs are L2-normalized before storage and querying. This makes cosine similarity equivalent to dot product, which is faster for Qdrant to compute.&lt;/p&gt;

&lt;p&gt;The stored shape for each product is &lt;code&gt;[1, 768]&lt;/code&gt;, a matrix with one row. It's technically a 2D multivector with one element, but storing it as a matrix rather than a flat vector keeps the interface consistent and makes appending additional image patches straightforward later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text pipeline: ColBERT
&lt;/h3&gt;

&lt;p&gt;Product titles and spec sheets go through FastEmbed's &lt;code&gt;answerdotai/answerai-colbert-small-v1&lt;/code&gt;. This model produces token-level embeddings, 96 dimensions per token, without pooling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_late&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;late_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The return shape is &lt;code&gt;[num_texts, num_tokens, 96]&lt;/code&gt;. For one product title, you get a matrix of shape &lt;code&gt;[num_tokens, 96]&lt;/code&gt;. A spec-heavy product like "TrailForge StormShield Black Hiking Boots. support: nylon shank and molded arch support. upper: black ripstop textile. waterproof_rating: IPX6 waterproof membrane for heavy rain" tokenizes to around 25-30 tokens, so the stored matrix is &lt;code&gt;[~28, 96]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The text document is assembled in the Product model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/models.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Title first, then all specs as key-value pairs. The spec keys (&lt;code&gt;support&lt;/code&gt;, &lt;code&gt;waterproof_rating&lt;/code&gt;, &lt;code&gt;upper&lt;/code&gt;) are included as text so ColBERT can match a query token like "waterproof" to the spec key, not just the spec value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review pipeline: BGE with semantic finding extraction
&lt;/h3&gt;

&lt;p&gt;Raw reviews don't go straight to the embedder. They go through a finding extraction step first. This is where the architecture differs most from a naive approach.&lt;/p&gt;

&lt;p&gt;A review like "Waterproof in heavy rain and mud. Good arch support on long hikes. Excellent grip on wet rock." holds three separate factual claims. Embedding the full review as one vector averages all three. A product with ten reviews embedded whole gives you ten averaged blobs. You lose the specific claim that matters to a specific query.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;extract_semantic_findings()&lt;/code&gt; pulls out specific claims using regex patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/reviews.py
&lt;/span&gt;
&lt;span class="n"&gt;FINDING_PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waterproof(?: in [a-z ]+)?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kept feet dry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good arch support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arch support is [a-z]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supportive footbed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;excellent grip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not enough grip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;highly durable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs a little small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strong ankle support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_semantic_findings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FINDING_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# deduplication
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The findings for the TrailForge boot come out as: &lt;code&gt;["waterproof in heavy rain and mud", "good arch support", "excellent grip"]&lt;/code&gt;. Each finding then gets embedded separately with &lt;code&gt;BAAI/bge-small-en-v1.5&lt;/code&gt; into a 384-dimensional vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review_findings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;_unit_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty-review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REVIEW_DIM&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stored review matrix is &lt;code&gt;[num_findings, 384]&lt;/code&gt;. Three findings become a &lt;code&gt;[3, 384]&lt;/code&gt; matrix. A product with ten reviews and 15 extracted findings becomes &lt;code&gt;[15, 384]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a query includes "good arch support," it gets embedded as a single 384-d vector and compared via MAX_SIM against this matrix. The most similar finding wins. That matching finding is "good arch support" from a review, not an approximation averaged across all review content.&lt;/p&gt;

&lt;p&gt;The full ingestion step assembles all three matrices into a single point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/ingest.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Embedder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text_matrix&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_late&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_document&lt;/span&gt;&lt;span class="p"&gt;()])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;review_matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;review_findings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;extract_semantic_findings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;visual_matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image_patches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;point_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;visual_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# shape [1, 768]
&lt;/span&gt;            &lt;span class="n"&gt;TEXT_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;text_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# shape [~28, 96]
&lt;/span&gt;            &lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# shape [num_findings, 384]
&lt;/span&gt;        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One product. One point. Three matrices, each solving a different retrieval problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Query decomposition: routing intent before embedding
&lt;/h2&gt;

&lt;p&gt;When a user submits a query, the first step is decomposition. The query does not go directly to all three vector fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/query.py
&lt;/span&gt;
&lt;span class="n"&gt;TEXT_KEYWORDS&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waterproof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;water-resistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;membrane&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insulated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;VISUAL_KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;black&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charcoal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hiking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sneakers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;REVIEW_KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;durable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comfortable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;excellent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decompose_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;QueryPlan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text_terms&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TEXT_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;visual_terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;VISUAL_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;review_terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REVIEW_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;phrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# phrase-level overrides for known compound terms
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good arch support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;review_terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;black hiking boots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;visual_terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;black&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hiking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waterproof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text_terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waterproof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;QueryPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;original_query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text_terms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_terms&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;visual_terms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visual_terms&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;review_terms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review_terms&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For "Waterproof black hiking boots with good arch support", the decomposition produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;text_terms = ["waterproof"]&lt;/code&gt; → routed to ColBERT spec matching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;visual_terms = ["black", "hiking", "boots"]&lt;/code&gt; → routed to SigLIP visual matching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;review_terms = ["support", "arch", "good"]&lt;/code&gt; → routed to BGE review matching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;QueryPlan&lt;/code&gt; model exposes three properties that join these terms back into sub-query strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/models.py
&lt;/span&gt;
&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;text_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_terms&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visual_terms&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@property&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_terms&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no terms match a category, the original query falls back as the sub-query for that field. This prevents empty queries.&lt;/p&gt;

&lt;p&gt;I'll be direct about the limitation: this decomposition uses keyword matching, which is coarse. A better production implementation would call an LLM to classify query intent. The architecture is the same either way. What matters is that you route before you embed, not that you route using a fancy classifier. Get the structure right first.&lt;/p&gt;




&lt;h2&gt;
  
  
  The search: prefetch then final scoring
&lt;/h2&gt;

&lt;p&gt;Qdrant's &lt;code&gt;query_points&lt;/code&gt; supports a &lt;code&gt;prefetch&lt;/code&gt; parameter. It lets you retrieve candidate sets from multiple vector fields in parallel, then apply a final scoring stage against only those candidates. This is the mechanism that ties the three vector fields together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/search.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decompose_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;query_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Embed all three sub-queries
&lt;/span&gt;    &lt;span class="n"&gt;text_query&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_late&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_query&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;review_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;review_findings&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;review_query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;visual_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;candidate_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Text and review run as prefetch candidates
&lt;/span&gt;    &lt;span class="n"&gt;prefetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TEXT_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;candidate_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;review_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;candidate_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Visual query scores and ranks the prefetch candidates
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;prefetch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;with_payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;with_vectors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text and review prefetch stages each return up to &lt;code&gt;candidate_limit&lt;/code&gt; results. Qdrant unions them. The final visual query then scores only those candidates using the &lt;code&gt;VISUAL_VECTOR&lt;/code&gt; field. Payload filters apply at every stage.&lt;/p&gt;

&lt;p&gt;After Qdrant responds, the code computes per-aspect MAX_SIM scores to get a named breakdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;commerce_engine.scoring&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;maxsim_score&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;vectors&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;doc_visual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="n"&gt;doc_text&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXT_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="n"&gt;doc_review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="n"&gt;v_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;maxsim_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visual_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_visual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doc_visual&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;t_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;maxsim_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;doc_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doc_text&lt;/span&gt;   &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;r_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;maxsim_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_review&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doc_review&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;combined_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t_score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r_score&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MAX_SIM function itself is three lines of NumPy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/scoring.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;maxsim_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;                &lt;span class="c1"&gt;# [query_tokens, doc_tokens]
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="c1"&gt;# max per query token, then sum
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each query token: find the highest similarity against all document tokens. Sum those per-token maximums. That sum is the MAX_SIM score. This is the full ColBERT late-interaction formula.&lt;/p&gt;

&lt;p&gt;Computing it manually after retrieval gives you per-aspect scores (&lt;code&gt;visual=0.81&lt;/code&gt;, &lt;code&gt;text=0.44&lt;/code&gt;, &lt;code&gt;review=0.69&lt;/code&gt;) in the explanation field. That breakdown is the best debugging tool in the system. When a result feels wrong, the aspect scores tell you immediately whether the visual, spec, or review signal failed to match.&lt;/p&gt;




&lt;h2&gt;
  
  
  Personalization: reranking the same candidates differently
&lt;/h2&gt;

&lt;p&gt;After Qdrant returns results, a personalization layer reranks them based on the requesting user's profile. The Qdrant retrieval step is identical for every user. Only the reranking changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/scoring.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;personalization_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preferred_brands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preferred brand: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price_range&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price in user range: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eco_preference&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_sustainable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eco preference matched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;favorite_categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;favorite category: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;reranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;personalization_boost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;final_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qdrant_score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;reranked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;final_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;personalization_boost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;boost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explanation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explanation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boost is multiplicative, not additive. A product with a semantic score of 0.90 and a total boost of 0.55 finishes at 0.90 × 1.55 = 1.395. A product with a score of 0.95 and no matching preferences stays at 0.95. Personalization can flip the ranking.&lt;/p&gt;

&lt;p&gt;Every matched boost reason gets appended to the result's explanation string, so a top result might show: &lt;code&gt;"preferred brand: TrailForge | price in user range: 80-165 | review evidence: arch support"&lt;/code&gt;. This matters for two reasons: users trust recommendations they can understand, and you can debug a bad result in seconds by looking at why its score was what it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Updating products without rebuilding anything
&lt;/h2&gt;

&lt;p&gt;Production catalogs are not static. A product gets a new image. Reviews come in daily. The system needs to handle both without downtime.&lt;/p&gt;

&lt;p&gt;Both update operations follow the same pattern: retrieve the existing vector field, concatenate new vectors, push the updated matrix back with &lt;code&gt;update_vectors&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/commerce_engine/updates.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;point&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_semantic_findings&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;new_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;review_findings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;updated_matrix&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;new_vecs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;update_named_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REVIEW_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;updated_matrix&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;point_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;point&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image_patches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;updated_matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;new_vecs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;update_named_vectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;VISUAL_VECTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;updated_matrix&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added_patches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_vecs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_patches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_matrix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;append_review&lt;/code&gt;, the product's review matrix grows by one row per extracted finding. After &lt;code&gt;append_image&lt;/code&gt;, the visual matrix gains one row. No collection rebuild. Running queries keep working.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds. If adding a single review required re-ingesting the whole product, you'd batch updates and your review data would always lag. With &lt;code&gt;update_vectors&lt;/code&gt;, a webhook can process incoming reviews in real time, and the next search query immediately benefits from the new signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the benchmarks actually showed
&lt;/h2&gt;

&lt;p&gt;The benchmark runs 104 queries against the fixture product set (4 hiking boot variants), generated from product attribute combinations: color + category, brand + category, spec-based, review-fragment, and compound queries. Latencies measured with &lt;code&gt;time.perf_counter&lt;/code&gt; against a live Qdrant instance.&lt;/p&gt;

&lt;p&gt;Results for the baseline profile (no quantization, HNSW disabled for text):&lt;/p&gt;

&lt;p&gt;95.2% Recall@3 means 99 of 104 queries returned the target product in the top 3. The 5 failures are edge cases where the query decomposer routes everything to one aspect but the product's signal lives in a different field.&lt;/p&gt;

&lt;p&gt;Honest caveats worth stating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is 4 products. The 2.2ms will not hold at 100k products.&lt;/li&gt;
&lt;li&gt;Benchmark queries were generated from the same fixture data they retrieve. Some overfitting is baked in.&lt;/li&gt;
&lt;li&gt;Binary quantization knocked Recall@3 down noticeably. Compressing 32-bit floats to 1 bit is extreme for ColBERT's token-level matching. Start with scalar INT8 and measure recall before deploying.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When not to build this
&lt;/h2&gt;

&lt;p&gt;Three embedding pipelines, a query decomposer, and prefetch orchestration is not always the right answer. Skip it if any of these apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short, uniform queries&lt;/strong&gt; like "red shoes" or "wool sweater." Single dense vector handles this fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No clean product images.&lt;/strong&gt; SigLIP needs consistent product photography. Blurry or inconsistent shots produce weak visual signal. Drop that field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No customer reviews.&lt;/strong&gt; The review field adds complexity for zero gain. Skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog under 10k products with low query load.&lt;/strong&gt; The simpler architecture will likely be fast enough and much cheaper to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken baseline retrieval.&lt;/strong&gt; Bad base embeddings, wrong chunking, or misconfigured filters won't get fixed by adding more vectors. Multivectors amplify signal. If there's no signal to amplify, they amplify noise.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When this is worth building
&lt;/h2&gt;

&lt;p&gt;The complexity pays off when all of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queries contain mixed intent.&lt;/strong&gt; Spec questions, visual questions, and review-based questions in the same search string. Common in outdoor gear, fashion, electronics, and beauty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean product images at scale.&lt;/strong&gt; 100k+ SKUs with consistent photography. The SigLIP channel earns its storage cost here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviews arrive continuously.&lt;/strong&gt; If reviews update daily, the &lt;code&gt;update_vectors&lt;/code&gt; pattern becomes genuinely valuable versus batch re-indexing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have user profile data.&lt;/strong&gt; The personalization layer needs signal to work with. Without it, the reranking step is adding noise.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;p&gt;![Photo from AI&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjltb2ikl5c7zbfbqy4rx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjltb2ikl5c7zbfbqy4rx.png" alt=" " width="798" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](&lt;a href="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/52ipdt5rv5se2mch6f2h.png" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/52ipdt5rv5se2mch6f2h.png&lt;/a&gt;)&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementation path if you're starting from scratch
&lt;/h2&gt;

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

&lt;p&gt;Do not skip step 1. The single-vector baseline is not just scaffolding. It is the control you need to measure whether the additional complexity actually improves results for your specific catalog.&lt;/p&gt;

&lt;p&gt;I have seen teams add three embedding pipelines on day one because the architecture sounds impressive, then spend three weeks trying to figure out why recall got worse. It was worse because their base embeddings were bad. Fix the foundation before you add floors.&lt;/p&gt;


&lt;h2&gt;
  
  
  The thing that actually changed after building this
&lt;/h2&gt;

&lt;p&gt;I went back and ran the original "Waterproof black hiking boots with good arch support" query through the finished system. Rank 1 was the TrailForge StormShield. Rank 2 was the EcoTrek TerraDry.&lt;/p&gt;

&lt;p&gt;Both were actual hiking boots with documented arch support in customer reviews.&lt;/p&gt;

&lt;p&gt;The Chelsea boots were gone.&lt;/p&gt;

&lt;p&gt;The insight isn't that multivectors are magic. The insight is that the original setup was asking one number to represent three different kinds of evidence, and one number is not enough. The fix was to stop compressing and start separating.&lt;/p&gt;

&lt;p&gt;Most search quality problems in production are not model problems. They are signal representation problems. The right model pointed at mixed-up data will keep returning wrong answers. Separating the signals is the work. The models are just the tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your search returns technically relevant results that nobody clicks? Ask yourself how many different types of user intent you are compressing into one similarity score."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The full source code, tests, benchmark runner, FastAPI endpoints, Streamlit UI, and real product dataset are all at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dvy246/qdrant-multivector" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything described in this article maps to a real file in &lt;code&gt;src/commerce_engine&lt;/code&gt;. No placeholder functions, no pseudo-code, no hand-waved implementation details.&lt;/p&gt;


&lt;h2&gt;
  
  
  Project at a glance
&lt;/h2&gt;

&lt;p&gt;A quick summary of everything this system does, for anyone who wants the short version before going through the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A multi-aspect semantic search engine for e-commerce that splits every query into visual, spec, and review signals before retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vector database: Qdrant &lt;code&gt;v1.15.3&lt;/code&gt; with &lt;code&gt;qdrant-client&amp;gt;=1.15.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Visual embeddings: &lt;code&gt;google/siglip-base-patch16-224&lt;/code&gt; (768-d, via Hugging Face Transformers)&lt;/li&gt;
&lt;li&gt;Text embeddings: &lt;code&gt;answerdotai/answerai-colbert-small-v1&lt;/code&gt; (96-d, via FastEmbed, ColBERT late interaction)&lt;/li&gt;
&lt;li&gt;Review embeddings: &lt;code&gt;BAAI/bge-small-en-v1.5&lt;/code&gt; (384-d, via FastEmbed, per-finding)&lt;/li&gt;
&lt;li&gt;API: FastAPI + Typer CLI&lt;/li&gt;
&lt;li&gt;UI: Streamlit&lt;/li&gt;
&lt;li&gt;Python: 3.11+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Benchmark results (baseline profile, 4 fixture products):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mean latency: 2.2 ms / P95: 2.5 ms&lt;/li&gt;
&lt;li&gt;Recall@3: 95.2% / Recall@5: 100%&lt;/li&gt;
&lt;li&gt;MRR: 0.7204 / NDCG@5: 0.7920&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Run it yourself:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/dvy246/qdrant-multivector.git
&lt;span class="nb"&gt;cd &lt;/span&gt;qdrant-multivector
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--extra&lt;/span&gt; dev
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; qdrant
&lt;span class="nv"&gt;EMBEDDING_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deterministic uv run engine init-qdrant
&lt;span class="nv"&gt;EMBEDDING_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deterministic uv run engine ingest &lt;span class="nt"&gt;--fixtures&lt;/span&gt;
&lt;span class="nv"&gt;EMBEDDING_BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deterministic uv run engine search &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Waterproof black hiking boots with good arch support"&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; user_a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/manage-data/vectors/" rel="noopener noreferrer"&gt;Qdrant vectors and multivectors documentation&lt;/a&gt; - the definitive reference for multivector configuration, MAX_SIM comparator behavior, and named vector field setup.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/search/hybrid-queries/" rel="noopener noreferrer"&gt;Qdrant hybrid queries and prefetch API&lt;/a&gt; - documents the &lt;code&gt;query_points&lt;/code&gt; prefetch parameter used in the multi-stage search flow.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/manage-data/indexing/" rel="noopener noreferrer"&gt;Qdrant payload indexing documentation&lt;/a&gt; - covers keyword, bool, and float index schema types used in &lt;code&gt;create_payload_indexes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/manage-data/quantization/" rel="noopener noreferrer"&gt;Qdrant quantization documentation&lt;/a&gt; - covers scalar INT8 and binary quantization config, &lt;code&gt;always_ram&lt;/code&gt;, and quantile settings.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://qdrant.tech/documentation/fastembed/fastembed-colbert/" rel="noopener noreferrer"&gt;FastEmbed ColBERT documentation&lt;/a&gt; - covers &lt;code&gt;LateInteractionTextEmbedding&lt;/code&gt;, model selection, and token-matrix output format.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/docs/transformers/model_doc/siglip" rel="noopener noreferrer"&gt;Hugging Face SigLIP model card&lt;/a&gt; - covers &lt;code&gt;google/siglip-base-patch16-224&lt;/code&gt;, the vision and text tower architecture, and pooler output shape.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/answerdotai/answerai-colbert-small-v1" rel="noopener noreferrer"&gt;answerdotai/answerai-colbert-small-v1&lt;/a&gt; - the specific FastEmbed ColBERT model used for 96-dimensional token-level text embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/BAAI/bge-small-en-v1.5" rel="noopener noreferrer"&gt;BAAI/bge-small-en-v1.5&lt;/a&gt; - the BGE model used for 384-dimensional review finding embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kaggle.com/datasets/nicapotato/womens-ecommerce-clothing-reviews" rel="noopener noreferrer"&gt;Women's E-Commerce Clothing Reviews dataset&lt;/a&gt; - the real product dataset derived from Kaggle, used in the Streamlit demo.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Long-Running Claude Managed Agents: Why State Matters More Than Compute</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:50:30 +0000</pubDate>
      <link>https://dev.to/divy_ai/building-long-running-claude-managed-agents-why-state-matters-more-than-compute-57oi</link>
      <guid>https://dev.to/divy_ai/building-long-running-claude-managed-agents-why-state-matters-more-than-compute-57oi</guid>
      <description>&lt;p&gt;&lt;strong&gt;A build story with real code, real failures, and the specific reasons one sandbox provider fixed problems I didn't know I had.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;At 9:03am on a Tuesday, my research agent said hello and stared at an empty &lt;code&gt;/workspace/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Six hours of analysis from the night before. Gone.&lt;/p&gt;

&lt;p&gt;The cloned repository. The installed packages. The notes it had spent hours writing. Gone.&lt;/p&gt;

&lt;p&gt;I had assumed that if an agent stopped working for the night, it could simply continue the next morning. That was wrong.&lt;/p&gt;

&lt;p&gt;Over the next three weeks, I rebuilt the same workflow on Tensorlake, Cloudflare, and Daytona to figure out what had happened. The hardest part of running Claude Managed Agents isn't the model. It's everything underneath it.&lt;/p&gt;

&lt;p&gt;This is the exact code I ran, the things that broke, and the mistake that cost me two weeks to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What Claude Managed Agents is, before anything else&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;If you've never built with Claude Managed Agents, the architecture needs a minute. Skip this if you already know it.&lt;/p&gt;

&lt;p&gt;Anthropic runs the reasoning. You run the execution.&lt;/p&gt;

&lt;p&gt;The agent loop, session state, work queue, and retry logic all live on Anthropic's infrastructure. You configure a Self-hosted Environment in the Claude Console. When your application starts a session, Anthropic queues the work, your orchestrator picks it up, spins up a sandbox, and the model starts issuing tool calls into that sandbox.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, and &lt;code&gt;edit&lt;/code&gt; call executes inside an environment you own. Anthropic never touches it. You decide what that environment looks like, what it can access, and what happens between sessions.&lt;/p&gt;

&lt;p&gt;Anthropic's intelligence is fixed. Your engineering determines whether that intelligence has a stable, stateful environment to work in, or a clean slate that forgets everything the moment it goes idle.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I was building and why it mattered&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I needed an agent that could do real deep-work research on a codebase: clone a repository, read through the module structure, build an understanding of how the pieces fit together, write notes, and propose refactoring strategies. The kind of work that takes a senior engineer a full day and an AI agent about six hours.&lt;/p&gt;

&lt;p&gt;The key constraint: the agent couldn't do this all at once. Sometimes I'd kick off a session at 8pm, let it run until midnight, and pick it back up the next morning. The filesystem it had built during that first session — the analysis notes, the installed tools, the half-read source files — had to be there when the next session started. Rebuilding from scratch each time wasn't viable.&lt;/p&gt;

&lt;p&gt;That constraint is what drove every provider decision I made.&lt;/p&gt;

&lt;h3&gt;
  
  
  The requirements I didn't know I had
&lt;/h3&gt;

&lt;p&gt;At the start I thought I needed a Linux environment that could run Claude Managed Agents. By the end, I realized I actually needed three things. I found them all in one place, but not until I had looked in two others first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A filesystem that survived between work sessions.&lt;/li&gt;
&lt;li&gt;Near-zero cost while the agent was idle.&lt;/li&gt;
&lt;li&gt;The ability to branch from an already-completed analysis state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I did not discover all three requirements on day one.I discovered them one mistake at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;How a session actually starts: the code before the sandbox&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You drive a session through the reference orchestrator using a simple command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make session &lt;span class="nv"&gt;PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Clone the repository at github.com/tensorlakeai/tensorlake. &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  Read through the module structure. Write a summary to /workspace/analysis.md. &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  Note any components that look like they could be simplified."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator sends this prompt to Anthropic as a new session. Anthropic picks it up, starts the agent loop, and immediately begins issuing tool calls. Those tool calls arrive at your sandbox. The agent reads files, runs bash commands, writes notes. The session runs until the task is complete or you stop it.&lt;/p&gt;

&lt;p&gt;The agent stream looks roughly like this as it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[thinking] The repository appears to be a Python SDK for...
[bash] git clone https://github.com/tensorlakeai/tensorlake
[bash] ls -la /workspace/tensorlake/
[read] /workspace/tensorlake/tensorlake/sandbox.py
[write] /workspace/analysis.md
[thinking] The Sandbox class handles...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each bracketed event is a tool call going into your sandbox. The session accumulates state inside &lt;code&gt;/workspace/&lt;/code&gt; across all those calls. By the end of a six-hour session, that directory contains the cloned repo, installed packages, analysis files, and intermediate notes. That's the state that needs to survive overnight.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Build 1: Cloudflare&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;My first assumption was that I needed a platform that could efficiently run Claude Managed Agents. Cloudflare is optimized for high-concurrency execution. My problem turned out to be different.&lt;/p&gt;

&lt;p&gt;The agent I was building accumulated hours of filesystem state between bursts of work. Notes, cloned repositories, installed dependencies, and intermediate analysis all needed to survive overnight. Cloudflare's execution model wasn't designed around that requirement.That was the first time I realized I wasn't looking for compute.&lt;/p&gt;

&lt;p&gt;I was looking for persistent state.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Build 2: Daytona&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;The second build solved part of the problem.The agent could accumulate state throughout a session, which initially felt like progress.&lt;/p&gt;

&lt;p&gt;Then I wanted to test three different refactoring strategies starting from the same six-hour analysis. Instead of branching from that state, I found myself repeating the setup work each time: rebuilding context, reinstalling dependencies, and re-running analysis before I could begin the actual experiment.&lt;/p&gt;

&lt;p&gt;That was when I discovered my second requirement.Preserving state wasn't enough.I also needed a way to branch from an existing state without repeating hours of work.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Build 3: Tensorlake&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;The first thing that caught my attention was not a feature.&lt;/p&gt;

&lt;p&gt;It was an architectural decision.&lt;/p&gt;

&lt;p&gt;Most platforms preserve state by keeping compute alive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This one treated compute and state as separate problems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The docs described a suspended sandbox that could preserve its state and resume in approximately 0.6 seconds. That was the first time I saw a design that directly addressed the problem I'd been running into.&lt;/p&gt;

&lt;p&gt;I wanted to know whether it actually worked.&lt;/p&gt;

&lt;p&gt;I started with the problem that had sent me down this path in the first place.Could an agent suspend overnight and resume with its state intact?&lt;/p&gt;

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

&lt;p&gt;And once I tested checkpointing and branching, I finally had both things I'd been looking for.That's when the architecture started to make sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How the webhook architecture works&lt;/strong&gt;
&lt;/h3&gt;

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

&lt;p&gt;The deployment model here is different from a traditional always-on server, and understanding it made everything else click.&lt;/p&gt;

&lt;p&gt;The orchestrator itself runs inside a Tensorlake sandbox with a public HTTPS endpoint. Anthropic pushes incoming work to that endpoint via webhook. When there's no traffic, the orchestrator sandbox suspends. When a new work item arrives, it wakes in under a second, processes the request, and creates a worker sandbox for that session.&lt;/p&gt;

&lt;p&gt;Two independent lifecycles:&lt;/p&gt;

&lt;p&gt;The orchestrator sandbox suspends when idle, preserving its memory state including the running uvicorn process. It doesn't accumulate per-session filesystem state, so suspending it between work items costs only storage rates.&lt;/p&gt;

&lt;p&gt;The worker sandboxes — one per session — accumulate filesystem state throughout a session and suspend when the session ends. Their state is preserved in storage, not held by running compute.&lt;/p&gt;

&lt;p&gt;Neither one has to stay alive to preserve the other's state. On every other platform I'd tried, "preserve state" meant "keep something running." Here it means "checkpoint and stop billing."&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How session routing works&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When you call &lt;code&gt;make session PROMPT="..."&lt;/code&gt;, the orchestrator sends a new session to Anthropic. Anthropic validates it, adds it to the work queue, and pushes a work item payload to your orchestrator's webhook endpoint.&lt;/p&gt;

&lt;p&gt;That payload contains three things the orchestrator needs: the session ID, the work ID, and the environment ID. The orchestrator wakes, reads the payload, and creates a worker sandbox from your registered image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-lc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec python3 /opt/sandbox_entrypoint.py &amp;gt; /tmp/runner.log 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANTHROPIC_ENVIRONMENT_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;environment_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANTHROPIC_SESSION_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANTHROPIC_WORK_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANTHROPIC_ENVIRONMENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;environment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things tripped me up here. Credentials belong in &lt;code&gt;start_process(env={...})&lt;/code&gt;, not &lt;code&gt;Sandbox.create()&lt;/code&gt; — they're session specific, not image-level configuration. And sandbox names must be valid slugs. Neither was hard to fix once I knew what was happening, but both cost me time.&lt;/p&gt;

&lt;p&gt;The worker sandbox runs &lt;code&gt;sandbox_entrypoint.py&lt;/code&gt;, which attaches to the Anthropic session and begins consuming tool calls. From that point, the agent has a full Linux environment: bash, git, Python, and whatever else you built into the image.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Building the agent image&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Every tool the agent needs has to live in the image before the session starts:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake/ubuntu-minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apt-get update &amp;amp;&amp;amp; apt-get install -y ca-certificates curl git gh python3 python3-pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip install --break-system-packages &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;anthropic&amp;gt;=0.103&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;httpx&amp;gt;=0.27&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sandbox_entrypoint.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/sandbox_entrypoint.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registered_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added my analysis tools here: Python packages, jq, ripgrep. If it needed to run inside the sandbox, it lived in the image.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How suspend and resume actually work&lt;/strong&gt;
&lt;/h3&gt;

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

&lt;p&gt;When a session ends, the worker sandbox suspends. Not terminates. The process state and filesystem are checkpointed to storage. Compute billing stops. The sandbox sits there as a stored snapshot until the next session begins.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;RESUME_SUSPENDED_SESSIONS=true&lt;/code&gt; tells the orchestrator what to do when the next work item arrives for an existing session: restore the suspended sandbox instead of creating a fresh one from the base image.&lt;/p&gt;

&lt;p&gt;I set the flag and expected the sandbox to wake up immediately.&lt;/p&gt;

&lt;p&gt;Nothing happened.&lt;/p&gt;

&lt;p&gt;For a few minutes I thought the integration was broken. It wasn't. The flag doesn't trigger a resume directly. A new incoming webhook does. The flag just tells the orchestrator which action to take when that webhook arrives. Once I understood that, the behavior made complete sense .Why would a sandbox wake up before there's work to do?&lt;/p&gt;

&lt;p&gt;The practical difference between fresh and resumed is simple: a fresh sandbox starts with a clean &lt;code&gt;/workspace/&lt;/code&gt;. A resumed sandbox starts with whatever the last session left there.For a research agent that had already spent hours building an understanding of a codebase, those are completely different starting points.&lt;/p&gt;

&lt;p&gt;The session ran overnight, suspended, and resumed the next morning with its state intact. The filesystem was exactly where I had left it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Checkpointing and parallel exploration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Resume solved the overnight persistence problem. Checkpointing solved the one I'd discovered the hard way during the Daytona build.&lt;/p&gt;

&lt;p&gt;After the full analysis was complete, I created a checkpoint and launched three sandboxes from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-strategy-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each child started from exactly the same state as the parent. The cloned repository was already there. The installed dependencies were already there. The analysis notes were already there. Nothing needed to be rebuilt.&lt;/p&gt;

&lt;p&gt;Instead of repeating six hours of analysis three separate times, I ran three parallel experiments from the same verified baseline. By the next morning I had three implementations to compare.&lt;/p&gt;

&lt;p&gt;The fork I'd dismissed as an edge case turned out to be the feature that mattered most.&lt;/p&gt;

&lt;p&gt;Without it, every experiment required repeating hours of work. With it, experimentation became nearly free. This generalizes beyond refactoring: any time you want to test N variations with different prompts, different dependency versions, different approaches — you run the expensive setup once, checkpoint, and branch into as many parallel experiments as you need. &lt;/p&gt;

&lt;p&gt;The cost of exploration drops to the cost of the experiments themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why I chose Tensorlake, stated plainly&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;My agent needed two things: zero idle compute cost while preserving filesystem state overnight, and the ability to fork mid-session to explore refactoring strategies in parallel. Those two requirements narrowed the field to one option.&lt;/p&gt;

&lt;p&gt;I didn't choose Tensorlake because it was the fastest or the cheapest. I chose it because it directly addressed the two problems I was trying to solve.&lt;/p&gt;

&lt;p&gt;The suspend/resume workflow preserved state without keeping compute running. Checkpointing made it possible to branch from an existing analysis instead of repeating hours of setup work. I tested both and they worked exactly the way I needed them to.&lt;/p&gt;

&lt;p&gt;The hard part wasn't choosing a provider. It was understanding my requirements. Once I understood those, the decision became obvious.&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;strong&gt;How to Pick the Right Sandbox&lt;/strong&gt;
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Tensorlake&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Daytona&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cloudflare Containers&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cloudflare Isolates&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle cost&lt;/td&gt;
&lt;td&gt;Suspended at storage rate&lt;/td&gt;
&lt;td&gt;VM stays alive&lt;/td&gt;
&lt;td&gt;Scales to zero after sleep timeout&lt;/td&gt;
&lt;td&gt;No filesystem to preserve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem on resume&lt;/td&gt;
&lt;td&gt;State preserved&lt;/td&gt;
&lt;td&gt;Full sandbox creation from snapshot&lt;/td&gt;
&lt;td&gt;Disk is wiped on restart&lt;/td&gt;
&lt;td&gt;No persistent filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resume speed&lt;/td&gt;
&lt;td&gt;~0.6s memory restore&lt;/td&gt;
&lt;td&gt;Full sandbox creation&lt;/td&gt;
&lt;td&gt;Full VM restart&lt;/td&gt;
&lt;td&gt;Milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-session fork&lt;/td&gt;
&lt;td&gt;Yes: checkpoint() + N&lt;/td&gt;
&lt;td&gt;Yes, experimental fork method in core SDK (not documented in Claude integration guide)&lt;/td&gt;
&lt;td&gt;Not documented&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Linux&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted option&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent scale&lt;/td&gt;
&lt;td&gt;Up to 100k&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;td&gt;Tens of thousands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup effort&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;One-click&lt;/td&gt;
&lt;td&gt;One-click&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Does your agent accumulate state that's expensive to rebuild between bursts?
&lt;/h3&gt;

&lt;p&gt;Tensorlake. This is the exact problem suspend/resume was designed to solve. If your agent starts fresh every session, continue to the next question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you need to explore multiple approaches from the same mid-session state?
&lt;/h3&gt;

&lt;p&gt;Tensorlake. Checkpointing and branching from an existing state eliminates hours of repeated setup work. If not, continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you need massive concurrency?
&lt;/h3&gt;

&lt;p&gt;Cloudflare isolates are designed for very large-scale concurrent execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you need self-hosted infrastructure?
&lt;/h3&gt;

&lt;p&gt;Daytona is the strongest fit if infrastructure ownership or data residency is a hard requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  How important is setup speed?
&lt;/h3&gt;

&lt;p&gt;Cloudflare offers the fastest path to a prototype. Tensorlake requires more initial setup but provides capabilities that become valuable once agents accumulate long-lived state.&lt;/p&gt;




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

&lt;p&gt;When I started, I thought I was choosing a sandbox provider.What I was actually discovering were my requirements.&lt;/p&gt;

&lt;p&gt;First I learned I needed persistent state. Then I learned I needed a way to branch from that state without repeating hours of work.&lt;/p&gt;

&lt;p&gt;Tensorlake was the first platform I tried that treated compute and state as separate problems.&lt;/p&gt;

&lt;p&gt;Once I understood that distinction, the decision became obvious.The hardest part of running long-lived agents isn't getting them to work. It's making sure their work survives.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Tensorlake: Run Claude Managed Agents on Tensorlake Sandboxes — &lt;a href="https://docs.tensorlake.ai/sandboxes/claude-managed-agents" rel="noopener noreferrer"&gt;https://docs.tensorlake.ai/sandboxes/claude-managed-agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tensorlake: Sandbox Images (Image builder SDK) — &lt;a href="https://docs.tensorlake.ai/sandboxes/images" rel="noopener noreferrer"&gt;https://docs.tensorlake.ai/sandboxes/images&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Daytona: Run Claude Managed Agents on Daytona — &lt;a href="https://www.daytona.io/docs/en/guides/claude/claude-managed-agents/" rel="noopener noreferrer"&gt;https://www.daytona.io/docs/en/guides/claude/claude-managed-agents/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cloudflare: Announcing Claude Managed Agents on Cloudflare — &lt;a href="https://blog.cloudflare.com/claude-managed-agents/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/claude-managed-agents/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic: Claude Managed Agents Self-Hosted Sandboxes — &lt;a href="https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Concurrency sandbox benchmarks results: &lt;a href="https://platform.computesdk.com/scale-invitational" rel="noopener noreferrer"&gt;https://platform.computesdk.com/scale-invitational&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Tue, 16 Jun 2026 10:57:41 +0000</pubDate>
      <link>https://dev.to/divy_ai/-1nhd</link>
      <guid>https://dev.to/divy_ai/-1nhd</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00" class="crayons-story__hidden-navigation-link"&gt;Why Most Multi-Agent AI Systems Waste 90% of Their Time (And How to Fix It)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/divy_ai" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845890%2Fc910b35f-87b4-4888-b116-ad2cde414360.png" alt="divy_ai profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/divy_ai" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Divy Yadav
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Divy Yadav
                
              
              &lt;div id="story-author-preview-content-3906909" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/divy_ai" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845890%2Fc910b35f-87b4-4888-b116-ad2cde414360.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Divy Yadav&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00" id="article-link-3906909"&gt;
          Why Most Multi-Agent AI Systems Waste 90% of Their Time (And How to Fix It)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/software"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;software&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            15 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why Most Multi-Agent AI Systems Waste 90% of Their Time (And How to Fix It)</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:32:52 +0000</pubDate>
      <link>https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00</link>
      <guid>https://dev.to/divy_ai/the-biggest-bottleneck-in-multi-agent-ai-isnt-what-most-engineers-think-2l00</guid>
      <description>&lt;h3&gt;
  
  
  Most engineers treat multi-agent speed as a concurrency problem. It is not. The bottleneck is setup time, and memory snapshots change the math entirely.
&lt;/h3&gt;




&lt;p&gt;Most engineers think multi-agent performance is a concurrency problem.&lt;/p&gt;

&lt;p&gt;I did too.&lt;/p&gt;

&lt;p&gt;So when five AI agents running in parallel barely outperformed a sequential run, I assumed something was wrong with my orchestration.&lt;/p&gt;

&lt;p&gt;I was looking in the wrong place.&lt;/p&gt;

&lt;p&gt;Each agent was spending more time preparing to work than actually working.&lt;/p&gt;

&lt;p&gt;The fix wasn’t more threads, better async code, or a faster model.&lt;/p&gt;

&lt;p&gt;It was a memory snapshot.&lt;/p&gt;

&lt;p&gt;And once I saw where the time was really going, an entire class of multi-agent bottlenecks suddenly made sense.&lt;/p&gt;

&lt;p&gt;Here is what that looks like, what took me three iterations to get right, and where it still has rough edges.&lt;/p&gt;

&lt;p&gt;Let’s get the mental model first.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Does (30 Seconds)
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;The idea is straightforward:&lt;/strong&gt; instead of five agents each spending 90 seconds installing the same tools, install them once, freeze that environment, and stamp out five identical copies. &lt;/p&gt;

&lt;p&gt;Each copy runs a different analysis in parallel. A lead LLM reads all five results and tells you what to fix first.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Creates one Linux VM, installs code analysis tools (bandit, radon) and writes a sample Python project&lt;/li&gt;
&lt;li&gt;Freezes the entire VM state into a memory snapshot (filesystem, memory, running processes included)&lt;/li&gt;
&lt;li&gt;Forks 5 independent copies, each agent assigned a different analysis task (Security, Complexity, Docstrings, Tests, Structure)&lt;/li&gt;
&lt;li&gt;Runs all 5 in parallel via &lt;code&gt;asyncio.gather&lt;/code&gt;, finishing in seconds instead of minutes&lt;/li&gt;
&lt;li&gt;Feeds all results to a lead LLM that produces a single prioritized fix list&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setup time is paid once, upfront, before any agent runs. The rest of this article explains how.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Sandboxes Matter for Agent Workloads
&lt;/h2&gt;

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

&lt;p&gt;If you have not worked with sandboxes before: think of one as a disposable computer that lives in the cloud. &lt;/p&gt;

&lt;p&gt;You spin it up, run whatever code you need inside it, and throw it away when you're done. It has its own filesystem, its own processes, its own network. Nothing it does can touch your machine or any other sandbox running at the same time.&lt;/p&gt;

&lt;p&gt;In short: Sandboxes provide the agent with a secure and isolated enviornement&lt;/p&gt;

&lt;p&gt;That isolation is the whole point. Your agent can install packages, write files, crash badly, or spin up a browser, and none of it bleeds out. When the task is done, you terminate the VM and it is gone. &lt;/p&gt;

&lt;p&gt;The next agent starts clean.&lt;/p&gt;

&lt;p&gt;Most agent frameworks treat the execution environment as an afterthought. The LLM call is the interesting part. The environment is just "wherever the code runs."&lt;/p&gt;

&lt;p&gt;That works fine for single-turn tasks. It breaks down fast for anything multi-step.&lt;/p&gt;

&lt;p&gt;When an agent needs to install packages, write intermediate files, maintain a browser session across multiple pages, or resume a task from a different machine, you need the execution environment to behave like a persistent object, not a function call that resets on every invocation.&lt;/p&gt;

&lt;p&gt;Tensorlake gives each agent a MicroVM backed by Firecracker and CloudHypervisor, optimized for fast boot times and strong isolation. Each sandbox is a full Linux VM. It boots in hundreds of milliseconds, persists filesystem and memory state across sessions, and can be snapshotted at any point in its lifecycle.&lt;/p&gt;

&lt;p&gt;Tensorlake also lets you spin up multiple sandboxes in parallel for concurrent agent execution, and honestly it is one of my favourite things about it.&lt;/p&gt;

&lt;p&gt;it also ranks in the top 5 of  &lt;a href="https://www.computesdk.com/benchmarks/sandboxes/" rel="noopener noreferrer"&gt;SandboxBenchmarks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changes the math is a single question:&lt;/strong&gt; what does the snapshot actually capture?&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Kinds of Snapshots. Very Different Behavior.
&lt;/h2&gt;

&lt;p&gt;Quick vocabulary before the details. Tensorlake sandboxes have four lifecycle modes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; An &lt;strong&gt;ephemeral&lt;/strong&gt; sandbox runs a task and disappears when done, with no name and no persistence between runs. &lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;named&lt;/strong&gt; sandbox outlives the process that created it and can be suspended then reconnected to from any machine. &lt;strong&gt;Suspend&lt;/strong&gt; freezes the VM exactly as it is and &lt;strong&gt;resume&lt;/strong&gt; brings it back to that same state. &lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;snapshot&lt;/strong&gt; is that frozen moment saved as a reusable artifact. &lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;fork&lt;/strong&gt; is a snapshot restored into a fresh, independent VM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project uses the last two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspend&lt;/strong&gt; and &lt;strong&gt;Snapshot&lt;/strong&gt; both preserve state, but serve different purposes : Suspend is for pausing &lt;em&gt;this&lt;/em&gt; sandbox to resume later, while a snapshot is a reusable artifact for retrying from a checkpoint or cloning an environment.&lt;/p&gt;

&lt;p&gt;Tensorlake supports two checkpoint types. Most tutorials only mention one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CheckpointType.FILESYSTEM&lt;/code&gt; captures disk state only. Restore from it and the new sandbox does a full cold boot: processes restart from scratch, packages get re-imported. Your pip installs survive. Nothing that was in memory does.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CheckpointType.MEMORY&lt;/code&gt; is different. It captures disk state, VM memory, and all running processes. The restored VM resumes mid-stride, exactly as the source was at checkpoint time. No boot sequence. No re-initialization. If Python had already imported bandit, the fork starts with it loaded. The environment is not rebuilt. It is copied.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The checkpoint type is not a performance detail. It determines whether your fork is a clone or a restart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The default when you call &lt;code&gt;sandbox.checkpoint()&lt;/code&gt; with no arguments is filesystem. That is the wrong choice for a parallel swarm where agents share a prepared environment. You want memory.&lt;/p&gt;

&lt;p&gt;One more constraint worth knowing upfront: for memory snapshots, resources (CPUs, RAM) are baked into the snapshot at checkpoint time. You cannot override them when creating forks. Set the right &lt;code&gt;cpus&lt;/code&gt; and &lt;code&gt;memory_mb&lt;/code&gt; on the base sandbox before you checkpoint. Every fork inherits them automatically.&lt;/p&gt;




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

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

&lt;p&gt;The pattern has five distinct phases. Each one has a single responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Base Snapshot:&lt;/strong&gt; Spins up a single baseline sandbox, installs analysis tools (&lt;code&gt;bandit&lt;/code&gt;, &lt;code&gt;radon&lt;/code&gt;), writes the target code, and checkpoints the entire running VM state using &lt;code&gt;CheckpointType.MEMORY&lt;/code&gt;. The base sandbox is then terminated, leaving behind the reusable snapshot ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Agent Forking:&lt;/strong&gt; Restores 5 independent sandboxes concurrently from the base snapshot using &lt;code&gt;sandbox.fork(...)&lt;/code&gt;. Each fork is a warm start that inherits all installed tools, environment settings, and target files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Sequential Baseline (Timing):&lt;/strong&gt; Runs each agent's analysis script (&lt;code&gt;analyze.py&lt;/code&gt;) one-by-one inside its respective sandbox to measure sequential time as a benchmark denominator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Parallel Swarm:&lt;/strong&gt; Executes all 5 agents concurrently using &lt;code&gt;asyncio.gather(...)&lt;/code&gt;. Each agent runs the same analysis script inside its isolated sandbox but with a different focus configuration passed via the &lt;code&gt;PERSPECTIVE&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — LLM Aggregation:&lt;/strong&gt; Collects the individual reports (Security, Complexity, Docstrings, Tests, Structure) alongside the timing data, and passes them to the lead LLM (GPT) to synthesize a single prioritized fix list.&lt;/p&gt;

&lt;p&gt;Phase 1 runs once. Phases 2 through 4 run every time you want results. The fork is cheap. The base environment build is not, but you only pay that cost once per snapshot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Build and Snapshot
&lt;/h2&gt;

&lt;p&gt;The base sandbox installs the analysis tools, writes the target codebase into the VM, then snapshots the entire state. Every fork inherits both the tools and the target project automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncSandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CheckpointType&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_base_snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncSandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base-swarm-env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="c1"&gt;# Install analysis tools. These are baked into the snapshot
&lt;/span&gt;        &lt;span class="c1"&gt;# and available to every forked agent at no extra install cost.
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bandit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;radon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--break-system-packages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip install failed:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Write a sample Python project with intentional issues for agents to find.
&lt;/span&gt;        &lt;span class="c1"&gt;# All forks inherit this from the snapshot; no need to write per-agent.
&lt;/span&gt;        &lt;span class="n"&gt;target_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/target/auth.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
import subprocess
DB_PASSWORD = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hardcoded_secret_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

def authenticate(user_input):
    return eval(user_input)

def run_command(cmd):
    return subprocess.call(cmd, shell=True)
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/target/logic.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
def classify(a, b, c, d, e, f, g, h):
    if a and b:
        if c or d:
            if not e and f:
                return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
            elif e and not f:
                return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
            elif g and h:
                return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
            else:
                return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
        elif g:
            return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
    return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path_f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mkdir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Verify tools work before snapshotting.
&lt;/span&gt;        &lt;span class="c1"&gt;# A broken tool in the snapshot means broken forks.
&lt;/span&gt;        &lt;span class="n"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bandit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tool verification failed:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;checkpoint_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CheckpointType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEMORY&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Context manager terminates the base sandbox here.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snapshot failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;async with&lt;/code&gt; pattern guarantees &lt;code&gt;terminate()&lt;/code&gt; is called on exit, including on exceptions. Without it, any exception before a manual &lt;code&gt;terminate()&lt;/code&gt; call leaves an orphaned VM running in the background. TensorLake's async documentation shows this pattern explicitly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;result.exit_code&lt;/code&gt; comes from &lt;code&gt;CommandResult&lt;/code&gt;, the SDK's return type for &lt;code&gt;run()&lt;/code&gt;. It has &lt;code&gt;stdout: str&lt;/code&gt;, &lt;code&gt;stderr: str&lt;/code&gt;, and &lt;code&gt;exit_code: int&lt;/code&gt;. Note that &lt;code&gt;stdout&lt;/code&gt; is already a string, not bytes, so no &lt;code&gt;.decode()&lt;/code&gt; is needed anywhere.&lt;/p&gt;

&lt;p&gt;The status check after &lt;code&gt;checkpoint()&lt;/code&gt;: &lt;code&gt;SnapshotStatus&lt;/code&gt; is an enum, so &lt;code&gt;.value&lt;/code&gt; gives you &lt;code&gt;"completed"&lt;/code&gt;, &lt;code&gt;"in_progress"&lt;/code&gt;, or &lt;code&gt;"failed"&lt;/code&gt;. The documentation shows &lt;code&gt;checkpoint()&lt;/code&gt; returns a &lt;code&gt;SnapshotInfo&lt;/code&gt; with a &lt;code&gt;status&lt;/code&gt; field. Checking that status before proceeding is a useful defensive practice. I learned this after a failed snapshot left me debugging downstream agent failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: Fork and Run an Agent
&lt;/h2&gt;

&lt;p&gt;This is the actual fork. The call is &lt;code&gt;AsyncSandbox.create(snapshot_id=snapshot_id)&lt;/code&gt;. No special &lt;code&gt;fork()&lt;/code&gt; method. No copy-on-write API. Just &lt;code&gt;create()&lt;/code&gt; with a snapshot ID. Every call produces a fully independent VM starting from that snapshot's frozen state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PERSPECTIVES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Complexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docstrings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Structure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AgentReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;perspective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PERSPECTIVES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PERSPECTIVES&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;t_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# cpus and memory_mb intentionally omitted.
&lt;/span&gt;    &lt;span class="c1"&gt;# For MEMORY snapshots, resources are inherited from the snapshot
&lt;/span&gt;    &lt;span class="c1"&gt;# and cannot be overridden at restore time.
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncSandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;allow_internet_access&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# code analysis is offline; no outbound needed
&lt;/span&gt;        &lt;span class="n"&gt;timeout_secs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/analyze.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ANALYSIS_SCRIPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/analyze.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERSPECTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;perspective&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t_start&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AgentReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;perspective&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;perspective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;execution_time_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allow_internet_access=False&lt;/code&gt; is safe here because bandit and radon analyze source code and do not make network calls. This parameter is not locked by MEMORY snapshots. TensorLake's networking documentation recommends disabling outbound internet access for untrusted code.&lt;/p&gt;

&lt;p&gt;The dispatch script gets written fresh into each forked VM via &lt;code&gt;sandbox.write_file()&lt;/code&gt;. Each agent's VM is fully isolated: writing to &lt;code&gt;/workspace/analyze.py&lt;/code&gt; in fork 0 has no effect on fork 1. The target project files are already there, inherited from the snapshot.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;result.stdout&lt;/code&gt; is already a Python string, &lt;code&gt;json.loads(result.stdout.strip())&lt;/code&gt; works directly. The &lt;code&gt;.strip()&lt;/code&gt; handles the trailing newline from &lt;code&gt;print()&lt;/code&gt; inside the sandbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Sequential First, Then Parallel
&lt;/h2&gt;

&lt;p&gt;The sequential baseline exists for one reason: to give the speedup calculation a real denominator. Without it, you have a time with no context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sequential&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_parallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# asyncio.gather returns a list of results when awaited.
&lt;/span&gt;    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parallel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;asyncio.gather&lt;/code&gt; is what TensorLake's async documentation recommends for concurrent sandbox fan-out. The &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; approach works too (the sync Sandbox API supports it), but if you are already in an async context, &lt;code&gt;gather&lt;/code&gt; is cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4:What the Analysis Script Does
&lt;/h2&gt;

&lt;p&gt;The dispatch script runs inside each forked sandbox. It reads the &lt;code&gt;PERSPECTIVE&lt;/code&gt; environment variable, routes to the right analysis function, and prints one JSON line to stdout. All five analyses are fully offline, with no network calls needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ANALYSIS_SCRIPT — runs INSIDE each forked sandbox
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;PERSPECTIVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERSPECTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_security&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;bandit: find hardcoded secrets, unsafe eval, shell injection.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bandit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bandit parse error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue_severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Minor issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_complexity&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;radon: cyclomatic complexity per function.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;radon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;radon parse error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;file_blocks&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;file_blocks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;complex_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;complex_blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (cc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;complex_blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;complexity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;complex_blocks&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All within threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;complex_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complex_blocks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_cc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complex_blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_docstrings&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;ast: count functions and classes that lack docstrings.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documented&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FunctionDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsyncFunctionDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClassDef&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_docstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;documented&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documented&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documented&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;documented&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;documented&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; documented (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Count test files relative to source files.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;all_py&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;test_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_py&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_py&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_py&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_py&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_files&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_py&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files are tests (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_structure&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;ast: count functions, classes, imports across the codebase.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FunctionDef&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClassDef&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;           &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Import&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImportFrom&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imports&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;fpr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions_per_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fpr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; functions across &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;run_security&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Complexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_complexity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docstrings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_docstrings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Structure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;run_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PERSPECTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown perspective: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PERSPECTIVE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;perspective&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PERSPECTIVE&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth keeping when you adapt this.&lt;/p&gt;

&lt;p&gt;Parameters via environment variables: &lt;code&gt;sandbox.run(env={"KEY": "val"})&lt;/code&gt; passes per-command variables and avoids shell escaping issues when values contain spaces or special characters. It also keeps the dispatch script stateless, with no hardcoded perspective names inside the script itself.&lt;/p&gt;

&lt;p&gt;JSON to stdout: the orchestrator reads &lt;code&gt;result.stdout.strip()&lt;/code&gt; and passes it directly to &lt;code&gt;json.loads()&lt;/code&gt;. The script has one job: print exactly one valid JSON line. Any other stdout output (debug prints, progress bars) breaks the parse. Keep it strict.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5: Lead Agent Synthesis
&lt;/h2&gt;

&lt;p&gt;After all five agents return, a single GPT-4o call synthesizes their findings into a prioritized action list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;aggregate_with_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SwarmResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;speedup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_time_s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_time_s&lt;/span&gt;

    &lt;span class="n"&gt;reports_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perspective&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100 | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior engineering lead reviewing a parallel code analysis report.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent Findings:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reports_block&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Benchmark:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Sequential : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sequential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_time_s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Parallel   : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_time_s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Speedup    : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;speedup&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provide: overall codebase health score, top three issues to fix immediately &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(with file and severity), recommended next actions, and one sentence on what &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the parallel speedup means for running this at scale.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lead agent sees both the analysis findings and the timing benchmark in the same context. That is the reduce step in a map-reduce agent pattern: give the aggregator everything the workers produced, not just the domain data. The call is synchronous because there is nothing left to concurrently await at this point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the Time Actually Goes
&lt;/h2&gt;

&lt;p&gt;Both timelines contain the same agents doing the same work. What changes is when setup happens. These numbers are structural projections based on typical pip install times and sandbox warm-restore behavior, not measured results. Your numbers will vary by workload and network conditions. Run the demo to measure your case.&lt;/p&gt;

&lt;p&gt;Without memory snapshots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent 0: [setup ~90s][work ~8s]
Agent 1: [setup ~90s][work ~9s]
Agent 2: [setup ~90s][work ~8s]
Agent 3: [setup ~90s][work ~9s]
Agent 4: [setup ~90s][work ~8s]

Sequential total: ~490s
Parallel total:   ~100s  (setup still paid by each fork separately)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With memory snapshots (MEMORY type):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base build:  [setup ~90s][checkpoint ~3s]  ← paid once, outside the loop
Agent 0: [warm fork ~1s][work ~8s]
Agent 1: [warm fork ~1s][work ~9s]
Agent 2: [warm fork ~1s][work ~8s]
Agent 3: [warm fork ~1s][work ~9s]
Agent 4: [warm fork ~1s][work ~8s]

Sequential total: ~48s
Parallel total:   ~10s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The speedup ratio looks similar on paper. The absolute time is not. At five agents the gap is 450 seconds versus 5 seconds of overhead. At fifty agents it is 4,500 seconds versus 50 seconds.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Setup time does not scale down with parallelism. It multiplies. The snapshot moves it outside the loop entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The benchmark captures four numbers:&lt;/strong&gt; sequential total time (the denominator), parallel total time (wall-clock from first fork to last return), speedup (sequential divided by parallel), and efficiency (speedup divided by agent count, multiplied by 100).&lt;/p&gt;

&lt;p&gt;Efficiency is the one most benchmarks skip. A 4.2x speedup across five agents is 84% parallel efficiency: 16% is lost to fork startup, scheduling, and I/O contention. That number matters when you scale from five agents to fifty.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Code Does Not Handle
&lt;/h2&gt;

&lt;p&gt;The demo covers the happy path. Three things to add before production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM rate limits.&lt;/strong&gt; Twenty or thirty concurrent agents all hitting the OpenAI API will trigger rate limit errors. The demo has no retry logic. Add exponential backoff before you scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot storage.&lt;/strong&gt; Snapshots may incur charges depending on your plan. Use &lt;code&gt;Sandbox.delete_snapshot(snapshot_id)&lt;/code&gt; when done. The demo has a &lt;code&gt;CLEANUP_SNAPSHOT_ON_EXIT&lt;/code&gt; flag at the top of the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent error isolation.&lt;/strong&gt; If one &lt;code&gt;run_agent()&lt;/code&gt; coroutine raises inside &lt;code&gt;asyncio.gather&lt;/code&gt;, the whole batch fails. In production, wrap each coroutine with &lt;code&gt;asyncio.create_task()&lt;/code&gt; and handle errors per-agent.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When to Use This Pattern (And When Not To)
&lt;/h2&gt;

&lt;p&gt;Use it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple agents need the same environment&lt;/li&gt;
&lt;li&gt;Their tasks are independent (no inter-agent communication mid-run)&lt;/li&gt;
&lt;li&gt;Setup time is a meaningful fraction of total runtime&lt;/li&gt;
&lt;li&gt;Reproducibility matters: every fork starts from an identical state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents need to share state during execution. Forks are fully isolated. If agent 2 needs to react to what agent 1 found, use shared storage or message queues instead.&lt;/li&gt;
&lt;li&gt;The task is fast enough for a single agent. Forking five sandboxes for a 3-second job adds overhead, not speed.&lt;/li&gt;
&lt;li&gt;Environment setup takes under 5 seconds. The snapshot overhead only pays off when setup is the actual bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Your situation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Right choice&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiple agents, shared dependencies, independent outputs&lt;/td&gt;
&lt;td&gt;Memory snapshot, fork N copies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single agent, long task, needs to pause and resume&lt;/td&gt;
&lt;td&gt;Named sandbox with suspend/resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure browser automation, no code execution&lt;/td&gt;
&lt;td&gt;Stagehand or BrowserBase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless task, resets every run&lt;/td&gt;
&lt;td&gt;Ephemeral sandbox, no snapshot needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment setup under 5 seconds&lt;/td&gt;
&lt;td&gt;Filesystem snapshot or skip snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On filesystem performance: Tensorlake publishes performance benchmarks on their GitHub comparing sandbox execution times across providers. Refer to their repository for current numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running This
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tensorlake openai
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TENSORLAKE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key"&lt;/span&gt;
python3 agent.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier at cloud.tensorlake.ai, no credit card required. The demo takes 3-5 minutes end to end. After it runs, &lt;code&gt;benchmark_results.json&lt;/code&gt; has the full per-agent timing data.&lt;/p&gt;

&lt;p&gt;Phase 1 (base build and snapshot) runs once. If you want to run the benchmark multiple times, pass your existing snapshot ID directly and skip Phase 1. The snapshot persists between runs until you delete it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Took Three Iterations
&lt;/h2&gt;

&lt;p&gt;The first version had plain &lt;code&gt;await sandbox.terminate()&lt;/code&gt; at the end of each function. Two exceptions during testing left sandboxes running and billing for idle compute. Switched to &lt;code&gt;async with await AsyncSandbox.create(...) as sandbox:&lt;/code&gt; and that stopped.&lt;/p&gt;

&lt;p&gt;The second version called &lt;code&gt;sandbox.checkpoint(sandbox.sandbox_id)&lt;/code&gt;. I had copied the pattern from a CLI reference (&lt;code&gt;tl sbx checkpoint &amp;lt;sandbox-id&amp;gt;&lt;/code&gt;) and assumed the Python SDK matched. It does not. The Python instance method takes no positional arguments: &lt;code&gt;sandbox.checkpoint(checkpoint_type=CheckpointType.MEMORY)&lt;/code&gt;. That is it.&lt;/p&gt;

&lt;p&gt;The third version was the first one that ran end to end, but with &lt;code&gt;CheckpointType.FILESYSTEM&lt;/code&gt; by default because I had not read the snapshots documentation carefully. The benchmark looked reasonable. The forks were doing full cold boots and I was measuring them alongside the actual work. Switching to &lt;code&gt;CheckpointType.MEMORY&lt;/code&gt; was the change that made setup time disappear from per-fork timing.&lt;/p&gt;

&lt;p&gt;Small mistakes individually. What they share: Tensorlake's API is well documented, but the snapshot docs, the SDK reference, and the async docs are three separate pages. Read only the quickstart and you miss two of the three things that matter most for this pattern.&lt;/p&gt;




&lt;p&gt;You can also check the complete project on my github here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dvy246/Tensorlake-Agentic-Swarm.git" rel="noopener noreferrer"&gt;click_here&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thing That Changes
&lt;/h2&gt;

&lt;p&gt;Running the same five agents sequentially and then in parallel is one of those moments where the architecture becomes legible in a way that documentation does not fully convey.&lt;/p&gt;

&lt;p&gt;The snapshot moves setup cost from inside the loop to outside it. The agents still do the same work on the same hardware. The savings come from not rebuilding an environment five times when it only needed to be built once.&lt;/p&gt;

&lt;p&gt;Most multi-agent optimization advice focuses on LLM calls: batching, caching, cheaper models. That advice is right. But if you have five agents each spending 90 seconds on pip installs before making a single inference call, no amount of LLM optimization helps until you address setup time first.&lt;/p&gt;

&lt;p&gt;The bottleneck was never the agents. It was rebuilding the same environment on every run. Snapshot it once, fork cheaply, and parallel execution finally delivers what you expected when you first wrote &lt;code&gt;asyncio.gather&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.tensorlake.ai/sandboxes/introduction" rel="noopener noreferrer"&gt;Tensorlake Sandboxes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.tensorlake.ai/sandboxes/sdk-reference" rel="noopener noreferrer"&gt;SDK Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.tensorlake.ai/sandboxes/snapshots" rel="noopener noreferrer"&gt;Snapshots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.tensorlake.ai/sandboxes/async" rel="noopener noreferrer"&gt;Async SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.computesdk.com/benchmarks/sandboxes/" rel="noopener noreferrer"&gt;https://www.computesdk.com/benchmarks/sandboxes/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>software</category>
    </item>
    <item>
      <title>MCP Is Dead. The Downloads Just Don't Know It Yet.</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Fri, 05 Jun 2026 12:54:22 +0000</pubDate>
      <link>https://dev.to/divy_ai/mcp-is-dead-the-downloads-just-dont-know-it-yet-47ph</link>
      <guid>https://dev.to/divy_ai/mcp-is-dead-the-downloads-just-dont-know-it-yet-47ph</guid>
      <description>&lt;p&gt;&lt;strong&gt;30 CVEs in 60 days, a maintenance tax nobody warned you about, and what engineers are quietly switching to.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Your AI agent ran a query on a fake database last month.&lt;/p&gt;

&lt;p&gt;It got real results. The tool worked perfectly. Your SSH keys left in the background.&lt;/p&gt;

&lt;p&gt;The agent didn't flag it. The registry didn't catch it. Nobody warned you.&lt;/p&gt;

&lt;p&gt;That's not a hypothetical. That's MCP in 2026, with 97 million monthly downloads and a Linux Foundation home.&lt;/p&gt;

&lt;p&gt;The hype was real. So are the cracks.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: what is MCP, and why should you care
&lt;/h2&gt;

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

&lt;p&gt;If you've never built AI agents before, this matters. Skip it if you have.&lt;/p&gt;

&lt;p&gt;Say you're building an AI assistant that needs to do real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look up customer records in a database&lt;/li&gt;
&lt;li&gt;Create tickets in Jira&lt;/li&gt;
&lt;li&gt;Send a Slack message&lt;/li&gt;
&lt;li&gt;Pull a file from Google Drive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those lives in a different system. Different API, different auth, different data format.&lt;/p&gt;

&lt;p&gt;To connect your AI to all of them, you'd write a custom integration for each one. Fine for two tools. Painful for ten. Then you switch models and rewrite everything.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;N×M problem&lt;/strong&gt;: N tools multiplied by M AI models equals a mountain of glue code nobody wants to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP — the Model Context Protocol&lt;/strong&gt; — solves that. Released by Anthropic in November 2024, it's an open standard that gives AI models one universal way to talk to external tools. You build an MCP server once around a tool, and any MCP-compatible AI can use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your agent  →  MCP Client  →  MCP Server  →  Real Tool (Slack, Postgres, GitHub)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP Host&lt;/strong&gt;: your app (Claude Desktop, VS Code, a custom agent)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Client&lt;/strong&gt;: the component inside your app that speaks MCP, discovers tools, calls them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Server&lt;/strong&gt;: a small process wrapping a real tool, exposing it in a format any MCP client can use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. The N×M problem disappears. One integration per tool, works with every AI.&lt;/p&gt;

&lt;p&gt;The pitch was real. The adoption proved it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How MCP went from zero to everywhere in 14 months
&lt;/h2&gt;

&lt;p&gt;The adoption happened fast. Unusually fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nov 2024&lt;/strong&gt; — Anthropic launches MCP. ~2M monthly SDK downloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apr 2025&lt;/strong&gt; — OpenAI adopts it. Downloads jump to 22M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jul 2025&lt;/strong&gt; — Microsoft integrates it into Copilot Studio. 45M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nov 2025&lt;/strong&gt; — AWS adds support. 68M.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mar 2026&lt;/strong&gt; — Every major AI vendor on board. 97M downloads. 10,000+ public MCP servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In December 2025, Anthropic donated MCP to the Agentic AI Foundation under the Linux Foundation, co-founded with OpenAI and Block. It stopped being Anthropic's protocol and became the industry's.&lt;/p&gt;

&lt;p&gt;The "USB-C for AI" comparison spread everywhere. Everyone plugged in.&lt;/p&gt;

&lt;p&gt;And then engineers started running it in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why developers hate MCP
&lt;/h2&gt;

&lt;p&gt;The complaints have been building in forums, GitHub issues, and private Slack channels for months. Not from people who misunderstood MCP. From people who ran it in production and got surprised by the same things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It's unauthenticated by default."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Out of the box, an MCP server trusts whatever connects to it.&lt;/p&gt;

&lt;p&gt;No built-in check that the server is who it claims. No built-in check that the client is who it claims. You're responsible for adding that layer.&lt;/p&gt;

&lt;p&gt;Most tutorials don't mention this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The STDIO transport executes arbitrary OS commands."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The official MCP STDIO transport runs any OS command you point at it to launch a server. Even when the server startup fails. No sanitization warnings. Nothing in the developer toolchain flags it.&lt;/p&gt;

&lt;p&gt;OX Security documented this in April 2026.&lt;/p&gt;

&lt;p&gt;Anthropic's response: expected behavior, sanitization is the developer's responsibility. LangChain said the same. Microsoft said the same.&lt;/p&gt;

&lt;p&gt;Three major vendors. Same answer: your problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The spec moves, community servers don't."&lt;/strong&gt;&lt;br&gt;
MCP servers published in community registries frequently fall behind spec updates. A server that worked last month may behave differently after the protocol updates. The registry has no enforcement mechanism. You find out in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Every REST API I already have needs a new wrapper process."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding MCP to a tool that already has a clean REST API means building an entire MCP server around it.&lt;/p&gt;

&lt;p&gt;That server needs to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployed and monitored&lt;/li&gt;
&lt;li&gt;Updated when the underlying API changes&lt;/li&gt;
&lt;li&gt;Secured separately from both the agent and the tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For ten existing APIs, that's ten new processes to own. Month three of production, you feel every one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The registries are basically npm circa 2015."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In early 2026, OX Security cloned &lt;code&gt;mcp-server-postgres&lt;/code&gt; and named it &lt;code&gt;mcp-server-postgress&lt;/code&gt; (extra 's'). Functionally identical. Same queries, same responses.&lt;/p&gt;

&lt;p&gt;Hidden inside: a payload that silently pulled SSH keys and environment files to an outside server.&lt;/p&gt;

&lt;p&gt;They submitted it to eleven major MCP registries.&lt;/p&gt;

&lt;p&gt;Nine published it. No automated security review. No source code analysis. Nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It eats my context window before the user says anything."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When your MCP client connects to a server, it loads the full tool schema into your context window — names, descriptions, parameters for every tool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One server, five tools: ~500 tokens gone before the first message&lt;/li&gt;
&lt;li&gt;Ten servers: 2,000–3,000 tokens gone before the first message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is already reasoning over a smaller budget. Before your user typed a word.&lt;/p&gt;

&lt;p&gt;These aren't edge cases. They're the standard experience for anyone who's moved past a local demo.&lt;/p&gt;


&lt;h2&gt;
  
  
  The 3 problems that actually break production systems
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The trust problem
&lt;/h3&gt;

&lt;p&gt;Your agent has no way to verify an MCP server is who it claims to be.&lt;/p&gt;

&lt;p&gt;The OX Security incident made this real: nine out of eleven registries accepted a typosquatted credential-stealing package. The malicious server functioned correctly. Ran database queries. Returned results. And silently pulled your SSH keys in the background. Nothing in the protocol flagged it.&lt;/p&gt;

&lt;p&gt;Since January 2026, researchers have filed 30 CVEs against the MCP ecosystem in 60 days. Prompt injection through tool descriptions. Credential theft via config file reads. "Tool poisoning," where a server description manipulates the agent's next decision. These aren't exotic attack vectors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your agent can't tell your Postgres server from an attacker's. That's not a code bug. It's a design gap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MCP was built for a trusted local environment. Production isn't that.&lt;/p&gt;


&lt;h3&gt;
  
  
  The wrapper tax
&lt;/h3&gt;

&lt;p&gt;Every tool you connect to MCP needs its own MCP server. Ten tools means ten additional processes to own.&lt;/p&gt;

&lt;p&gt;Each one needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stay in sync when the underlying tool's API changes&lt;/li&gt;
&lt;li&gt;Be monitored for failures in production&lt;/li&gt;
&lt;li&gt;Be secured separately from the agent and the tool itself&lt;/li&gt;
&lt;li&gt;Be deployed as part of your infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first two tools, manageable. Month three with fifteen integrations, it's a job.&lt;/p&gt;

&lt;p&gt;The N×M problem is solved. The "N new processes" problem quietly replaced it.&lt;/p&gt;


&lt;h3&gt;
  
  
  The context window bill
&lt;/h3&gt;

&lt;p&gt;Tool schemas are not free. They're tokens. And they arrive before your user's message does.&lt;/p&gt;

&lt;p&gt;A team building a customer service agent connected to ten MCP servers found their available reasoning budget had shrunk by 30% before the first user question arrived. Same model. Same prompts. Just more tools.&lt;/p&gt;

&lt;p&gt;In a long multi-step agent session, schema tokens compound. Quality drifts. Costs climb. Most teams don't trace this back to tool schema overhead until they look at what's actually in the context window.&lt;/p&gt;


&lt;h2&gt;
  
  
  What engineers are using instead
&lt;/h2&gt;

&lt;p&gt;A few patterns have emerged for teams that ran into the problems above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct REST API calls&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For tools with a clean existing API, skip MCP entirely. Call the API directly from your agent. No new server to maintain, no schema overhead, existing auth covers it.&lt;/p&gt;

&lt;p&gt;Works well when you control the tool and the API is stable. Doesn't scale when you need multiple AI systems to share the same integrations.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Native provider tool use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anthropic and OpenAI both have built-in tool calling that needs no MCP infrastructure. You define the tool schema inline, pass it with the request, the model calls it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No server process&lt;/li&gt;
&lt;li&gt;No registry&lt;/li&gt;
&lt;li&gt;Your auth sits directly on the call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams running focused single-purpose agents in 2026 are doing this. Simpler to reason about, harder to share across systems.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;UTCP (Universal Tool Calling Protocol)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UTCP skips the wrapper entirely. Instead of wrapping a tool in an MCP server, it calls the tool's existing HTTP endpoints directly, with a discovery layer on top.&lt;/p&gt;

&lt;p&gt;As of early 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1,000+ GitHub stars&lt;/li&gt;
&lt;li&gt;Implementations in Python, Go, and TypeScript&lt;/li&gt;
&lt;li&gt;Growing community from teams that wanted lower latency and less infrastructure overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for teams with well-designed existing APIs who don't want a separate server layer. Not a full MCP replacement if you need the ecosystem breadth — but for many production use cases, materially simpler.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;MCP with a gateway layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For teams committed to MCP, the answer to most of the problems above is an MCP gateway — a controlled layer between your agent and your servers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your agent  →  MCP Gateway  →  MCP Server 1  →  Tool
                            →  MCP Server 2  →  Tool
                            →  MCP Server N  →  Tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A gateway handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — verifies server identity before your agent calls anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool filtering&lt;/strong&gt; — loads only schemas relevant to the current task, not all of them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logging&lt;/strong&gt; — records every tool call for compliance and debugging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — stops runaway tool calls from blowing your budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As of April 2026, 86–89% of AI agent pilots fail before reaching production. Governance gaps and audit visibility are the two most common reasons. A gateway is what closes both.&lt;/p&gt;




&lt;h2&gt;
  
  
  So do we actually need MCP?
&lt;/h2&gt;

&lt;p&gt;Yes. With caveats that matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use MCP when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple AI systems need to share the same tools&lt;/li&gt;
&lt;li&gt;You're a SaaS company giving AI agents access to your product&lt;/li&gt;
&lt;li&gt;You need dynamic tool discovery across a large integration ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip MCP when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building a focused agent with two or three tools you already own&lt;/li&gt;
&lt;li&gt;Your tools have clean REST APIs you control&lt;/li&gt;
&lt;li&gt;You need low latency and minimal infrastructure overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "MCP for everything" era is over. It's the right call when standardization pays off at scale. When you just need your agent to hit an API you already control, MCP is overhead pretending to be infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cheat sheet: what to actually do
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;What makes sense&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local dev, one or two tools, just exploring&lt;/td&gt;
&lt;td&gt;Bare MCP or native tool calls. Don't over-engineer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent using tools you own, clean REST APIs&lt;/td&gt;
&lt;td&gt;Direct API calls or native tool use. Skip MCP overhead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production agent, 5+ tools, or external users&lt;/td&gt;
&lt;td&gt;MCP with a gateway. Authentication is not optional.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise, compliance, or regulated industry&lt;/td&gt;
&lt;td&gt;MCP gateway with audit logs and SSO. Non-negotiable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pulling from community MCP registries&lt;/td&gt;
&lt;td&gt;Treat every server as untrusted. Verify before deploying.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need to share tools across multiple AI systems&lt;/td&gt;
&lt;td&gt;MCP is the right call. This is exactly what it's for.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The actual state of things
&lt;/h2&gt;

&lt;p&gt;MCP isn't going away. The downloads are real. The Linux Foundation governance is serious. Multi-vendor adoption means the protocol has institutional staying power.&lt;/p&gt;

&lt;p&gt;But the MCP of early tutorials — install a community server, plug it in, done — that version is dead.&lt;/p&gt;

&lt;p&gt;It was never safe for production. It was never meant to be.&lt;/p&gt;

&lt;p&gt;The engineers moving to UTCP or direct API calls aren't abandoning MCP because it failed. They're routing around the parts that weren't built for what they're building.&lt;/p&gt;

&lt;p&gt;I keep coming back to the OX Security test. Nine out of eleven registries. No automated review. The agent called the fake server, ran its queries, and handed over credentials it didn't know it was handing over.&lt;/p&gt;

&lt;p&gt;Your agent does what it's told by the tools it trusts.&lt;/p&gt;

&lt;p&gt;MCP hasn't fully answered how it decides what to trust. Until it does, treat every community MCP server the way you'd treat a random npm package in 2015.&lt;/p&gt;

&lt;p&gt;You know how that era ended.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Built a Stateful Research Agent Inside a Sandbox. Here's What the Numbers Actually Looked Like.</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Wed, 27 May 2026 04:37:05 +0000</pubDate>
      <link>https://dev.to/divy_ai/i-built-a-stateful-research-agent-inside-a-sandbox-heres-what-the-numbers-actually-looked-like-29ac</link>
      <guid>https://dev.to/divy_ai/i-built-a-stateful-research-agent-inside-a-sandbox-heres-what-the-numbers-actually-looked-like-29ac</guid>
      <description>

&lt;p&gt;Three steps into a multi-page research task, the agent lost everything.&lt;/p&gt;

&lt;p&gt;Not a crash. Not a thrown exception. &lt;/p&gt;

&lt;p&gt;The function returned, context reset, and the pricing data it had just collected vanished. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This failure is predictable:&lt;/strong&gt; stateless execution environments were never built to hold state across browser sessions that run for twenty minutes. &lt;/p&gt;

&lt;p&gt;You hit it eventually, usually at the worst moment.&lt;/p&gt;

&lt;p&gt;The two standard workarounds are both annoying. Stuffing state into the prompt works until token costs starts becoming an issue. An external state store solves the problem but now you are maintaining another service.&lt;/p&gt;

&lt;p&gt;I had been using E2B for short-lived code execution. It handles that well, and they have added persistence features over time, including early-stage snapshot support. But for agents that need to pause mid-task and resume from a different process, state management is still mostly on you.&lt;/p&gt;

&lt;p&gt;Someone in my Discord mentioned TensorLake. I opened the docs and decided to build against this specific problem.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the steps using which you can build a desktop using an agent in a sandbox.&lt;/p&gt;

&lt;p&gt;Let's start with setting up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visual Explanation First
&lt;/h2&gt;

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




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

&lt;p&gt;What caught my attention first: named sandboxes with &lt;code&gt;suspend()&lt;/code&gt; and &lt;code&gt;resume()&lt;/code&gt; that preserve the full VM state, not just files, but running processes and open browser sessions. Sub-second resume, according to their docs.&lt;/p&gt;

&lt;p&gt;Ten minutes from zero to running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tensorlake
tl login   &lt;span class="c"&gt;# or TENSORLAKE_API_KEY env var&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier, no credit card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;secret_names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake/ubuntu-vnc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tensorlake/ubuntu-vnc&lt;/code&gt; image is what gives you a real desktop and Firefox inside the VM. You need an actual browser because modern pricing pages heavily use client-side rendering and bot detection that stops headless scrapers cold. Firefox inside a sandbox just looks like a person browsing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Playwright is not pre-installed in &lt;code&gt;ubuntu-vnc&lt;/code&gt;. Install it before the agent runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playwright&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playwright&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chromium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two to three minutes on first setup. After that, packages persist across suspend/resume so you pay the cost once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Latency: What I Actually Measured
&lt;/h2&gt;

&lt;p&gt;First sandbox was running in roughly 800-900ms from the &lt;code&gt;Sandbox.create()&lt;/code&gt; call to status &lt;code&gt;running&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is where time actually goes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sandbox creation:        ~800ms          (named sandbox, first time)
Sandbox resume:          ~400ms          (from suspended state)
LLM call (GPT-4o):       2,000-4,000ms   (per step, dominates everything)
Browser screenshot:      ~300ms          (capture + transfer)
Page load in sandbox:    1,000-2,000ms   (varies by site)
File read/write:         &amp;lt;50ms           (block-based storage)
Sandbox suspend:         ~200ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM calls dominate by a large margin. Sandbox overhead is not the bottleneck. The main optimization is batching browser operations before each model call rather than interleaving individual round trips.&lt;/p&gt;

&lt;p&gt;Tensorlake publishes a SQLite filesystem benchmark claiming 1.6-1.9x faster I/O than E2B and Modal. Self-reported numbers. I could not independently verify them. What I can say is that the block-based storage felt responsive for frequent small writes, which is exactly the pattern a research agent uses when checkpointing after every step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Computer Use: What Worked and What Didn't
&lt;/h2&gt;

&lt;p&gt;The desktop API itself is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_desktop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tensorlake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;png_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move_mouse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinecone.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Screenshot as PNG bytes, decode it, figure out where to click, send coordinates. Each browser interaction takes 1-3 seconds depending on page load. Slow compared to an API call. But it works on pages that block scrapers, because from the server's side it is just a person using Firefox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; coordinates assume a fixed layout, and layouts do not stay fixed.&lt;/p&gt;

&lt;p&gt;Weaviate's pricing page ran an A/B test between two of my agent's steps. The toggle moved 30px down. The agent clicked empty space. No error, no exception. Just a screenshot showing nothing happened, and twenty minutes of debugging before I identified the offset.&lt;/p&gt;

&lt;p&gt;The fix: pass screenshots to GPT-4o Vision to identify element positions dynamically rather than hardcoding coordinates. Adds about 2 seconds per interaction, handles layout drift reliably. Worth it for reliability; too slow for high-frequency operations.&lt;/p&gt;

&lt;p&gt;When the DOM is accessible, Playwright inside the sandbox is the better path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
import asyncio
from playwright.async_api import async_playwright

async def get_pricing():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://pinecone.io/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
        pricing_text = await page.inner_text(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pricing-section&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
        print(pricing_text)
        await browser.close()

asyncio.run(get_pricing())
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hybrid strategy I landed on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Situation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Site with bot detection&lt;/td&gt;
&lt;td&gt;Vision + coordinates&lt;/td&gt;
&lt;td&gt;Playwright gets blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessible DOM&lt;/td&gt;
&lt;td&gt;Playwright directly&lt;/td&gt;
&lt;td&gt;Faster, no coordinate drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown or variable layout&lt;/td&gt;
&lt;td&gt;Screenshot + GPT-4o Vision&lt;/td&gt;
&lt;td&gt;Resolves position dynamically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-frequency operations&lt;/td&gt;
&lt;td&gt;Playwright only&lt;/td&gt;
&lt;td&gt;Vision adds ~2s per call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use vision as a fallback, not a first tool. Vision handles layout variation. Playwright handles speed. Neither does both well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Statefulness: The Part That Actually Mattered
&lt;/h2&gt;

&lt;p&gt;After three steps (Pinecone free tier limits noted, $70/mo Starter plan recorded, Weaviate docs started), I called &lt;code&gt;sandbox.suspend()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The sandbox froze. Filesystem, memory, running browser: all paused. Twelve minutes later, from a different terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;About 400ms. The Weaviate pricing tab was still open. Tensorlake's suspend/resume preserves the full VM state, including memory and running processes. &lt;/p&gt;

&lt;p&gt;Everything written to &lt;code&gt;/workspace/research_notes.json&lt;/code&gt; was intact.&lt;/p&gt;

&lt;p&gt;The workflow I settled on: write state explicitly after each meaningful step, then suspend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After each step, before suspending:
&lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinecone_pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pinecone_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weaviate_started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://weaviate.io/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suspend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# On next invocation, from any process:
&lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="c1"&gt;# picks up from state["next_url"]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state file is the continuity mechanism. Not elegant, but it removes the need for an external database and the filesystem is fast, durable across suspend, and readable from any reconnecting process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling and Failure Handling
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Sandbox.create()&lt;/code&gt; is a blocking synchronous call. For parallel workloads, wrap in &lt;code&gt;concurrent.futures&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;research_competitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;research-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;secret_names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu-vnc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... agent logic ...
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="n"&gt;competitors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinecone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinecone.io/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weaviate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weaviate.io/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant.tech/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;research_competitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;competitors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three concurrent sandboxes ran without delay. I have not tested at twenty or fifty. Their docs mention hundreds per second. Take that at face value until you have load data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Tensorlake's Python SDK v0.5.8 introduced native async APIs that offer a cleaner alternative to threading for I/O-bound orchestration. If you are on v0.5.8 or later, those are worth reaching for before wrapping synchronous calls in a thread pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns worth building from day one:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent state writes.&lt;/strong&gt; Write state after each meaningful step. If the agent fails mid-run, the next invocation reads the file and skips completed work. This does not happen automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint before risky operations.&lt;/strong&gt; &lt;code&gt;sandbox.checkpoint()&lt;/code&gt; creates a restorable snapshot. By default, snapshots preserve the filesystem state. Preserving full memory state is supported as an explicit option. Either way, you can restore into a fresh sandbox if an operation goes wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Filesystem snapshot (default)
&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate_to_pricing_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Restore filesystem state into a new sandbox
&lt;/span&gt;    &lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Named sandboxes.&lt;/strong&gt; If the orchestration process dies, any other process reconnects with &lt;code&gt;Sandbox.connect("sandbox-name")&lt;/code&gt; and resumes from the last written state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architectural boundary:&lt;/strong&gt; Tensorlake provides the execution environment and runtime for agents: the VM, the filesystem, the process lifecycle, the networking. It is not an agent framework. Retry logic, circuit breakers, and LLM rate-limit backoff belong in the orchestration layer above it: LangChain, LlamaIndex, a custom harness, or whatever you are using to drive the agent. That separation is deliberate, not a gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

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

&lt;p&gt;The part that shifted how I thought about the design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│                 Your Agent                   │
│    (LLM + tool calling logic)                │
└──────────────────┬──────────────────────────┘
                   │ tool calls
┌──────────────────▼──────────────────────────┐
│           Tensorlake Sandbox                 │
│  ┌──────────────────────────────────────┐   │
│  │ State Layer: /workspace filesystem   │   │
│  │  state.json, research_notes.json     │   │
│  └──────────────────────────────────────┘   │
│  ┌──────────────────────────────────────┐   │
│  │ Execution Layer: processes, scripts  │   │
│  └──────────────────────────────────────┘   │
│  ┌──────────────────────────────────────┐   │
│  │ Computer Use: VNC, screenshots, mouse│   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sandbox is not the agent. It is the stable environment the agent operates in. When it resumes, the environment is exactly where the agent left it. The agent's logic lives outside and reconnects to a world that did not reset.&lt;/p&gt;

&lt;p&gt;That changes what you can build. An agent that runs for an hour, navigates fifteen pages, and writes a structured report is feasible when the execution environment outlasts the orchestration session. With purely ephemeral execution, it is not.&lt;/p&gt;




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

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

&lt;p&gt;&lt;strong&gt;vs E2B:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both use Firecracker microVMs. E2B markets sub-200ms cold starts; community reports put real-world p50 closer to 400-600ms. Tensorlake named sandbox creation was ~800ms in my testing.&lt;/li&gt;
&lt;li&gt;E2B has added snapshot and pause-resume in recent releases. The statefulness gap is narrower than a year ago. Tensorlake's suspend/resume preserves the full running VM state, including open processes, browser sessions, all in under a second. E2B's memory snapshot support is still described as early-stage.&lt;/li&gt;
&lt;li&gt;Tensorlake claims 1.6-1.9x faster filesystem I/O on their own benchmarks. Self-reported. For an independent reference: Tensorlake recently ranked top 2 across all three categories in the ComputeSDK sandbox benchmarks.&lt;/li&gt;
&lt;li&gt;Neither provides DOM-level element selection at the SDK layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;vs Modal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modal uses gVisor rather than Firecracker, designed around stateless function execution. Stateful long-running agents work but need more setup. Cold starts are around 1-1.5 seconds per their docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;vs Stagehand (BrowserBase):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stagehand has DOM-level selectors (CSS, XPath, natural language) via &lt;code&gt;locator()&lt;/code&gt;. For pure browser automation, this is a real ergonomic advantage.&lt;/li&gt;
&lt;li&gt;Tensorlake gives you a full VM. Code execution, file management, package installs, and browser use in the same environment. If that combination is what you need, the full VM model is worth the coordinate complexity.&lt;/li&gt;
&lt;li&gt;Browser automation only? Stagehand is the more focused tool.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SandboxClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SandboxClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sandbox_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the Build Produced
&lt;/h2&gt;

&lt;p&gt;By the end of the session, the agent had produced the comparison: Pinecone versus Weaviate pricing, extracted across seven pages, with notes preserved across two suspensions and a full restart of the orchestrating machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;report_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/workspace/comparison_report.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accurate. Correct tier names and numbers.&lt;/p&gt;

&lt;p&gt;Tensorlake did not solve the hard parts: the retrieval logic, state schema, hybrid browser strategy. It stayed out of the way while those got built. Most of the infrastructure friction came down to state management, and most of that went away once the sandbox filesystem became the state store.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things to Know Before You Start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Speed is a systems problem, not a sandbox problem.&lt;/strong&gt; LLM calls account for the bulk of per-step latency. Optimize by batching browser operations before each model call, not by chasing sandbox startup time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for interruption from day one.&lt;/strong&gt; Write state after every meaningful step. Not because the sandbox will crash, but because resuming from a different process after an unexpected interruption is a real scenario, not an edge case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Computer use is a primitive.&lt;/strong&gt; The coordinate-based API works, but layout drift will break hardcoded positions. Use Playwright when the DOM is accessible. Fall back to vision when you need a real browser session. Do not automate full workflows with raw coordinates.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Is the sandbox infrastructure production-ready?&lt;/strong&gt; Yes. Suspend/resume held up, filesystem persistence was consistent, and Firecracker isolation did what it was supposed to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the computer use layer production-ready?&lt;/strong&gt; Not without additional engineering. The raw coordinate API is a reasonable primitive, but element resolution needs to be built on top of it. A vision-backed &lt;code&gt;click_element()&lt;/code&gt; in the SDK would change the story significantly. Until then, budget the time to build that layer yourself.&lt;/p&gt;

&lt;p&gt;Worth using? Yes, if you go in with clear expectations about what the platform handles and what it leaves to you. That boundary is sharper than most, which makes it easier to work with once you have internalized it.&lt;/p&gt;




&lt;p&gt;You can also check the complete project on my github here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dvy246/Sandboxes.git" rel="noopener noreferrer"&gt;click_here&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Tensorlake.&lt;/strong&gt; &lt;em&gt;Tensorlake Documentation &amp;amp; Sandbox SDK.&lt;/em&gt; &lt;a href="https://tensorlake.ai" rel="noopener noreferrer"&gt;https://tensorlake.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E2B.&lt;/strong&gt; &lt;em&gt;E2B Sandbox Infrastructure.&lt;/em&gt; &lt;a href="https://e2b.dev" rel="noopener noreferrer"&gt;https://e2b.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modal.&lt;/strong&gt; &lt;em&gt;Modal Serverless Infrastructure.&lt;/em&gt; &lt;a href="https://modal.com" rel="noopener noreferrer"&gt;https://modal.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stagehand (BrowserBase).&lt;/strong&gt; &lt;em&gt;Stagehand Browser Automation.&lt;/em&gt; &lt;a href="https://browserbase.com/stagehand" rel="noopener noreferrer"&gt;https://browserbase.com/stagehand&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Web Services.&lt;/strong&gt; &lt;em&gt;Firecracker MicroVMs.&lt;/em&gt; &lt;a href="https://firecracker-microvm.github.io/" rel="noopener noreferrer"&gt;https://firecracker-microvm.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft.&lt;/strong&gt; &lt;em&gt;Playwright Browser Automation.&lt;/em&gt; &lt;a href="https://playwright.dev" rel="noopener noreferrer"&gt;https://playwright.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark:&lt;/strong&gt; &lt;a href="https://www.computesdk.com/benchmarks/sandboxes/" rel="noopener noreferrer"&gt;https://www.computesdk.com/benchmarks/sandboxes/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>LLMs, RAG, Agents, MCP: The AI Evolution You Actually Need to Understand</title>
      <dc:creator>Divy Yadav</dc:creator>
      <pubDate>Wed, 20 May 2026 11:03:00 +0000</pubDate>
      <link>https://dev.to/divy_ai/llms-rag-agents-mcp-the-ai-evolution-you-actually-need-to-understand-47jm</link>
      <guid>https://dev.to/divy_ai/llms-rag-agents-mcp-the-ai-evolution-you-actually-need-to-understand-47jm</guid>
      <description>&lt;p&gt;Most people still think AI is just a chatbot.&lt;/p&gt;

&lt;p&gt;That idea is already outdated.&lt;/p&gt;

&lt;p&gt;Modern AI systems browse the web, remember your preferences, execute code, query databases, call APIs, and coordinate workflows. They operate more like software employees than like a search bar.&lt;/p&gt;

&lt;p&gt;This did not happen because models got smarter. It happened because the &lt;strong&gt;architecture&lt;/strong&gt; changed.&lt;/p&gt;

&lt;p&gt;Every layer of the modern AI stack exists because the previous layer had a real failure. Understanding what failed and why something new was built is the fastest way to understand how any serious AI product works today.&lt;/p&gt;

&lt;p&gt;That is what this article covers. Every stage: LLMs, RAG, Agents, and MCP.&lt;/p&gt;




&lt;p&gt;In this article, you will get a good idea about the full AI Evolution, and it took me a lot of research and work for this one.&lt;/p&gt;

&lt;p&gt;So if you want more such information about AI, consider subscribing to my newsletter, where you will get noise-free information every week&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link for the newsletter:&lt;a href="https://aiengsimplified.beehiiv.com/" rel="noopener noreferrer"&gt; Newsletter&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




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

&lt;h2&gt;
  
  
  Stage 1: The LLM Era
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What LLMs Actually Are
&lt;/h3&gt;

&lt;p&gt;An LLM is a prediction engine.&lt;/p&gt;

&lt;p&gt;Not a reasoning engine. Not a database. Not a search system.&lt;/p&gt;

&lt;p&gt;Given text, it predicts what comes next. That prediction runs over and over, token by token, until a full response is generated. The model learns these predictions from enormous amounts of human text: books, articles, code, research papers, websites.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input:  "The capital of France is"
Model:  [predicts next token]
Output: "Paris"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple idea. The scale is what makes it work.&lt;/p&gt;

&lt;p&gt;A token is roughly 3 to 4 characters of English text. "Hello, world!" is about 4 tokens. Everything the model processes and generates is counted in tokens. This affects cost, speed, and the limits we will cover shortly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Felt Like a Big Deal
&lt;/h3&gt;

&lt;p&gt;For the first time, a machine could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hold a fluent conversation in any language&lt;/li&gt;
&lt;li&gt;Write code that actually ran&lt;/li&gt;
&lt;li&gt;Summarize a 50-page document in seconds&lt;/li&gt;
&lt;li&gt;Explain complex topics to a non-expert&lt;/li&gt;
&lt;li&gt;Answer questions across almost any domain&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where It Broke Down
&lt;/h3&gt;

&lt;p&gt;Then people started building real products. The limitations became obvious fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallucination:&lt;/strong&gt; The model predicts what is plausible, not what is true. It will state wrong facts with total confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowledge cutoff:&lt;/strong&gt; Training data has a date. Ask about last week and it guesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No memory:&lt;/strong&gt; Every conversation starts blank. The model has no idea what you talked about yesterday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No access to your data:&lt;/strong&gt; Your company documents, your database, your internal systems. The model knows none of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No ability to act:&lt;/strong&gt; It produces text. It cannot send an email, run a query, or update a record.&lt;/p&gt;

&lt;p&gt;Ask a pure LLM: "What was Apple's stock price yesterday?" It will either refuse or make up a number.&lt;/p&gt;

&lt;p&gt;It has no connection to live systems. It is a very smart autocomplete engine. Autocomplete alone does not run a business.&lt;/p&gt;

&lt;p&gt;This limitation is what created the next stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: RAG Changes the Game
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Core Idea
&lt;/h3&gt;

&lt;p&gt;RAG stands for Retrieval-Augmented Generation. One sentence covers it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before generating a response, retrieve the relevant information and give it to the model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of relying only on training data, the system fetches fresh, relevant context at the moment of each query.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Simple Way to Think About It
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pure LLM:&lt;/strong&gt; A student answering an exam entirely from memory. Sometimes brilliant. Sometimes confidently wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG:&lt;/strong&gt; The same student, but allowed to open their notes before answering. Answers are grounded in actual sources.&lt;/p&gt;

&lt;p&gt;The model did not get smarter. It got better information to work with.&lt;/p&gt;

&lt;h3&gt;
  
  
  How RAG Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER QUERY
    ↓
RETRIEVE relevant documents
(from a vector database, using semantic search)
    ↓
INJECT those documents into the prompt as context
    ↓
LLM generates an answer grounded in the retrieved content
    ↓
RESPONSE (accurate, with sources)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Technology Behind Retrieval
&lt;/h3&gt;

&lt;p&gt;Embeddings are what make semantic search work.&lt;/p&gt;

&lt;p&gt;Documents are converted into vectors, which are lists of numbers that represent meaning mathematically. Similar meanings end up close together in vector space. "Car" and "automobile" are close. "Car" and "photosynthesis" are not.&lt;/p&gt;

&lt;p&gt;When a user query arrives, it is also converted to a vector. The system finds the stored vectors nearest to that query vector. Those are the semantically relevant documents, retrieved and injected into context.&lt;/p&gt;

&lt;p&gt;Common vector databases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pinecone&lt;/td&gt;
&lt;td&gt;Managed, production-ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weaviate&lt;/td&gt;
&lt;td&gt;Open-source, rich query support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chroma&lt;/td&gt;
&lt;td&gt;Development and small-scale use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FAISS&lt;/td&gt;
&lt;td&gt;Fast, local, no managed infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What RAG Unlocked
&lt;/h3&gt;

&lt;p&gt;RAG became the foundation of a lot of serious AI products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enterprise knowledge assistants&lt;/li&gt;
&lt;li&gt;Customer support bots grounded in actual policy&lt;/li&gt;
&lt;li&gt;PDF and document Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;Internal search that surfaces the right document&lt;/li&gt;
&lt;li&gt;Any system needing up-to-date or private data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What RAG Still Could Not Do
&lt;/h3&gt;

&lt;p&gt;Retrieval solves the knowledge problem. It does not solve the action problem.&lt;/p&gt;

&lt;p&gt;RAG can find the answer to "what is our refund policy?" It cannot process the refund. It can tell you flight options. It cannot book the ticket.&lt;/p&gt;

&lt;p&gt;For that, a different capability was needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3: The Rise of AI Agents
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Core Shift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Traditional AI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User asks → Model answers → Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User sets a goal → Agent plans → Agent uses tools →
Agent observes results → Agent decides next step →
Agent continues until goal is complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents reason, plan, use tools, and execute multi-step workflows. They operate rather than just respond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool Calling: How Agents Reach the Real World
&lt;/h3&gt;

&lt;p&gt;An LLM by itself cannot search Google, call an API, write to a database, or run code. Tool calling extends the model's reach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Find the cheapest flights from Delhi to Singapore next month."

Agent Step 1: Call flight search API with parameters
Agent Step 2: Receive results
Agent Step 3: Sort and compare options
Agent Step 4: Summarize the three cheapest options
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model decides which tool to call, with what arguments, and what to do with the result. It manages the whole workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Agents Can Do
&lt;/h3&gt;

&lt;p&gt;A capable AI agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse websites and extract information&lt;/li&gt;
&lt;li&gt;Write, execute, and debug code&lt;/li&gt;
&lt;li&gt;Send emails and messages&lt;/li&gt;
&lt;li&gt;Query and update databases&lt;/li&gt;
&lt;li&gt;Call any API with proper credentials&lt;/li&gt;
&lt;li&gt;Coordinate with other agents&lt;/li&gt;
&lt;li&gt;Schedule and manage workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frameworks That Made This Practical
&lt;/h3&gt;

&lt;p&gt;Building agents from scratch is tedious. Frameworks handle the boilerplate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangChain / LangGraph&lt;/strong&gt; - most widely used, graph-based orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AutoGen&lt;/strong&gt; - multi-agent conversations, good for collaborative tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrewAI&lt;/strong&gt; - role-based agent crews for structured workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Agents SDK&lt;/strong&gt; - native tool calling with built-in orchestration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where Agents Break
&lt;/h3&gt;

&lt;p&gt;More power introduced more failure modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context overflow:&lt;/strong&gt; Long agent runs fill the context window. Earlier instructions get lost. Accuracy drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory fragmentation:&lt;/strong&gt; Without a coherent memory system, agents lose track of what they were doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool confusion:&lt;/strong&gt; Too many tools and the model picks the wrong one or misuses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallucinated actions:&lt;/strong&gt; The model invents results from tool calls it never actually made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runaway loops:&lt;/strong&gt; No stop condition means the agent keeps going when it should have asked for clarification.&lt;/p&gt;

&lt;p&gt;There was also a deeper infrastructure problem. Every agent integration was custom-built. Connecting to Slack required one connector. Google Drive required another. Salesforce required another. There was no standard. Scaling meant a growing stack of hand-built code.&lt;/p&gt;

&lt;p&gt;That is what MCP was built to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: MCP, The Protocol That Standardizes Everything
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem Before MCP
&lt;/h3&gt;

&lt;p&gt;Before November 2024, connecting an AI system to external tools meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom integration for every tool&lt;/li&gt;
&lt;li&gt;Different formats for every API&lt;/li&gt;
&lt;li&gt;No standard for how models discover what tools are available&lt;/li&gt;
&lt;li&gt;No consistent way to pass context or results between systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every new data source required its own implementation. This was not an AI limitation. It was an infrastructure limitation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What MCP Is
&lt;/h3&gt;

&lt;p&gt;MCP stands for Model Context Protocol. It is a standard for connecting AI assistants to the systems where data lives, including content repositories, business tools, and development environments.&lt;/p&gt;

&lt;p&gt;Anthropic announced MCP in November 2024 and open-sourced it on day one.&lt;/p&gt;

&lt;p&gt;MCP defines a universal interface for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading files and data sources&lt;/li&gt;
&lt;li&gt;Executing functions and tools&lt;/li&gt;
&lt;li&gt;Handling context and prompts&lt;/li&gt;
&lt;li&gt;Coordinating between AI systems and external environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The USB-C analogy is actually a good one here. Just as USB-C made it easy to connect any device to any peripheral, MCP makes it easier to connect any AI model to any data source or tool. One protocol, many integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works Architecturally
&lt;/h3&gt;

&lt;p&gt;MCP servers expose three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; actions the model can call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources:&lt;/strong&gt; data the model can read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts:&lt;/strong&gt; templates for interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model queries the server to discover what is available, then invokes tools in a structured, validated format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adoption
&lt;/h3&gt;

&lt;p&gt;MCP did not take years to catch on. Since launch, it has been adopted by OpenAI, Microsoft, Google, and Cloudflare. The Python and JavaScript SDKs together see over 20 million weekly downloads. Over 13,000 MCP servers launched on GitHub in 2025 alone.&lt;/p&gt;

&lt;p&gt;In December 2025, Anthropic donated MCP to the Agentic AI Foundation under the Linux Foundation, co-founded by Anthropic, Block, and OpenAI. It now sits alongside Kubernetes and PyTorch in that portfolio.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Honest Limitations
&lt;/h3&gt;

&lt;p&gt;MCP is not perfect. Security is a real concern.&lt;/p&gt;

&lt;p&gt;Security researchers identified multiple issues with the protocol, including prompt injection, tool permissions that allow data exfiltration, and lookalike tools that can silently replace trusted ones.&lt;/p&gt;

&lt;p&gt;The spec does not enforce audit trails, sandboxing, or verification. MCP solves the connectivity problem. Organizations deploying it at scale are responsible for building the security layer on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context Engineering: The Layer Tying It All Together
&lt;/h2&gt;

&lt;p&gt;Context engineering is what makes everything above work reliably in production.&lt;/p&gt;

&lt;p&gt;Prompt engineering is writing a good instruction. Context engineering is designing the entire information environment the model operates in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; what the model remembers from previous interactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval:&lt;/strong&gt; what documents or data are fetched for each query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; what actions are available and how they are described&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History:&lt;/strong&gt; how much of the conversation is included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System state:&lt;/strong&gt; what the model knows about its current task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow position:&lt;/strong&gt; where in a multi-step process the model is&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most capable AI systems today are not just better models. They are better systems designed around the model.&lt;/p&gt;

&lt;p&gt;Context engineering is what separates an agent that works in production from one that works in a demo.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Modern AI Stack Looks Like
&lt;/h2&gt;

&lt;p&gt;A serious AI product in 2026 is a system, not just an API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Interface
    ↓
Orchestration Layer (LangGraph, AutoGen, custom)
    ↓
Context Manager
├── Memory Layer (conversation history, user preferences)
├── Retrieval Layer (vector DB, semantic search)
└── State Manager (task progress, tool outputs)
    ↓
Tool Layer (via MCP or custom integrations)
├── Web search
├── Database queries
├── API calls
├── Code execution
└── File operations
    ↓
LLM (GPT-4o, Claude, Gemini, open-source)
    ↓
Response + Actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer solves a specific failure from the layer before it. Remove any layer and you reintroduce the problem it was built to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which Layer Do You Actually Need?
&lt;/h2&gt;

&lt;p&gt;Do not over-engineer this.&lt;/p&gt;

&lt;p&gt;A simple RAG pipeline is the right call for most document Q&amp;amp;A use cases. A complex agent adds coordination overhead you do not need if the task is just retrieval.&lt;/p&gt;

&lt;p&gt;Add a layer only when the simpler system actually cannot meet the requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;A few trends worth watching:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-term memory:&lt;/strong&gt; Agents that remember your preferences across months, not just sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent collaboration:&lt;/strong&gt; Networks of specialized agents coordinating on shared goals, where each handles one domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deeper real-world execution:&lt;/strong&gt; Tighter integration with operating systems and software, not just APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autonomous workflows:&lt;/strong&gt; Agents that manage their own task queues without step-by-step human orchestration.&lt;/p&gt;

&lt;p&gt;The bottleneck has moved. In 2020, it was model intelligence. In 2026, it is system design: how well you manage memory, retrieval, tool coordination, and context across a complex workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;The biggest mistake people make is thinking the model is the entire product.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;Modern AI systems are architectures: memory, retrieval, orchestration, tool ecosystems, context managers, and execution environments wrapped around a model.&lt;/p&gt;

&lt;p&gt;The future will not be decided only by which model is best.&lt;/p&gt;

&lt;p&gt;It will be decided by which system is built best around it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, I write about AI engineering weekly in my newsletter &lt;a href="https://aiengsimplified.beehiiv.com" rel="noopener noreferrer"&gt;AI Engineering Simplified&lt;/a&gt;. No hype, just practical breakdowns.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
