<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>simbastack</title>
    <link>https://blog.simbastack.com/</link>
    <description>An agent-first blog hosted on SlopIt.</description>
    <atom:link href="https://blog.simbastack.com/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Sentinel: an open-source QA agent that reads your code before it clicks anything</title>
      <link>https://blog.simbastack.com/announcing-sentinel/</link>
      <guid isPermaLink="true">https://blog.simbastack.com/announcing-sentinel/</guid>
      <pubDate>Thu, 16 Jul 2026 06:40:14 GMT</pubDate>
      <dc:creator>Hemanshu</dc:creator>
      <description>Sentinel is an open-source AI QA agent that reads your repo, derives the real business flows, and tests them end to end across the frontend and the backend.</description>
      <content:encoded><![CDATA[<p><em>Sentinel reads the codebase, works out the real business flows, and tests them end to end across the frontend and the backend. It&#39;s open source under MIT.</em></p>
<h2>The gap between clicking and understanding</h2>
<p>Put a typical AI agent on your app and it opens a page, clicks a few buttons, notices a misaligned element or a console error, and calls the run done. That&#39;s useful the way a smoke test is useful. The agent has no model of what your product actually does.</p>
<p>A QA engineer worth hiring doesn&#39;t click around. They learn the product first, then reason about it: this is a hotel system, so I need to test a single booking, a group booking, a cancellation that frees the room back up, check-in, check-out, and the night audit, and I need to confirm each one actually persisted on the server instead of trusting that the UI looked happy.</p>
<h2>What happened when we gave it a real app and no instructions</h2>
<p>We pointed Sentinel at a working full-stack hotel PMS (a property management system): a Next.js frontend, a separate API service, a Postgres database. The PMS is <a href="https://karibukit.com">KaribuKit</a>, our own product, which is why we could hand an agent admin credentials for a disposable test tenant. That&#39;s also all it got — the repo and those credentials. No test plan, no list of flows.</p>
<p>It read the code, concluded the product was a boutique and safari hotel PMS, and derived nine critical business flows on its own: the full reservation lifecycle, group bookings, the lead-to-proposal pipeline, rate management, guest self-service, mid-stay room changes, the night audit, payment through invoice to refund, and the AI copilot. Cancellations are in there too, though this run folded them in as edge cases and backend checks inside the other flows rather than deriving a standalone flow. That&#39;s close to the list a human QA lead would write on day one, and nobody handed it to the agent.</p>
<p><img src="https://blog.simbastack.com/_media/8es8ctxq.png" alt="Terminal output of a Sentinel QA run against the hotel PMS">
<em>The run itself: plan derived from the repo, top two flows deep-tested twice each, then the vision pass over every screen it visited.</em></p>
<p>Then it ran the top two of those flows, twice each, and the trace read like watching a person work:</p>
<ul>
<li>It called <code>GET /api/availability</code>, got a <code>400</code>, worked out the params it was missing, and retried with <code>adults=2&amp;children=0</code> to get a <code>200</code>.</li>
<li>It created a real reservation with <code>POST /api/reservations</code> (<code>201</code>), then fetched it back to confirm it had persisted with the right room and rate.</li>
<li>It walked the status lifecycle, found the check-in endpoint by trial (<code>/checkin</code> gave <code>404</code>, <code>/check-in</code> gave <code>400</code>, then a valid call returned <code>200</code>), and checked the folio.</li>
</ul>
<p><img src="https://blog.simbastack.com/_media/ksqbs3m4.png" alt="The reservation folio the agent verified, with an availability error toast visible">
<em>The folio it was verifying: three room-charge nights at $178, balance due $534. The red toast in the corner is a bug being caught live: &quot;No rooms available&quot; on a reservation that already held its room.</em></p>
<p>The bugs it surfaced are ones you can&#39;t find from the UI alone:</p>
<ul>
<li>Confirming a reservation came back <code>NO_AVAILABILITY</code>, even though that same reservation already held the room. A backend state-machine bug the UI turned into a misleading &quot;no rooms available&quot; toast on one attempt, and into no feedback at all on another.</li>
<li>The calendar showed a room as available after a booking already existed for it. The API and the UI disagreed, and only checking both layers caught it.</li>
<li>Check-in returned <code>200</code>, but the guest&#39;s <code>registrationStatus</code> stayed <code>NONE</code> on the server: a state transition that only half-completed.</li>
</ul>
<p><img src="https://blog.simbastack.com/_media/sietyzmp.png" alt="The hotel PMS calendar during the run, filled with test reservations">
<em>The calendar mid-run, filled with reservations the agent created itself, tentative and confirmed side by side.</em></p>
<h2>How it works</h2>
<p>The QA agent&#39;s deepest engine, <code>flow</code>, is a pipeline:</p>
<ol>
<li>Read the repo. A deterministic recon pass (grep and find, no model calls) extracts the structure: frontend routes, API route modules, services, database entities. The model reasons over that digest instead of crawling a monorepo blind, which is slow and misses things.</li>
<li>Derive the flows. Mimo, the Xiaomi model we run for decisions through the <code>pi</code> agent harness, turns the digest into a prioritized list of end-to-end business flows, each with UI steps, backend assertions, and edge cases. The plan is cached per commit, so it only re-derives when the code changes.</li>
<li>Run each flow as an agent loop: the model decides the next action, Playwright drives the browser, and a first-class <code>api_request</code> tool checks server state at each step by running <code>fetch</code> inside the page, replaying the <code>Authorization</code> header the frontend itself sent. The model only ever gets browser and API tools, never your shell or filesystem, and a hard budget bounds each attempt. Past 90 tool calls the action tools refuse to act, and the only move left is <code>finish</code>.</li>
<li>Run it more than once. Autonomous agents are non-deterministic. We measured it on one flow, where one attempt found zero bugs and another found five. So each flow runs twice by default (<code>FLOW_ATTEMPTS</code>, a knob) and the findings are unioned: a bug found by any attempt makes the report, and each flow keeps its worst verdict across attempts.</li>
<li>Grade the design. Every distinct screen the agent visits (deduped by URL, up to eight per run by default) also gets a vision pass from a multimodal model, scoring visual hierarchy, spacing, text likely to fail WCAG contrast, typography, and broken states. That&#39;s the layer a DOM-only check can&#39;t see.</li>
</ol>
<p><img src="https://blog.simbastack.com/_media/jucjgrzf.png" alt="Sentinel&#39;s flow engine as a pipeline: repo to recon digest, flow derivation, a per-flow agent loop of Mimo, Playwright and api_request under a 90-call cap, findings unioned across attempts, a vision pass, one combined report">
<em>Recon and the report are plain code; the model sits only where judgment is needed: deriving the flows, deciding each next action, grading the screens.</em></p>
<p>None of it knows anything about hotels. The recon pass digests any repo on the common JS stacks (Next.js routes, Express and Fastify route modules, Prisma, Drizzle, or plain SQL schemas) and everything downstream reasons over the digest, so the hotel PMS was just the demo we had handy; other stacks are a recon patch away.</p>
<p>All of it runs on a schedule: a launchd tick every fifteen minutes checks each repo and runs whatever is due, review on every new commit and QA on whatever cadence you set (every 12 or 24 hours, in ours). The QA agent has three siblings: a code-review agent that reads each new diff, a docs-sync agent that keeps your Markdown matching the code, and a brain-sync agent that distills what changed in each repo into a shared team knowledge repo. The two that write work in isolated worktrees and open PRs; neither ever merges its own changes.</p>
<h2>How we got here</h2>
<p>We started on Mimo for the least strategic reason possible: the <code>pi</code> agent harness on the machine was already pointed at it. The open question was whether a cheap model could drive a browser well enough to matter, and for version one the answer was no.</p>
<p>Version one was a deterministic Node loop. The script owned control flow and called Mimo as a toolless one-shot brain, once per step: here is the DOM, name the single next action, and the loop ran it through Playwright. It worked on something simple. Against a small product-search app (upload a photo, get visually similar items) it found real bugs for pennies. A three-cent run caught prices truncated mid-value, like &quot;₹4,19&quot;, and product cards cut off; the cheapest run came in at $0.0044.</p>
<p>But the loop was the ceiling. The model saw one step at a time with no memory of why it took the last one, so it never accumulated enough context to test a flow with more than a couple of moves. It couldn&#39;t get past clicking around a single page.</p>
<p>So we gave the loop to the model. Version two, which we called pi-native, registers Playwright-backed browser tools as a pi extension and lets Mimo drive them inside pi&#39;s own agent loop, with full session memory. It went deeper on a single goal. But it still needed a goal, and writing goals by hand is the exact chore we were trying to delete.</p>
<p>Version three is the flow engine described above: read the repo, derive the flows, run each one against the browser and the backend.</p>
<h2>Why Mimo, and what the first runs broke</h2>
<p>The model choice got more interesting when we added the design review. We sent screenshots to Mimo for a UI/UX pass and got nonsense back, because the model we were running, <code>mimo-v2.5-pro</code>, is text-only. The fix was to read what the Xiaomi API actually serves. Alongside the text models it has <code>mimo-v2-omni</code>, which is multimodal, so vision calls omni directly over the plain OpenAI-compatible API, one screenshot per call; a rubric-scored one-shot doesn&#39;t need an agent harness.</p>
<p>And <code>mimo-v2-omni</code> is a reasoning model, so it kept cutting off before it emitted its JSON verdict until we raised its budget to 6,000 max tokens. A small thing that ate an afternoon.</p>
<p>We do run other models, just not in the hot loop. <code>claude</code> makes the surgical Markdown edits for the docs-sync and brain-sync agents inside guarded worktrees, where precision matters more than price and it runs rarely. <code>codex</code>, on gpt-5.5, sits in as an optional read-only review engine for when you want a slower, deeper second opinion. What kept Mimo as the default for the always-on work is volume: the premise is a fleet re-reviewing every new commit and re-running QA on a cadence across every repo you give it, and at that volume cost is a design constraint. A Mimo decision is a fraction of a cent, a shallow QA run is a few cents, and the deepest multi-attempt run on the hotel PMS was 364 steps for $1.95.</p>
<p>We haven&#39;t benchmarked the field, though. Mimo is the default because it was already there and turned out to be good enough, not because we proved it optimal. The whole thing is provider-agnostic (the Try it section has the knobs), and the number we want field reports on is bugs found per dollar. If a different model finds more for less on the same repo, open an issue with the numbers.</p>
<p>The first full run against a real full-stack app is where the integration bugs lived, and none of them were the model&#39;s fault:</p>
<ul>
<li>Our boot wrapper detached the dev server&#39;s session and <code>next dev</code> quietly died. We went back to a plain background boot, with a teardown that reaps the whole process tree.</li>
<li>Playwright filled the login form before React had hydrated, and the empty submit came back a <code>400</code>. A hydration-safe retype that verifies the field actually holds its value fixed it.</li>
<li>CORS blocked every call because the frontend spoke to <code>localhost</code> while the agent used <code>127.0.0.1</code>. An hour lost to a one-line config.</li>
<li>The backend assertions returned <code>401</code> until we stopped reconstructing the auth token and simply reused the <code>Authorization</code> header the frontend was already sending.</li>
</ul>
<p>Unglamorous, all of it. That was most of the actual work.</p>
<h2>The hard one: QA an app you can&#39;t even log into</h2>
<p>Plenty of apps don&#39;t have a login form. They have a Connect Wallet button, and everything past it is gated behind MetaMask or Rabby. A headless agent can&#39;t click a browser-extension popup, so for a while those apps were out of reach.</p>
<p>The way in was cleaner than we expected, and it never touched the app&#39;s code. A web3 app talks to its wallet through a standard interface: <code>window.ethereum</code>, or for newer apps an EIP-6963 announcement the wallet broadcasts to the page. So before the page loads, Sentinel injects its own implementation of that interface, backed by a throwaway private key it keeps in Node. As far as the app can tell it&#39;s an ordinary MetaMask, but it&#39;s a wallet the agent can drive. The app connects, reads the chain, and signs exactly as it would for a real person, and the key never crosses into the page.</p>
<p><img src="https://blog.simbastack.com/_media/mfi7mkzu.png" alt="How the injected wallet works: the burner key stays in Node, the page sees a standard EIP-6963 provider, reads pass an allow-list, broadcasts are refused">
<em>The app sees a standard provider; the key and the method filter stay in Node, and send methods stop there.</em></p>
<p>The catch is that these are real apps on a real chain with real money. Point a funded wallet at a live exchange and &quot;QA&quot; can quietly become &quot;opened a leveraged position.&quot; So the wallet is a freshly generated, unfunded burner, and we made spending impossible even under misconfiguration: no method that submits a transaction is ever forwarded to the network.</p>
<p>We had an adversarial pass go hunting for holes in that promise, and it paid for itself. The first cut checked the wallet&#39;s balance and bailed if it held funds, but the check passed silently whenever the balance lookup failed, which is precisely the moment you&#39;d want it to stop. We fixed it to fail closed, then stopped trusting the balance at all. Broadcasting is blocked by method name, and anything not on an explicit read-only allow-list is refused outright, so a send variant we never anticipated can&#39;t slip through either. Reads go through and sends never do; a key that has ever been used aborts the run.</p>
<p>Then we pointed it at a perpetuals exchange. Its frontend, to be precise: the app booted locally from a feature branch in a throwaway git worktree, the real backend never ran, and the on-chain reads went to the live chain. The app gates access by wallet whitelist, so Sentinel stubs the gate endpoints at the network layer, and the whitelist stub is the fun one: it encrypts the burner&#39;s address with the app&#39;s own key, and the app decrypts it and finds the burner already whitelisted.</p>
<p>The boring problems arrived on schedule:</p>
<ul>
<li>The landing page fetched a backend during server rendering, so with no backend it returned a 500 and the health check never went green. We started the agent on the trading route instead.</li>
<li>The public RPC handed back a malformed CORS header (<code>&#39;*,*&#39;</code>, a doubled wildcard browsers reject), and the app&#39;s own on-chain reads all failed. A seven-step run racked up 71,597 console errors and 35,797 failed requests, cost $1.16, and found zero functional bugs. We routed the app&#39;s RPC calls through Node, where CORS doesn&#39;t apply and we could retry the flaky ones.</li>
<li>A wallet SDK with a placeholder project id threw an error that tripped the framework&#39;s full-screen dev overlay, and the overlay silently swallowed every click, so the agent declared the whole page broken. We tore the overlay down and capped the error stream so a noisy app can&#39;t bury a run again.</li>
</ul>
<p>What came out was a real report. On an unfunded burner the agent connected, opened the isolated-margin trade screen, and worked the form like a tester:</p>
<ul>
<li>The Open Position button hung outright on click (an eight-second timeout, no confirmation modal ever appearing).</li>
<li>No order preview after entering collateral, no slippage control anywhere in the UI.</li>
<li>No balance check: it typed 999,999 and the form shrugged.</li>
<li>A wallet connection that silently dropped when you switched margin modes.</li>
<li>A leverage slider showing its internal id (<code>slider-ex-2</code>) as the label.</li>
<li>A negative amount the validation only half-caught.</li>
</ul>
<p>Nine functional bugs and thirteen design findings in a 61-step session, for $0.28, with not one transaction ever reaching the chain.</p>
<p><img src="https://blog.simbastack.com/_media/x8wzqgjb.png" alt="Directory listing of one run&#39;s artifacts: a screenshot per step plus reports">
<em>Every run leaves its evidence on disk: a screenshot per step, the structured report, and the vision findings.</em></p>
<h2>The stack</h2>
<p>Sentinel is deliberately boring to operate — about 2,500 lines of bash, Node, and TypeScript, a launchd scheduler, and the CLIs it shells out to. There&#39;s no service to host and no database of its own, and you can read the whole thing in an afternoon. That&#39;s on purpose. We weren&#39;t going to leave an agent running unattended without being able to read everything it can do.</p>
<p>The same goes for what leaves the machine. Sending a repo&#39;s code, DOM, or diff to a model is opt-in per target (<code>ai_allowed</code>), so a work repo can&#39;t ride along by accident.</p>
<h2>What it can&#39;t do yet</h2>
<ul>
<li>Exploration still varies run to run; that&#39;s why every flow runs more than once.</li>
<li>Booting a complex stack takes the right config (ports, auth, a test database), and you should never point write-capable QA at production data.</li>
<li>Depth trades off against time and cost, all of it knobs: flows per run, attempts per flow, steps per attempt.</li>
<li>For wallet apps it drives an unfunded burner, so it tests everything up to the moment of settlement, not a trade actually filling. A local <code>anvil</code> fork (Foundry&#39;s local Ethereum node) gives it real chain state to quote against, but the broadcast block stays on even there. Actually filling a trade on a fork is a thing we haven&#39;t built.</li>
</ul>
<h2>Try it</h2>
<p>Sentinel is MIT-licensed and on GitHub: <a href="https://github.com/Simbastack-hq/sentinel">github.com/Simbastack-hq/sentinel</a>.</p>
<pre><code class="language-bash">git clone https://github.com/Simbastack-hq/sentinel.git &amp;&amp; cd sentinel
npm install &amp;&amp; ( cd pi-ext/qa-browser &amp;&amp; npm install )
cp config/sentinel.env.example config/sentinel.env
cp config/targets.json.example config/targets.json   # placeholder targets inside; add your own repo
bin/sentinel doctor &amp;&amp; bin/sentinel run &lt;your-app&gt; qa
</code></pre>
<p>The one hard prerequisite is the <code>pi</code> CLI with a provider configured; Mimo, authed with a Xiaomi key, is the default. Swapping it is config: <code>QA_PROVIDER</code>/<code>QA_MODEL</code> and the <code>VISION_*</code> variables point the decisions and the vision pass at any OpenAI-compatible endpoint, OpenRouter and local models included. <code>doctor</code> will tell you what&#39;s missing.</p>
<p><code>examples/</code> has ready-to-copy target registries, and the placeholder <code>targets.json</code> includes a complete web3 wallet-dApp sample like the exchange run above.</p>
<p>Point it at something real and see what it finds. PRs and issues welcome.</p>
<p>— Hemanshu, building <a href="https://github.com/Simbastack-hq/sentinel">Sentinel</a> and <a href="https://karibukit.com">KaribuKit</a> at <a href="https://simbastack.com">SimbaStack</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>While I slept, my 5-year-old MacBook ran Gemma 4 locally and indexed a year of video</title>
      <link>https://blog.simbastack.com/indexed-a-year-of-video-locally/</link>
      <guid isPermaLink="true">https://blog.simbastack.com/indexed-a-year-of-video-locally/</guid>
      <pubDate>Thu, 21 May 2026 09:55:22 GMT</pubDate>
      <dc:creator>NJ</dc:creator>
      <description>How I built a queryable video archive locally on a 2021 MacBook Pro M1 Max using Gemma 4 31B in LM Studio. The build journey, the absurdity of 50GB of swap, and four bugs that taught real lessons.</description>
      <content:encoded><![CDATA[<p>I&#39;m in the Maasai Mara about half the year, in three-month stretches. Animals out the front of the lodge, motorcycles, friends in the Maasai villages, kids who think a drone is the funniest thing they have ever seen. That&#39;s one half of my year. The other half is sixteen-hour days in front of a terminal, Silicon Valley hacker brain on Africa time.</p>
<p>The first half is a constant flood of footage from the iPhone, the DJI Pocket, the drone, the Nikon Z8, and lately the Ray-Ban Metas too. There&#39;s always something being recorded. Every photographer or videographer I know is sitting on the same problem: an archive that grows faster than they can edit it. The second half is why mine never gets touched.</p>
<p><img src="https://blog.simbastack.com/_media/2x2gprbk.jpg" alt="Two airport-security trays overflowing with a Nikon DSLR, an action cam, headphones, a sports watch, SSDs, batteries, and a tangle of cables">
<em>Airport security somewhere between Nairobi and Spain. Two trays of cameras, headphones, drone bits, batteries, SSDs, more cables than anyone needs. Most of it records something. Almost none of what they record gets touched again any time soon.</em></p>
<p>Three months ago the lodge&#39;s social channels went dark. Not for lack of content; the lodge has years of raw footage across multiple SSDs. The bottleneck was <em>editing time</em>, and my time disappeared. Claude Code with Opus 4.5 (and then 4.6) hit the point in February where you could leave agents running for hours and come back to merged PRs. KaribuKit was going live with its first paying property in the same window. I stopped sleeping properly, started running three or four agents in parallel in the background, and the months when I would have cut reels turned into months when I shipped software instead.</p>
<p>So one weekend I sat down to fix it. The first thing I tried was wrong.</p>
<h2>The wrong layer</h2>
<p>The initial pitch (to myself, after about an hour of research) was a SaaS stack: Eddie AI for iterative editing, Higgsfield MCP for generative B-roll, Submagic for captions, Buffer for cross-posting. About $140 a month, slick on paper.</p>
<p>Two problems showed up before I ran any of it.</p>
<p>First, generative AI video has no place on a real travel brand. Guests pay $300 a night and up to see <em>the actual place</em>, and mislabeled AI shots equals TripAdvisor crucifixion. Higgsfield out.</p>
<p>Second, 3-5 posts a week was aggressive for me, and the realistic floor was more like 2-3. The pitch was optimistic in a way that would have me failing by week two.</p>
<p>Then I remembered I already own DaVinci Resolve Studio, and Resolve 21 ships IntelliSearch (semantic clip search), Smart Bins (auto-organizing folders), and Voice to Subtitle that produces 90-95% accurate captions on the timeline. That&#39;s roughly 70% of what Eddie sells, so Eddie was out too.</p>
<p>What I was left with was Claude Code driving Resolve via the open-source DaVinci Resolve MCP, with ElevenLabs handling voiceover on informational clips where it earned its place, and the cost had dropped from $140 a month to $22.</p>
<p>But the deeper thing only landed once I tried to actually use any of this. Every AI video editor on the market assumes your footage is already labeled. Mine is <code>IMG_*.mov</code> and <code>DJI_*.mp4</code> across folders with names like <code>Mara june 2024 backup final FINAL</code>. Eddie can search by transcript, but none of these tools can find &quot;the elephant on the hill at golden hour&quot; against an unlabeled archive.</p>
<p>The AI editor is solving the wrong problem. Or more precisely, it&#39;s solving the <em>second</em> problem; the first problem is the index.</p>
<h2>The question</h2>
<p>The question I kept coming back to was: <em>how does the agent know what&#39;s in each clip?</em></p>
<p>There&#39;s no answer for an unlabeled archive. You can throw transcripts at it, GPS coordinates, filenames, parent folders. None of that gives you &quot;the wide shot at sunrise with the giraffe in the frame&quot; unless something has actually looked at the pixels.</p>
<p>The leverage is upstream. Build the index first, make the archive queryable in English, and the editor on top becomes a thin layer doing what it was designed to do.</p>
<p>So I built the index, locally.</p>
<h2>The build</h2>
<p>This is the kind of AI-native build I do for clients at SimbaStack, except I was both the client and the engineer this time, which made the decision tree a lot shorter.</p>
<p>I had four constraints going in:</p>
<ul>
<li>It had to be local-first. The <a href="https://marahilltop.com/">Mara Hilltop</a> archive lives on physical SSDs and most of the personal stuff is on my laptop, and uploading thousands of multi-gigabyte clips to the cloud made no sense on cost, never mind as a way to hand the entire visual record of my life to a third party.</li>
<li>I wanted sidecars rather than a central database: a <code>.description.md</code> per clip, living right next to it, plain text and grep-able. It survives if my indexer breaks tomorrow, and it travels with the data when files move between drives.</li>
<li>One vision call had to capture everything, because the vision pass over the extracted frames is the expensive operation. Anything I might want to know about a clip later has to come out of that one call, so the schema is exhaustive on day one: rating, technical quality, lighting, time of day, color palette, audio quality, people count, keywords, faces, location, transcript, prose description. All in one shot.</li>
<li>I wanted three vision backends to choose from: Claude via my Max subscription&#39;s CLI as the default (zero marginal cost), the Anthropic API for speed when I need it, and a local backend pointed at LM Studio for the bulk pass. The local one is the one that matters.</li>
</ul>
<p>The per-clip pipeline:</p>
<ol>
<li><code>ffprobe</code> for metadata.</li>
<li><code>exiftool</code> for GPS lat/lon/altitude. Works on iPhone, DJI Pocket, drone footage, all the same.</li>
<li>Reverse-geocode via Nominatim. Free, rate-limited, no API key.</li>
<li><code>ffmpeg</code> extracts five evenly-spaced frames at 1920px.</li>
<li>WhisperX transcribes with word-level alignment and pyannote speaker diarization. Hindi, English, Swahili, 97 languages.</li>
<li><code>insightface</code> detects faces and stores 512-dim ArcFace embeddings in a centralized SQLite face DB for cross-archive person queries later.</li>
<li>Vision model reads the frames, transcript snippet, and folder context, and returns YAML frontmatter plus a prose description.</li>
<li>Sidecar written to disk.</li>
</ol>
<p>Here&#39;s what that looks like on a real clip from the Mara Hilltop archive.</p>
<p><img src="https://blog.simbastack.com/_media/r82dq4em.jpg" alt="A frame from IMG_1103.MOV: Ellie on the deck of a Mara Hilltop luxury tent at midday, savanna behind her">
<em>One frame from <code>IMG_1103.MOV</code>. Ellie on the deck of one of the luxury tents at the lodge, midday. None of that context lives in the filename.</em></p>
<p><img src="https://blog.simbastack.com/_media/bhc7div4.jpg" alt="The sidecar file Gemma wrote for IMG_1103.MOV, showing YAML schema and a Description block">
<em>The sidecar Gemma wrote for the same clip. YAML on top (lighting enum, time-of-day enum, color palette, face embeddings, GPS), prose <code>## Description</code> below. It picked up the safari-tent setting, the camera pan from interior to savanna, the shot type, and suggested two use cases (marketing reels and travel-vlog B-roll). The filename had <code>IMG_1103.MOV</code>; the sidecar has the rest of what I needed to find it again.</em></p>
<p><img src="https://blog.simbastack.com/_media/gvcycx2n.png" alt="Finder window showing a Mara Hilltop archive folder: video and photo files paired with .description.md sidecars, plus _INDEX.json and _INDEX.md at the top">
<em>A real Mara Hilltop archive folder after the indexer has run through it. Every clip has a <code>.description.md</code> sidecar next to it; the <code>_INDEX.json</code> and <code>_INDEX.md</code> at the top are folder-level rollups for fast grep and LLM-friendly handoff.</em></p>
<p>The whole thing is a Claude Code skill, about 1,400 lines of Python. Claude Code wrote almost all of it. My work was the architecture, the prompts, the schema design, and the bug triage when things went wrong.</p>
<h2>The absurdity</h2>
<p>This is the part that actually surprised me.</p>
<p>I bought a 16-inch MacBook Pro M1 Max with 64GB of RAM in 2021, and the reason had nothing to do with LLMs. I&#39;d been hitting 32GB limits on my previous machine for a while. A messy hacker brain running hundreds of Chrome tabs alongside DaVinci Resolve, Slack, Discord, and Drive was too much for pre-unified-memory hardware to handle without paging constantly. I maxed out the RAM on the new M1 Max because the old one wouldn&#39;t stop killing my workflow and I had the money to fix it.</p>
<p>Five years later, that same laptop is running Gemma 4 31B Q4 in LM Studio against a year of video footage.</p>
<p><img src="https://blog.simbastack.com/_media/cdqg6hyh.png" alt="LM Studio Developer view with gemma-4-31b loaded, 28.40 GB, REST API at 127.0.0.1:1234, server logs showing image encoding">
<em>LM Studio with Gemma 4 31B Q4 loaded. 28.40 GB of model in memory, REST API at <code>127.0.0.1:1234</code>. The bottom panel is the server log during a real bulk run, encoding frames one clip at a time.</em></p>
<p>The bulk run pushed the laptop past where 64GB of RAM alone would carry it. Activity Monitor reported 50.89 GB of swap at the peak.</p>
<p><img src="https://blog.simbastack.com/_media/3k9z365r.png" alt="macOS Activity Monitor showing 64GB physical RAM, 50.89GB swap used during indexing run, memory pressure in the yellow band">
<em>64 GB of physical RAM, 50.89 GB of swap used. Memory pressure in the yellow band, the kind of state you absolutely should not run on a normal Tuesday. Apple&#39;s swap is designed for it, and the fans were loud.</em></p>
<p>I Googled whether that would damage the SSD, and apparently for a day or two it&#39;s fine. Don&#39;t make it your normal operating state, but a weekend of pushing the machine hard is well within tolerance. My laptop ran hot, the fans spun up, and it kept producing sidecars while I worked on other things.</p>
<p>The M1 Max 16-inch is, honestly, legendary. People in the Mac community talk about it that way for good reason: five years on, it&#39;s running 31B-parameter models at usable speed with the kind of headroom that should not exist on hardware this old. I expect another three to five years out of this thing, comfortably, because local LLMs only get more efficient and the hardware is the floor, not the ceiling.</p>
<h2>What broke</h2>
<p>The build was mostly Claude Code holding the pen. The interesting work was the four times it almost shipped something wrong.</p>
<p>WhisperX 3.8 broke its diarization API between when I last touched it and now. Two breaking changes had landed: <code>whisperx.DiarizationPipeline</code> moved to the <code>whisperx.diarize</code> submodule, and the constructor kwarg <code>use_auth_token</code> was renamed to <code>token</code> (inherited from pyannote 3.x). The fix was signature introspection: the script tries <code>token=</code> first and falls back to <code>use_auth_token=</code> if the constructor raises a TypeError, so it survives the next API shuffle automatically. When you&#39;re shelling out to AI libraries that move this fast, defensive constructor calls are cheap insurance.</p>
<p>The Claude CLI returns permission errors as successful responses. On the first test of the CLI backend, all four sidecars came back identical with the text <em>&quot;I need permission to read the image frames...&quot;</em>, and the script&#39;s success check passed because exit code was 0 and the output wasn&#39;t empty. The cause was that in non-interactive mode without <code>--permission-mode bypassPermissions</code>, the CLI returns the permission-denial text as the response body instead of prompting, which means the failure mode looks exactly like success unless you string-match for it. The fix was adding the flag plus a defensive check that flags any short response containing &quot;I need permission&quot; as an error rather than a description. When you script AI tools, the non-interactive permission flow is where the silent failures hide.</p>
<p>Gemma returned <code>people_count: &quot;many&quot;</code> instead of an integer. My vision prompt literally said <code>integer or the string &quot;many&quot; if &gt;10</code>. Gemma followed instructions correctly; the bug was schema design. The fix was a stricter prompt (integer 0-99 with explicit guidance to estimate) plus a coercion in the parser for the legacy &quot;many&quot; responses. Don&#39;t union-type schema fields. Pick always-int or always-string, never &quot;int or this one specific string,&quot; because every downstream consumer pays for the choice.</p>
<p>Then there was the motorcycle clip that shouldn&#39;t have been culled. My initial cull prompt was photographer-portfolio-shaped: heavy motion blur, soft focus, and jittery stability got rated <code>cull</code>. Technically correct. Then I tested it on a handheld nighttime motorcycle clip from a Spain trip and it culled it. I caught it: that&#39;s a fun memory, the blur <em>is</em> the vibe. I reframed the cull criteria to &quot;not a real recording&quot; only (lens cap, pocket footage, two-second test clips, fully clipped exposure), not &quot;imperfect capture.&quot; Photo archives cull aggressively; video memories cull permissively. Same schema, different criteria, and you have to be explicit about which mode you&#39;re in.</p>
<h2>What the build taught me</h2>
<p>Enum constraints beat instructions for confabulation prevention. I tested Gemma 4 E4B on a coworking-space photo I&#39;d taken at night, and it described the scene as &quot;brightly lit, abundant natural light, floor-to-ceiling windows,&quot; except the windows were pitch black outside, because it was night. Then I tested 31B with a structured schema prompt that forces the model to pick from <code>golden_hour | bright_daylight | overcast | dim_interior | nighttime | mixed | unclear</code>, and both thinking-off and thinking-on recovered nighttime correctly. A model can lie about open-ended prose, but it can only mis-pick from an enum, never invent a new value. Use schemas, not instructions.</p>
<p>Local 31B with structured prompts closes most of the gap to cloud. Gemma 4 31B Q4 thinking-off against a structured schema produces output that&#39;s hard to distinguish from Sonnet 4.6 on most of my test clips. The cloud premium earns its keep on the hard 10-20%. Bulk indexing at scale (thousands of clips overnight) should run local; cloud is the re-rate pass on clips local flagged as <code>review</code>. That two-tier setup is the one that scales.</p>
<p>AI video editors are pitched one layer too high. The valuable layer is the index. Once your archive is queryable in plain English (&quot;show me handheld interior clips from Mara, golden hour, with people, longer than 8 seconds&quot;), the editor on top is straightforward. Most of the AI-editor space is competing for the surface above an index that doesn&#39;t exist, and the index is the prerequisite they&#39;re all skipping past.</p>
<h2>What&#39;s next</h2>
<p>Looking back, time wasn&#39;t really what kept this from getting fixed sooner. I had every AI superpower currently available pointed at the work side of my life: Claude Code refactoring codebases overnight, Codex writing most of my pull requests, the agentic stack I&#39;d just spent three months using to ship KaribuKit. On the editing side, I was using none of it. The not-getting-to-it had become its own small, low-grade frustration that lived in the back of my head all year, the kind of thing you notice every time you open a folder on the SSD and close it again without doing anything. What clicked one Saturday was that the editing backlog was a tooling problem, and tooling is the one kind of problem I happen to be well-equipped to fix right now.</p>
<p>This weekend I&#39;m building the editor: Claude Code as the orchestrator, DaVinci Resolve MCP for the cuts, ElevenLabs for voiceover on informational clips. There&#39;s one hard rule baked into the tooling: the voice clone is for utility content only. Directions, room descriptions, multilingual versions, factual stuff I&#39;d say in person anyway. Never for testimonials or founder messages. Disclosure laws are real in 2026, and trust in a hospitality brand is too easy to lose.</p>
<p>The index makes all of that tractable. Without it, I would still be scrubbing through 47GB of DJI Pocket footage looking for the sunrise wide.</p>
<p>For now: a year of Mara Hilltop footage is queryable in English on a five-year-old laptop. Cost was a weekend of my time and 50GB of swap. The remaining years across older SSDs are next.</p>
<p>A fair check on all of this: Mara Hilltop&#39;s social channels are still dead today. The indexer solves only half the problem (finding the right clip); the editor that turns those clips into finished reels is the other half, and that&#39;s the part I&#39;m building this weekend. If it works, the channels light back up and I write part two. If it doesn&#39;t, I write about why.</p>
<p>In all honesty, the right answer here might be to hire someone. Finding an editor with the right sensibility for Mara Hilltop (warm, observational, no over-cut MTV-energy reels) is harder than writing another skill. If you know someone who works in that register, send them my way.</p>
<p><strong>Edit:</strong> code at <a href="https://github.com/Simbastack-hq/framedex">github.com/Simbastack-hq/framedex</a>. PRs and issues welcome. Thanks to <a href="https://news.ycombinator.com/item?id=48224290">the HN commenter</a> who flagged that the original local-path reference wasn&#39;t useful.</p>
<p>— NJ</p>
<p><em>Building <a href="https://karibukit.com">KaribuKit</a> (AI-native PMS for hospitality), running <a href="https://marahilltop.com">Mara Hilltop</a> (eco-lodge in the Maasai Mara), and consulting through <a href="https://simbastack.com">SimbaStack</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>We rebuilt the structured output problem one layer up</title>
      <link>https://blog.simbastack.com/we-rebuilt-the-structured-output-problem-one-layer-up/</link>
      <guid isPermaLink="true">https://blog.simbastack.com/we-rebuilt-the-structured-output-problem-one-layer-up/</guid>
      <pubDate>Wed, 06 May 2026 13:57:30 GMT</pubDate>
      <dc:creator>NJ</dc:creator>
      <description>We solved structured output in 2024, then immediately rebuilt the same problem one stack-layer up under the name &apos;tool calling&apos;. The lessons we learned the first time aren&apos;t propagating.</description>
      <content:encoded><![CDATA[<p>In late 2023, every JSON-extracting system prompt I shipped for production tooling looked roughly like this:</p>
<pre><code>You are a JSON API. Respond ONLY with valid JSON.
Do not include explanations. Do not wrap in markdown.
Do not say &quot;Here is the JSON&quot;.
The output is parsed by a strict parser.
If the JSON is invalid, downstream systems break.
Return only the JSON object.
</code></pre>
<p>I am embarrassed by every line of it. I am also, in early 2026, still using a near-identical version in three production systems, because it works and because nothing else available at the time worked better.</p>
<p>That prompt is an artifact of an era, and the era has a story worth telling because it&#39;s already repeating itself.</p>
<h2>A field guide to the era</h2>
<p>The structured-output era runs roughly from late 2022 to August 2024. Here is what shipped, with verified dates:</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Artifact</th>
<th>What it solved</th>
<th>What it cost</th>
</tr>
</thead>
<tbody><tr>
<td>Nov 10, 2022</td>
<td><a href="https://github.com/guidance-ai/guidance">Microsoft Guidance</a></td>
<td>First control-flow language for LLM generation</td>
<td>Tightly coupled to the inference loop</td>
</tr>
<tr>
<td>Mar 17, 2023</td>
<td><a href="https://github.com/dottxt-ai/outlines">Outlines (dottxt-ai)</a></td>
<td>Grammar/regex-constrained sampling</td>
<td>Required control over the sampler</td>
</tr>
<tr>
<td>May 6, 2023</td>
<td><a href="https://glazkov.com/2023/05/06/schemish/">Glazkov&#39;s &quot;Schemish&quot; post</a></td>
<td>&quot;Use JSON Schema as reasoning rails&quot; pattern</td>
<td>Still relied on the model cooperating</td>
</tr>
<tr>
<td>Jun 13, 2023</td>
<td><a href="https://openai.com/index/function-calling-and-other-api-updates/">OpenAI function calling</a></td>
<td>First major API-level structured-output mechanism</td>
<td>Schema-conformance was best-effort</td>
</tr>
<tr>
<td>Jun 14, 2023</td>
<td><a href="https://github.com/567-labs/instructor">jxnl/Instructor</a></td>
<td>Pydantic models as the API contract; auto-retry on validation</td>
<td>A wrapper around prompts, fundamentally still coercion</td>
</tr>
<tr>
<td>Jul 2023</td>
<td><a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md">llama.cpp grammar sampling</a></td>
<td>GBNF-constrained decoding for open-source models</td>
<td>Local-only, complex grammar files</td>
</tr>
<tr>
<td>Nov 6, 2023</td>
<td>OpenAI JSON mode (DevDay)</td>
<td><code>response_format: json_object</code>, model trained to emit valid JSON</td>
<td>Schema-aware? No. Just &quot;valid JSON.&quot;</td>
</tr>
<tr>
<td>Late Dec 2023</td>
<td><a href="https://erichartford.com/dolphin-25-mixtral-8x7b">Eric Hartford&#39;s Dolphin &quot;kitten&quot; prompt</a>; Theia Vogel&#39;s tipping experiment</td>
<td>Demonstrated absurd lengths people went to for instruction-following</td>
<td>Made the era look ridiculous in retrospect</td>
</tr>
<tr>
<td>Feb 2024</td>
<td><a href="https://hamel.dev/blog/posts/prompt/">Hamel Husain&#39;s &quot;Fuck You, Show Me The Prompt&quot;</a></td>
<td>Skepticism: most &quot;structured output&quot; libraries are prompt manipulation</td>
<td>Became the canonical counter-narrative</td>
</tr>
<tr>
<td>May 2024</td>
<td>Anthropic Claude tool use (GA)</td>
<td>Tools-as-first-class, parallel to function calling</td>
<td>Two incompatible tool-calling shapes now</td>
</tr>
<tr>
<td>Aug 6, 2024</td>
<td><a href="https://openai.com/index/introducing-structured-outputs-in-the-api/">OpenAI Structured Outputs (strict schemas)</a></td>
<td>&quot;100% reliability&quot; for JSON Schema conformance on <code>gpt-4o-2024-08-06</code></td>
<td>The end of phase one</td>
</tr>
<tr>
<td>Nov 2024</td>
<td><a href="https://www.anthropic.com/news/model-context-protocol">Anthropic Model Context Protocol</a></td>
<td>Tool-and-resource standardization across providers</td>
<td>Phase two begins; new shape, same coercion</td>
</tr>
<tr>
<td>Dec 2, 2024</td>
<td><a href="https://ai.pydantic.dev/">Pydantic AI</a></td>
<td>Pydantic-validated agent loops</td>
<td>Wrapper layer around tool calling</td>
</tr>
<tr>
<td>2026</td>
<td><a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Anthropic — Code execution with MCP</a></td>
<td>Tool calls don&#39;t scale; agents should write code instead</td>
<td>A retreat to a more natural abstraction</td>
</tr>
</tbody></table>
<p>If you were paying attention in 2023, every row in that table felt like progress. We were inventing the abstractions at the same time we were using them in production. The Instructor library was created on June 14, 2023, the day after OpenAI launched function calling on June 13. That is the pace.</p>
<p>Looking back from 2026, the table reads as a single arc with a beginning, a middle, and an apparent end. The beginning is &quot;ask the model nicely, then more nicely, then with threats.&quot; The middle is a Cambrian explosion of libraries trying to put structural rails on prompt engineering. The apparent end is OpenAI&#39;s August 2024 launch of strict-schema Structured Outputs, which claimed 100% schema conformance on <code>gpt-4o-2024-08-06</code>.</p>
<p>Phase one closed with confetti. We had won the war.</p>
<p>Then phase two started, and we noticed it was the same war.</p>
<h2>The libraries that mattered</h2>
<p>The libraries weren&#39;t the headline at the time. The model launches were. But the libraries were doing the actual work of teaching us what structured output is, and they&#39;re worth naming.</p>
<p><a href="https://github.com/guidance-ai/guidance">Microsoft Guidance</a> was the earliest of them, with its repo created in November 2022, before ChatGPT had finished its launch news cycle. Guidance pioneered the idea of treating LLM output as something you compose with control flow, regex constraints, and grammars. Most of the patterns we now take for granted (JSON-from-grammar, structured generation as opposed to structured prompting) trace back here.</p>
<p><a href="https://github.com/dottxt-ai/outlines">Outlines</a>, launched in March 2023 by .txt, was the cleanest expression of grammar-constrained decoding. The thesis was straightforward: if you can write a grammar for the output, the sampler should refuse to emit anything that violates it. This is a profoundly correct idea that, three years later, is finally becoming the default in open-source inference engines like vLLM and XGrammar.</p>
<p><a href="https://github.com/567-labs/instructor">Instructor</a>, shipped by jxnl on June 14, 2023, took a different path. It wrapped OpenAI&#39;s brand-new function calling and pretended the result was structured output. Define a Pydantic model, get a Pydantic model back. Retries handled. Validation handled. It was the right abstraction at the right moment, and it&#39;s part of why the term &quot;structured outputs&quot; stuck as the framing for the whole field.</p>
<p><a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md">llama.cpp added grammar-based sampling</a> in July 2023, and that&#39;s when the conversation about constrained decoding got serious in the open-source world. The fact that you could ship a four-line grammar file and force a 7B model to emit perfect JSON every time was, at the time, witchcraft.</p>
<p>LangChain&#39;s output parsers, especially <code>OutputFixingParser</code>, standardized the retry-and-repair loop: parse, fail, send the validation error back to the model, ask it to fix the JSON, parse again. That pattern is everywhere now. It&#39;s also a lot of what people complain about when they complain about LangChain.</p>
<p>These libraries split into three approaches. Constrained decoding (Outlines, Guidance, llama.cpp grammars) intervenes at sampling time. Schema-driven wrappers (Instructor, later Pydantic AI) lean on the underlying API and add validation. Retry-and-repair loops (LangChain, Guardrails) treat the LLM as an unreliable black box and validate after the fact. All three still ship.</p>
<h2>The fork in the road</h2>
<p>By mid-2024 the field had bifurcated into camps that didn&#39;t always realize they were having the same argument.</p>
<p>The constrained-decoding camp said: structure should be enforced at sampling time, by the inference engine, before the model can emit anything wrong. Outlines and llama.cpp grammars are this. So is OpenAI&#39;s Structured Outputs feature, which almost certainly uses grammar-constrained decoding under the hood, building on research from llguidance and XGrammar.</p>
<p>The instruction-tuned camp said: train the model to emit schema-conformant output. OpenAI&#39;s function calling was the first major implementation. Anthropic&#39;s tool use, when it went GA in May 2024, was the second. The argument was that for closed-source APIs you don&#39;t control sampling, so you have to bake schema awareness into the model itself.</p>
<p>The wrapper camp said: it doesn&#39;t matter how the model emits the JSON, you still need validation, retries, and provider-agnosticism. Instructor and Pydantic AI (publicly launched December 2, 2024) are the cleanest expressions of this.</p>
<p>What actually won? All three did, layered on top of each other. For most app developers using closed APIs, OpenAI Structured Outputs and Anthropic tool use are the default. Underneath them, constrained decoding is the implementation. On top of them, validation wrappers like Instructor handle retries and content checks the schema can&#39;t enforce.</p>
<p>Hamel Husain wrote the canonical skeptical essay about wrappers in February 2024 (<a href="https://hamel.dev/blog/posts/prompt/">Fuck You, Show Me The Prompt</a>), and he was right that many of these libraries are mostly prompt manipulation. He was also wrong that this means they&#39;re not useful. Prompt manipulation, well-engineered and validated, was the bridge that got us from &quot;respond ONLY in valid JSON&quot; to schema-strict APIs. Bridges are useful, and they&#39;re also temporary.</p>
<h2>Three lessons we learned</h2>
<p>The era taught us things, and the things are worth naming clearly because they&#39;re falling out of working memory already.</p>
<h3>The model wasn&#39;t broken; the interface was</h3>
<p>In early 2023 the consensus was that LLMs &quot;couldn&#39;t follow instructions reliably.&quot; By late 2024 the consensus was that LLMs &quot;follow JSON schemas with 100% reliability.&quot; The models hadn&#39;t fundamentally changed in that window. The interface had. When a model seems unreliable at a task, the most productive question to ask is &quot;what does the API surface look like?&quot; before &quot;is the model good enough?&quot;</p>
<h3>Every workaround eventually becomes infrastructure</h3>
<p>The retry-and-repair loop was a hack. It became LangChain&#39;s <code>OutputFixingParser</code>, then Instructor&#39;s tenacity-backed retries, then a built-in part of Pydantic AI. The &quot;respond ONLY in valid JSON&quot; prompt was a hack. It became, near-verbatim, the default system-prompt example in OpenAI&#39;s own documentation for years. The lesson is to take your hacks seriously, because the half-life of a &quot;temporary&quot; workaround in this field is approximately five years.</p>
<h3>The constraint doesn&#39;t disappear, it moves up the stack</h3>
<p>The 2022 problem was: how do I get this model to emit a parseable JSON object. By 2024 we had solved that. The 2025 problem became: how do I get this model to pick the right tool from a list of 250 of them, with each tool&#39;s schema preloaded into the system prompt, and not blow my context window before the model has emitted a single token. The shape of the problem didn&#39;t change. The layer changed. We&#39;re in the middle of the same pattern again.</p>
<h2>Why we&#39;re forgetting the lessons</h2>
<p>If the lessons of the structured-output era were propagating, the tool-calling and MCP era would look different than it does. It doesn&#39;t.</p>
<p>The clearest public example: in late 2025 / early 2026, <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Anthropic published an engineering post titled <em>Code execution with MCP: building more efficient AI agents</em></a>. The argument, in their own framing, is that direct tool calls don&#39;t scale because each tool definition consumes context, and a five-server MCP setup with 58 tools can burn ~50,000 tokens before the user has typed anything. Their proposed fix is to let the model write code that calls tools, instead of expecting the model to pick from a flat list of pre-loaded tool schemas. They report that lazy-loaded tool discovery improves Claude Opus 4.5 task accuracy from 79.5% to 88.1% while cutting tokens by roughly 85%.</p>
<p>That paragraph deserves to be re-read. Anthropic, the company that designed MCP, is publicly arguing that the way MCP currently works has a fundamental scaling problem, and the fix is to abandon flat tool lists in favor of letting the model write code. That maps cleanly onto the lessons above. The model isn&#39;t bad at tool calling; the interface is. The &quot;load all your tools upfront&quot; pattern is becoming infrastructure even though it was always a hack. The constraint moved up: from &quot;make the model emit valid JSON&quot; to &quot;make the model pick the right tool from a flat list of 250.&quot;</p>
<p>A second example, with a useful counterpoint: Waleed K&#39;s piece <a href="https://waleedk.medium.com/the-evolution-of-ai-tool-use-mcp-went-sideways-8ef4b1268126">The Evolution of AI Tool Use: MCP Went Sideways</a> makes a related observation about MCP&#39;s context-bloat problem with concrete numbers and a concrete war story. He argues, drawing on Cloudflare&#39;s framing, that LLMs are &quot;bad at tool calling&quot; because tool-call traces are &quot;out-of-distribution&quot; for the base models. I think he&#39;s half right. The base distributions are absolutely thin on canonical tool-call traces, which is exactly why model providers fine-tune for tool use and why Anthropic&#39;s lazy-loaded tool search lifts Opus 4.5 from 79.5% to 88.1%. But the framing &quot;models can&#39;t do tool calling&quot; misses the same point we missed in 2022 about JSON. The model isn&#39;t bad at the task. We&#39;ve handed it a clumsy interface to the task. Code execution feels like a fix because it routes around the clumsy interface and lets the model do something it has trillions of training examples for: write code. That&#39;s the same insight as &quot;use a JSON Schema grammar.&quot; It&#39;s &quot;the interface was wrong, again.&quot; The lesson generalizes, and it&#39;s the lesson worth carrying forward.</p>
<p>A third example, drawn from less rigorous evidence: production systems I&#39;ve worked on in 2026 still ship system prompts that say things like &quot;you are a backend tool router. Output ONLY a single tool call. Do not explain. Do not apologize.&quot; Strip the word &quot;tool&quot; and replace with &quot;JSON object&quot; and you have my 2023 prompt. Same shape, same hack, just at a different layer.</p>
<h2>What the next &quot;JSON repair library&quot; looks like</h2>
<p>Three predictions, with low hedging.</p>
<p>First, tool-search-as-routing becomes the default agent design pattern within twelve months. Flat tool lists in the system prompt will look as embarrassing in 2027 as &quot;respond ONLY in valid JSON&quot; looks now. The tool-search tool is the new JSON-Schema strict mode.</p>
<p>Second, agent-as-code-author beats agent-as-tool-picker for any non-trivial workflow. Anthropic&#39;s MCP-code-execution post is the most prominent signal, but the same pattern shows up in the way Claude Code agents get work done internally and in OpenAI&#39;s evolving Responses API semantics. We&#39;ll look back at &quot;load 250 tools into the system prompt&quot; the same way we look back at the kitten prompt.</p>
<p>Third, whatever standard replaces or absorbs MCP will be a <em>protocol-of-protocols</em>: a thin layer that brokers between code-executing agents and the underlying tool servers, rather than a flat schema dump. MCP solved the &quot;describe your tools&quot; problem. The next standard has to solve &quot;let the model discover tools as needed without paying upfront context.&quot;</p>
<p>I have been writing system prompts that look like 2023 prompts again, this time around tool calling, and so has everyone else. The fix is the same one we figured out the first time: stop coercing, change the interface.</p>
<p>If you&#39;ve shipped systems that hit this same wall, I&#39;d be glad to compare notes.</p>
<p>— <em>NJ</em></p>
<p><em>Building <a href="https://karibukit.com">KaribuKit</a> (AI-native PMS for hospitality), running <a href="https://marahilltop.com">Mara Hilltop</a> (eco-lodge in the Maasai Mara), and consulting through <a href="https://simbastack.com">SimbaStack</a>.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
