{
    "version": "https://jsonfeed.org/version/1",
    "title": "CatalEx Engineering",
    "home_page_url": "https://blog.catalex.co",
    "description": "Engineering notes from the team building the AI operating layer for AI-native companies.",
    "items": [
        {
            "id": "https://blog.catalex.co/the-model-is-a-component",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 13, 2026 · 09:00 UTC</div>\n<p>Every AI platform says it is model-agnostic. Almost none of them are. Not\nbecause the teams are lying — because agnosticism isn't a property you declare\nin a README. It's a seam in the code, and a seam that nothing routinely crosses\nis a seam that has already rusted shut. You just don't find out until the day\nyou try to move it.</p>\n<!-- -->\n<p>We ended an earlier post by saying the model is a component you swap. That's the\neasy half. The hard half is the follow-up question every engineer asks\nimmediately: <em>fine, how?</em> What does the code have to look like for that sentence\nto be true a year from now, under a team that has shipped a hundred features\nsince anyone last thought about it?</p>\n<p>Here is the test we've landed on, and it's blunter than most architecture\nadvice. <strong>A swappable dependency is one where something in your normal workflow\nalready swaps it.</strong> Not a test that could be written. Not an adapter that could\nbe added. Something that crosses the seam today, on a Tuesday, without anyone\ndeciding to. If the only time you'd exercise the seam is the day you need it,\nyou don't have a seam. You have a plan. Plans don't compile.</p>\n<p>This post walks four seams in our system, what keeps each one alive, and — for\none of them — an admission about where the architecture is ahead of the\ndeployment.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"seam-1-the-model-behind-an-alias\">Seam 1: the model behind an alias<a href=\"https://blog.catalex.co/the-model-is-a-component#seam-1-the-model-behind-an-alias\" class=\"hash-link\" aria-label=\"Direct link to Seam 1: the model behind an alias\" title=\"Direct link to Seam 1: the model behind an alias\" translate=\"no\">​</a></h2>\n<p>All inference goes through a model proxy. Nothing in our application code\nrequests a vendor model by name. Callers request an <strong>alias</strong>: a <code>default</code>\nalias, and a cheaper-tier alias for work that doesn't need the good stuff. The\nalias resolves to a real model in configuration, not in code.</p>\n<p>That much is unremarkable — it's the indirection everyone writes. The part that\nmakes it real is the routing table. <strong>Local development routes the same alias to\na locally-run model</strong> while deployed environments route it somewhere else\nentirely. Every engineer, every day, on every branch, runs the system with the\nalias pointed at a genuinely different provider than production uses. Nobody has\nto remember to test the indirection. You cannot get your work done without\nexercising it.</p>\n<p>That's the whole difference between a seam and a wish. The indirection isn't\nkept honest by discipline or by a lint rule. It's kept honest by the fact that\nthe local loop would break the instant someone hard-coded a model, and it would\nbreak for the person who did it, in the first minute.</p>\n<p><strong>The consequence shows up in the schema.</strong> There is no model column on an\nagent. An agent is a definition — instructions, tools, KPIs — and deliberately\nnot a model binding. Model choice is a service-level concern, resolved at\ndispatch, not a property an agent carries around. The reasoning is mechanical\nrather than philosophical: if every agent row named a model, \"swap the model\"\nwould be a data migration across customer rows instead of a config change. The\nseam would technically exist and be practically unusable, which is the most\nexpensive kind of abstraction to own.</p>\n<div class=\"cx-note\"><p><strong>The honest part.</strong> The architecture is model-agnostic. The deployment today is\neffectively single-model — both aliases currently resolve to the same model. The\nseam is maintained and it is exercised, but we are not running a heterogeneous\nfleet behind it, and saying otherwise would be exactly the unearned agnosticism\nthis post is about. What we've bought is optionality, and the price of\noptionality is that you keep paying for it before you use it.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"seam-2-the-sandbox-behind-a-protocol\">Seam 2: the sandbox behind a Protocol<a href=\"https://blog.catalex.co/the-model-is-a-component#seam-2-the-sandbox-behind-a-protocol\" class=\"hash-link\" aria-label=\"Direct link to Seam 2: the sandbox behind a Protocol\" title=\"Direct link to Seam 2: the sandbox behind a Protocol\" translate=\"no\">​</a></h2>\n<p>Agents run code in a box. The orchestrator has never met one. It depends only on\na Protocol — an interface — and three adapters satisfy it: a <strong>fake provider</strong>\nused locally and in CI, a <strong>microVM provider</strong> that is the real one, and a\n<strong>confidential-computing provider</strong> aimed at stricter deployments.</p>\n<p>That third one deserves a flag before it does any work in this argument: it is\n<strong>aspirational</strong>. The Protocol is written for it. The adapter is not built. We\nare not shipping confidential computing today, and it appears here only because\nof what it did to the interface.</p>\n<p>Which was this: the Protocol was designed against the harder provider so all\nthree satisfy one contract. Write your interface against the most constrained\nimplementation — including one that doesn't exist yet — and the easy cases fall\nout of it for free. Write it against the easy case and the hard one will not fit\nlater, no matter how much you want it to. The unbuilt adapter earned its keep by\nbeing a constraint during design, which is a legitimate use for a thing that\ndoesn't exist.</p>\n<p>The mechanism that keeps it from degrading is capability declaration. Adapters\nadvertise what they offer; the orchestrator branches on the capability, never on\nthe provider:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@dataclass</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">frozen</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">Capabilities</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    sudo</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">bool</span><span class=\"token plain\">           </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># can the agent take root inside the box?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    scratch_disk</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">bool</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># writable disk beyond the process?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    resumable</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">bool</span><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># can a box be suspended and woken later?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">SandboxProvider</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">Protocol</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"A place an agent can run. Nothing above this line knows which one.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">capabilities</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Capabilities</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">create</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> BoxSpec</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> BoxHandle</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">exec</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> box</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> BoxHandle</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cmd</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> ExecResult</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">destroy</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> box</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> BoxHandle</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><br></div></code></pre></div></div>\n<p><code>if provider == \"x\"</code> is how a seam dies. It always starts as one line, in one\nplace, for one good reason, and then it's forty lines in nine files and the\nabstraction is now decoration around a switch statement. Branching on\n<code>caps.resumable</code> instead means a new adapter answers a question rather than\njoining a conditional.</p>\n<p><strong>The Protocol also documents what must not cross it.</strong> For every adapter, the\nforbidden leaks are written down: the box's only external egress is the tool\ngate and the model proxy; tool results follow the preview-plus-reference\ncontract; delivery and box lifecycle stay behind the interface. Naming what must\n<em>not</em> pass through an interface is rarer than naming what must, and it's more\nuseful — leaks are how abstractions rot, and they rot from the inside, by\naddition, one convenient exception at a time.</p>\n<p>And the fake provider deserves its own beat. The entire system runs in CI with\nno external sandbox at all. That's not a testing convenience; it's the strongest\navailable proof the seam holds. An abstraction you can't run without is not an\nabstraction — it's a wrapper. Every green CI run is a swap of the most\ninfrastructure-heavy dependency we have, performed by nobody, hundreds of times\na week.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"seam-3-integrations-behind-a-uniform-shape\">Seam 3: integrations behind a uniform shape<a href=\"https://blog.catalex.co/the-model-is-a-component#seam-3-integrations-behind-a-uniform-shape\" class=\"hash-link\" aria-label=\"Direct link to Seam 3: integrations behind a uniform shape\" title=\"Direct link to Seam 3: integrations behind a uniform shape\" translate=\"no\">​</a></h2>\n<p>Two sourcing paths exist for third-party tools: sometimes we wrap a vendor's API\nourselves, sometimes we proxy a remote server someone else runs. These are\npeer-equal behind the same registry shape, and the line that matters is that\n<strong>agents can't tell them apart at call time.</strong> The agent-facing contract doesn't\nleak the sourcing decision — which means the sourcing decision can be revisited\nwithout touching a single agent definition.</p>\n<p>The registry is convention, not registration. Every vendor is a directory whose\nname <em>is</em> its slug, with a fixed file layout inside; discovery walks one level\ndown and finds it. Adding a vendor is \"write a new package + decorate\", not\n\"edit a shared elif chain\". A central registry file is two bad things at once: a\nmerge conflict and a coupling point. A naming convention is neither, and it\ncan't drift from itself.</p>\n<p>Metering follows the same instinct. A tool is metered if its canonical name\nisn't in the local-tools set — vendor-agnostic <strong>by construction</strong>, because\nthere is no per-vendor list anywhere for anyone to forget to update. A\ncanonicalizer collapses the two surface spellings of the same tool into one\nidentity, so the accounting doesn't care which call style the model happened to\nuse. And the tool catalog itself is data — in the database, read at runtime —\nnot code. The general shape: when correctness depends on a list staying current,\ndelete the list.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"seam-4-the-seam-has-to-move-while-traffic-flows\">Seam 4: the seam has to move while traffic flows<a href=\"https://blog.catalex.co/the-model-is-a-component#seam-4-the-seam-has-to-move-while-traffic-flows\" class=\"hash-link\" aria-label=\"Direct link to Seam 4: the seam has to move while traffic flows\" title=\"Direct link to Seam 4: the seam has to move while traffic flows\" translate=\"no\">​</a></h2>\n<p>The last one is smaller and travels further. A dispatch-shape change shipped\nconsumer-first: the client was taught to speak <strong>both</strong> the old and the new\nresponse shapes before any environment switched. Once the consumer is bilingual,\nthe two deploys can land in either order, and nothing is holding its breath.</p>\n<p>An interface change is two deploys, and the consumer goes first. That's the\nentire technique. It's worth saying out loud because the seam metaphor is\nusually static — a line in the architecture — when in practice a seam is also a\nthing you must be able to <em>move</em> without stopping the world. A boundary you can\nonly change during a maintenance window is a boundary you'll stop changing.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-four-seams-and-how-each-one-would-rust\">The four seams, and how each one would rust<a href=\"https://blog.catalex.co/the-model-is-a-component#the-four-seams-and-how-each-one-would-rust\" class=\"hash-link\" aria-label=\"Direct link to The four seams, and how each one would rust\" title=\"Direct link to The four seams, and how each one would rust\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Seam</th><th>What it abstracts</th><th>What crosses it routinely</th><th>How it would rust</th></tr></thead><tbody><tr><td>Model alias</td><td>Which model actually serves a request</td><td>Local dev routes the alias to a local runner; deployed routes elsewhere</td><td>A model column on the agent row</td></tr><tr><td>Sandbox Protocol</td><td>Where agent code executes</td><td>CI runs the whole system on the fake provider</td><td><code>if provider == \"microvm\"</code></td></tr><tr><td>Integration registry</td><td>Whether we wrapped it or proxy it</td><td>Every vendor added the same way — a directory, a decorator</td><td>A central elif chain; a per-vendor metering list</td></tr><tr><td>Dispatch shape</td><td>The runtime↔client response contract</td><td>Consumers taught both shapes before the switch</td><td>Changes that require both sides to land at once</td></tr></tbody></table>\n<p>Read the third column first. That's the only one that's load-bearing. The first\ntwo columns describe every model-agnostic architecture diagram ever drawn,\nincluding the ones belonging to teams who'd need three weeks to change a model.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-actually-costs\">What this actually costs<a href=\"https://blog.catalex.co/the-model-is-a-component#what-this-actually-costs\" class=\"hash-link\" aria-label=\"Direct link to What this actually costs\" title=\"Direct link to What this actually costs\" translate=\"no\">​</a></h2>\n<p>Model-agnosticism is not a property you announce. It's a maintenance cost you\nagree to pay, forever, in exchange for never being trapped. You pay it in a\nProtocol that's harder to write than the concrete class would have been. You pay\nit in a fake provider that must be kept faithful. You pay it in a capability\nflag where a provider check would have taken ten seconds. You pay it on days\nwhen nothing is being swapped, which is most days, which is the point.</p>\n<p>The way to know whether you're actually paying it is simple, and you can run it\non your own system this afternoon: name the last time something crossed the\nseam. Not a plan to. Not a test that would. An actual crossing, with a date.</p>\n<p>If you can't name one, the seam is decorative. The model isn't a component you\nswap — it's a component you married.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/the-model-is-a-component",
            "title": "The Model Is a Component: Designing for the Swap",
            "summary": "Model-agnosticism isn't a claim, it's a seam — and a seam nothing crosses has already rusted shut. How we keep four seams honest: the model behind an alias, the sandbox behind a Protocol, integrations behind one shape, and rollout.",
            "date_modified": "2026-07-13T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Model Routing",
                "Agent Harness",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/deny-by-default",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 12, 2026 · 09:00 UTC</div>\n<p>A chatbot that says something wrong is embarrassing. An agent that sends\nsomething wrong is a fact about the world. The moment an agent can send, post,\ncreate, or delete, it stops being a text generator and becomes a principal in\nyour system — with credentials and reach, taking actions other people receive.</p>\n<p>The instinct at that point is to write a better system prompt. Tell the model to\nbe careful. Tell it to confirm before sending. This doesn't work, and it doesn't\nfail in an exotic way — it fails because a system prompt is a suggestion\naddressed to the thing you're trying to constrain.</p>\n<!-- -->\n<p>Permissions for agents are an ordinary authorization problem. Same shape as any\nother: a caller, a resource, an action, a decision. But the caller has two\nproperties you don't usually design for. It is <strong>non-deterministic</strong> — the same\ninput can produce a different tool call tomorrow. And it is <strong>persuadable</strong> — the\ncontent it reads can change what it decides to do. A retrieved document, a\nmessage body, a web page: all of it is input, and any of it can argue.</p>\n<p>So you design accordingly. The rest of this is where the gate goes, and how it\nfails.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-gate-and-the-executor-is-dumb\">One gate, and the executor is dumb<a href=\"https://blog.catalex.co/deny-by-default#one-gate-and-the-executor-is-dumb\" class=\"hash-link\" aria-label=\"Direct link to One gate, and the executor is dumb\" title=\"Direct link to One gate, and the executor is dumb\" translate=\"no\">​</a></h2>\n<p>The tool gate is the sole approval authority. Not the primary one — the sole one.\nThe sandbox where the agent's code actually runs is a pure executor, and the\ndesign intent is written down in exactly those terms:</p>\n<blockquote>\n<p>The box is a PURE EXECUTOR: every piece of tool METADATA (catalog, enabled\nbindings, allowed accounts) is resolved run-side. The box NEVER calls the gate\nto discover tools — it has no whole-catalog access and cannot list. Its tools\nare therefore fixed at creation/dispatch time.</p>\n</blockquote>\n<p>The load-bearing word is <em>discover</em>. An executor that can enumerate its own\ncapabilities can be talked into enumerating more of them — a persuadable caller\nplus a listable catalog is a negotiation, and you've handed the model the agenda.\nFixing the toolset at dispatch and denying the box any catalog access means the\nblast radius is decided before the model gets a turn. Whatever the run argues for\nlater, it argues from a fixed hand.</p>\n<p>The second half of that principle matters just as much: two components both\nmaking authorization decisions is not defense in depth. It's two sources of\ntruth, and one of them will be stale. Depth is layers that <em>narrow</em> — a gate,\nthen a scope, then an audit. Two peers independently answering \"is this allowed?\"\nisn't depth; it's a race where the wrong answer only has to win once.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fail-closed-at-the-bottom-of-the-function\">Fail closed, at the bottom of the function<a href=\"https://blog.catalex.co/deny-by-default#fail-closed-at-the-bottom-of-the-function\" class=\"hash-link\" aria-label=\"Direct link to Fail closed, at the bottom of the function\" title=\"Direct link to Fail closed, at the bottom of the function\" translate=\"no\">​</a></h2>\n<p>Every permission resolver is a cascade. Check the user's setting. Check the\nbinding. Check the catalog. Check the tier above. And then, at the bottom of the\nfunction, after every rule has declined to match, there is a <code>return</code> — and that\nreturn is your actual security posture. Everything above it is decoration.</p>\n<p>Ours ends with an explicit default: manual approval required. Not \"allow, because\nwe didn't find a rule against it.\" <em>Require a human, because we didn't find a\nrule.</em></p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Nothing matched: no user override, no binding rule, no catalog entry.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># This is not \"we found nothing to object to\" — it is \"we know nothing.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># An unknown action is exactly the case where a human should look.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> GuardrailDecision</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    requires_approval</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    tier</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"default\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    reason</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"no matching rule; unknown actions require manual approval\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The failure mode this prevents is quiet. A new toolkit gets added, nobody writes\na rule, and a permissive default means it's live and unsupervised on day one —\nnot because anyone decided that, but because a fallthrough decided it. Write the\ndefault branch first, and the rest of the cascade can only ever <em>loosen</em> from a\nsafe floor.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"allowlist-reads-gate-everything-else\">Allowlist reads, gate everything else<a href=\"https://blog.catalex.co/deny-by-default#allowlist-reads-gate-everything-else\" class=\"hash-link\" aria-label=\"Direct link to Allowlist reads, gate everything else\" title=\"Direct link to Allowlist reads, gate everything else\" translate=\"no\">​</a></h2>\n<p>The tempting move is a blocklist: enumerate the dangerous actions and gate those.\nThis is an infinite, ever-growing set, maintained by hand, always one integration\nbehind. You lose that race permanently on the first day you run it.</p>\n<p>So we inverted it. The platform seeds an explicit auto-allowlist of <strong>2,298 read\nactions across 86 toolkits</strong>, derived mechanically from the integration catalog's\nown schemas. Anything not on the list is manual by default. The list grows when\nthe catalog grows, by the same mechanism, with no committee.</p>\n<p>Take a mail toolkit as the concrete case. Exactly <strong>27 actions are auto-allowed\n— every one of them a read.</strong> Fetch a message. List drafts. Read a profile. List\nhistory. Zero send actions. Zero create, reply, or delete. An agent that decides\nto send mail surfaces an approval, always, unless the user has explicitly chosen\notherwise for themselves.</p>\n<table><thead><tr><th>Action class</th><th>Default</th><th>Why</th></tr></thead><tbody><tr><td>Read / list / fetch</td><td>Auto-allowed</td><td>Recoverable. Re-running it just repeats the request.</td></tr><tr><td>Send / post / reply</td><td>Manual approval</td><td>Lands in someone else's inbox. Not undoable.</td></tr><tr><td>Create / update</td><td>Manual approval</td><td>Changes state others depend on.</td></tr><tr><td>Delete</td><td>Manual approval</td><td>The one with no inverse at all.</td></tr><tr><td>Anything unrecognized</td><td>Manual approval</td><td>We don't know what it is. That's the point.</td></tr></tbody></table>\n<p>The rule underneath the table is the useful part: <strong>reads are recoverable, writes\nare not.</strong> That asymmetry — not a threat model, not a severity score — is the\ncleanest line available, and the only one we've found that a machine can draw\nfrom a schema instead of a human drawing it from judgment in a meeting. Severity\nscores need someone to assign them and someone else to disagree. \"Does this\nmutate anything?\" is a property of the API.</p>\n<div class=\"cx-note\"><p><strong>Defaults are written for onboarding, not for you.</strong> Wiring up a chat platform's\nbot, we hand-assembled the permission integer bit by bit rather than taking the\nintegration's suggested default — because the convenient default granted full\nadministrator rights. Least privilege is work. The default is someone else's\noptimization, and their objective was time-to-first-success, not your blast\nradius.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"permissions-are-personal\">Permissions are personal<a href=\"https://blog.catalex.co/deny-by-default#permissions-are-personal\" class=\"hash-link\" aria-label=\"Direct link to Permissions are personal\" title=\"Direct link to Permissions are personal\" translate=\"no\">​</a></h2>\n<p>Guardrails resolve from the <strong>invoking user's</strong> own settings plus the catalog.\nNothing another person can edit changes your approval behavior.</p>\n<p>This sounds like a small storage decision. It's actually the whole multi-tenant\nargument. A shared \"this agent is trusted\" flag means one teammate's convenience\nsilently becomes everyone's standing exposure — including people who never saw\nthe toggle and have no reason to suspect their approvals stopped firing. \"I trust\nthis agent\" is a statement about <em>you</em>. It has to be stored against <em>you</em>, or it\nisn't the statement you thought you were making.</p>\n<p>Configuration history is append-only. A change soft-deletes the old row and\ninserts a new version, so every past configuration stays queryable — you can ask\nwhat the rules were at any moment, not just what they are now. And the gate logs\neach decision <em>with the tier that won it</em>. That detail earns its keep: \"the agent\nwas allowed\" is not an answer to any question worth asking. \"The agent was\nallowed by this rule, set by this person, at this time\" is.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"approval-state-must-not-lie-to-the-retry-layer\">Approval state must not lie to the retry layer<a href=\"https://blog.catalex.co/deny-by-default#approval-state-must-not-lie-to-the-retry-layer\" class=\"hash-link\" aria-label=\"Direct link to Approval state must not lie to the retry layer\" title=\"Direct link to Approval state must not lie to the retry layer\" translate=\"no\">​</a></h2>\n<p>Here's the one that took real thought.</p>\n<p>Tool calls carry idempotency keys, so retries are safe. Standard. But an approval\ngate returns HTTP 200 with no error — and it is <em>not an executed call</em>:</p>\n<blockquote>\n<p>An <code>approval_required</code> GATE response is HTTP 200 with no <code>error</code> key but is NOT\nan executed call — the tool was suspended for the user's decision and never\nran. Recording it would dedup the REAL execution on the resume re-invoke (the\nbox presents the SAME idempotency key after approval), so the approved tool\nwould return the \"already ran\" duplicate guidance with no result.</p>\n</blockquote>\n<p>Read that failure through: a user approves an action, the resume arrives with the\nsame idempotency key, the store says \"seen it,\" and the tool the human explicitly\napproved never runs — while everything reports success. The gate would have\nconverted an approval into a silent no-op.</p>\n<p>So the idempotency store records <strong>only completed calls</strong>. A gate is a pause, not\nan outcome. Approval ids are deterministic — <code>{run_id}:{tool_call_id}</code> — so the\nsame decision resolves the same call across a resume. The general lesson: any\n\"we've seen this\" cache needs a crisp definition of <em>seen</em>, and \"returned 200\" is\nnot it. Suspension isn't completion.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"write-your-errors-for-the-model-because-the-model-is-the-client\">Write your errors for the model, because the model is the client<a href=\"https://blog.catalex.co/deny-by-default#write-your-errors-for-the-model-because-the-model-is-the-client\" class=\"hash-link\" aria-label=\"Direct link to Write your errors for the model, because the model is the client\" title=\"Direct link to Write your errors for the model, because the model is the client\" translate=\"no\">​</a></h2>\n<p>When a genuine duplicate <em>is</em> detected, the response is HTTP 200 carrying prose.\nNot a 409. Not an error code. And the reason is precise: a raw error would read\nas a transient failure the model might blindly retry.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">DUPLICATE TOOL CALL — this exact call already completed on an earlier attempt;</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">your previous response was most likely lost in transit. The call ran exactly</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">once and was NOT lost. If this tool WRITES or takes an action (send, create,</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">update, delete, post, pay, …) it is already done — do NOT call it again. If it</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">is a READ / query and you still need the data, issue a NEW call to fetch it</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">again...</span><br></div></code></pre></div></div>\n<p>This generalizes further than it looks. When your caller is a language model,\nyour API's error strings are prompt engineering. A 409 is a token sequence whose\ntraining-data association is <em>transient conflict, back off, try again</em> — so it\nteaches the model to retry the send. A sentence teaches it what happened. Status\ncodes were designed for clients that parse; you now have a client that reads.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-dont-have\">What we don't have<a href=\"https://blog.catalex.co/deny-by-default#what-we-dont-have\" class=\"hash-link\" aria-label=\"Direct link to What we don't have\" title=\"Direct link to What we don't have\" translate=\"no\">​</a></h2>\n<p>Approval is <strong>binary per action</strong>: auto or manual. There's no \"draft for\napproval\" tier, no read/draft/send gradient. That's a real limit, and it's worth\ndefending honestly rather than pretending it's finished design: a binary gate is\nlegible and enforceable — you can describe it in one sentence and verify it in\none query. A gradient is more expressive and much easier to get subtly wrong. A\npermission system that's subtly wrong is worse than one that's coarsely right,\nbecause you'll believe it.</p>\n<p>Approval scopes are defined for both a single call and a whole session. Only the\nsingle-call scope is wired today.</p>\n<p>And the one nobody wants to say out loud: an approval gate's real enemy isn't\nbypass, it's fatigue. A gate that fires on everything trains people to click\napprove without reading — worse than no gate at all, because it manufactures the\n<em>appearance</em> of oversight. You get the audit trail, the sign-off, the timestamp,\nand none of the thinking. That is the actual argument for allowlisting reads. Not\nconvenience. Preserving the meaning of the clicks that remain.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-boundary-lives-outside-the-loop\">The boundary lives outside the loop<a href=\"https://blog.catalex.co/deny-by-default#the-boundary-lives-outside-the-loop\" class=\"hash-link\" aria-label=\"Direct link to The boundary lives outside the loop\" title=\"Direct link to The boundary lives outside the loop\" translate=\"no\">​</a></h2>\n<p>An agent that can act needs a boundary that exists outside its own reasoning. Not\na stronger instruction, not a more emphatic prompt, not a self-check the model\nperforms on itself — those all live inside the thing you're trying to constrain,\nand a persuadable caller auditing its own persuasion is not a control.</p>\n<p>The model can be talked into things. That's not a defect; it's most of what makes\nit useful. A deny-by-default gate that fails closed at the bottom of the\nfunction, allowlists only what's recoverable, resolves against the person who\nactually asked, and logs who allowed what and when — that cannot be talked into\nanything. It doesn't listen. That's the feature.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/deny-by-default",
            "title": "Deny by Default: Permissions for Agents That Act",
            "summary": "The moment an agent can send, post, or delete, it becomes a principal in your system — and a system prompt is not an access control. How we designed a deny-by-default tool gate, and the places it gets subtle.",
            "date_modified": "2026-07-12T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Governance",
                "AI in Production",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/judging-the-judge",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 11, 2026 · 09:00 UTC</div>\n<p>We argued that you can't ship what you can't measure. Here's the uncomfortable\nhalf of that sentence: your measurement is a model too, and it is also wrong\nsometimes.</p>\n<!-- -->\n<p>We score agent runs with an LLM judge — it reads a run and grades it against the\nKPIs that agent was given. That judge is treated, by everyone downstream of it,\nas an instrument. A thermometer. Numbers come out; people make decisions. But a\njudge is not a thermometer. It's a second fallible system wearing a lab coat,\nand every number your team trusts is resting on it.</p>\n<p>So we built an eval of the judge. An LLM-as-judge eval of an LLM-as-judge. This\npost is what it found — including the part where the judge was starving for\nevidence and we nearly rewrote the rubric instead.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-eval-has-to-drive-the-real-thing\">The eval has to drive the real thing<a href=\"https://blog.catalex.co/judging-the-judge#the-eval-has-to-drive-the-real-thing\" class=\"hash-link\" aria-label=\"Direct link to The eval has to drive the real thing\" title=\"Direct link to The eval has to drive the real thing\" translate=\"no\">​</a></h2>\n<p>The first design decision is the one most people get wrong, and it's not\nsubtle: the eval drives the exact production path. Same prompt file, same input\nrenderer, same parser, same model alias.</p>\n<p>That sounds like hygiene. It's load-bearing. The moment your eval builds its own\ntidy little prompt and calls the model directly, it is measuring a system that\ndoes not exist. Every divergence — a renderer your eval skips, a parser it\nreimplements more forgivingly — is a place where the eval is green and production\nis not. You didn't evaluate your judge. You evaluated a fictional cousin of it\nthat happens to share a rubric.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"thirteen-ways-a-judge-goes-wrong\">Thirteen ways a judge goes wrong<a href=\"https://blog.catalex.co/judging-the-judge#thirteen-ways-a-judge-goes-wrong\" class=\"hash-link\" aria-label=\"Direct link to Thirteen ways a judge goes wrong\" title=\"Direct link to Thirteen ways a judge goes wrong\" translate=\"no\">​</a></h2>\n<p>The suite started at 17 synthetic scenarios across 13 failure families, plus 7\npairwise ordering rules. It later grew to 24 scenarios and 9 orderings. Real-run\nfixtures with hand-labeled score bands sit alongside the synthetics, because\nsynthetic scenarios test the failure you imagined and real runs test the ones you\ndidn't.</p>\n<p>The failure families are the part worth stealing. None of them are obvious until\na judge has burned you:</p>\n<p><strong>Calibration</strong> — does the score land in the band a human would give?\n<strong>Correctness-trap</strong> — a confident, articulate, <em>wrong</em> answer must score near\nzero. This is the one that separates a judge from a style critic.\n<strong>Sycophancy</strong> — the agent claims success; the tool results say it failed. Who\ndoes the judge believe? A judge that takes the agent's word for it is a\ncompliance machine, not a grader.\n<strong>Grounding</strong> — fabricated citations.\n<strong>Injection</strong> — instructions embedded inside tool results, aimed at the judge.\nYour judge reads attacker-influenced text by design; treat it that way.\n<strong>Estimation</strong> — unmeasurable KPIs like \"time saved.\"\n<strong>Verbosity bias</strong> — does length buy score?\n<strong>Relevance</strong> — beautiful output, wrong topic.\n<strong>Truncation</strong> — a display artifact the judge must not read as incomplete work.</p>\n<p>Plus over-penalty, trajectory, structural load (eight KPIs at once), rubric\nanchoring, and autonomy. Thirteen families, because a judge doesn't fail in one\ndirection — it fails in whichever direction your rubric was quietest about.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"orderings-the-metric-that-survives-your-rubric-edits\">Orderings: the metric that survives your rubric edits<a href=\"https://blog.catalex.co/judging-the-judge#orderings-the-metric-that-survives-your-rubric-edits\" class=\"hash-link\" aria-label=\"Direct link to Orderings: the metric that survives your rubric edits\" title=\"Direct link to Orderings: the metric that survives your rubric edits\" translate=\"no\">​</a></h2>\n<p>If you take one thing from this post, take this one.</p>\n<p>Most judge metrics are absolute. In-band rate asks whether a score landed where a\nhuman would put it. That's useful and it is <em>fragile</em> — it drifts with every\nrubric edit, every anchor you reword, every criterion you add. Tighten the rubric\non Tuesday and Monday's calibration is partly fiction.</p>\n<p>Orderings don't drift. An ordering is a pairwise constraint: run A must score\nabove run B. Not by how much. Just above. Whatever your absolute numbers are\ndoing, whatever the anchors say this week, the thorough grounded run outranks the\nconfident fabricated one — or your judge is broken in a way no amount of decimal\nprecision will save.</p>\n<p>This is a <strong>calibration-free signal</strong>, and that property is the whole point. It\nsurvives rubric changes, prompt rewrites, and model swaps, because it asks the\njudge to do the thing you actually need it to do: distinguish better work from\nworse work. A judge emitting 74.3 and 68.1 looks precise. If it can't reliably\nput the right run on top, that precision is decoration.</p>\n<p>The rest of the metrics: in-band rate, parse/coverage rate, and stddev across\ntrials — trial-to-trial consistency, which is just asking whether the judge is\nstable against <em>itself</em> before you ask it to be right about anything else. The\nsuite passes only when in-band ≥ 0.9, parse ≥ 0.98, and <strong>all</strong> orderings hold.\nOrderings are the one with no partial credit. Six out of seven is a failing\ngrade.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"seven-iterations\">Seven iterations<a href=\"https://blog.catalex.co/judging-the-judge#seven-iterations\" class=\"hash-link\" aria-label=\"Direct link to Seven iterations\" title=\"Direct link to Seven iterations\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Iteration</th><th>Change</th><th>In-band</th><th>Orderings</th><th>Stddev</th></tr></thead><tbody><tr><td>baseline</td><td>—</td><td>76.8%</td><td>6/7</td><td>11.0</td></tr><tr><td>1</td><td>renderer: final response 800→4000 chars, trail summaries 100→400</td><td>91.7%</td><td>6/7</td><td>7.9</td></tr><tr><td>2</td><td>prompt: KPI independence, fraction rule, citation matching</td><td>94.0%</td><td>6/7</td><td>5.7</td></tr><tr><td>3</td><td>renderer: tool-call arguments added to the decision trail</td><td>95.8%</td><td>6/7</td><td>5.7</td></tr><tr><td>4</td><td>prompt: calibration anchors reworked</td><td>96.8%</td><td>7/7</td><td>4.3</td></tr><tr><td>5</td><td>judge temperature 0; empty transcripts score ≤10</td><td>98.2%</td><td>7/7</td><td>4.0</td></tr><tr><td>6</td><td>head+tail windowing + adversarial scenarios added</td><td>91.0%†</td><td>9/9</td><td>7.8</td></tr><tr><td>7</td><td>prompt: truncation is a display artifact; padding earns nothing</td><td>95.2%†</td><td>9/9</td><td>5.9</td></tr></tbody></table>\n<p>† Iterations 6–7 run against the harder 24-scenario suite and are <strong>not\ncomparable</strong> to iterations 1–5. The suite got harder; the number went down. We\ncould have reported 98.2% and stopped at iteration 5, and it would have been the\nbetter headline. It would also have been the exact failure this post is about.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-prompt-is-usually-not-the-problem\">The prompt is usually not the problem<a href=\"https://blog.catalex.co/judging-the-judge#the-prompt-is-usually-not-the-problem\" class=\"hash-link\" aria-label=\"Direct link to The prompt is usually not the problem\" title=\"Direct link to The prompt is usually not the problem\" translate=\"no\">​</a></h2>\n<p>Read the Change column again. The two biggest wins are iterations 1 and 3, and\nneither one touched the rubric. Both are <strong>renderer</strong> changes. They changed what\nthe judge could <em>see</em>, not what it was asked.</p>\n<p>Iteration 1 moved in-band fifteen points by raising the final-response window\nfrom 800 to 4,000 characters and tool summaries from 100 to 400. Nothing about\nthe judge's instructions changed. We just stopped hiding the evidence from it.</p>\n<p>Call it <strong>evidence starvation</strong>. A judge asked to verify grounding, handed\n100-character tool summaries, cannot verify anything — the citation it's meant to\ncheck got truncated three fields ago. So it does what any reasonable grader does\nwith missing evidence and a demand for a verdict: it rates good work as\nfabricated and incomplete. The rubric was fine. The rubric was never the problem.\nThe judge was reading through a keyhole.</p>\n<p>Iteration 3 is the same lesson in a different key: tool-call <em>arguments</em> were\nmissing from the decision trail, so the judge could see that a tool ran but not\nwhat it was asked. That's not a rubric gap. That's a hole in the evidence.</p>\n<p>Nearly everyone tunes the prompt first. It's the most available surface — a text\nfile, right there, infinitely adjustable. So teams write increasingly elaborate\ninstructions to a judge that structurally cannot see the thing they're\ninstructing it about. <strong>Before you rewrite the rubric, check whether your judge\ncan actually see the thing you're asking it about.</strong> Most judge quality problems\nare platform problems in a prompt costume.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"hash-what-the-judge-reads-not-what-the-human-reads\">Hash what the judge reads, not what the human reads<a href=\"https://blog.catalex.co/judging-the-judge#hash-what-the-judge-reads-not-what-the-human-reads\" class=\"hash-link\" aria-label=\"Direct link to Hash what the judge reads, not what the human reads\" title=\"Direct link to Hash what the judge reads, not what the human reads\" translate=\"no\">​</a></h2>\n<p>Two mechanisms keep the numbers honest over time, and both are about\ncomparability — the property you destroy by accident.</p>\n<p>Each rubric carries a <strong>content hash of its static prompt parts</strong>. Verdicts\nproduced under different rubric text are never aggregated together. Change the\nrubric, get a new hash, get a new bucket. You cannot accidentally average\nyesterday's scores with today's under a different definition of \"good,\" because\nthe system won't let the two meet. Every dashboard that silently mixes verdicts\nacross rubric versions is reporting a number that means nothing.</p>\n<p>The nice detail: display-only help text is deliberately <strong>excluded</strong> from the\nhash. Improving the human-facing explanation shouldn't trigger a pointless\nre-judge of the corpus. Hash what the judge reads, not what the human reads.</p>\n<p>The same instinct shows up in rubric edits. When one rubric was hardened against\nover-tagging, the output criteria and schema were left unchanged specifically so\nscores stay comparable. Improving your judge while destroying your ability to\ncompare to last month is a bad trade — and it's one you make by accident, in a\ncommit that felt like pure upside.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"guards-keyed-on-what-the-judge-already-sees\">Guards keyed on what the judge already sees<a href=\"https://blog.catalex.co/judging-the-judge#guards-keyed-on-what-the-judge-already-sees\" class=\"hash-link\" aria-label=\"Direct link to Guards keyed on what the judge already sees\" title=\"Direct link to Guards keyed on what the judge already sees\" translate=\"no\">​</a></h2>\n<p>When a judge over-tags \"missing tool call,\" the instinct is to write it a\nlecture. Lectures are weak. A guard tied to metadata already in front of the\njudge is strong: emit that tag <em>only</em> when run metadata shows zero tool calls\n<strong>and</strong> the request needed external data; <strong>never</strong> emit it when the tool count\nis above zero.</p>\n<p>That guard only works if its premise is true, so we verified in code that the\ntool count really is rendered in the block the judge reads. That step is not\noptional and it is routinely skipped. A guard whose premise is false is worse\nthan no guard, because now you've got a rule that reads as rigorous and fires on\nvibes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-still-cant-claim\">What we still can't claim<a href=\"https://blog.catalex.co/judging-the-judge#what-we-still-cant-claim\" class=\"hash-link\" aria-label=\"Direct link to What we still can't claim\" title=\"Direct link to What we still can't claim\" translate=\"no\">​</a></h2>\n<p>Two admissions, and they're the point.</p>\n<p><strong>A guard in place is not a problem fixed.</strong> Proving it requires a re-judge of\nthe corpus. Shipping the guard and declaring victory is precisely the error the\nwhole apparatus exists to catch — asserting a result you didn't measure, with\nmore confidence than the evidence supports. Judges do it. So do we.</p>\n<p><strong>Even at temperature 0, a judge is not fully deterministic across serving\nproviders.</strong> Perfect reproducibility isn't on the menu. Knowing your noise floor\nis — which is why stddev across trials is a first-class metric rather than a\nfootnote. You can't eliminate the noise. You can refuse to mistake it for signal.</p>\n<p>Two more places where the honest answer is a shape, not a number. Where tool\noutput isn't captured, the grounding criterion returns <strong>unknown</strong> — not\n<strong>fail</strong>. A judge that can't see the evidence must say so rather than guess;\nabsence of proof is not proof of fabrication. And a run that legitimately\nsuspended or failed is judged on its trajectory so far, not penalized for a final\nanswer it had no reason to produce.</p>\n<p>The eval is the product. And the judge is part of the eval — which means every\nnumber you trust is resting on a system that, until you evaluate it, nobody has\nevaluated. The judge doesn't announce that it's drifting. It just keeps\nreturning numbers, in the same confident format, forever.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/judging-the-judge",
            "title": "Judging the Judge: When Your Eval Is the Thing That's Wrong",
            "summary": "An LLM judge is not an instrument — it's another fallible system that needs its own eval. How we built an eval of our judge, and why the two biggest accuracy wins were platform fixes, not prompt fixes.",
            "date_modified": "2026-07-11T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Evaluation",
                "Loop Engineering",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/token-efficient-agents",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 10, 2026 · 09:00 UTC</div>\n<p>Most advice about token efficiency is prompt golf. Trim the system prompt. Cut\nthe pleasantries. Replace the paragraph with a bullet. Shave two hundred tokens\noff the instructions and feel briefly virtuous about it.</p>\n<p>It's the wrong game. Two hundred tokens is a rounding error next to a single\ntool result you didn't need to look at, dragged forward through every turn of a\nthirty-turn run. The wins that matter are <strong>structural</strong>: they come from\ndeciding what never enters the context window in the first place. Not what gets\nsaid more tersely — what never gets said at all.</p>\n<p>This is a practical guide to making that decision well. Four moves, each one a\nplace where a real agent either touches something or doesn't, and then a section\non what to do about it when you sit down to build your own.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-prompt-golf-loses\">Why prompt golf loses<a href=\"https://blog.catalex.co/token-efficient-agents#why-prompt-golf-loses\" class=\"hash-link\" aria-label=\"Direct link to Why prompt golf loses\" title=\"Direct link to Why prompt golf loses\" translate=\"no\">​</a></h2>\n<p>A system prompt is written once and sent every turn. So is the tool list. So is\nevery tool result you left sitting in the message history. The prompt is the\nonly one of those three you can see in a text editor, which is why it's the only\none anybody optimizes — and it's usually the smallest.</p>\n<p>Here's the asymmetry that decides everything: <strong>the context window is not a\nbudget you spend, it's a budget you re-spend.</strong> A message that enters the\nhistory on turn four is present on turn five, and six, and every turn after,\nuntil something removes it. Put a 60,000-character API response in context on\nturn four of a twenty-turn run and you haven't made that decision once. You've\nmade it sixteen times.</p>\n<p>Prompt golf optimizes the constant. Structure optimizes the multiplier.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-four-moves\">The four moves<a href=\"https://blog.catalex.co/token-efficient-agents#the-four-moves\" class=\"hash-link\" aria-label=\"Direct link to The four moves\" title=\"Direct link to The four moves\" translate=\"no\">​</a></h2>\n<table><thead><tr><th></th><th>The naive version</th><th>The structural version</th></tr></thead><tbody><tr><td><strong>Large results</strong></td><td>Tool returns 60,000 chars into the message history</td><td>Preview + a file reference; the bulk is queried, never carried</td></tr><tr><td><strong>Repetition</strong></td><td>Fifty tool calls across fifty turns</td><td>One loop inside one code cell, one turn</td></tr><tr><td><strong>Tool surface</strong></td><td>Bind forty tools, ship all forty schemas every turn</td><td>Names in the prompt; fetch the schema you're about to use</td></tr><tr><td><strong>Context pressure</strong></td><td>Fight compaction, or hit it at full speed</td><td>Arrive at it rarely; let it run early when you do</td></tr></tbody></table>\n<p>Every row is the same instinct applied to a different surface. Let's take them\none at a time.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"move-1-keep-the-bulk-out-of-context-entirely\">Move 1: keep the bulk out of context entirely<a href=\"https://blog.catalex.co/token-efficient-agents#move-1-keep-the-bulk-out-of-context-entirely\" class=\"hash-link\" aria-label=\"Direct link to Move 1: keep the bulk out of context entirely\" title=\"Direct link to Move 1: keep the bulk out of context entirely\" translate=\"no\">​</a></h2>\n<p>Our tool layer enforces an offload contract. Any tool result over roughly 4,000\ncharacters never enters the message history at all. What the model gets back is\nan ~800-character preview and a <strong>file reference</strong> — a handle to the full\nresult, sitting on disk, which the agent queries with SQL when it actually needs\nsomething out of it.</p>\n<p>The instruction the agent carries is blunt on purpose: <em>the bulk is NEVER in\ncontext; query it.</em></p>\n<p>The preview is the part people notice. The consequence is the part that matters:\na large result never enters the message history, and therefore never enters the\ncheckpoint either — so it isn't re-sent on every subsequent turn.</p>\n<div class=\"cx-note\"><p><strong>A big result you put in context isn't paid for once.</strong> It's paid for on every\nturn after. This is the single most underrated fact about agent design: the\nmessage history is not a log, it's an input, and it is re-read from scratch on\nevery single turn until something takes it out.</p></div>\n<p>These reference files are read-only by instruction, and the reason is\nunglamorous and useful: they can run to tens or hundreds of megabytes. Opening\none whole would exhaust the sandbox's memory long before it exhausted anything\nelse. The file is not a smaller version of the result. It's the full result,\nsomewhere the model doesn't have to look at all of it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"move-2-loop-in-code-not-in-turns\">Move 2: loop in code, not in turns<a href=\"https://blog.catalex.co/token-efficient-agents#move-2-loop-in-code-not-in-turns\" class=\"hash-link\" aria-label=\"Direct link to Move 2: loop in code, not in turns\" title=\"Direct link to Move 2: loop in code, not in turns\" translate=\"no\">​</a></h2>\n<p>The agent has a real Python kernel, and it's taught a three-tier ladder for\ndeciding how to act:</p>\n<ol>\n<li class=\"\"><strong>Answer directly</strong> — no tool at all.</li>\n<li class=\"\"><strong>Call a tool directly</strong> — for ONE discrete action. Don't wrap a one-shot\ncall in code.</li>\n<li class=\"\"><strong>Write and run code</strong> — for orchestration: chaining, looping, bulk fetching.</li>\n</ol>\n<p>The rule that does the work sits at tier three: <em>calling the same tool across a\nlist is a LOOP — write it once in code.</em></p>\n<p>Consider fetching fifty records. Done one per turn, that's fifty round trips\nthrough the model, and — worse — fifty results sitting in the message history,\nre-sent on every turn that follows. Done in a loop inside one cell, it's one\nturn and one offloaded reference.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># The per-turn version: 50 turns, 50 results in history, 50 re-sends per turn.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">#   turn 1: get_record(id=\"r_001\") -&gt; {...}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">#   turn 2: get_record(id=\"r_002\") -&gt; {...}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">#   ... 48 more turns, each one carrying every result before it ...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># The in-code version: 1 turn, 1 offloaded reference, history untouched.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">records </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">get_record</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">id</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> record_ids</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 50 calls, 0 extra turns</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">open_ones </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">r </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> records </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"status\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"open\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">summary </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"total\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">records</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"open\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">open_ones</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">summary</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># small enough to enter context — that's the point</span><br></div></code></pre></div></div>\n<p>Only <code>summary</code> crosses back into the message history. The fifty records stay in\nkernel memory, and because the kernel persists across cells, they stay reachable\n— you don't re-fetch to re-use. State carries.</p>\n<p>The tier-two caveat is real, though, and worth respecting. A single discrete\naction wrapped in a code cell is <em>worse</em> than just calling the tool: you've added\na kernel round trip and a layer of indirection to buy nothing. The ladder has\nthree rungs because there are three right answers.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"move-3-names-in-the-prompt-bodies-on-demand\">Move 3: names in the prompt, bodies on demand<a href=\"https://blog.catalex.co/token-efficient-agents#move-3-names-in-the-prompt-bodies-on-demand\" class=\"hash-link\" aria-label=\"Direct link to Move 3: names in the prompt, bodies on demand\" title=\"Direct link to Move 3: names in the prompt, bodies on demand\" translate=\"no\">​</a></h2>\n<p>Every bound tool is a permanent line item. It's in the prompt on turn one and\nit's in the prompt on turn thirty, whether or not the agent ever calls it.</p>\n<p>So we split the tool surface in two. Tool <em>names</em> are all listed in the system\nprompt. Their parameter <em>schemas</em> are not. The agent fetches a schema only for\nthe tools it's about to use, and it's told to batch that fetch: <em>pass EVERY tool\nyou're about to use in ONE call, instead of calling this repeatedly — each call\nis a separate step.</em></p>\n<p>The name list is itself capped, at 50 names, after which it truncates with a\n<code>(+N more)</code>. That number is a judgment call, not a law of nature: long enough to\nfunction as a menu the model can browse, short enough not to become a tax on\nevery turn.</p>\n<p><strong>The same principle governs skills.</strong> A skill is loaded on demand, which means\nthe agent pays the token cost only when the skill is actually relevant to the\nturn. A skill you might need on one turn in fifty should not be present on the\nother forty-nine.</p>\n<p>And here's the transferable version for anyone building on the platform: <strong>bind\nfewer tools.</strong> An agent with forty tools pays for forty tools on turn one and\nturn thirty. Not because forty is too many in the abstract, but because most of\nthose forty were bound speculatively — \"it might need to update the record\" —\nand a speculative binding is overhead on every turn that follows.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"move-4-let-the-harness-compact-and-dont-fight-it\">Move 4: let the harness compact, and don't fight it<a href=\"https://blog.catalex.co/token-efficient-agents#move-4-let-the-harness-compact-and-dont-fight-it\" class=\"hash-link\" aria-label=\"Direct link to Move 4: let the harness compact, and don't fight it\" title=\"Direct link to Move 4: let the harness compact, and don't fight it\" translate=\"no\">​</a></h2>\n<p>Compaction is the safety net, and it works. It triggers at a threshold — 180,000\ntokens — keeps only the most recent few tool outputs in full, and reports\nexactly what it freed. There's a reactive path too, forcing compaction when the\nmodel itself reports an overflow the static threshold didn't predict. And rolling\nmemory consolidation runs at around 70% of the context budget and on a turn\ninterval: deliberately <em>before</em> the pressure point rather than at it, because a\nconsolidation with room to be thoughtful is a better one.</p>\n<p>All of that is machinery you get for free. But compaction is not free in the\nsense that matters: something has to read the history to summarize it, and\nwhatever gets summarized is no longer there in full. The goal isn't to compact\nefficiently. It's to <strong>arrive at compaction rarely</strong> — which loops straight back\nto Moves 1 and 2. An agent that offloads its bulk and loops in code approaches\nthe threshold slowly. One that carries every result forward sprints at it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"when-you-build-your-own-agent\">When you build your own agent<a href=\"https://blog.catalex.co/token-efficient-agents#when-you-build-your-own-agent\" class=\"hash-link\" aria-label=\"Direct link to When you build your own agent\" title=\"Direct link to When you build your own agent\" translate=\"no\">​</a></h2>\n<p>The four moves above are platform mechanics. These are the decisions that are\nyours, in roughly the order they'll bite you.</p>\n<p><strong>Bind the tools the agent needs, not the tools it might need.</strong> This is Move 3\nwith your name on it, and it's the one lever where you can do more damage than\nthe harness can undo. Every speculative binding is permanent overhead on every\nturn of every run.</p>\n<p><strong>Name the deliverable and the stopping condition.</strong> An agent that doesn't know\nwhat \"done\" looks like spends turns discovering it — and those exploratory turns\nare the most token-hungry kind, because each one adds to the history that the\nnext one re-reads. \"Produce a summary table of open items, grouped by owner;\nstop when every open item appears once\" is a brief. \"Look into the open items\"\nis an invitation to wander.</p>\n<p><strong>Set the KPI measurement interval to something sane.</strong> Measurement runs on\nevery Nth run, not every run. A KPI you measure every single run means a judged\nevaluation every single run — and a metric that moves slowly does not need\nmeasuring quickly. Weekly-drifting quality does not need a per-run verdict.</p>\n<p><strong>Keep KPI slots few.</strong> There are 8. Using all 8 because they're there means\njudging 8 criteria on every measured run. Pick the ones whose movement would\nactually change what you do.</p>\n<p><strong>Prefer one capable agent with a narrow brief</strong> over a broad agent that must\nfirst work out what you meant. Interpretation is a real workload, and you pay for\nit in turns before any of the useful work starts.</p>\n<p><strong>Match the schedule to the decision it feeds.</strong> A schedule that runs hourly for\na report nobody reads hourly is the most common waste there is. It's also the\neasiest to fix — nobody has to refactor anything, someone just has to admit the\nreport is a daily report.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-mindset\">The mindset<a href=\"https://blog.catalex.co/token-efficient-agents#the-mindset\" class=\"hash-link\" aria-label=\"Direct link to The mindset\" title=\"Direct link to The mindset\" translate=\"no\">​</a></h2>\n<p>Read the four moves back to back and they collapse into one instinct. Don't\ncarry the bulk — reference it. Don't repeat a call across turns — loop it in\ncode. Don't ship schemas for tools you aren't using — fetch them when you are.\nDon't sprint at the compaction threshold — arrive slowly.</p>\n<p>Every one of those is a version of the same question: <em>what does the model never\nhave to look at?</em></p>\n<p>The efficient agent isn't the one with the terser prompt. It's the one that\ntouches the least.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/token-efficient-agents",
            "title": "Spend Credits Like They're Yours: Building Token-Efficient Agents",
            "summary": "Token efficiency doesn't come from trimming your system prompt. It comes from four structural decisions about what never enters the context window at all — and how to make them when you build your own agent.",
            "date_modified": "2026-07-10T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Token Efficiency",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/knowing-when-to-stop",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 9, 2026 · 09:00 UTC</div>\n<p>Everyone who designs an agent loop spends their time on two steps. They tune the\ngenerate step, because that's where the intelligence appears to live. They\nharden the verify step, because that's where the discipline lives. Almost nobody\nbudgets serious design effort for the third step, which is the one that actually\ndetermines whether the loop is trustworthy: <strong>the stop.</strong></p>\n<p>Here is the uncomfortable mechanical fact underneath every loop you have ever\nbuilt. A turn that produces text with <strong>no tool call</strong> is the only place a run\ncan stop. That's it. That's the entire exit condition — the absence of a\nfunction call. And that single signal conflates three completely different\nsituations, which is why agents routinely \"finish\" tasks they never did.</p>\n<p>This post assumes you already have a loop — generate, verify, correct — and that\nit has burned you at least once. We're going one level down, into the exit.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-signal-three-meanings\">One signal, three meanings<a href=\"https://blog.catalex.co/knowing-when-to-stop#one-signal-three-meanings\" class=\"hash-link\" aria-label=\"Direct link to One signal, three meanings\" title=\"Direct link to One signal, three meanings\" translate=\"no\">​</a></h2>\n<p>When the model emits a turn with no tool call, exactly one of these is true:</p>\n<ul>\n<li class=\"\"><strong><code>done</code></strong> — the agent delivered a real, complete answer.</li>\n<li class=\"\"><strong><code>question</code></strong> — the agent is deliberately asking the user for something. An\nagent is <em>always</em> allowed to ask. A genuine ask is a first-class stopping\npoint, never a stall.</li>\n<li class=\"\"><strong><code>stalled</code></strong> — the agent stopped with the task demonstrably unfinished\n<em>without</em> asking: it narrated an action it never took, returned partial data,\nor skipped a step.</li>\n</ul>\n<p>Read that third one again, because it's the whole post. <em>Narrated an action it\nnever took.</em> Every agent builder recognises this instantly. \"I've updated the\nconfiguration and re-run the checks.\" No it hasn't. There is no tool call in\nthat turn. The model produced a paragraph describing work in the past tense, the\nloop saw no function call, the loop exited, and the harness handed a fluent lie\nto whoever was waiting.</p>\n<p>The naive loop treats all three as one event, because at the transport layer\nthey <em>are</em> one event: a message with an empty tool-call list. Left alone,\n<code>done</code>, <code>question</code>, and <code>stalled</code> all become \"the run ended successfully,\" and\nyour completion signal is decoupled from reality.</p>\n<p>So we put a gate exactly there — at the no-tool-call boundary — and make it\nbranch.</p>\n<!-- -->\n<p>This pattern has a name in coding agents — a <em>stop hook</em> — and it generalises\ncleanly to any agent loop. The interesting part isn't the shape. It's the four\ndesign decisions inside it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"decision-one-no-heuristics-on-purpose\">Decision one: no heuristics, on purpose<a href=\"https://blog.catalex.co/knowing-when-to-stop#decision-one-no-heuristics-on-purpose\" class=\"hash-link\" aria-label=\"Direct link to Decision one: no heuristics, on purpose\" title=\"Direct link to Decision one: no heuristics, on purpose\" translate=\"no\">​</a></h2>\n<p>The gate asks the LLM to classify the stop. There are deliberately <strong>no\nstring or phrase heuristics</strong>, because only the model that produced the turn can\nreason about whether it actually finished.</p>\n<p>This is the decision people push back on hardest, and it's the one we're most\nconfident about. The tempting move is obvious: regex the turn for \"I've\ncompleted\", \"Done!\", \"Let me know if\". Ship it in an afternoon, zero extra model\ncalls, deterministic.</p>\n<p>It's a trap, and the reason is structural. <strong>The sentence \"I've completed the\nmigration\" is exactly as likely to appear in a <code>done</code> turn as in a <code>stalled</code>\nturn.</strong> That's what makes stalls dangerous — a stalled agent is not confused or\nhedging. It's confident. It writes the completion sentence <em>because</em> it believes\nthe completion sentence. The surface text is identical in both cases; the only\ndifference is whether the preceding transcript contains the tool calls that\nwould make the sentence true. A regex cannot see that. It matches the claim, not\nthe evidence for the claim.</p>\n<p>The thing that <em>can</em> see it is a model looking at the turn and the work that\nled to it. Judging \"did this actually happen\" is a reading-comprehension problem\nover a transcript, and the only tool we have that does reading comprehension is\nthe model. So we use it, and we accept the cost of an extra call rather than\npretend a pattern match is a judgement.</p>\n<p>The gate reads a bounded window — the last 8 messages — not the whole run. The\nclassification is about the <em>stop</em>, not the history. You're asking \"is this turn\nhonest given what just happened\", not \"summarise the last forty minutes.\"</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"decision-two-put-the-expensive-check-at-the-boundary\">Decision two: put the expensive check at the boundary<a href=\"https://blog.catalex.co/knowing-when-to-stop#decision-two-put-the-expensive-check-at-the-boundary\" class=\"hash-link\" aria-label=\"Direct link to Decision two: put the expensive check at the boundary\" title=\"Direct link to Decision two: put the expensive check at the boundary\" translate=\"no\">​</a></h2>\n<p>The gate runs <strong>only</strong> at the no-tool-call boundary. Turns that emit tool calls\nroute straight to the tool node and never reach it.</p>\n<p>That one sentence is what makes the design affordable. A long run might be forty\nturns, thirty-eight of which call tools. Those thirty-eight never touch the\ngate. The judging happens at candidate stopping points — roughly once per run,\na handful of times if the agent stalls and recovers.</p>\n<p><strong>Generalise it: put your expensive check at the decision boundary, not in the\nhot loop.</strong> People reach for per-turn monitors and per-turn critics, discover\nthe overhead is untenable, and rip the whole idea out. Usually the check didn't\nneed to run per turn. It needed to run at the moment a decision becomes\nirreversible. Ending a run is irreversible — the answer goes to the user.\nCalling a tool isn't; another turn is coming that will re-examine everything.\nSpend your judgement where the door closes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"decision-three-the-plan-inverts-authority\">Decision three: the plan inverts authority<a href=\"https://blog.catalex.co/knowing-when-to-stop#decision-three-the-plan-inverts-authority\" class=\"hash-link\" aria-label=\"Direct link to Decision three: the plan inverts authority\" title=\"Direct link to Decision three: the plan inverts authority\" translate=\"no\">​</a></h2>\n<p>This is the strongest idea we have on the subject.</p>\n<p>When a run carries a plan, <strong>the plan — not the model and not the judge —\ndecides completion.</strong> Every step marked completed ⇒ stop. Any step unmarked ⇒\nthe harness rejects the stop and re-injects the specific next step, so the model\ndrives its own plan to completion and cannot hallucinate the whole task done\nwith work remaining.</p>\n<p>Sit with the inversion. The agent writes the plan. The plan then binds the\nagent. A model that decided at step two what needed to happen does not get to\ndecide at step five that actually it's finished — the artifact it produced when\nit had a clear view of the task now overrules the model that has drifted, gotten\ntired of the task, or convinced itself the remaining steps were optional.</p>\n<p>That's the trick, and it's more general than agents: <strong>externalise the\ncompletion criterion out of the model's judgement and into an artifact it can't\nretroactively edit.</strong> Completion stops being a vibe the model reports and starts\nbeing a property you can check. Six of seven boxes ticked is not a matter of\nopinion.</p>\n<p>And note what the rejection <em>does</em>. It doesn't scold. It doesn't say \"you're not\ndone, try harder.\" It re-injects the specific next step, because a loop is only\nas good as its feedback string — and \"step 4: verify the new records resolve\" is\na task, while \"incomplete\" is a mood.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"decision-four-a-budget-that-cant-be-gamed\">Decision four: a budget that can't be gamed<a href=\"https://blog.catalex.co/knowing-when-to-stop#decision-four-a-budget-that-cant-be-gamed\" class=\"hash-link\" aria-label=\"Direct link to Decision four: a budget that can't be gamed\" title=\"Direct link to Decision four: a budget that can't be gamed\" translate=\"no\">​</a></h2>\n<p>There's a cap of 20 continuations. The anti-gaming rule matters more than the\nnumber:</p>\n<p>Each stall consumes one unit, and <strong>a tool call in between does NOT refund it</strong>\n— so an agent that keeps making hollow tool calls but never completes (e.g. a\nfetch that always returns partial data) still terminates.</p>\n<p>Refunding on tool calls is the obvious-looking design and it's wrong. If real\nwork resets the budget, then anything shaped like real work resets the budget,\nand an agent stuck in a genuinely unresolvable situation — a source that always\nreturns half the rows — will loop forever while looking productive the entire\ntime. Non-refundable units mean the budget measures <em>stalls</em>, not activity.</p>\n<p>Same reasoning excludes bookkeeping tools. Updating the plan and marking a task\ncomplete don't count as real work. Otherwise an agent keeps its budget alive by\nticking its own boxes, which is a very sophisticated way of doing nothing.</p>\n<p>On the last unit the gate injects one \"wrap up with what you have\" nudge, so the\nrun ends with a best-effort answer rather than a dangling \"let me re-fetch.\" A\nbudget that expires silently mid-thought produces the worst possible artifact: a\ntruncated intention. Land the plane.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"decision-five-precedence-is-policy\">Decision five: precedence is policy<a href=\"https://blog.catalex.co/knowing-when-to-stop#decision-five-precedence-is-policy\" class=\"hash-link\" aria-label=\"Direct link to Decision five: precedence is policy\" title=\"Direct link to Decision five: precedence is policy\" translate=\"no\">​</a></h2>\n<p>One small thing at the router with a real lesson in it. The router checks the\nhard tool-budget stop first, before checking whether a user requested a stop,\nthen falls through to continuing.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">route</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># This is checked FIRST — a runaway that also has a</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># pending stop must still end.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">tool_calls </span><span class=\"token operator\" style=\"color:#393A34\">&gt;=</span><span class=\"token plain\"> TOOL_BUDGET</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"end\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">user_stop_requested</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"end\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"continue\"</span><br></div></code></pre></div></div>\n<p>Both branches end the run, so the order looks cosmetic. It isn't. The ordering\nencodes which authority wins when two conditions fire at once, and every one of\nthese little <code>if</code> chains is a policy document that nobody reviews as one. Write\nthem in the order you'd defend out loud: hard safety limits, then user intent,\nthen the default. <strong>In a router, precedence <em>is</em> policy</strong> — you are ranking your\nprinciples whether or not you meant to.</p>\n<div class=\"cx-note\"><p><strong>The honest tradeoff.</strong> The gate is a model call, which means it has a\nfalse-positive rate: sometimes it calls a real completion a stall and the agent\ndoes a redundant lap. We take that trade deliberately. A redundant lap costs\nlatency; a missed stall costs trust in every answer the system produces. When\nyour classifier is imperfect, choose the error you can survive.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-stop-is-really-for\">What the stop is really for<a href=\"https://blog.catalex.co/knowing-when-to-stop#what-the-stop-is-really-for\" class=\"hash-link\" aria-label=\"Direct link to What the stop is really for\" title=\"Direct link to What the stop is really for\" translate=\"no\">​</a></h2>\n<p>Upstream of the exit, everything is optional in a way you can talk yourself out\nof. You can ship a loop with a weak verifier and tell yourself the model is\nusually right. You can skip the plan. The system will look like it works.</p>\n<p>The stop is where that stops being true, because the stop is the only step whose\noutput the user actually sees. A loop that can't tell \"I'm done\" from \"I gave\nup\" from \"I need you\" will confidently hand you all three as the same thing —\nand the one it hands you most often is the middle one wearing the first one's\nclothes.</p>\n<p>Getting the stop right is what makes everything upstream of it worth building.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/knowing-when-to-stop",
            "title": "Knowing When to Stop: The Hardest Part of an Agent Loop",
            "summary": "An agent loop stops when a turn produces text with no tool call — and that one signal means three different things. How we classify the stop, why heuristics don't work, and how a plan can override the model's own sense of completion.",
            "date_modified": "2026-07-09T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Loop Engineering",
                "Agent Harness",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/budgets-that-stop-a-runaway-agent",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 8, 2026 · 09:00 UTC</div>\n<p>Ask an engineer how to stop an agent that won't stop, and you'll get the same\nanswer every time: add a limit. It's the right instinct and the wrong plan.\nThere is no single limit that catches every shape of runaway, because runaways\ndon't have one shape — and each shape slips past a different guard for a\ndifferent structural reason.</p>\n<!-- -->\n<p>The failure mode people picture is a model producing one bad answer. That's not\nthe interesting failure. The interesting failure is a loop that keeps making\nplausible, individually-reasonable calls forever. Every call, read on its own,\nis a call you would have approved. Only the hundredth one tells you anything is\nwrong — and by then the run has been busy a long time, doing work that looked\nlike progress the whole way down.</p>\n<p>That we build the harness rather than treat it as glue is settled ground here.\nThis post is one level deeper: the specific mechanics of putting a floor under\nan autonomous loop, and why bounding one takes four independent guards instead\nof a number in a config file.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-insight-the-whole-thing-hangs-on\">The insight the whole thing hangs on<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#the-insight-the-whole-thing-hangs-on\" class=\"hash-link\" aria-label=\"Direct link to The insight the whole thing hangs on\" title=\"Direct link to The insight the whole thing hangs on\" translate=\"no\">​</a></h2>\n<p>Start with the guard everyone reaches for first: refuse a call the agent has\nalready made. Deduplicate. If it asks the same question twice, it's stuck.</p>\n<p>That guard never fires.</p>\n<p>A runaway per-record loop calls the same tool with <strong>different arguments</strong> every\ntime. Record 1, record 2, record 3. Message A, message B, message C. No call\never repeats itself, so a de-duplicator sees a stream of perfectly novel,\nperfectly legitimate requests and waves each one through. The loop isn't\nrepeating. It's <em>enumerating</em> — and enumeration is indistinguishable from\ndiligence, one call at a time.</p>\n<p>Every wall below exists because some loop shape is invisible to the others.\nThat's the design constraint — not defense in depth for its own sake, but four\nguards keying on four genuinely different things, because a loop that's obvious\nalong one axis is featureless along the rest.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-1--the-per-tool-name-budget\">Wall 1 — the per-tool-name budget<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#wall-1--the-per-tool-name-budget\" class=\"hash-link\" aria-label=\"Direct link to Wall 1 — the per-tool-name budget\" title=\"Direct link to Wall 1 — the per-tool-name budget\" translate=\"no\">​</a></h2>\n<p>The first wall caps calls at <strong>40 per tool name</strong>, and it ignores arguments\nentirely.</p>\n<p>That last part is the point, and it's a direct response to the shape above: the\nobserved failure was the same tool called hundreds of times with <em>different</em>\nids, so an identical-call guard would not catch it. Strip the arguments out of\nthe identity and the loop becomes trivially visible. Forty reads is forty reads\nwhether it read the same record forty times or forty different ones.</p>\n<p>Crude on purpose. A tool name is the coarsest possible key, which is exactly why\nnothing about the arguments can hide from it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-2--the-aggregate-budget-and-the-trick-inside-it\">Wall 2 — the aggregate budget, and the trick inside it<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#wall-2--the-aggregate-budget-and-the-trick-inside-it\" class=\"hash-link\" aria-label=\"Direct link to Wall 2 — the aggregate budget, and the trick inside it\" title=\"Direct link to Wall 2 — the aggregate budget, and the trick inside it\" translate=\"no\">​</a></h2>\n<p>Wall 1 has an obvious hole: an agent that calls eight different tools thirty-nine\ntimes each never trips a per-name cap and has still done three hundred\noperations. So the second wall is tool-agnostic — a <strong>soft cap of 40 across all\nexternal tool operations</strong>. A mail read, a repository listing, and a knowledge\nsearch all count identically. There is no notion of a cheap call.</p>\n<p>The clever part isn't the cap. It's what happens past it.</p>\n<p>Past the cap, a call identity is refused <strong>once</strong>. An <em>identical</em> repeat is\nallowed through.</p>\n<p>Sit with that for a second, because it's doing something subtle. A runaway\nper-record loop iterates different args each call, so no call ever repeats its\nidentity and the loop stays refused — forever, every call, one after another. A\ngenuinely-required call proceeds on its confirmed retry: the agent gets told no,\ndecides the call really is necessary, asks again the same way, and gets it.</p>\n<p><strong>A loop cannot confirm itself, because confirming requires repeating — and the\nloop never repeats.</strong> The exact property that makes a runaway invisible to a\nde-duplicator is the property that keeps it behind this wall — the behavior that\ndefeats one guard is what the other is built to catch. The agent isn't asked to\nprove intent. The mechanics of its own loop answer for it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-3--the-delivery-reserve\">Wall 3 — the delivery reserve<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#wall-3--the-delivery-reserve\" class=\"hash-link\" aria-label=\"Direct link to Wall 3 — the delivery reserve\" title=\"Direct link to Wall 3 — the delivery reserve\" translate=\"no\">​</a></h2>\n<p>Send, post, deliver: these draw on a <strong>separate reserve of 5 operations</strong>,\nargument-independent, exempt from the data-gathering cap.</p>\n<p>Without it, the arithmetic goes badly. A run spends its entire budget reading and\nhas nothing left to deliver the result. All research, no answer — the worst place\nto stop, because all of the expensive work happened and none of it reached\nanyone. A budget that can strand its own output is a budget with a bug in it.</p>\n<p>There's an honest design note here too. The refuse-once-then-confirm cycle from\nWall 2 was too fragile for sends. It required the agent to repeat the <em>exact</em>\ncall — defeated the moment it reworded the message between attempts. And of\ncourse it rewords the message; that's a reasonable thing to do when you've been\ntold no.</p>\n<p><strong>A read is idempotent enough to retry; a send is not.</strong> Different semantics,\ndifferent budget. When a mechanism's assumptions don't hold for a class of tool,\nthe answer is a second mechanism, not a stretched first one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-4--the-query-loop-guard\">Wall 4 — the query-loop guard<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#wall-4--the-query-loop-guard\" class=\"hash-link\" aria-label=\"Direct link to Wall 4 — the query-loop guard\" title=\"Direct link to Wall 4 — the query-loop guard\" translate=\"no\">​</a></h2>\n<p>The last wall caps at <strong>10</strong> and keys on <code>(file_ref, normalized_sql)</code>. A\nnormalizer strips string and number literals down to a skeleton, so <code>WHERE ts='A'</code> and <code>WHERE ts='B'</code> collapse to the same signature.</p>\n<p>This catches the loop that both other guards structurally miss. A per-tool-name\ncount sees one tool used a reasonable number of times. An identical-call guard\nsees ten distinct queries. Neither is wrong. The loop is simply invisible along\nboth of their axes — and visible immediately along a third: same query shape,\ndifferent literal, over and over.</p>\n<p>Normalization is what makes an enumerating loop's signature stop moving.</p>\n<table><thead><tr><th>Wall</th><th>Keys on</th><th>Catches</th><th>Can't see</th></tr></thead><tbody><tr><td>Per-tool-name (40)</td><td>Tool name only</td><td>One tool hammered, any args</td><td>Many tools, each under cap</td></tr><tr><td>Aggregate (40, soft)</td><td>Call identity, all tools</td><td>Broad enumeration across tools</td><td>Nothing — but it only <em>refuses</em>, it can't end a run</td></tr><tr><td>Delivery reserve (5)</td><td>Send-class tools</td><td>Budget starvation before delivery</td><td>Data-gathering loops (by design)</td></tr><tr><td>Query loop (10)</td><td><code>(file_ref, normalized_sql)</code></td><td>Same query shape, rotating literals</td><td>Loops that vary query <em>structure</em></td></tr></tbody></table>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-hard-stop-above-them\">The hard stop above them<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#the-hard-stop-above-them\" class=\"hash-link\" aria-label=\"Direct link to The hard stop above them\" title=\"Direct link to The hard stop above them\" translate=\"no\">​</a></h2>\n<p>All four are soft. They refuse; they don't terminate. Above them sits a single\nhard aggregate cap on external tool operations — tunable per deployment, and\nsetting it to 0 disables it.</p>\n<p>The mechanism is worth showing because it's clean. At the cap, the harness\ninjects a terminal assistant message with <strong>no tool calls</strong>. The router reads\nthat marker and routes to <code>__end__</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># At the hard cap: don't raise. Speak, then let the router do its job.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> hard_cap </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> external_ops </span><span class=\"token operator\" style=\"color:#393A34\">&gt;=</span><span class=\"token plain\"> hard_cap</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    state</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">messages</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        assistant_message</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">hard_stop_notice</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">external_ops</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> hard_cap</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> state          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># note: no tool calls attached</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">route</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    last </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">messages</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> last</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">tool_calls</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tools\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__end__\"</span><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># a terminal message is already the end shape</span><br></div></code></pre></div></div>\n<p>No exception. No special termination path. The run ends at the graph's normal\ncompletion shape — the same one a successful run uses — because \"an assistant\nmessage with nothing left to call\" <em>already means</em> finished. This mirrors a\nprecedent in the same router, where a synthesis-complete message ends the run\nidentically. The hard stop didn't need new machinery. It needed to say the thing\nthe router already understood.</p>\n<p>And the terminal message is written for the user, not the log:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">I stopped after {n} tool calls — this run hit its hard limit of {cap} tool</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">calls, which guards against a runaway loop. I've summarised what I gathered so</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">far; if the task genuinely needs more tool calls, break it into smaller runs</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">or raise the agent's tool-call budget.</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"error-as-data-the-principle-underneath-all-of-it\">Error-as-data: the principle underneath all of it<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#error-as-data-the-principle-underneath-all-of-it\" class=\"hash-link\" aria-label=\"Direct link to Error-as-data: the principle underneath all of it\" title=\"Direct link to Error-as-data: the principle underneath all of it\" translate=\"no\">​</a></h2>\n<p>None of the four walls work as exceptions. Refusals are returned as tool\n<strong>errors</strong> the model reads and re-plans against.</p>\n<p>The governing rule: <strong>a refusal must never read like a success.</strong> Every refusal\nmessage states explicitly that the call did NOT execute.</p>\n<p>This sounds fussy and isn't. A guard that silently returns nothing teaches the\nmodel that the tool is broken — and a model that believes its tool is broken does\nexactly what you'd do: retry it, try a neighbouring tool, work around the outage.\nYou've converted a budget into a stimulus for more calls. A guard that <em>explains</em>\nteaches the model to change strategy, which is the only outcome you wanted. The\nrefusal is an input to the next planning step, so it has to be written like one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"metering-by-construction\">Metering by construction<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#metering-by-construction\" class=\"hash-link\" aria-label=\"Direct link to Metering by construction\" title=\"Direct link to Metering by construction\" translate=\"no\">​</a></h2>\n<p>A tool is metered if its canonical name isn't in the local-tools set. That's the\nwhole rule — external ops get counted uniformly, and nobody maintains a\nper-vendor list that goes stale the day someone adds an integration.</p>\n<p>Two details make it hold. A canonicalizer collapses the two surface spellings of\nthe same tool — the function-call form and the code-mode form — into one\nidentity, so an agent can't dodge a budget by switching call styles. And\nsynthetic messages the harness injects itself never count; the meter measures the\nagent, not the scaffolding around it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-wall-thats-soft-on-purpose\">The wall that's soft on purpose<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#the-wall-thats-soft-on-purpose\" class=\"hash-link\" aria-label=\"Direct link to The wall that's soft on purpose\" title=\"Direct link to The wall that's soft on purpose\" translate=\"no\">​</a></h2>\n<p>One budget is deliberately not a wall. The turn budget advances in steps of 50 —\n50, then 100, then 150 — and injects a warning the agent is free to ignore. It\ngates nothing.</p>\n<p>That's not a weaker version of a hard cap. It's a different tool. <strong>A soft signal\nshapes behavior; a hard wall guarantees termination.</strong> A warning lets a\nstill-reasoning agent wrap up on its own terms, which is a better ending than any\nforced stop — but it promises nothing, because an agent that ignores it just\nkeeps going. A hard cap promises termination and can only deliver it bluntly. You\nneed both, and you need them separate: a soft signal that gates is a hard wall\nwith worse ergonomics, and a hard wall that warns is a wall that doesn't work.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-seam-worth-naming\">A seam worth naming<a href=\"https://blog.catalex.co/budgets-that-stop-a-runaway-agent#a-seam-worth-naming\" class=\"hash-link\" aria-label=\"Direct link to A seam worth naming\" title=\"Direct link to A seam worth naming\" translate=\"no\">​</a></h2>\n<p>Only the turn budget reaches the box today. Token and wall-clock budgets are\ncomputed but not enforced in-box — the outer bound on wall-clock is the sandbox's\nown lifetime. That's a real gap, and we'd rather write it down than let the\ndiagram imply coverage the code doesn't have.</p>\n<p>The four walls are the shape of the argument, though, and the shape generalizes.\nThe question is never \"what's the limit?\" It's \"what does this loop look like,\nand which key makes it stop moving?\" Answer that four times and you have\nsomething that holds.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/budgets-that-stop-a-runaway-agent",
            "title": "The Four Walls: Budgets That Stop a Runaway Agent",
            "summary": "A runaway agent loop calls the same tool with different arguments every time, so an identical-call guard never fires. Four independent budgets — plus one hard stop — and why each catches exactly what the others structurally cannot.",
            "date_modified": "2026-07-08T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Harness",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/writing-a-good-agent-skill",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 7, 2026 · 09:00 UTC</div>\n<p>A skill is a self-contained Markdown directive with a little YAML frontmatter on\ntop, sitting in its own directory. That description makes it sound like\ndocumentation, which is exactly the trap. The interesting part is not what a\nskill contains. It's <em>when the agent pays for it</em>.</p>\n<p>Here is the sentence we keep in our own skill registry, and it is the whole\ndesign: a skill is a directive the agent reads <strong>on demand</strong> via a <code>read_skill</code>\ntool, rather than something injected into every prompt — so the agent pays the\ntoken cost only when the skill is actually relevant to the turn.</p>\n<!-- -->\n<p>Read that twice, because every rule below is a consequence of it. On demand is\nnot an implementation detail you can bolt on afterward. It is the property that\nmakes a skill a skill instead of a system prompt with extra steps. And it means\nthe best skill you can write is one that is <em>absent</em> from most of the agent's\nturns — present, correct, and completely unloaded.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-are-actually-writing\">What you are actually writing<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#what-you-are-actually-writing\" class=\"hash-link\" aria-label=\"Direct link to What you are actually writing\" title=\"Direct link to What you are actually writing\" translate=\"no\">​</a></h2>\n<p>Structurally there is almost nothing to it:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token punctuation\" style=\"color:#393A34\">---</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> design</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">core</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">description</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  Read BEFORE designing any visual deliverable — decks</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> documents</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> pages.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  Carries the non</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">negotiable design bans and the layout method. Load this</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  first</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> then the medium</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">specific skill.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">---</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Core design directive</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">## Absolute anti-slop bans</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> No Times. No Arial.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> No pure black text on pure white.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> No default 1px grey borders.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">...</span><br></div></code></pre></div></div>\n<p>Frontmatter, a body, a directory. The agent's harness knows the file exists and\nknows what the <code>description</code> says. It does not know a word of the body until it\nasks. That gap — the harness knowing the <em>name</em> while deferring the <em>content</em> —\nis the entire budget you have to work with, and four rules follow from spending\nit well.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"rule-1-load-it-through-a-tool-not-a-shell-command\">Rule 1: load it through a tool, not a shell command<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#rule-1-load-it-through-a-tool-not-a-shell-command\" class=\"hash-link\" aria-label=\"Direct link to Rule 1: load it through a tool, not a shell command\" title=\"Direct link to Rule 1: load it through a tool, not a shell command\" translate=\"no\">​</a></h2>\n<p>The agent could read the file with <code>cat</code>. Same bytes into context, no tool\nrequired. Do not do this.</p>\n<p>Loading through a dedicated tool keeps the raw directive text out of the\nuser-facing terminal stream. It rides to the model as a tool result; the UI\nshows a compact card. If the agent <code>cat</code>s the file instead, the user watches a\nwall of imperatives about font choices scroll through their chat — a\nconversation they were never party to, rendered as if it were output.</p>\n<p><strong>The tokens are identical. The product is worse.</strong> That asymmetry is worth\ninternalizing, because it's the first place skill design stops being a context\nproblem and starts being an interface problem. Your directive is addressed to\nthe model. Anything that leaks it to the human is a bug, even when it's free.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"rule-2-the-description-is-the-routing-logic\">Rule 2: the description <em>is</em> the routing logic<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#rule-2-the-description-is-the-routing-logic\" class=\"hash-link\" aria-label=\"Direct link to rule-2-the-description-is-the-routing-logic\" title=\"Direct link to rule-2-the-description-is-the-routing-logic\" translate=\"no\">​</a></h2>\n<p>If bodies load on demand, something has to decide <em>when</em>. That something is the\n<code>description</code> field — and it works because the tool that loads skills enumerates\nthe valid skills in its own description. The model sees the full menu of names\nand triggers without loading a single body.</p>\n<p>Names in the prompt. Bodies on demand.</p>\n<p>We apply the same principle one layer down, to tool schemas: every tool name is\nin the system prompt, but the parameter schemas are fetched only when needed. The\nguidance we give the fetching tool says it plainly — <em>your tools are ALL listed\nby exact name in the system prompt; this is NOT for discovery, it fetches the\nparam schema you don't yet have.</em> Discovery is free. Detail is not. Skills are\nthat idea applied to prose instead of JSON.</p>\n<p>Which reframes what your <code>description</code> is for. It is not a summary of the skill.\nNobody is browsing. It is a <strong>trigger</strong> — the condition under which a model that\nhas never read your body should decide it needs to. Write it as an instruction\nabout <em>when</em>, not a précis of <em>what</em>:</p>\n<ul>\n<li class=\"\">Summary: \"Guidance and conventions for building presentations.\"</li>\n<li class=\"\">Trigger: \"Read before creating any slide deck, after the core design skill.\"</li>\n</ul>\n<p>The second one routes. The first one describes.</p>\n<div class=\"cx-note\"><p><strong>Selection should be boring.</strong> Ours is prompt-driven routing through a\nhard-coded table — deliberately not embedding search, not auto-selection. A\npreamble names the deliverable type and the skill to load. Clever selection\nfails in ways you cannot reproduce; a table fails in ways you can read.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"rule-3-layer-dont-lump\">Rule 3: layer, don't lump<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#rule-3-layer-dont-lump\" class=\"hash-link\" aria-label=\"Direct link to Rule 3: layer, don't lump\" title=\"Direct link to Rule 3: layer, don't lump\" translate=\"no\">​</a></h2>\n<p>The instinct when writing a skill is to make it complete. Resist it. Our six\nshipped skills are deliberately layered: one medium-agnostic core design\ndirective is read before designing <em>anything</em>, and the medium-specific skills\nbuild on top of it with the format rules for exactly one output type. The agent\nreads the core, then the layer.</p>\n<table><thead><tr><th>Skill</th><th>Role</th><th>Size</th></tr></thead><tbody><tr><td>Core design directive</td><td>Read before designing anything</td><td>~7.5 KB</td></tr><tr><td>Presentation</td><td>Slide-specific rules on top of the core</td><td>~8 KB</td></tr><tr><td>Self-contained HTML</td><td>Single-file page rules</td><td>~5.6 KB</td></tr><tr><td>Document (PDF)</td><td>Print-oriented format rules</td><td>~3.2 KB</td></tr><tr><td>Document (DOCX)</td><td>Editable-document format rules</td><td>~2.7 KB</td></tr><tr><td>Spreadsheet (XLSX)</td><td>Tabular output rules</td><td>~2.5 KB</td></tr></tbody></table>\n<p>The sizes are the point. Not one of them is a manual. The core carries what is\ntrue of every visual deliverable; each layer carries only what is true of its\nmedium and nothing that the core already said. A spreadsheet task loads roughly\n10 KB total and never sees a word about slide transitions.</p>\n<p>The failure this prevents is subtle. A single lumped skill isn't just bigger — it\nforces every task to carry every medium, which means the moment it starts\ncrowding the context window, someone \"fixes\" it by making it vaguer. Layering\nlets you stay <em>specific</em>, because specificity is cheap when it's scoped.</p>\n<p>Specificity is also what makes a skill bite. Our core design directive opens with\n\"Absolute anti-slop bans\": no Times, no Arial, no pure black on pure white, no\ndefault 1px grey borders. Concrete prohibitions beat vague encouragement, every\ntime. \"Use tasteful typography\" is a sentence a model can agree with while\nproducing Arial. Skills carry a persona and hard bans because that's what\nsurvives the trip through a generative process. And when prose is the wrong\ninstrument, ship an executable script alongside the markdown — a skill directory\nis not obligated to be only words.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"rule-4-reference-big-data-by-path-never-by-content\">Rule 4: reference big data by path, never by content<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#rule-4-reference-big-data-by-path-never-by-content\" class=\"hash-link\" aria-label=\"Direct link to Rule 4: reference big data by path, never by content\" title=\"Direct link to Rule 4: reference big data by path, never by content\" translate=\"no\">​</a></h2>\n<p>Our presentation skill needs a 54 KB theme catalog. It does not contain a 54 KB\ntheme catalog.</p>\n<p>The loader appends the catalog's resolved absolute path to the skill text, so the\nagent can <code>json.load</code> it inside <code>run_python</code> without the prompt ever carrying the\ndata — and the skill explicitly instructs the agent not to dump the thing into\nthe chat. A 54 KB asset costs a path.</p>\n<p><strong>Generalize this.</strong> Any time your skill wants to <em>contain</em> a lookup table, a\nschema dump, a catalog, or a corpus of examples, it wants a path instead. The\nagent has a filesystem and an interpreter. Prose is for judgment; data is for\ndisk. A skill that inlines its data spends the whole context window on a lookup\nthat a single line of Python does better.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-part-where-we-admit-its-hard\">The part where we admit it's hard<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#the-part-where-we-admit-its-hard\" class=\"hash-link\" aria-label=\"Direct link to The part where we admit it's hard\" title=\"Direct link to The part where we admit it's hard\" translate=\"no\">​</a></h2>\n<p>The skills we ship inside the product run 2.5–8 KB. The skills our own engineers\nwrite for their internal dev tooling run 25–85 times larger. One internal QA\nskill is 215 KB — far, far past any progressive-disclosure guidance we would give\nanyone, including ourselves.</p>\n<p>The detail that makes it instructive rather than embarrassing: that skill <em>has</em>\nan <code>envs/</code> subdirectory. The author knew the pattern. They applied it to the\nperipheral material and not to the main body.</p>\n<p>That's the failure mode, and it's always the same one. Nobody sits down to write\na 215 KB skill. They write a good 8 KB one, then answer a question in it, then\nanother, then paste in the thing that keeps coming up — and each addition is\nlocally correct. The skill grows into a manual because <strong>nobody pays the cost of\nloading it at review time.</strong> The author reads the diff. The agent reads the\nwhole file, on every task that touches it, forever. The discipline is easy to\nstate and genuinely hard to keep, and pretending otherwise would make the rest of\nthis post less useful.</p>\n<p>The pattern that actually holds the line is two-tier. One of our better internal\nskills opens by declaring what it is: it <em>orients and routes</em>, and the deep,\nauthoritative reference is a longer document — the single source of truth. Read\nit before non-trivial work; don't duplicate it here. Eight kilobytes of skill\npointing at twenty-nine kilobytes of reference.</p>\n<p>Skills should route to depth, not contain it. The skill's job is to know that the\nreference exists, say when it matters, and get out of the way. The reference's\njob is to be right. Conflating those two jobs is how you get 215 KB.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-to-know-yours-is-good\">How to know yours is good<a href=\"https://blog.catalex.co/writing-a-good-agent-skill#how-to-know-yours-is-good\" class=\"hash-link\" aria-label=\"Direct link to How to know yours is good\" title=\"Direct link to How to know yours is good\" translate=\"no\">​</a></h2>\n<p>Read your skill and ask when it should <em>not</em> load. If you can't answer crisply,\nyour description isn't a trigger and your routing will be guesswork. Then check\nthat nothing in the body would be equally at home in the core, in a sibling\nlayer, or in a file on disk — those are the three things a bloated skill is made\nof.</p>\n<p>Then apply the test that subsumes all of it:</p>\n<p><strong>If your skill would be equally useful pasted into every system prompt, it\nisn't a skill. It's a system prompt you haven't admitted to.</strong></p>\n<p>That's not a rhetorical flourish; it's a diagnostic with a fix on either side. If\nthe content genuinely belongs in every turn, move it to the system prompt and\nstop paying the indirection. If it doesn't — if it's for decks, or for one\nlanguage, or for the third Tuesday of the release cycle — then earn the deferral.\nSharpen the trigger, split the layer, push the data to a path.</p>\n<p>A skill earns its keep by being absent most of the time. Write it so the agent is\nglad to see it, and doesn't have to.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/writing-a-good-agent-skill",
            "title": "Writing a Skill an Agent Will Actually Read",
            "summary": "A skill is a directive an agent loads on demand rather than something injected into every prompt. That one design choice dictates how you write it — the four rules that follow, and the discipline that is easy to state and hard to keep.",
            "date_modified": "2026-07-07T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Skills",
                "Agent Engineering",
                "Agent Harness"
            ]
        },
        {
            "id": "https://blog.catalex.co/scheduling-agents-at-scale",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 6, 2026 · 09:00 UTC</div>\n<p>\"Run this agent every morning at 9\" sounds like a one-line feature. There's a\ncron expression, there's a job table, there's a thing that reads the table.\nShip it Friday.</p>\n<p>Cron is the easy 5%. The other 95% is not firing twice, not losing a fire, not\nholding a database transaction open across a task that runs for minutes, and\ndeciding whose 9am you actually mean. A cron expression is a <em>trigger</em>. A\nscheduler is everything you build around it.</p>\n<!-- -->\n<p>This is a tour of that everything — the design of the scheduling layer under\nCatalEx, and the reasoning behind the parts that look strange until you've been\nbitten.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-shape-of-the-stack\">The shape of the stack<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#the-shape-of-the-stack\" class=\"hash-link\" aria-label=\"Direct link to The shape of the stack\" title=\"Direct link to The shape of the stack\" translate=\"no\">​</a></h2>\n<p>A managed cron trigger fires a tick endpoint every minute. The jobs service\nscans the database for anything due, and dispatches each due job through a\nmanaged task queue. The queue calls back into the owning service's handler,\nwhich does the actual work.</p>\n<!-- -->\n<p>Four moving parts, three network hops, and every one of them is a place where\nthe same job can fire twice or not at all. Everything below is about closing\nthose gaps.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"at-the-top-of-the-stack-a-retry-is-worse-than-a-miss\">At the top of the stack, a retry is worse than a miss<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#at-the-top-of-the-stack-a-retry-is-worse-than-a-miss\" class=\"hash-link\" aria-label=\"Direct link to At the top of the stack, a retry is worse than a miss\" title=\"Direct link to At the top of the stack, a retry is worse than a miss\" translate=\"no\">​</a></h2>\n<p>The cron trigger is configured with <strong>zero retries</strong>. Deliberately. The comment\nin the code is the whole argument:</p>\n<blockquote>\n<p>The next tick will re-evaluate any jobs that were due; re-trying a failed\ntick risks double-dispatch against the same window.</p>\n</blockquote>\n<p>This inverts the instinct you've built everywhere else in your system. Down in\nthe leaves, retry is free and correct. At the <em>root</em> of a scheduling stack,\nretry is a duplicate-fire generator. The tick is not a request that must\nsucceed; it's a sample of a clock that will be sampled again in sixty seconds.\nA missed tick costs you a job running one minute late. A retried tick that\npartially succeeded costs you a job that ran twice — and \"ran twice\" for an\nagent means two emails sent, two tickets filed, two rows written into someone's\nCRM.</p>\n<p>Fail-open on the way down. Fail-closed on the way up.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"four-layers-against-double-fire\">Four layers against double-fire<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#four-layers-against-double-fire\" class=\"hash-link\" aria-label=\"Direct link to Four layers against double-fire\" title=\"Direct link to Four layers against double-fire\" translate=\"no\">​</a></h2>\n<p>Zero retries on the tick is a policy, not a mechanism. Below it sit four\nmechanisms, each one assuming the layer above it failed.</p>\n<table><thead><tr><th>#</th><th>Mechanism</th><th>What it stops</th></tr></thead><tbody><tr><td>0</td><td>Zero retries on the tick</td><td>Two ticks evaluating the same due window</td></tr><tr><td>1</td><td><code>FOR UPDATE SKIP LOCKED</code> on the due-job scan</td><td>Two dispatcher instances claiming the same row</td></tr><tr><td>2</td><td><code>next_run</code> advanced in the same committed transaction as the execution row</td><td>The job still looking due after it's been claimed</td></tr><tr><td>3</td><td>Task-name dedup — the enqueue passes <code>execution_id</code> as the queue's task name</td><td>Queue redelivery inside the dedup window</td></tr><tr><td>4</td><td>Idempotency claim at the handler — insert <code>ON CONFLICT DO NOTHING</code> on <code>execution_id</code></td><td>Everything the first four missed</td></tr></tbody></table>\n<p><strong>Layer 1 is why you can run more than one dispatcher.</strong> <code>SELECT … FOR UPDATE SKIP LOCKED</code> on the due-job scan means concurrent dispatcher instances never\ndouble-dispatch the same job — the second instance doesn't block on the locked\nrow, it walks past it and takes the next one. Without <code>SKIP LOCKED</code> you get\neither a convoy (everyone blocks behind row one) or a single-instance\nscheduler, which is a single point of failure with extra steps.</p>\n<p><strong>Layer 2 is the one people skip.</strong> <code>next_run</code> is advanced in the same\ncommitted transaction that writes the execution row. The moment that commit\nlands, the job is <em>no longer due</em> — before the dispatch has even been\nattempted. The scan can't pick it up again on the next tick, because as far as\nthe query is concerned, the window has already passed.</p>\n<p><strong>Layer 3 costs one argument.</strong> Every enqueue passes the execution id as the\nqueue's task name. A queue that redelivers the same task name inside its dedup\nwindow drops it. This is free protection you get by naming a thing carefully.</p>\n<p><strong>Layer 4 is the backstop, and its reasoning is the elegant bit.</strong> The handler,\nbefore doing any work at all, inserts a claim row keyed on <code>execution_id</code> with\n<code>ON CONFLICT DO NOTHING</code>. If the insert claimed nothing, someone else already\nhas this execution, and the handler returns <code>{\"skipped\": \"duplicate execution\"}</code> and goes home.</p>\n<div class=\"cx-note\"><p><strong>Why this is safe.</strong> A legitimate retry mints a NEW execution_id, so the SAME\nexecution_id arriving twice is always a duplicate delivery, never a lost retry.</p></div>\n<p>The usual objection to handler-side idempotency is that you'll swallow a real\nretry. You won't — but only because retries are minted upstream as new\nexecutions, so identity and attempt are never conflated. Idempotency keys work\nwhen the key means <em>this delivery</em>, not <em>this task</em>. Get that distinction wrong\nand your dedup layer starts eating work.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"three-rules-about-transaction-boundaries\">Three rules about transaction boundaries<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#three-rules-about-transaction-boundaries\" class=\"hash-link\" aria-label=\"Direct link to Three rules about transaction boundaries\" title=\"Direct link to Three rules about transaction boundaries\" translate=\"no\">​</a></h2>\n<p>These are design rules, not scars. Each one exists because the alternative has\na specific, predictable failure mode.</p>\n<p><strong>Never hold a transaction across the dispatch.</strong> A downstream agent run takes\nminutes. Wrap the dispatch in the transaction that read the due jobs and you're\nholding an idle-in-transaction connection for the duration — long enough to\ntrip the database's idle-in-transaction timeout, at which point the connection\ndies <em>mid-update</em> and you get to find out what your code does about that. Hence\nthe split: read due jobs, commit, then dispatch.</p>\n<p><strong>Commit before you dispatch — in that order.</strong> Dispatch first and the callback\nopens a fresh transaction that cannot see your uncommitted execution row. The\nhandler looks up an execution that doesn't exist, and here's the vicious part:\nit still returns 200. The queue sees success, never retries, and the work\nvanishes with no error anywhere. Silent loss is worse than loud failure, and\nthe ordering is the only thing between you and it.</p>\n<p><strong>Report failures in-band.</strong> A malformed job returns HTTP 200 with\n<code>status=\"failed\"</code> in the body. It's tempting to return a 5xx — it feels honest.\nBut the tick's transport layer would read that as a <em>transport</em> error and apply\ntransport retry semantics to a permanently broken job, re-dispatching a payload\nthat will never parse. Status belongs in the body so the tick can apply its own\nretry policy. HTTP status codes describe the conversation, not the work.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whose-9am\">Whose 9am?<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#whose-9am\" class=\"hash-link\" aria-label=\"Direct link to Whose 9am?\" title=\"Direct link to Whose 9am?\" translate=\"no\">​</a></h2>\n<p>Now the part that looks like a bug and isn't.</p>\n<p>Cron parsing is a standard 5-field expression, <strong>always UTC</strong>. There is no\ntimezone column. Not \"we haven't added one yet\" — the API contract explicitly\nforbids timezone-bearing fields on the wire. The database stores a UTC cron and\nnothing else.</p>\n<p>All local↔UTC math lives in the browser, because the browser is the only place\nthat knows the user's local zone. The server never guesses, never infers from a\nheader, never stores a stale zone that was true when the user signed up in a\ncity they've since left. There's one authority for \"your 9am\" and it's the\nmachine sitting in front of the user.</p>\n<p>There's no interval primitive either. \"Every N hours\" compiles in the browser\ninto <code>${minute} */${n} * * *</code>, with the divisors restricted to\n<code>[1,2,3,4,6,8,12]</code> — the clean divisors of 24. That restriction is doing real\nwork: <code>*/5</code> hours doesn't tile 24 evenly, so it silently skips at the midnight\nwraparound. You'd get 0,5,10,15,20 and then a four-hour gap, forever, and\nnobody would notice for weeks. Restricting the input is cheaper than explaining\nthe arithmetic.</p>\n<p>Alongside the cron sits a display descriptor — frequency, time_of_day,\ndays_of_week, interval_hours — stored purely so the UI can round-trip\nlosslessly. Rendering a UTC cron through a timezone-naive formatter prints UTC\nnumbers styled as wall-clock time, which is how you end up telling a user their\njob runs at 2pm when it runs at 9am.</p>\n<p>Weekly schedules roll <strong>each weekday independently</strong> through UTC, then dedupe. A\nlocal Monday 11pm legitimately lands on Tuesday UTC while the rest of the week\ndoesn't move. The naive version — shift the time, then shift every day by +1 —\nis wrong for exactly the days that cross midnight, which is the only case where\nit matters.</p>\n<p><strong>The honest tradeoff:</strong> because only a UTC cron is stored, schedules do not\nfollow DST. A 9am local schedule becomes 8am or 10am local when the offset\nchanges. Nothing recomputes crons on DST boundaries. This is a real limitation\nand we'd rather say it plainly than let you discover it in March. The\nalternative isn't free: following DST means storing the user's zone, which means\nrecomputing every affected cron whenever a government changes its rules, and\nowning the ambiguity of schedules that land in the hour that doesn't exist or\nthe hour that happens twice. UTC-only buys a schema with one interpretation.\nThat's the trade, made with open eyes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-unglamorous-rest\">The unglamorous rest<a href=\"https://blog.catalex.co/scheduling-agents-at-scale#the-unglamorous-rest\" class=\"hash-link\" aria-label=\"Direct link to The unglamorous rest\" title=\"Direct link to The unglamorous rest\" translate=\"no\">​</a></h2>\n<p>The remaining details are the ones that decide whether the thing survives a\nyear.</p>\n<p>Execution timeout is a per-job <code>timeout_minutes</code>, constrained at the database\nbetween 1 and 15. A stale sweep marks anything past its timeout as failed and\nretries it — which is the <em>only</em> place a retry is minted, and it mints a new\nexecution id, which is what makes layer 4 sound.</p>\n<p>Primary keys are UUIDv7 — time-ordered, so inserts land at the end of the index\ninstead of scattering across it. Hot-path indexes are partial: the index on\n<code>next_run</code> carries <code>WHERE status = 'active'</code>, because the dispatcher never asks\nabout inactive jobs and there's no reason to make it page past them.</p>\n<p>A duplicate-schedule guard allows one schedule per agent per cron expression,\nenforced at the application layer, returning 409. Distinct agents may share a\ncron — the constraint is against a user double-clicking their way into two\nidentical jobs, not against 9am being popular.</p>\n<p>Orphan protection runs both ways. Deleting an agent purges its schedules; and if\na run finds its agent already gone, the handler purges the schedules itself.\nOtherwise an orphaned schedule fires every tick forever, doing nothing, and\nnobody notices because nothing is failing.</p>\n<p>And a run that suspends — waiting on a human approval — ends without a terminal\nframe. The execution still counts as <strong>completed</strong>, and records a status\nbreadcrumb instead. The dispatch did its job; a retry would double-run the\nagent. The scheduler's contract is \"the work was handed off correctly,\" not\n\"the work finished.\"</p>\n<p>That's the shape of it. One cron expression at the top, and underneath it four\nlayers of dedup, three rules about where a transaction may not go, a timezone\nmodel with a stated limitation, and a fistful of indexes. The cron was the easy\npart. It's always the easy part.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/scheduling-agents-at-scale",
            "title": "Cron Is Not a Scheduler: Running Agents on Time at Scale",
            "summary": "A cron expression is a trigger. A scheduler is everything you build around it — not firing twice, not losing a fire, keeping transactions off the dispatch path, and deciding whose 9am you mean.",
            "date_modified": "2026-07-06T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Scheduling",
                "AI in Production",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/what-agents-can-do",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 5, 2026 · 09:00 UTC</div>\n<p>\"What can your agent do?\" is two questions wearing one coat. The first is about\nthe <strong>action space</strong>: once the agent is awake and thinking, what can it reach,\ncall, read, and change? The second is about the <strong>trigger space</strong>: what makes it\nwake up in the first place? Teams conflate them constantly, and the conflation\nis expensive, because the two want opposite answers.</p>\n<!-- -->\n<p>We build for a deliberate asymmetry. The action space is as wide as we can\nresponsibly make it — a real Python kernel, a shell, roughly a hundred curated\nthird-party integrations, and permission to write loops instead of taking turns.\nThe trigger space is three entries long. Not three for now, pending a roadmap.\nThree because every entry point into an autonomous system is a surface someone\nhas to reason about, and we would rather have three surfaces we understand\ncompletely than thirty we understand approximately.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"three-ways-in-enforced-in-the-schema\">Three ways in, enforced in the schema<a href=\"https://blog.catalex.co/what-agents-can-do#three-ways-in-enforced-in-the-schema\" class=\"hash-link\" aria-label=\"Direct link to Three ways in, enforced in the schema\" title=\"Direct link to Three ways in, enforced in the schema\" translate=\"no\">​</a></h2>\n<p>Here is the entire trigger space, as a <code>StrEnum</code> backed by an eight-character\ndatabase column:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">TriggerType</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">StrEnum</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    CHAT </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"chat\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    RUN </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"run\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    SCHEDULE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"schedule\"</span><br></div></code></pre></div></div>\n<p>That column width is not an accident of history. Eight characters is a wall.\nAdding a fourth trigger means a migration, which means a conversation, which is\nexactly the friction we want on this particular axis. Cheap to add is the wrong\nproperty for an entry point into a system that takes real actions.</p>\n<table><thead><tr><th>Trigger</th><th>Surface</th><th>What it means</th></tr></thead><tbody><tr><td><code>chat</code></td><td><code>studio_chat</code></td><td>Interactive conversation with the agent</td></tr><tr><td><code>run</code></td><td><code>studio_run</code></td><td>A person clicks \"run now\"</td></tr><tr><td><code>schedule</code></td><td><code>scheduled_job</code></td><td>Cron fires</td></tr></tbody></table>\n<p>That's the list. There is no webhook trigger. No email-arrival trigger. No\n\"someone posted in a channel\" trigger. We know those are the first three\nfeatures on most people's wish list, and we know exactly what the absence looks\nlike from outside: a gap.</p>\n<p>We'd argue it's a position.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-a-narrow-trigger-set-is-the-feature\">Why a narrow trigger set is the feature<a href=\"https://blog.catalex.co/what-agents-can-do#why-a-narrow-trigger-set-is-the-feature\" class=\"hash-link\" aria-label=\"Direct link to Why a narrow trigger set is the feature\" title=\"Direct link to Why a narrow trigger set is the feature\" translate=\"no\">​</a></h2>\n<p>The argument against webhooks isn't that they're hard. They're easy — that's the\nproblem. Every webhook you accept is an entry point where the outside world\ndecides when your agent runs, how often, and with what payload. Each one needs\nits own authentication story, its own replay semantics, its own idempotency\nreasoning, its own answer to \"what happens when this fires four hundred times in\na minute because an upstream system had a bad afternoon.\" That reasoning doesn't\namortize across endpoints. It's per-endpoint, forever.</p>\n<p><strong>A schedule is a trigger you can reason about.</strong> It fires when you said it\nwould. Its concurrency is bounded by its own period. Its payload is whatever the\nagent goes and fetches. And critically — you can poll from a schedule. Nearly\nevery \"I need a webhook\" requirement is really \"I need to know about this\nwithin N minutes,\" and a schedule answers that with a mechanism whose failure\nmodes fit in your head. You trade a little latency for an entry point that\ncannot be triggered by anyone outside your system. That trade is available to\nyou at any time, and it's usually the right one.</p>\n<p>The discipline shows up elsewhere too. There is an internal <code>event</code> job type in\nthe platform, and it would have been the natural place to sneak a fourth\ntrigger in. It doesn't start agent runs. It handles post-run work —\nconsolidating memory, scoring KPIs. Work that happens <em>after</em> an execution, on\nthe same rails, never as a way in. The trigger set stayed three because we kept\nit three when it was inconvenient.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-path-many-entry-points\">One path, many entry points<a href=\"https://blog.catalex.co/what-agents-can-do#one-path-many-entry-points\" class=\"hash-link\" aria-label=\"Direct link to One path, many entry points\" title=\"Direct link to One path, many entry points\" translate=\"no\">​</a></h2>\n<p>Here's the part that makes the narrow set work rather than merely sound\nprincipled.</p>\n<!-- -->\n<p>A scheduled run is not a special execution mode. It is not batch mode. It is not\na lighter-weight cousin of the interactive path with a few features stubbed out.\nIt goes down the <em>identical</em> orchestration path as a user-initiated run —\ndifferent surface, different <code>trigger_type</code>, same everything else. The intent is\nwritten into the module itself: keep the scheduled run \"on the identical\norchestration path as a user-initiated run.\"</p>\n<p>You can see the discipline hold under pressure at the one place you'd expect it\nto bend. When someone manually triggers a schedule — the \"run this now, don't\nwait for 6am\" button — the obvious implementation is a shortcut: skip the\nschedule machinery, fire a direct run. We don't. It creates an execution with\n<code>trigger_source=\"manual\"</code> and dispatches immediately, leaving the recurring\nschedule intact, and it reaches the agent <em>through the same schedule path</em>. The\nmanual trigger is a nudge to an existing mechanism, not a bypass around it.</p>\n<p><strong>The alternative is where divergence bugs live.</strong> A separate batch mode starts\nas ninety percent shared code. Then the scheduled path gets a fix the\ninteractive one doesn't. Then someone adds a guard to the interactive path\nbecause that's where the user complaints come from. Six months later you have\ntwo systems with one name, and the failure you're debugging only reproduces at\n4am on Tuesdays. One path with many entry points means a fix lands once, and\nevery trigger inherits it. It also means the trigger set can stay small <em>without\ncosting capability</em> — because narrowing the ways in doesn't narrow what happens\nonce you're in.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"now-the-wide-half\">Now the wide half<a href=\"https://blog.catalex.co/what-agents-can-do#now-the-wide-half\" class=\"hash-link\" aria-label=\"Direct link to Now the wide half\" title=\"Direct link to Now the wide half\" translate=\"no\">​</a></h2>\n<p>Everything above is about restraint. None of it applies to the action space,\nwhere the answer is: as much as we can hand it.</p>\n<p><strong>The agent has a real computer.</strong> Inside the box, a set of tools run locally —\n<code>run_python</code>, <code>bash</code>, <code>text_editor</code>, <code>query_file</code>, <code>read_file</code>, <code>update_plan</code>,\n<code>load_tools</code>, <code>get_tool_details</code>, <code>fetch_full_result</code>, <code>query_resource</code>,\n<code>user_input</code>, <code>task_complete</code>, <code>save_memory</code>. These aren't metered as external\noperations, because they aren't external. They're the agent using its own\nmachine.</p>\n<p>Above that sit first-party servers: web search, the Brain knowledge and search\nlayer, a platform \"system\" server, and a tool catalog the agent queries to find\nout what else exists. And beyond those, roughly a hundred curated third-party\nintegrations reached through our integration layer — CRM and sales,\ncommunication, finance, documents and storage, calendar and meetings. Wide by\ncategory, curated rather than infinite, but wide.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"code-mode-the-part-most-readers-dont-expect\">Code-mode: the part most readers don't expect<a href=\"https://blog.catalex.co/what-agents-can-do#code-mode-the-part-most-readers-dont-expect\" class=\"hash-link\" aria-label=\"Direct link to Code-mode: the part most readers don't expect\" title=\"Direct link to Code-mode: the part most readers don't expect\" translate=\"no\">​</a></h2>\n<p>The default mental model of an agent is a turn-taker. Think, call one tool, read\nthe result, think again. That model is a ceiling, and most of it is\nself-imposed.</p>\n<p>The preamble teaches a three-tier ladder instead:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">1. Answer directly — no tool needed.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">2. Call a tool directly — for ONE discrete action.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">   Don't wrap a one-shot call in code.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">3. Write and run code — for orchestration: chaining calls,</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">   looping, fetching in bulk.</span><br></div></code></pre></div></div>\n<p>The rung that matters is the third, and the guidance that makes it click is\nblunt: <strong>\"Calling the same tool across a list is a LOOP — write it once in\ncode.\"</strong> An agent asked to touch forty records shouldn't take forty turns. It\nshould write the loop. The Python kernel persists across cells, so state carries\nbetween steps — fetch in one, transform in the next, act in a third, without\nrebuilding the world each time.</p>\n<p>Tier two matters as much, in the other direction. The failure mode of a capable\nagent is wrapping a single API call in eleven lines of Python because code feels\nmore thorough. \"Don't wrap a one-shot call in code\" is the counterweight. The\nladder is a ladder — you're supposed to stop climbing when you've reached the\nrung that fits.</p>\n<div class=\"cx-note\"><p><strong>Also worth knowing:</strong> large tool results never enter the conversation. The\nagent gets a short preview plus a file reference it queries with SQL. So \"what\nit can do\" includes working over data far larger than a context window — the\nwindow holds the pointer, not the payload.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-asymmetry-stated-plainly\">The asymmetry, stated plainly<a href=\"https://blog.catalex.co/what-agents-can-do#the-asymmetry-stated-plainly\" class=\"hash-link\" aria-label=\"Direct link to The asymmetry, stated plainly\" title=\"Direct link to The asymmetry, stated plainly\" translate=\"no\">​</a></h2>\n<p>Widen what an agent can do once it's running. Narrow the number of ways it can\nstart running. Those pull in opposite directions and they should — they're\nprotecting different things.</p>\n<p>The action space is bounded by the agent's judgment inside a run you initiated,\nobserved under permissions you set. Getting it wrong costs you one bad run. The\ntrigger space is bounded by nothing except the entry points you built, and every\none you add is a door someone else's system can knock on at a time you didn't\nchoose. Getting <em>that</em> wrong costs you the property that made the whole thing\ngovernable.</p>\n<p>So: three triggers, one path, a real computer, and permission to write a loop.\nIf you want an agent to react to something we don't trigger on, poll for it from\na schedule. It's less elegant. It's a mechanism you can hold in your head, and\nafter enough production systems, that stops feeling like a consolation prize.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/what-agents-can-do",
            "title": "What an Agent Can Actually Do (And When It Does It)",
            "summary": "An agent's action space and its trigger space are different questions. We made the action space wide — a real computer, ~100 integrations, code-mode orchestration — and the trigger space exactly three entries wide. Here's why that asymmetry is the right one.",
            "date_modified": "2026-07-05T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Engineering",
                "Integrations",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/creating-an-agent-in-catalex",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 4, 2026 · 09:00 UTC</div>\n<p>From the outside, creating an agent looks like filling in a form. You describe\nwhat you want, you look at a preview, you click a button. Thirty seconds, maybe\nless. The interface is doing everything it can to feel like nothing much is\nhappening.</p>\n<!-- -->\n<p>That impression is the product working correctly and it is also completely\nwrong. The interesting engineering in an agent-creation path isn't the form —\nit's the handful of seconds after you confirm, where the system decides whether\nyou're getting a working agent or a plausible-looking broken one. Those seconds\nare made of validation, versioning, and a few unglamorous atomicity decisions.</p>\n<p>This is a walk through ours, in the order the system experiences it, and an\nargument about what a well-built creation path owes you.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"three-doors\">Three doors<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#three-doors\" class=\"hash-link\" aria-label=\"Direct link to Three doors\" title=\"Direct link to Three doors\" translate=\"no\">​</a></h2>\n<p>There are three shipped ways to get an agent in CatalEx. You can talk one into\nexistence through a chat wizard — the \"from scratch\" path. You can pick from a\ntemplate catalog of 159 templates across 13 categories, weighted toward the work\npeople actually automate: 41 specialized, 30 marketing, 29 engineering, then a\nlong tail through design, sales, testing, support, finance, and product. Or you\ncan do nothing at all: every account is provisioned an agent at signup.</p>\n<p>The three doors converge on the same write path. That's the first design\ndecision worth naming — the wizard is not a privileged client. It ends up\ncalling the same create contract as everything else, which means the guarantees\nbelow are guarantees for all three, not features of one nice UI.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-wizard-refuses-to-let-you-one-shot-it\">The wizard refuses to let you one-shot it<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#the-wizard-refuses-to-let-you-one-shot-it\" class=\"hash-link\" aria-label=\"Direct link to The wizard refuses to let you one-shot it\" title=\"Direct link to The wizard refuses to let you one-shot it\" translate=\"no\">​</a></h2>\n<p>The \"from scratch\" flow is a three-phase state machine:</p>\n<div class=\"language-ts codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-ts codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token keyword\" style=\"color:#00009f\">type</span><span class=\"token plain\"> </span><span class=\"token class-name\">Phase</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'chatting'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'synthesizing'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'reviewing'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// User turns only. One assistant turn can span several streamed</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// messages, so counting messages would let a single exchange</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">// masquerade as a conversation.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">const</span><span class=\"token plain\"> </span><span class=\"token constant\" style=\"color:#36acaa\">MIN_TURNS_FOR_CREATE</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><br></div></code></pre></div></div>\n<p>In phase one you're talking to a purpose-built system agent whose only job is to\ninterview you. The \"Create Agent\" button doesn't exist until you've taken at\nleast three turns. Not three messages — three <em>user</em> turns, because a single\nassistant reply can arrive as several streamed messages and counting those would\nlet one exchange impersonate a conversation.</p>\n<p><strong>The constant is a product opinion wearing a type annotation.</strong> You cannot\none-shot an agent definition here, by construction. The reason is not\npaternalism; it's that a one-line description underdetermines an agent. \"Write\nmy release notes\" doesn't say which repository, what tone, who reads them, what\nhappens when a release has no user-facing changes. A model handed that will\ninvent answers, and the invented answers are exactly the ones you'll be\ndebugging in three weeks. Forcing a conversation front-loads the questions to\nthe moment when answering them is cheap.</p>\n<p>The interviewer runs under its own budgets: 15 turns, 50,000 tokens, 120 seconds\nof wall clock. An agent whose job is to elicit requirements should not be able to\nelicit them forever. Budgets on a helper agent aren't a safety measure so much as\na design constraint — they force the interview to converge.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"synthesis-and-a-menu-on-disk\">Synthesis, and a menu on disk<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#synthesis-and-a-menu-on-disk\" class=\"hash-link\" aria-label=\"Direct link to Synthesis, and a menu on disk\" title=\"Direct link to Synthesis, and a menu on disk\" translate=\"no\">​</a></h2>\n<p>Hit the button and you're in phase two. The synthesizer streams over SSE, and a\nlive checklist tracks it through <code>plan_snapshot</code> frames. The seeded plan is\nexactly five steps: understand request, discover tools, draft instructions, draft\nKPIs, validate definition. You watch them tick.</p>\n<p>The second step is more interesting than it sounds. The synthesizer discovers\nwhich tools are available by reading a seeded file on disk — once — rather than\ncalling a discovery tool. Its prompt says so plainly: <em>\"There is no <code>tool_catalog</code>\ntool to call.\"</em></p>\n<p><strong>Put the menu on disk instead of spending a tool call on it.</strong> A tool call for a\ncatalog is a round trip, a chunk of context, and a step in the loop, all to\nretrieve something that was static the whole time. If the model needs a fixed\nreference to do its job, hand it the reference. Save the tool calls for the\nthings that actually require going somewhere.</p>\n<p>From there the synthesizer writes into a fixed skeleton — About, Role, Core\nWorkflows, Communication Style, Constraints, Decision-Making, Output Format —\ntargeting 400–800 words across five to eight sections. A skeleton is not a\ncreative restriction. It's what makes the output diffable, reviewable, and\ncomparable across 159 templates and every agent a user writes by hand.</p>\n<p>Phase three is an editable preview. Nothing has been written yet. You can rewrite\nany of it before it becomes real.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-part-that-matters-what-happens-on-confirm\">The part that matters: what happens on confirm<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#the-part-that-matters-what-happens-on-confirm\" class=\"hash-link\" aria-label=\"Direct link to The part that matters: what happens on confirm\" title=\"Direct link to The part that matters: what happens on confirm\" translate=\"no\">​</a></h2>\n<p>Here's the write path.</p>\n<!-- -->\n<p><strong>First, every requested tool key is validated against the live catalog.</strong> Not a\ncached list, not the list the synthesizer thought it had. The reason is in the\ncode comment: this guards against model hallucination, because a synthesizer that\nemits a plausible-but-wrong tool key \"would silently produce an agent with 0 tools\nat runtime\". Invalid keys come back as a 400 with the offending keys named.</p>\n<p>That sentence is the best single lesson in the whole path, so let it land. A\nhallucinated tool key is not a loud failure. It doesn't throw. It produces an\nagent that looks completely fine in the preview, saves without complaint, appears\nin your list with a green dot, and then does nothing useful when you run it —\nbecause the tool it was told to use resolves to nothing. The failure surfaces\ndays later as \"the agent seems dumb.\"</p>\n<div class=\"cx-note\"><p><strong>The general form:</strong> anywhere a model's output becomes a key, an ID, or a\nreference into another system, the failure mode is silence. Validate at write\ntime, against the live source of truth, and fail loudly with the specific bad\nvalue. The alternative isn't an error — it's a mystery.</p></div>\n<p><strong>Second, the create contract requires an explicit quota field.</strong> There's no\ndefault. A caller cannot create an agent without stating the cap, because — from\nthe code — the service \"never creates without an explicit cap so the invariant\n'every create is cap-checked' holds at the contract layer.\"</p>\n<p>Encode invariants in the contract, not in a caller's good intentions. A default\nwould work fine right up until the day someone adds a fourth creation path, and\nthen the invariant is a comment in a file nobody opened.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-isnt-in-the-row\">What isn't in the row<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#what-isnt-in-the-row\" class=\"hash-link\" aria-label=\"Direct link to What isn't in the row\" title=\"Direct link to What isn't in the row\" translate=\"no\">​</a></h2>\n<p>The agent table holds what you'd expect — name, instructions (a DB CHECK caps\nthem at 16,384 characters), description, tags, status (CHECK IN 'active',\n'paused', 'draft'), KPIs as JSONB, the memory and evolution flags, run counters,\n<code>current_version</code>, <code>template_id</code>. The absences are more informative.</p>\n<p><strong>There is no model column.</strong> Model choice is a service-level concern, not a\nper-agent field. Bake a model into the agent row and every agent becomes a small\nmonument to whatever was best the week it was created, and upgrading the fleet\nbecomes a migration.</p>\n<p><strong>There is no schedule column.</strong> Schedules live in a separate service, keyed by\nagent id. An agent is a definition of behavior; when it runs is somebody else's\nquestion. Fusing them into one row is how you end up with a scheduler you can't\nchange without touching the agent model.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"version-1-or-nothing\">Version 1, or nothing<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#version-1-or-nothing\" class=\"hash-link\" aria-label=\"Direct link to Version 1, or nothing\" title=\"Direct link to Version 1, or nothing\" translate=\"no\">​</a></h2>\n<p>If the row is inserted, two more things must happen: build version 1 — a snapshot\nof instructions, KPIs, memories, and tools — and pin it. If either step fails, the\nagent row is rolled back and the API returns 503 with a <code>Retry-After</code>. You never\nget a half-created agent, and you never get an agent at version zero whose first\nedit has nothing to diff against.</p>\n<p>The 503 is deliberate. Failing the whole create and telling you to try again is\nstrictly better than handing back a live-looking agent with no version history,\nbecause the second one is a debugging problem you inherit silently.</p>\n<p><strong>And now the honest seam.</strong> Schedules and tool bindings attached at create time\nare <em>not</em> covered by that guarantee. They're best-effort — attempted, and on\nfailure logged and swallowed. Which means a binding failure can produce a live\nagent that's missing a tool. We know. The v1 pin is load-bearing for correctness,\nso it's atomic; the bindings are recoverable by re-attaching, so they're not. It's\na real seam, chosen rather than overlooked, and worth stating plainly because\nevery atomicity boundary is a decision about which failures you'd rather have.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lex-and-matching-on-provenance\">Lex, and matching on provenance<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#lex-and-matching-on-provenance\" class=\"hash-link\" aria-label=\"Direct link to Lex, and matching on provenance\" title=\"Direct link to Lex, and matching on provenance\" translate=\"no\">​</a></h2>\n<p>The agent you get at signup is named Lex. Lex is not a special row. It's an\nordinary agent provisioned from the <code>research-assistant</code> template, subject to the\nsame validation, the same v1 pin, the same everything.</p>\n<p>The code never identifies it by name. It matches on template provenance, and the\ncomment explains why: <em>\"What makes it 'built-in' is provenance, so that's what we\nmatch on — never the display name, which the user is free to change.\"</em> When a\nmigration needed to rename the default agent, it only touched rows still carrying\nthe stock name. If you'd renamed yours, it was left alone.</p>\n<p><strong>Match on the immutable fact, not the mutable label.</strong> Display names are user\ndata. Any logic keyed on one is a bug waiting for the first person who\npersonalizes something.</p>\n<p>The provisioning itself is best-effort by design. Every failure path returns 200\nwith <code>provisioned: false</code> and a reason. Signup does not fail because agent\ncreation did — the account is the thing that must exist; the starter agent is a\nconvenience, and convenience never gets to block the front door.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-a-creation-path-owes-you\">What a creation path owes you<a href=\"https://blog.catalex.co/creating-an-agent-in-catalex#what-a-creation-path-owes-you\" class=\"hash-link\" aria-label=\"Direct link to What a creation path owes you\" title=\"Direct link to What a creation path owes you\" translate=\"no\">​</a></h2>\n<p>Validate the model's output against the live world before you persist it. Make\nthe invariant a required field, not a default. Draw the atomicity boundary\nconsciously and say where it is. Keep the mutable labels out of your logic.</p>\n<p>None of this shows up in the form. That's the point — the whole job is to make\nsure that the thing you clicked \"create\" on is the thing you actually got.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/creating-an-agent-in-catalex",
            "title": "Creating an Agent: What Happens After You Hit Save",
            "summary": "Creating an agent looks like filling in a form. The real engineering is in the seconds after you confirm — tool validation against a live catalog, an atomic version-1 pin, and the contract decisions that stop you shipping a broken agent.",
            "date_modified": "2026-07-04T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 3, 2026 · 09:00 UTC</div>\n<p>Every team that builds seriously with AI walks the same road, usually without\nnoticing. It starts with a chat window and a good feeling, and — if the work\nsurvives contact with production — it ends somewhere far more disciplined. This\nis a field note on that road: the three stages we see teams pass through, what\neach one is genuinely good at, and the specific failure that forces the jump to\nthe next.</p>\n<!-- -->\n<p>We are writing it down because the stages are easy to confuse. \"Vibe coding,\"\n\"spec-driven development,\" and \"loop engineering\" get used loosely, as vibes\nthemselves. They are not interchangeable moods. They are three different answers\nto one question — <em>who is responsible for correctness, and how is it checked?</em> —\nand the answer changes as the stakes rise.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-short-version\">The short version<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#the-short-version\" class=\"hash-link\" aria-label=\"Direct link to The short version\" title=\"Direct link to The short version\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Stage</th><th>The unit of work</th><th>Correctness comes from</th><th>Breaks when</th></tr></thead><tbody><tr><td><strong>Vibe coding</strong></td><td>A prompt</td><td>The human eyeballing the output</td><td>You can't hold the whole system in your head</td></tr><tr><td><strong>Spec-driven development</strong></td><td>A specification</td><td>The spec + acceptance tests</td><td>The spec is right but the run is flaky</td></tr><tr><td><strong>Loop engineering</strong></td><td>A loop</td><td>The system verifying and correcting itself</td><td>— (this is where production lives)</td></tr></tbody></table>\n<p>Each row is a reaction to the row above it. Read top to bottom, it is a story\nabout moving responsibility for correctness <em>out of the human's head</em> and <em>into\nan artifact</em>, then into a <em>system</em>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"stage-one-vibe-coding\">Stage one: vibe coding<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#stage-one-vibe-coding\" class=\"hash-link\" aria-label=\"Direct link to Stage one: vibe coding\" title=\"Direct link to Stage one: vibe coding\" translate=\"no\">​</a></h2>\n<p>Vibe coding is building by feel. You describe what you want in natural language,\nthe model produces something, you look at it, and you react. If it looks right,\nyou keep it. If it looks wrong, you nudge. The loop is fast, tactile, and — this\nis the important part — <strong>the only verifier is you, looking at the screen.</strong></p>\n<p>It is genuinely great. For a prototype, a throwaway script, a UI you can see\nwith your own eyes, or the first exploratory hour of any project, nothing beats\nit. The feedback is immediate and the cost of being wrong is a re-roll.</p>\n<p>Here is the shape of it:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">You:   \"Build a page that lists our open roles from the careers API.\"</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">Model: &lt;200 lines of code&gt;</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">You:   *scrolls, runs it* \"Close. Make the cards clickable and sort by team.\"</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">Model: &lt;revised code&gt;</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">You:   *looks* \"Ship it.\"</span><br></div></code></pre></div></div>\n<p>The reason it works is also the reason it doesn't scale: <strong>verification is\nimplicit and human.</strong> You are the test suite. That is fine when \"correct\" means\n\"looks right in the browser\" and the whole thing fits on one screen. It falls\napart the moment correctness depends on something you can't see at a glance — a\nconcurrency edge case, a compliance rule, a contract with another service, a\nbehavior that only shows up on the 10,000th request.</p>\n<div class=\"cx-note\"><p><strong>The tell that you've outgrown vibe coding:</strong> you find yourself re-checking the\nsame things by hand every time you make a change, because you no longer trust\nthat a small edit didn't break something out of view. That manual re-checking\n<em>is</em> a test suite — you just haven't written it down yet.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"stage-two-spec-driven-development\">Stage two: spec-driven development<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#stage-two-spec-driven-development\" class=\"hash-link\" aria-label=\"Direct link to Stage two: spec-driven development\" title=\"Direct link to Stage two: spec-driven development\" translate=\"no\">​</a></h2>\n<p>Spec-driven development moves the source of truth out of the conversation and\ninto a document. Instead of steering the model turn by turn, you write down what\n\"correct\" means — the behavior, the constraints, the acceptance criteria — and\nthe model implements against that. The spec becomes the artifact you review and\nversion; the code becomes a build output of it.</p>\n<p>The mental shift is subtle but total. In vibe coding you review <em>code</em>. In\nspec-driven development you review the <em>spec</em>, and you let tests review the code.</p>\n<p>A spec doesn't have to be heavy. Often it's a short, precise document:</p>\n<div class=\"language-markdown codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-markdown codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token title important punctuation\" style=\"color:#393A34\">##</span><span class=\"token title important\"> Feature: careers listing</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token title important punctuation\" style=\"color:#393A34\">###</span><span class=\"token title important\"> Behavior</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> Fetch open roles from GET /api/roles.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> Show role title, team, location, and an apply link.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> Sort by team (A→Z), then by title (A→Z).</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token title important punctuation\" style=\"color:#393A34\">###</span><span class=\"token title important\"> Constraints</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> Roles with </span><span class=\"token code-snippet code keyword\" style=\"color:#00009f\">`status != \"open\"`</span><span class=\"token plain\"> MUST NOT be shown.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> If the API is unreachable, render the last cached response and a stale badge.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token title important punctuation\" style=\"color:#393A34\">###</span><span class=\"token title important\"> Acceptance</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> [ ] A closed role in the fixture never appears in the DOM.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> [ ] With the API mocked to 500, the stale badge renders.</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token list punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> [ ] Sorting is stable and matches the fixture snapshot.</span><br></div></code></pre></div></div>\n<p>Now correctness lives in two places you can actually point at: the <strong>constraints</strong>\n(what must be true) and the <strong>acceptance tests</strong> (how we'll know). The model can\ngenerate the implementation, regenerate it, or a different model can — and the\ntests decide whether the result is acceptable. The human reviews intent, not\nline-by-line syntax.</p>\n<p>This is a large step up, and for a great deal of software it is the right and\nfinal stop. It gives you determinism where you need it: the same spec produces a\nresult that either passes the acceptance criteria or doesn't.</p>\n<p>So why is there a stage three? Because specs describe a <em>destination</em>, not a\n<em>journey</em>. A spec tells you what a correct result looks like. It says nothing\nabout what to do when the run that's supposed to produce that result is\nnon-deterministic — which is exactly the situation once an AI agent is doing the\nwork at runtime, in the wild, against inputs you didn't write fixtures for.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"stage-three-loop-engineering\">Stage three: loop engineering<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#stage-three-loop-engineering\" class=\"hash-link\" aria-label=\"Direct link to Stage three: loop engineering\" title=\"Direct link to Stage three: loop engineering\" translate=\"no\">​</a></h2>\n<p>Loop engineering is what you build when the <em>doer</em> is itself an AI system\noperating without you in the chair. The spec is still there — you still need to\nknow what correct means — but now the thing that checks the work, catches the\nmistakes, and tries again has to be part of the system, because you're asleep or\nhandling ten thousand concurrent runs and cannot be the verifier.</p>\n<p>The unit of work is no longer a prompt or a spec. It's a <strong>loop</strong>: generate a\ncandidate, verify it against the spec, and if it fails, feed the failure back in\nand correct. The loop runs until the work passes or a budget is exhausted.</p>\n<!-- -->\n<p>Written as a control loop, the idea is almost boring — which is the point:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">task</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> max_attempts</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    feedback </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> attempt </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">range</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">max_attempts</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        candidate </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">generate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">task</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> prior_feedback</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">feedback</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        report </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> verify</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">candidate</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">criteria</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># tests, evals, checks</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> report</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">passed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> candidate</span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">        feedback </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> report</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">failures                   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># what went wrong, precisely</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    escalate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">task</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> feedback</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># hand to a human, with context</span><br></div></code></pre></div></div>\n<p>Everything hard about loop engineering is hidden inside <code>verify</code> and <code>feedback</code>.\nThe loop is trivial. Making the verifier <em>trustworthy</em> is the entire discipline:</p>\n<ul>\n<li class=\"\"><strong>The verifier has to be real.</strong> Assertions, type checks, and tests are the\ncheap layer. Above them sit <strong>evals</strong> — graded checks on fuzzy outputs where\nthere is no single right answer, only better and worse. A loop is only as\nhonest as its weakest check; a verifier that rubber-stamps is worse than none,\nbecause it manufactures false confidence at scale.</li>\n<li class=\"\"><strong>Feedback has to be actionable.</strong> \"Failed\" teaches the model nothing. \"Role\n<code>r_912</code> had status <code>closed</code> but appeared in the output at index 3\" tells it\nexactly what to fix. The quality of the feedback string is, in practice, the\nquality of the loop.</li>\n<li class=\"\"><strong>The loop needs a budget and an exit.</strong> Real loops cost money and can thrash.\nBounded attempts, then a clean escalation to a human <em>with the full trace</em>, is\nnot a failure mode — it's the design.</li>\n</ul>\n<p>This is the stage where the earlier two are absorbed rather than discarded. The\nspec supplies the criteria the verifier checks against. The vibe-coding instinct\n— generate fast, react to what you see — becomes the inner <code>generate</code> step,\nexcept the thing reacting is now the verifier instead of your eyes.</p>\n<div class=\"cx-note\"><p><strong>Why this is the stage that ships.</strong> The industry's open secret is that most AI\nprojects never reach production — not because the models are weak, but because\nthey're wrapped like demos: one shot, no verifier, a human implicitly in the\nloop who quietly isn't there at 3 a.m. Loop engineering is what a demo becomes\nwhen you make the verification explicit and the human optional.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-we-think-about-it-at-catalex\">How we think about it at CatalEx<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#how-we-think-about-it-at-catalex\" class=\"hash-link\" aria-label=\"Direct link to How we think about it at CatalEx\" title=\"Direct link to How we think about it at CatalEx\" translate=\"no\">​</a></h2>\n<p>Running agents in production is loop engineering as a full-time job. The\nplatform-level features we talk about — agents that are <strong>self-correcting</strong>\n(they catch and fix their own mistakes mid-run), <strong>self-metriced</strong> (they pick and\ntrack the criteria they're judged on), and <strong>self-evolving</strong> (they get better\nfrom each outcome instead of plateauing on day one) — are all names for parts of\na well-built loop:</p>\n<ul>\n<li class=\"\"><em>Self-correcting</em> is the <code>fail → diagnose → regenerate</code> edge, made reliable.</li>\n<li class=\"\"><em>Self-metriced</em> is the agent participating in defining <code>spec.criteria</code>, so the\nverifier isn't frozen at whatever we guessed on day one.</li>\n<li class=\"\"><em>Self-evolving</em> is what happens when the feedback from thousands of loops feeds\nback into how the next generation is produced.</li>\n</ul>\n<p>None of it is exotic once you see it as one control loop drawn at different\naltitudes. The demo is the inner box. The spec is the criteria. The loop is the\nproduct.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"where-you-actually-are\">Where you actually are<a href=\"https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering#where-you-actually-are\" class=\"hash-link\" aria-label=\"Direct link to Where you actually are\" title=\"Direct link to Where you actually are\" translate=\"no\">​</a></h2>\n<p>You don't pick a stage as an identity; you pick it per piece of work, honestly.</p>\n<ul>\n<li class=\"\">Exploring, prototyping, building something you can see? <strong>Vibe code it.</strong> Speed\nis the whole point, and you are a perfectly good verifier for things that fit\non a screen.</li>\n<li class=\"\">Building something that has to keep being correct as it changes, with more than\none person or model touching it? <strong>Write the spec.</strong> Move correctness out of\nyour head and into acceptance criteria.</li>\n<li class=\"\">Putting an AI system in the position of doing the work unattended, at scale,\nagainst inputs you didn't anticipate? <strong>Engineer the loop.</strong> Make verification\nand correction part of the system, because you won't be there to be the test.</li>\n</ul>\n<p>The mistake is not being at an early stage. Vibe coding a prototype is correct.\nThe mistake is staying at a stage after the work has quietly moved past it —\nshipping vibe-coded code as if it were verified, or shipping a spec as if the run\nthat fulfills it were deterministic. Each stage exists to fix the specific way\nthe previous one lies to you once the stakes go up.</p>\n<p>Know which lie you can currently afford. Build the next loop before you need it.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/vibe-coding-to-spec-driven-to-loop-engineering",
            "title": "From Vibe Coding to Spec-Driven Development to Loop Engineering",
            "summary": "How building with AI matures in three stages — vibe coding, spec-driven development, and loop engineering — and why each stage exists to fix the failure mode of the one before it.",
            "date_modified": "2026-07-03T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Vibe Coding",
                "Spec-Driven Development",
                "Loop Engineering",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/agent-memory-that-compounds",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 2, 2026 · 09:00 UTC</div>\n<p>\"Give the agent memory\" is one of those phrases that sounds like a feature and\nis actually an architecture. Everyone agrees agents should remember things\nacross sessions. Almost no one agrees on what that means — and the default\nimplementation, a vector database that everything gets dumped into, produces\nagents that are confidently wrong about their own past.</p>\n<!-- -->\n<p>This is a field note on how we think about memory for agents that run in\nproduction: what the pieces are, what each one is for, and the single principle\nthat governs all of them — <strong>an agent's memory is only as useful as what it\nrefuses to remember.</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-default-that-quietly-fails\">The default that quietly fails<a href=\"https://blog.catalex.co/agent-memory-that-compounds#the-default-that-quietly-fails\" class=\"hash-link\" aria-label=\"Direct link to The default that quietly fails\" title=\"Direct link to The default that quietly fails\" translate=\"no\">​</a></h2>\n<p>The reflexive design is: embed every message, every tool result, every document,\nand store the vectors. At recall time, embed the current query and pull the top\n<em>k</em> nearest neighbors into the context window. This is easy, it demos beautifully,\nand it degrades in a specific, predictable way.</p>\n<p>The failure isn't that retrieval \"misses.\" It's that retrieval succeeds at the\nwrong thing. Nearest-neighbor search returns what is <em>semantically similar</em> to\nthe query, not what is <em>relevant</em> to the task. Ask an agent that has processed a\nthousand invoices about \"the Acme invoice\" and it will happily surface eleven\nAcme invoices from three different quarters, none flagged as superseded, and\nsplice them into context with equal authority. The model, being agreeable, will\nreconcile them into a confident answer that is subtly wrong.</p>\n<div class=\"cx-note\"><p><strong>The tell:</strong> your agent gives a great answer, then gives a contradictory great\nanswer to the same question a day later, and both are traceable to \"something it\nremembered.\" That's not a retrieval bug you can tune away. It's a sign that\nstorage and recall were never separated from <em>judgment</em> about what mattered.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"four-kinds-of-memory-four-different-jobs\">Four kinds of memory, four different jobs<a href=\"https://blog.catalex.co/agent-memory-that-compounds#four-kinds-of-memory-four-different-jobs\" class=\"hash-link\" aria-label=\"Direct link to Four kinds of memory, four different jobs\" title=\"Direct link to Four kinds of memory, four different jobs\" translate=\"no\">​</a></h2>\n<p>It helps to stop saying \"memory\" and name the pieces. Human cognition is a loose\nanalogy, but a useful one, because each type answers a different question.</p>\n<p><strong>Working memory</strong> is the current context window: the task, the recent turns,\nthe tool outputs from <em>this</em> run. It is bounded, it is expensive, and it is the\nonly memory the model can actually reason over. Everything else exists to decide\nwhat gets promoted into it.</p>\n<p><strong>Episodic memory</strong> is \"what happened.\" Specific events, tied to time and\noutcome: on June 3rd we ran the reconciliation for Acme and it failed because\nthe currency field was null; the human corrected it this way. Episodic memory is\nhow an agent learns from its own history instead of repeating it.</p>\n<p><strong>Semantic memory</strong> is \"what is true.\" Distilled facts that outlived the event\nthat produced them: Acme bills in EUR; this customer's fiscal year starts in\nApril; the approval threshold is 5,000. Semantic memory is what episodic memory\nbecomes once a pattern has been seen enough times to trust.</p>\n<p><strong>Procedural memory</strong> is \"how we do this here.\" The learned workflow — the order\nof steps, the checks that matter, the tool that actually works for a given task.\nIt's the difference between an agent that re-derives the process every run and\none that has a way of doing things.</p>\n<p>The mistake is storing all four as undifferentiated embeddings. They have\ndifferent write rules, different retrieval rules, and very different lifespans. A\nnull-currency incident is an episode that should decay. \"Acme bills in EUR\" is a\nfact that should persist and be retrieved by <em>entity</em>, not by vector similarity\nto the current sentence.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"writing-is-a-decision-not-a-side-effect\">Writing is a decision, not a side effect<a href=\"https://blog.catalex.co/agent-memory-that-compounds#writing-is-a-decision-not-a-side-effect\" class=\"hash-link\" aria-label=\"Direct link to Writing is a decision, not a side effect\" title=\"Direct link to Writing is a decision, not a side effect\" translate=\"no\">​</a></h2>\n<p>The most important design choice in an agent's memory is not how it retrieves.\nIt's what it writes down, and that should be a deliberate step, not an automatic\nlog of everything that streamed through the context.</p>\n<p>A memory-write worth making usually clears three bars:</p>\n<ul>\n<li class=\"\"><strong>It's durable.</strong> It will still be true, or still be instructive, next week.\nA transient tool error is not; the <em>lesson</em> from a recurring tool error is.</li>\n<li class=\"\"><strong>It's decision-relevant.</strong> It changes what a future run would do. If\nremembering it wouldn't alter any future action, it's noise wearing a fact's\nclothes.</li>\n<li class=\"\"><strong>It's attributable.</strong> It carries where it came from and when. A memory with\nno provenance can't be trusted, updated, or retired — it can only accumulate.</li>\n</ul>\n<p>In practice this means an agent should <em>reflect</em> before it writes: at the end of\na run, summarize what changed, promote the durable facts to semantic memory,\nkeep the specific episode with its outcome, and drop the rest. This is the same\ngenerate-then-verify discipline that governs a good agent loop, pointed inward.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"retrieval-by-structure-not-just-by-similarity\">Retrieval by structure, not just by similarity<a href=\"https://blog.catalex.co/agent-memory-that-compounds#retrieval-by-structure-not-just-by-similarity\" class=\"hash-link\" aria-label=\"Direct link to Retrieval by structure, not just by similarity\" title=\"Direct link to Retrieval by structure, not just by similarity\" translate=\"no\">​</a></h2>\n<p>Once memory is typed, retrieval stops being one vector search and becomes a small\nset of targeted ones. Facts about an entity are fetched by that entity. Recent\nepisodes are fetched by recency and relevance, with the outcome attached so the\nmodel can weight a success differently from a failure. Procedures are fetched by\ntask type. Vector similarity is one signal among several — useful for the fuzzy\n\"have we seen anything like this before,\" dangerous as the only lens.</p>\n<p>The other half of retrieval is <em>budgeting</em>. Working memory is finite and\nattention is not free; a context window stuffed with fourteen marginally relevant\nmemories reasons worse than one holding the three that matter. Recall should be\nranked and capped, and the cap should be small. More retrieved memory past a\npoint makes agents worse, not better — the signal gets buried and the model\nstarts pattern-matching on the loudest neighbor instead of the most relevant one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"forgetting-is-a-feature\">Forgetting is a feature<a href=\"https://blog.catalex.co/agent-memory-that-compounds#forgetting-is-a-feature\" class=\"hash-link\" aria-label=\"Direct link to Forgetting is a feature\" title=\"Direct link to Forgetting is a feature\" translate=\"no\">​</a></h2>\n<p>Here is the part that feels wrong and is essential: a good memory system spends\nas much design effort on removal as on storage.</p>\n<p>Facts get superseded — the approval threshold changed, the customer switched\ncurrencies, the API you learned to call was deprecated. If memory only appends,\nevery one of those updates becomes a contradiction the model has to resolve at\nread time, usually by picking one at random. So memories need to be\n<em>updatable</em> (the new fact replaces the old, with the old one retired, not\ndeleted silently) and <em>decayable</em> (episodes lose weight over time unless\nreinforced). \"Remembers everything\" is not the goal. An agent that cannot forget\na fact that stopped being true is worse than one with no memory at all, because\nit is wrong <em>with history behind it</em>.</p>\n<p>This is also where trust and safety live. Memory is an injection surface: content\nan agent reads today can be written into memory and re-executed as instruction\ntomorrow. Typed, attributed, expirable memory is what lets you audit that — to\nanswer \"why did the agent believe this?\" with a specific provenance chain rather\nthan a shrug at a vector store.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-this-compounds\">Why this compounds<a href=\"https://blog.catalex.co/agent-memory-that-compounds#why-this-compounds\" class=\"hash-link\" aria-label=\"Direct link to Why this compounds\" title=\"Direct link to Why this compounds\" translate=\"no\">​</a></h2>\n<p>The reason to do the harder version is that memory is the mechanism by which an\nagent gets better instead of just staying the same. A stateless agent performs\nthe same on run 10,000 as on run 1 — it re-derives everything, repeats every\navoidable mistake, and never accumulates the specific, hard-won knowledge of\n<em>your</em> domain that a good human teammate builds over months. An agent with\ntyped, curated, decaying memory does the opposite: each run leaves the next one a\nlittle more competent, because the durable lessons persist and the noise doesn't.</p>\n<p>At CatalEx this is what we mean when we say agents are self-evolving and carry\ncontext across sessions. It isn't a bigger vector store. It's the discipline of\ndeciding, every run, what was worth remembering — and what the agent is better\noff forgetting.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/agent-memory-that-compounds",
            "title": "Memory That Compounds: What \"Persistent\" Actually Means for an Agent",
            "summary": "Agent memory is not a vector database. A practical model for what an agent should write, retrieve, and forget — and why \"remembers everything\" is a bug, not a feature.",
            "date_modified": "2026-07-02T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Memory",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/the-eval-is-the-product",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published July 1, 2026 · 09:00 UTC</div>\n<p>Every team that has tried to ship an AI feature has hit the same wall. The demo\nworks. A stakeholder asks, \"is it good?\" and the honest answer is a shrug. You\n<em>feel</em> like it's good. You changed a prompt and it feels a little better, or a\nlittle worse, and you cannot say which, because \"feels\" is the only instrument\nyou have.</p>\n<!-- -->\n<p>That wall is the absence of an eval. And the reason so many AI projects stall at\n80% — good enough to demo, never good enough to trust — is almost never the\nmodel. It's that the team never built the one thing that turns \"feels good\" into\na number you can defend: a way to measure whether the system actually works.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"tests-answer-yesno-evals-answer-how-well\">Tests answer yes/no. Evals answer how well.<a href=\"https://blog.catalex.co/the-eval-is-the-product#tests-answer-yesno-evals-answer-how-well\" class=\"hash-link\" aria-label=\"Direct link to Tests answer yes/no. Evals answer how well.\" title=\"Direct link to Tests answer yes/no. Evals answer how well.\" translate=\"no\">​</a></h2>\n<p>Traditional software has a comfortable property: correctness is usually binary.\nThe function returns the right value or it doesn't. A test suite is a wall of\ngreen checks, and green means ship.</p>\n<p>AI systems break that comfort. \"Correct\" becomes a spectrum. A summary can be\naccurate but bury the lede. An agent can complete the task but take a reckless\npath to get there. Two answers can both be right and one can be clearly better.\nThe output space is open-ended, the same input can produce different outputs run\nto run, and the thing you care about — helpfulness, faithfulness, safety — isn't\na return value you can assert on.</p>\n<p>An eval is what you build to measure a spectrum instead of a boolean. At its\ncore it is three things: a <strong>dataset</strong> of representative inputs, a <strong>grader</strong>\nthat scores each output, and an aggregate <strong>score</strong> you can track over time.\nThat's it. The engineering is entirely in making each of those three trustworthy.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"without-an-eval-you-are-flying-on-vibes\">Without an eval, you are flying on vibes<a href=\"https://blog.catalex.co/the-eval-is-the-product#without-an-eval-you-are-flying-on-vibes\" class=\"hash-link\" aria-label=\"Direct link to Without an eval, you are flying on vibes\" title=\"Direct link to Without an eval, you are flying on vibes\" translate=\"no\">​</a></h2>\n<p>It's worth being blunt about what the absence of an eval costs, because the costs\nare invisible until they compound.</p>\n<p>You can't tell improvement from regression. You tweak a prompt to fix one bad\ncase and silently break five others you never looked at. You have no way to know,\nso you ship it.</p>\n<p>You can't compare models. A cheaper model comes out; is it good enough for your\ntask? Without an eval, \"good enough\" is a meeting, not a measurement, and the\nmeeting is won by whoever is most confident.</p>\n<p>You can't safely change anything. Every prompt edit, tool change, or model\nupgrade is a gamble on a system whose behavior you can only spot-check. So teams\nstop changing things, and the product freezes at its first acceptable version.</p>\n<p>And you can't close the loop. This is the one that matters most to us: an\nautonomous agent that verifies and corrects its own work is only as honest as the\nverifier inside it. That verifier <em>is</em> an eval, running at runtime. A team that\nnever learned to write a trustworthy grader offline has no chance of embedding a\ntrustworthy one at runtime.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-grader-is-the-whole-game\">The grader is the whole game<a href=\"https://blog.catalex.co/the-eval-is-the-product#the-grader-is-the-whole-game\" class=\"hash-link\" aria-label=\"Direct link to The grader is the whole game\" title=\"Direct link to The grader is the whole game\" translate=\"no\">​</a></h2>\n<p>Anyone can assemble a dataset. The discipline is in the grader, and graders come\nin a rough hierarchy of cost and fuzziness.</p>\n<p><strong>Programmatic checks</strong> are the cheap, sharp layer: exact match, JSON-schema\nvalidity, \"did it call the right tool with the right arguments,\" \"does the code\ncompile and pass tests.\" When a task <em>has</em> a checkable ground truth, use it.\nThese are fast, deterministic, and free of the ambiguity that dooms softer\ngraders. Most teams underuse them because they reach for a model before checking\nwhether the property is mechanically verifiable.</p>\n<p><strong>LLM-as-judge</strong> is for the genuinely open-ended: is this summary faithful to the\nsource, is this reply helpful, is this reasoning sound. A second model grades the\nfirst against a rubric. It's powerful and it's a trap in equal measure, because a\nsloppy judge manufactures false confidence at scale. The failure modes are\nwell-documented and consistent: judges prefer longer answers, prefer the first\noption shown, and prefer their own style. A judge is only usable once you have\n<em>evaluated the evaluator</em> — checked its scores against human labels on a sample\nand confirmed it agrees often enough to trust.</p>\n<p><strong>Human grading</strong> is the ground truth the other two are calibrated against. It's\nslow and expensive, so you spend it deliberately: to build the golden set, to\naudit the automated graders, and to adjudicate the cases the judges disagree on.\nYou don't scale it; you use it to make the things that scale honest.</p>\n<div class=\"cx-note\"><p><strong>A verifier that rubber-stamps is worse than no verifier.</strong> A green eval that\nisn't measuring what you think it is doesn't just fail to help — it actively\nhides regressions behind a number people trust. Before you believe a grader,\nmake it disagree with you at least once on a case where you know the answer.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"evals-rot-so-treat-them-like-code\">Evals rot, so treat them like code<a href=\"https://blog.catalex.co/the-eval-is-the-product#evals-rot-so-treat-them-like-code\" class=\"hash-link\" aria-label=\"Direct link to Evals rot, so treat them like code\" title=\"Direct link to Evals rot, so treat them like code\" translate=\"no\">​</a></h2>\n<p>An eval is not a milestone you pass once. It's a living asset, and it decays in\ntwo directions.</p>\n<p>It decays against reality: your dataset was drawn from last quarter's inputs, and\nproduction has moved on. New kinds of requests appear; the eval doesn't cover\nthem; your score stays green while real quality slips. The fix is a flywheel —\nevery genuinely wrong production case that surfaces becomes a new eval example, so\nthe dataset tracks the distribution it's supposed to represent.</p>\n<p>And it decays against gaming: any single metric, optimized hard enough, stops\ncorrelating with the thing it was a proxy for. Optimize a helpfulness judge and\nyou get verbose, hedging answers that <em>read</em> as helpful. The defense is a small\npanel of graders looking at different failure modes — faithfulness, concision,\nsafety, task success — so that gaming one costs you another.</p>\n<p>The teams that get this right run evals the way they run tests: in CI, on every\nchange, with the score gating the merge. A prompt edit that drops the eval is a\nregression, full stop, the same as a failing unit test. That single move —\nmaking the number <em>block the merge</em> — is what converts evaluation from a report\nnobody reads into the thing that actually governs the system.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-we-say-the-eval-is-the-product\">Why we say the eval is the product<a href=\"https://blog.catalex.co/the-eval-is-the-product#why-we-say-the-eval-is-the-product\" class=\"hash-link\" aria-label=\"Direct link to Why we say the eval is the product\" title=\"Direct link to Why we say the eval is the product\" translate=\"no\">​</a></h2>\n<p>Point the whole picture at an autonomous agent and the stakes get concrete. The\nagent generates a candidate, a verifier scores it against criteria, and on\nfailure it corrects and tries again. That verifier is an eval running in the\nhot path. Its dataset is the live task, its grader is your checks and judges, its\nscore is the gate between \"commit\" and \"try again.\" Every property that makes an\noffline eval trustworthy — real coverage, calibrated graders, resistance to\ngaming — is exactly what makes the runtime loop safe to leave unattended.</p>\n<p>Which is why we don't treat evaluation as QA that happens after the product is\nbuilt. The eval is the part of the product that decides whether any of the rest\nof it can be trusted. Build that first, and everything downstream — shipping,\nimproving, running agents without a human in the chair — becomes possible. Skip\nit, and you're left with a great demo and a shrug.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/the-eval-is-the-product",
            "title": "The Eval Is the Product: Why You Can't Ship What You Can't Measure",
            "summary": "Evals are not a testing afterthought — they are the artifact that decides whether an AI system can ship, improve, or be trusted. A practical case for treating evaluation as the product.",
            "date_modified": "2026-07-01T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Evaluation",
                "Agent Engineering",
                "Loop Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/harness-vs-model",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published June 30, 2026 · 09:00 UTC</div>\n<p>The most expensive misconception in applied AI is that performance lives in the\nmodel. Pick the best model, the thinking goes, and the rest is glue. So teams\nspend their attention on which model to call and treat everything around it — the\ntools, the loop, the context they assemble, the way they check the output — as\nplumbing.</p>\n<!-- -->\n<p>The data says the opposite. The scaffolding around the model — what we'll call\nthe <strong>harness</strong> — routinely moves results as much as swapping the model does,\nand sometimes more. The same weights, handed a better harness, become a\ndifferent product. If you're deciding where to spend the next month of\nengineering, this is the argument for spending it on the harness.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-harness-actually-is\">What the harness actually is<a href=\"https://blog.catalex.co/harness-vs-model#what-the-harness-actually-is\" class=\"hash-link\" aria-label=\"Direct link to What the harness actually is\" title=\"Direct link to What the harness actually is\" translate=\"no\">​</a></h2>\n<p>The harness is everything between the user's intent and the model's tokens:</p>\n<ul>\n<li class=\"\"><strong>Tools</strong> the model can call, and how well their inputs and outputs are shaped</li>\n<li class=\"\"><strong>The loop</strong> — how many steps it gets, when it retries, when it stops</li>\n<li class=\"\"><strong>Context assembly</strong> — what you retrieve and put in the window, and what you\nleave out</li>\n<li class=\"\"><strong>Verification</strong> — the checks that decide whether an output is accepted or sent\nback for another pass</li>\n<li class=\"\"><strong>Memory</strong> — what persists across steps and sessions</li>\n</ul>\n<p>None of that is the model. All of it decides what the model can accomplish.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"same-model-very-different-results\">Same model, very different results<a href=\"https://blog.catalex.co/harness-vs-model#same-model-very-different-results\" class=\"hash-link\" aria-label=\"Direct link to Same model, very different results\" title=\"Direct link to Same model, very different results\" translate=\"no\">​</a></h2>\n<p>The cleanest evidence comes from SWE-bench, a benchmark where an agent has to\nresolve real GitHub issues by editing a codebase until the project's tests pass.\nIt's a good measuring stick precisely because the grader is objective — the tests\neither pass or they don't.</p>\n<p>Watch what happens to the <em>field's</em> score over a couple of years, as both models\nand harnesses improved together:</p>\n<figure class=\"cx-chart\"><div class=\"cx-chart-title\">SWE-bench Verified — resolved rate over time</div><svg viewBox=\"0 0 680 220\" role=\"img\" aria-label=\"Bar chart of SWE-bench Verified resolved rate climbing from about 1 percent in 2023 to about 67 percent in 2025\"><line x1=\"246\" y1=\"14\" x2=\"246\" y2=\"206\" stroke=\"currentColor\" stroke-opacity=\"0.18\" stroke-width=\"1\"></line><g font-family=\"var(--ifm-font-family-base)\" font-size=\"13\" fill=\"currentColor\"><text x=\"6\" y=\"42\">2023 — retrieval baseline</text><text x=\"6\" y=\"92\">2024 — SWE-agent scaffold</text><text x=\"6\" y=\"142\">2024 — agentic harness</text><text x=\"6\" y=\"192\">2025 — frontier agents</text></g><g fill=\"var(--ifm-color-primary)\"><rect x=\"246\" y=\"24\" width=\"8\" height=\"26\" rx=\"3\"></rect><rect x=\"246\" y=\"74\" width=\"100\" height=\"26\" rx=\"3\"></rect><rect x=\"246\" y=\"124\" width=\"273\" height=\"26\" rx=\"3\"></rect><rect x=\"246\" y=\"174\" width=\"373\" height=\"26\" rx=\"3\"></rect></g><g font-family=\"var(--ifm-font-family-monospace)\" font-size=\"12\" fill=\"currentColor\"><text x=\"262\" y=\"42\">~1.4%</text><text x=\"354\" y=\"92\">~18%</text><text x=\"527\" y=\"142\">~49%</text><text x=\"627\" y=\"192\">~67%</text></g></svg><figcaption><p>Approximate resolved rates, SWE-bench / SWE-bench Verified. Sources:\nSWE-bench leaderboard; Yang et al., \"SWE-agent\" (2024); public vendor\nreports. Exact figures depend on model version and evaluation date.</p></figcaption></figure>\n<p>A jump from roughly 1% to roughly 67% is not a story about weights getting 60x\nbetter. Model quality improved a great deal in that window — but a large share of\nthe climb came from learning how to <em>wrap</em> the model: giving it a code-navigation\ntool instead of raw file dumps, letting it iterate against the test suite,\nmanaging what stayed in context. The harness matured alongside the model.</p>\n<p>You can see the harness's contribution in isolation by holding the model fixed\nand changing only the scaffold. The pattern is consistent across published\nresults: a capable model, asked to emit a patch in one shot, badly underperforms\nthe same model given tools and a loop.</p>\n<figure class=\"cx-chart\"><div class=\"cx-chart-title\">One fixed model, three harnesses (SWE-bench Verified, illustrative)</div><svg viewBox=\"0 0 680 170\" role=\"img\" aria-label=\"Bar chart showing the same model scoring about 5 percent single-shot, 27 percent with a basic loop, and 49 percent with a purpose-built harness\"><line x1=\"246\" y1=\"14\" x2=\"246\" y2=\"156\" stroke=\"currentColor\" stroke-opacity=\"0.18\" stroke-width=\"1\"></line><g font-family=\"var(--ifm-font-family-base)\" font-size=\"13\" fill=\"currentColor\"><text x=\"6\" y=\"42\">Single-shot patch</text><text x=\"6\" y=\"92\">Basic ReAct loop</text><text x=\"6\" y=\"142\">Purpose-built harness</text></g><g fill=\"var(--ifm-color-primary)\"><rect x=\"246\" y=\"24\" width=\"36\" height=\"26\" rx=\"3\"></rect><rect x=\"246\" y=\"74\" width=\"191\" height=\"26\" rx=\"3\"></rect><rect x=\"246\" y=\"124\" width=\"347\" height=\"26\" rx=\"3\"></rect></g><g font-family=\"var(--ifm-font-family-monospace)\" font-size=\"12\" fill=\"currentColor\"><text x=\"290\" y=\"42\">~5%</text><text x=\"445\" y=\"92\">~27%</text><text x=\"601\" y=\"142\">~49%</text></g></svg><figcaption><p>Representative of the gap reported between minimal and agentic scaffolds on a\nfixed model. Illustrative figures consistent with SWE-agent (2024) and vendor\nagent reports; the point is the shape, not the decimal.</p></figcaption></figure>\n<p>Same weights. Roughly a 40-point spread. The harness is not glue around the\nperformance — a large fraction of the performance <em>is</em> the harness.</p>\n<div class=\"cx-note\"><p><strong>Why this happens:</strong> a single forward pass has to be right the first time, with\nwhatever context it was handed. A harness converts a one-shot guess into a\nprocess — look, act, observe, check, correct. That turns the model's real\nstrength (reacting well to feedback) into task success, and it's exactly the\nstrength a single shot never gets to use.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-harness-compounds-the-model-resets\">The harness compounds; the model resets<a href=\"https://blog.catalex.co/harness-vs-model#the-harness-compounds-the-model-resets\" class=\"hash-link\" aria-label=\"Direct link to The harness compounds; the model resets\" title=\"Direct link to The harness compounds; the model resets\" translate=\"no\">​</a></h2>\n<p>There's a second reason to invest in the harness, and it's about time, not just\nscores. Model choice is a decision you re-make every few months — a new frontier\nmodel ships and you re-evaluate. Every dollar of prompt-whispering tuned to one\nmodel's quirks partially evaporates at the next upgrade.</p>\n<p>Harness investment does the opposite. A good code-navigation tool, a\nwell-designed verification step, a context-assembly strategy that keeps the\nwindow clean — these carry <em>forward</em> onto the next model, and usually get better\nunderneath it, because a stronger model uses a good tool more effectively. The\nharness is the part of your system that accrues value across model generations\ninstead of being reset by them.</p>\n<p>This also reframes the \"which model\" debate. On a mature harness, the gap between\nthe top two or three frontier models is often smaller than the gap between a good\nand a mediocre harness on <em>either</em> of them. The model is a component you swap; the\nharness is the product you build.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"so-what-should-you-actually-build\">So what should you actually build?<a href=\"https://blog.catalex.co/harness-vs-model#so-what-should-you-actually-build\" class=\"hash-link\" aria-label=\"Direct link to So what should you actually build?\" title=\"Direct link to So what should you actually build?\" translate=\"no\">​</a></h2>\n<p>If the harness is where the leverage is, a few principles follow.</p>\n<p><strong>Make it model-agnostic.</strong> Treat the model as a replaceable component behind an\ninterface. When the next model ships, you want to run your eval suite against it\nin an afternoon, not rewrite your prompts. The teams that move fastest on new\nmodels are the ones whose harness didn't hard-code the old one.</p>\n<p><strong>Invest in tools and their ergonomics, not just their existence.</strong> A tool whose\noutput is a wall of unstructured text costs the model context and attention. The\nsame tool returning a tight, structured result is worth more than a model\nupgrade. Shape tool I/O the way you'd shape an API for a junior engineer who\nreads fast but tires easily.</p>\n<p><strong>Build the verification step first, not last.</strong> The loop that lets a model check\nand correct its own work is the single highest-leverage piece of the harness — it\nconverts a capable one-shot model into a reliable one. Everything we've written\nabout loop engineering and evals is really about this component of the harness.</p>\n<p><strong>Manage context like a budget.</strong> More retrieved text is not more capability past\na point; it's noise that buries the signal. A harness that puts the three right\nthings in the window beats one that puts fourteen plausible ones.</p>\n<p>At CatalEx this is the whole thesis of the product. The platform <em>is</em> a harness —\ntools, orchestration, memory, verification, model routing — built so that the\nmodel underneath is a component you can swap as the frontier moves, while the\nscaffolding that actually determines whether an agent works keeps compounding.\nPick your model carefully. Then spend the real effort on everything around it,\nbecause that's where the performance was hiding all along.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/harness-vs-model",
            "title": "The Harness Is Half the Model: Why Scaffolding Decides Performance",
            "summary": "The same model can swing 40 points on the same benchmark depending on the harness around it. A data-grounded look at why the scaffolding — tools, loop, context, verification — is as decisive as the model you pick.",
            "date_modified": "2026-06-30T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Agent Harness",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/mcp-vs-cli-for-integration",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published June 29, 2026 · 09:00 UTC</div>\n<p>There's a tidy argument going around: skip the Model Context Protocol, just give\nthe agent a shell and a CLI. No server to run, no schemas to maintain, and — the\nclincher — it saves tokens, because a command is short and a tool definition is\nlong. It's a good argument. It's also only half right, and the missing half is\nwhere most of the cost actually lives.</p>\n<!-- -->\n<p>This is a look at the two ways to wire an agent into a third-party system — an\nMCP server versus a command-line tool — with particular attention to the claim\nthat the CLI is the cheaper one. The honest answer is \"sometimes, and not for the\nreason people think.\"</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"two-ways-to-hand-an-agent-a-capability\">Two ways to hand an agent a capability<a href=\"https://blog.catalex.co/mcp-vs-cli-for-integration#two-ways-to-hand-an-agent-a-capability\" class=\"hash-link\" aria-label=\"Direct link to Two ways to hand an agent a capability\" title=\"Direct link to Two ways to hand an agent a capability\" translate=\"no\">​</a></h2>\n<p><strong>MCP</strong> is a protocol for exposing tools and data to an agent in a structured\nway. You run a server that advertises a set of tools, each with a typed schema\nfor its inputs and outputs. The agent's runtime loads those definitions, the\nmodel sees them, and it calls them as structured functions with structured\nresults coming back.</p>\n<p><strong>The CLI approach</strong> skips all of that. You give the agent a shell tool and tell\nit a command exists. It composes a command string, the command runs, and the raw\nstdout comes back as text for the model to read. No schema, no server — the\nintegration surface is \"the agent knows how to use a terminal.\"</p>\n<p>Both work. They fail and cost differently, and the difference is almost entirely\nabout tokens and structure.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"where-the-tokens-actually-go\">Where the tokens actually go<a href=\"https://blog.catalex.co/mcp-vs-cli-for-integration#where-the-tokens-actually-go\" class=\"hash-link\" aria-label=\"Direct link to Where the tokens actually go\" title=\"Direct link to Where the tokens actually go\" translate=\"no\">​</a></h2>\n<p>The pro-CLI token argument focuses on one side of the ledger: <strong>tool-definition\noverhead.</strong> An MCP server that exposes twenty tools injects twenty schemas into\ncontext on every request, whether or not any of them get used. That's real, and\nit's not small — a directory of chatty MCP servers can burn thousands of tokens\nbefore the model has done anything. A CLI, by contrast, costs almost nothing to\nadvertise: one line saying the command exists.</p>\n<p>But there are two sides to the ledger, and the other side is <strong>output.</strong></p>\n<div class=\"cx-note\"><p><strong>The half that gets ignored:</strong> a CLI returns whatever it prints — and most CLIs\nprint for humans, not models. <code>git status</code>, <code>kubectl get</code>, <code>npm install</code>, a\nverbose API client: they emit banners, progress bars, decoration, and pages of\ndetail the model doesn't need. Every character is input tokens on the <em>next</em>\nturn. A single noisy command can cost more tokens than the MCP schema you were\ntrying to avoid.</p></div>\n<p>So \"CLI saves tokens\" collapses into a real question: does your integration make\n<em>few</em> calls against <em>many</em> tools (definition overhead dominates → CLI looks\ncheaper), or <em>many</em> calls returning <em>verbose</em> output (output dominates → the CLI\ncan be far more expensive)? The answer depends on the workload, not on the\nmechanism.</p>\n<p>There's a reason a whole category of tooling exists to sit between an agent and\nits CLIs and strip the output down — filtering, summarizing, and reshaping\ncommand results before they hit the context window. (Our own developer setup\nleans on exactly this kind of proxy; more on that in a follow-up.) The existence\nof that category is the tell: raw CLI output is a token liability that teams\nactively engineer around. The CLI isn't lean by default. It's lean <em>after</em> you do\nthe work MCP's typed outputs give you for free.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"beyond-tokens-the-other-four-axes\">Beyond tokens: the other four axes<a href=\"https://blog.catalex.co/mcp-vs-cli-for-integration#beyond-tokens-the-other-four-axes\" class=\"hash-link\" aria-label=\"Direct link to Beyond tokens: the other four axes\" title=\"Direct link to Beyond tokens: the other four axes\" translate=\"no\">​</a></h2>\n<p>Token cost is the loudest part of the debate and the least important once you've\nsized it. Four other differences matter more in production.</p>\n<p><strong>Structure and reliability.</strong> MCP hands the model typed arguments and typed\nresults. The CLI hands it a string to compose and a string to parse. Every\nun-typed boundary is a place the model can get the flag wrong, misread a table,\nor hallucinate a field that isn't there. Structured I/O isn't just tidier; it\nremoves whole classes of silent error.</p>\n<p><strong>Discoverability.</strong> With MCP, the agent can enumerate what's available and how\nto call it. With a CLI, it relies on training-data familiarity or on reading\n<code>--help</code> (more tokens) — and it will confidently invent flags for tools it half-\nremembers. Popular tools it knows well; your bespoke internal CLI it does not.</p>\n<p><strong>Composition.</strong> This one favors the CLI. Shells pipe, chain, and glue tools\ntogether in ways that are genuinely powerful, and a model that's good at bash can\naccomplish a lot with primitives you never explicitly exposed. MCP tools compose\nonly as far as you designed them to.</p>\n<p><strong>Security and blast radius.</strong> A shell is arbitrary code execution by\nconstruction. Handing an autonomous agent a terminal is handing it the ability to\ndo anything the shell can, which means sandboxing, allowlists, and audit become\nyour problem. An MCP tool is a narrow, declared capability — the agent can do\n<em>that</em>, and only that. For anything touching production or customer data, that\ncontainment is worth a lot.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"so-what-should-you-build\">So what should you build?<a href=\"https://blog.catalex.co/mcp-vs-cli-for-integration#so-what-should-you-build\" class=\"hash-link\" aria-label=\"Direct link to So what should you build?\" title=\"Direct link to So what should you build?\" translate=\"no\">​</a></h2>\n<p>Neither wins categorically. The decision falls out of the workload.</p>\n<p><strong>Reach for MCP when</strong> the capability is a durable, high-value operation you'll\ncall often; when the output should be structured for the model rather than for a\nhuman; when you need containment and audit; or when the same capability will be\nreused across many agents. Typed, discoverable, narrow — that's worth the server.</p>\n<p><strong>Reach for the CLI when</strong> the tool already exists and is well-known, when you\nneed ad-hoc composition more than a fixed contract, when you're prototyping and\ndon't want to stand up infrastructure, or when the operations are one-off enough\nthat schema maintenance would cost more than it returns. Just budget for the\noutput problem — plan to filter what comes back.</p>\n<p><strong>And often, build both.</strong> A common, effective shape is a thin MCP server that\nwraps a CLI: the model gets a typed, discoverable, containable interface, while\nthe server internally shells out and — critically — reshapes the raw output into\na compact, structured result before it returns. You get the CLI's reach and the\nMCP's ergonomics, and you pay the output-token cost once, in code you control,\ninstead of on every model turn.</p>\n<div class=\"cx-note\"><p><strong>The metric that settles it:</strong> optimize tokens-per-<em>successful-task</em>, not\ntokens-per-call. A CLI that's cheap to invoke but returns noise the model has to\nre-read across five correction turns is more expensive than an MCP call that\ncosts more up front and gets it right the first time. Measure the loop, not the\nline.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-uncomfortable-conclusion\">The uncomfortable conclusion<a href=\"https://blog.catalex.co/mcp-vs-cli-for-integration#the-uncomfortable-conclusion\" class=\"hash-link\" aria-label=\"Direct link to The uncomfortable conclusion\" title=\"Direct link to The uncomfortable conclusion\" translate=\"no\">​</a></h2>\n<p>\"The CLI saves tokens\" is true in exactly the narrow case where you have many\nrarely-used tools and terse, model-friendly output — and false the moment output\ngets verbose, which for real-world CLIs is most of the time. The tokens you save\non definitions, you often hand right back on results, plus a parsing tax and a\nsecurity surface.</p>\n<p>So don't pick the mechanism for its reputation. Size your workload — call\nfrequency, tool count, output verbosity, containment needs — and pick the\ninterface that minimizes tokens-per-successful-task while keeping the blast radius\nsurvivable. Usually that's MCP for the durable core, CLIs for the long tail, and\nan output filter wherever raw command text would otherwise flood the window. The\n\"cheaper\" option is the one you engineered to be cheap, not the one that started\nthat way.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/mcp-vs-cli-for-integration",
            "title": "MCP vs. CLI for Third-Party Integration: Is the CLI Really Cheaper?",
            "summary": "Giving an agent a CLI feels leaner than standing up an MCP server — but \"CLI saves tokens\" is only half true. A practical comparison of the two integration styles and what to actually build.",
            "date_modified": "2026-06-29T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "MCP",
                "Integrations",
                "Agent Engineering"
            ]
        },
        {
            "id": "https://blog.catalex.co/five-papers-that-shaped-modern-ai",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published June 28, 2026 · 09:00 UTC</div>\n<p>If you want to understand why AI works the way it does today, you don't need to\nread a hundred papers. You need to read about five, in order, and understand what\neach one changed. Everything since is, to a first approximation, a refinement or\na scaling-up of ideas these introduced.</p>\n<!-- -->\n<p>This is that list — the five we'd hand a strong engineer joining the field. Not\nthe five \"best\" papers by citation count, but the five that each moved the ground\nunder everyone's feet. Read in sequence, they tell one continuous story: how we\ngot from machine translation to agents.</p>\n<figure class=\"cx-chart\"><div class=\"cx-chart-title\">The through-line, 2017–2022</div><svg viewBox=\"0 0 680 116\" role=\"img\" aria-label=\"Timeline of five papers from 2017 to 2022\"><line x1=\"40\" y1=\"60\" x2=\"630\" y2=\"60\" stroke=\"currentColor\" stroke-opacity=\"0.2\" stroke-width=\"2\"></line><g><circle cx=\"55\" cy=\"60\" r=\"14\" fill=\"var(--ifm-color-primary)\"></circle><circle cx=\"235\" cy=\"60\" r=\"14\" fill=\"var(--ifm-color-primary)\"></circle><circle cx=\"430\" cy=\"60\" r=\"14\" fill=\"var(--ifm-color-primary)\"></circle><circle cx=\"520\" cy=\"60\" r=\"14\" fill=\"var(--ifm-color-primary)\"></circle><circle cx=\"610\" cy=\"60\" r=\"14\" fill=\"var(--ifm-color-primary)\"></circle></g><g font-family=\"var(--ifm-font-family-base)\" font-size=\"13\" font-weight=\"600\" fill=\"#ffffff\" text-anchor=\"middle\"><text x=\"55\" y=\"65\">1</text><text x=\"235\" y=\"65\">2</text><text x=\"430\" y=\"65\">3</text><text x=\"520\" y=\"65\">4</text><text x=\"610\" y=\"65\">5</text></g><g font-family=\"var(--ifm-font-family-monospace)\" font-size=\"11\" fill=\"currentColor\" text-anchor=\"middle\"><text x=\"55\" y=\"96\">2017</text><text x=\"235\" y=\"96\">2020</text><text x=\"430\" y=\"96\">Jan '22</text><text x=\"520\" y=\"96\">Mar '22</text><text x=\"610\" y=\"96\">Mar '22</text></g></svg><figcaption><p>Architecture → scale → reasoning → alignment → efficient training. Three of\nthe five landed within a few months of each other in early 2022.</p></figcaption></figure>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-attention-is-all-you-need-2017\">1. Attention Is All You Need (2017)<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#1-attention-is-all-you-need-2017\" class=\"hash-link\" aria-label=\"Direct link to 1. Attention Is All You Need (2017)\" title=\"Direct link to 1. Attention Is All You Need (2017)\" translate=\"no\">​</a></h2>\n<p><strong>Vaswani et al. · June 2017 · <a href=\"https://arxiv.org/abs/1706.03762\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">arXiv:1706.03762</a></strong></p>\n<p><em>Why it mattered:</em> it invented the Transformer, the architecture underneath every\nlarge model since.</p>\n<p>Before this, sequence models were recurrent — they processed tokens one at a\ntime, which made them slow to train and forgetful over long inputs. The\nTransformer threw out recurrence entirely and replaced it with <strong>self-attention</strong>:\na mechanism that lets every token look directly at every other token in one step.\nThat single change did two things. It let models learn long-range relationships\nthat recurrence kept losing, and — because attention is parallelizable — it let\ntraining scale across hardware in a way recurrence never could. The paper's title\nwas a swagger, and it earned it. Almost nothing that came after is possible\nwithout it; when people say \"an LLM,\" they mean a very large Transformer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-language-models-are-few-shot-learners--gpt-3-2020\">2. Language Models are Few-Shot Learners — GPT-3 (2020)<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#2-language-models-are-few-shot-learners--gpt-3-2020\" class=\"hash-link\" aria-label=\"Direct link to 2. Language Models are Few-Shot Learners — GPT-3 (2020)\" title=\"Direct link to 2. Language Models are Few-Shot Learners — GPT-3 (2020)\" translate=\"no\">​</a></h2>\n<p><strong>Brown et al. · May 2020 · <a href=\"https://arxiv.org/abs/2005.14165\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">arXiv:2005.14165</a></strong></p>\n<p><em>Why it mattered:</em> it showed that scale alone unlocks a new capability —\nlearning from the prompt, with no training.</p>\n<p>GPT-3 took the Transformer and made it enormous: 175 billion parameters trained on\na large slice of the internet. The headline wasn't the size, though — it was\n<strong>in-context learning.</strong> Give the model a few examples of a task <em>in the prompt</em>,\nand it performs the task, with no fine-tuning and no weight updates. This\nreframed the entire practice of using models. You stopped training a model per\ntask and started <em>prompting</em> one general model, and \"prompt engineering\" became a\ndiscipline overnight. It's also the paper that made scale the field's central\nbet: make the model bigger, and qualitatively new abilities appear.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-chain-of-thought-prompting-2022\">3. Chain-of-Thought Prompting (2022)<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#3-chain-of-thought-prompting-2022\" class=\"hash-link\" aria-label=\"Direct link to 3. Chain-of-Thought Prompting (2022)\" title=\"Direct link to 3. Chain-of-Thought Prompting (2022)\" translate=\"no\">​</a></h2>\n<p><strong>Wei et al. · January 2022 · <a href=\"https://arxiv.org/abs/2201.11903\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">arXiv:2201.11903</a></strong></p>\n<p><em>Why it mattered:</em> it showed that asking a model to <em>show its work</em> unlocks\nreasoning it otherwise can't do.</p>\n<p>Large models were oddly bad at multi-step problems — arithmetic, logic, word\nproblems — even as they aced other tasks. The fix turned out to be almost\nembarrassingly simple: prompt the model to produce intermediate reasoning steps\nbefore the final answer, and accuracy on hard problems jumps. \"Let's think step\nby step\" became a genuine capability unlock, not a party trick. More deeply, it\nrevealed that a lot of a model's competence is <em>latent</em> — present in the weights\nbut only accessible if you give it room to compute. Every \"reasoning model\" that\nfollowed is a descendant of this observation.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"4-training-language-models-to-follow-instructions-with-human-feedback--instructgpt-2022\">4. Training Language Models to Follow Instructions with Human Feedback — InstructGPT (2022)<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#4-training-language-models-to-follow-instructions-with-human-feedback--instructgpt-2022\" class=\"hash-link\" aria-label=\"Direct link to 4. Training Language Models to Follow Instructions with Human Feedback — InstructGPT (2022)\" title=\"Direct link to 4. Training Language Models to Follow Instructions with Human Feedback — InstructGPT (2022)\" translate=\"no\">​</a></h2>\n<p><strong>Ouyang et al. · March 2022 · <a href=\"https://arxiv.org/abs/2203.02155\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">arXiv:2203.02155</a></strong></p>\n<p><em>Why it mattered:</em> it turned raw, unruly language models into helpful assistants —\nthe step that made the technology usable by everyone.</p>\n<p>A raw pretrained model predicts likely text; it does not, by default, do what you\n<em>ask</em>. InstructGPT introduced <strong>RLHF</strong> — reinforcement learning from human\nfeedback — to close that gap: humans rank model outputs, a reward model learns\nthose preferences, and the language model is tuned to produce what humans prefer.\nThe result was startling: a much smaller RLHF-tuned model was preferred over the\ngiant base model, because it was actually trying to be helpful. This is the paper\nthat made the assistant paradigm work, and it's the direct methodological\nancestor of ChatGPT and every chat model since. Alignment stopped being a safety\nfootnote and became the productization step.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"5-training-compute-optimal-large-language-models--chinchilla-2022\">5. Training Compute-Optimal Large Language Models — Chinchilla (2022)<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#5-training-compute-optimal-large-language-models--chinchilla-2022\" class=\"hash-link\" aria-label=\"Direct link to 5. Training Compute-Optimal Large Language Models — Chinchilla (2022)\" title=\"Direct link to 5. Training Compute-Optimal Large Language Models — Chinchilla (2022)\" translate=\"no\">​</a></h2>\n<p><strong>Hoffmann et al. · March 2022 · <a href=\"https://arxiv.org/abs/2203.15556\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">arXiv:2203.15556</a></strong></p>\n<p><em>Why it mattered:</em> it corrected how the whole field spends its compute — and\nproved most big models were trained wrong.</p>\n<p>The prevailing wisdom after GPT-3 was \"bigger is better,\" so labs raced to add\nparameters. Chinchilla showed that for a fixed compute budget, parameters and\ntraining <em>data</em> should scale roughly together — and that the giant models of the\nday were badly <strong>undertrained</strong>, starved of data relative to their size. A\nsmaller model trained on more tokens beat a larger one trained on fewer. This\nreshaped training strategy across the industry: data quality and quantity became\nfirst-class, and \"compute-optimal\" entered the vocabulary. It's why the strongest\nmodels aren't simply the biggest ones.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"where-the-story-goes-next\">Where the story goes next<a href=\"https://blog.catalex.co/five-papers-that-shaped-modern-ai#where-the-story-goes-next\" class=\"hash-link\" aria-label=\"Direct link to Where the story goes next\" title=\"Direct link to Where the story goes next\" translate=\"no\">​</a></h2>\n<p>Read those five and you have the spine: an architecture that scales (1), the\ndiscovery that scale creates capability (2), a way to summon reasoning from it\n(3), a way to make it helpful (4), and a way to train it efficiently (5).</p>\n<p>Everything we work on sits one step past the end of this list. Once models could\nreason and follow instructions, the obvious next move was to let them <em>act</em> — to\ncall tools, observe results, and loop — which is the agent paradigm and the line\nof work (ReAct, Toolformer, and what followed) that this blog mostly lives in. If\nyou want the sequel to this reading list, that's where it starts. But start here.\nThese five are the grammar; the rest is composition.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/five-papers-that-shaped-modern-ai",
            "title": "Five Papers That Shaped Modern AI (Start Here)",
            "summary": "A short, opinionated reading list — from \"Attention Is All You Need\" to compute-optimal scaling — with a timeline, references, and a paragraph on why each one still matters.",
            "date_modified": "2026-06-28T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "AI Research",
                "Engineering Culture"
            ]
        },
        {
            "id": "https://blog.catalex.co/managing-context-with-claude",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published June 27, 2026 · 09:00 UTC</div>\n<p>When you build software with an AI agent day to day, the thing you run out of\nfirst isn't intelligence — it's <strong>context</strong>. The window is finite, attention\ninside it isn't free, and a session that starts sharp gets duller as it fills\nwith the exhaust of the work: verbose command output, whole files read to find\none function, stale documentation, the residue of three tasks ago.</p>\n<!-- -->\n<p>So a real part of building CatalEx is context hygiene — keeping the model's\nwindow full of signal and empty of everything else. This is the setup we\nactually run, and how each piece is configured. The theme underneath all of it\nis the same one from our post on harnesses: <strong>spend context like a budget, and\nmake every token earn its place.</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-principle-tokens-per-successful-task\">The principle: tokens-per-successful-task<a href=\"https://blog.catalex.co/managing-context-with-claude#the-principle-tokens-per-successful-task\" class=\"hash-link\" aria-label=\"Direct link to The principle: tokens-per-successful-task\" title=\"Direct link to The principle: tokens-per-successful-task\" translate=\"no\">​</a></h2>\n<p>It's tempting to optimize the wrong number. The instinct is to minimize tokens\nper call — shorter prompts, fewer tools. But the metric that actually matters is\ntokens per <em>successful task</em>. A window bloated with noise makes the model reason\nworse, which causes correction turns, which costs far more tokens than the noise\nyou were trying to trim.</p>\n<p>Everything below is in service of that number. Some tools cut what enters the\nwindow; some cut what the model has to emit; some replace a large, vague context\nwith a small, precise one. All of them are trying to get the task done in fewer\ntotal tokens by keeping the window clean, not just short.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"rtk-filter-command-output-at-the-source\">RTK: filter command output at the source<a href=\"https://blog.catalex.co/managing-context-with-claude#rtk-filter-command-output-at-the-source\" class=\"hash-link\" aria-label=\"Direct link to RTK: filter command output at the source\" title=\"Direct link to RTK: filter command output at the source\" translate=\"no\">​</a></h2>\n<p>The single biggest source of context bloat in day-to-day development is command\noutput. <code>git status</code>, <code>npm install</code>, a test run, a container log — these print\nfor humans, in full, and every character lands back in the model's context on the\nnext turn.</p>\n<p><strong>RTK</strong> (a token-optimizing CLI proxy) intercepts this at the source. It runs as\na <code>PreToolUse</code> hook, so it's transparent — the agent runs a normal command and\nRTK rewrites it through a proxy that strips the output down to what matters\nbefore it ever reaches the window. In our global Claude config it's wired as:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"hooks\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"PreToolUse\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"hooks\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"command\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"command\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"rtk hook claude\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The effect is invisible and large: routine dev operations come back 60–90%\nsmaller with no change to how you drive the agent. <code>git status</code> returns the\nsignal, not the decoration. When you genuinely need the raw firehose, there's an\nescape hatch (<code>rtk proxy &lt;cmd&gt;</code>) that bypasses the filtering — but the default is\nlean, which is the right default.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"caveman-compress-the-conversation-itself\">caveman: compress the conversation itself<a href=\"https://blog.catalex.co/managing-context-with-claude#caveman-compress-the-conversation-itself\" class=\"hash-link\" aria-label=\"Direct link to caveman: compress the conversation itself\" title=\"Direct link to caveman: compress the conversation itself\" translate=\"no\">​</a></h2>\n<p>RTK trims what tools put <em>in</em>; <strong>caveman</strong> trims what the model and you put into\nthe conversation. It's a plugin that switches communication into an\nultra-compressed mode — the tagline is \"why use many token when few do trick\" —\ncutting roughly 75% of conversational tokens while preserving technical accuracy.\nIt's enabled per-repo in <code>.claude/settings.json</code>:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"enabledPlugins\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"caveman@caveman\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"extraKnownMarketplaces\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"caveman\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"source\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"source\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"github\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"repo\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"JuliusBrussee/caveman\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The tradeoff is real and worth stating: compression is lossy, and there are\nmoments — subtle design discussion, careful reasoning about a tricky bug — where\nyou want the model spending tokens, not saving them. Caveman is a lever, not a\nlaw. We lean on it for the high-volume, low-nuance stretches of work and ease off\nwhen the thinking gets delicate.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"context7-fresh-docs-instead-of-stale-memory-or-a-doc-dump\">context7: fresh docs instead of stale memory (or a doc dump)<a href=\"https://blog.catalex.co/managing-context-with-claude#context7-fresh-docs-instead-of-stale-memory-or-a-doc-dump\" class=\"hash-link\" aria-label=\"Direct link to context7: fresh docs instead of stale memory (or a doc dump)\" title=\"Direct link to context7: fresh docs instead of stale memory (or a doc dump)\" translate=\"no\">​</a></h2>\n<p>A quieter context problem is <em>wrong</em> context. A model's training data has a\ncutoff, so its memory of a fast-moving library is often months stale — and the\nusual fix, pasting in whole documentation pages, floods the window to answer one\nquestion.</p>\n<p><strong>context7</strong> (an Upstash MCP server) solves both ends. It pulls version-specific\ndocumentation and code examples from the source on demand, so the model gets the\n<em>current</em> API for the exact version we're on, scoped to what was asked, instead\nof hallucinating from stale memory or drowning in a doc dump. It's enabled the\nsame way:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"enabledPlugins\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"context7@claude-plugins-official\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>This is context management by <em>substitution</em> — replacing a large, unreliable\ncontext (training memory, or a pasted doc site) with a small, accurate one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"code-intelligence-read-symbols-not-whole-files\">Code intelligence: read symbols, not whole files<a href=\"https://blog.catalex.co/managing-context-with-claude#code-intelligence-read-symbols-not-whole-files\" class=\"hash-link\" aria-label=\"Direct link to Code intelligence: read symbols, not whole files\" title=\"Direct link to Code intelligence: read symbols, not whole files\" translate=\"no\">​</a></h2>\n<p>One of the least obvious context sinks is exploration. To change one function, an\nagent will happily read the whole file it lives in — and the two files it imports\n— just to understand the types. That's a lot of tokens to answer \"what's the\nsignature of this thing.\"</p>\n<p>We run language-server plugins (<code>pyright-lsp</code>, <code>typescript-lsp</code>) so the agent has\nprecise, symbol-level intelligence: go-to-definition, hover types, references. It\ncan ask \"what is this symbol\" and get a one-line answer instead of reading three\nfiles to infer it. Across a working session that's an enormous saving, and it\nalso makes the edits more reliable — the model is acting on the actual type, not\na guess assembled from surrounding code.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"worktree-isolation-keep-each-tasks-context-scoped\">Worktree isolation: keep each task's context scoped<a href=\"https://blog.catalex.co/managing-context-with-claude#worktree-isolation-keep-each-tasks-context-scoped\" class=\"hash-link\" aria-label=\"Direct link to Worktree isolation: keep each task's context scoped\" title=\"Direct link to Worktree isolation: keep each task's context scoped\" translate=\"no\">​</a></h2>\n<p>The last piece is structural. When several pieces of work happen in parallel,\ntheir contexts bleed into each other — and a window carrying the residue of three\nhalf-finished tasks reasons worse on the one in front of it.</p>\n<p>We use git worktrees with lifecycle hooks so each task runs in its own isolated\ncheckout, with its environment set up and torn down automatically:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#141a1f;--prism-background-color:#f2f7fc\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#141a1f;background-color:#f2f7fc\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"hooks\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"WorktreeCreate\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"hooks\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"command\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"command\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\".claude/hooks/setup-worktree.sh\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"WorktreeRemove\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"hooks\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"command\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"command\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\".claude/hooks/remove-worktree.sh\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#141a1f\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The context benefit is that each session's window stays scoped to a single task —\nits files, its branch, its environment — instead of accumulating the state of\neverything else in flight. Isolation is a context-management technique as much as\na workflow one.</p>\n<div class=\"cx-note\"><p><strong>The pattern across all of these:</strong> none is a clever prompt. Each is a piece of\n<em>harness</em> — a hook, a plugin, a proxy, an isolation boundary — that shapes what\nreaches the model. Context management isn't something you do by asking nicely in\nthe prompt; it's something you build into the environment the agent runs in.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-shape-of-a-lean-setup\">The shape of a lean setup<a href=\"https://blog.catalex.co/managing-context-with-claude#the-shape-of-a-lean-setup\" class=\"hash-link\" aria-label=\"Direct link to The shape of a lean setup\" title=\"Direct link to The shape of a lean setup\" translate=\"no\">​</a></h2>\n<p>Put together, the setup layers cleanly by what it targets:</p>\n<ul>\n<li class=\"\"><strong>What tools emit →</strong> RTK filters command output at the source.</li>\n<li class=\"\"><strong>What the conversation costs →</strong> caveman compresses the exchange.</li>\n<li class=\"\"><strong>What the model knows →</strong> context7 substitutes fresh, scoped docs for stale\nmemory.</li>\n<li class=\"\"><strong>What exploration costs →</strong> LSP intel answers \"what is this symbol\" without\nreading whole files.</li>\n<li class=\"\"><strong>What leaks between tasks →</strong> worktree isolation keeps each window scoped.</li>\n</ul>\n<p>Every one of them is pointed at the same number — total tokens to get the task\ndone right — and every one is configuration, not willpower. That's the real\nlesson: managing context while building with Claude is the same discipline as\nbuilding a good agent harness, turned around and aimed at your own development\nloop. Set the environment up so the window stays lean, and the model does its\nbest work for longer before the exhaust builds up.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/managing-context-with-claude",
            "title": "How We Keep Claude's Context Lean While Building CatalEx",
            "summary": "Context is the scarce resource when you build with an AI agent. The actual setup we run to keep the window lean — RTK, caveman, context7, LSP intel, and worktree isolation — and how each is configured.",
            "date_modified": "2026-06-27T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "Engineering Culture",
                "Agent Engineering",
                "AI in Production"
            ]
        },
        {
            "id": "https://blog.catalex.co/write-the-wiki-for-the-llm",
            "content_html": "<div class=\"cx-kicker\">CatalEx Engineering · Published June 26, 2026 · 09:00 UTC</div>\n<p>Here is a shift that has already happened, whether or not your documentation\nnoticed: a large and growing share of the readers of everything you write are no\nlonger human. They're language models — reading your docs to answer a question,\nyour README to use your library, your marketing page to tell someone what you do.\nAndrej Karpathy has been making this point for a while, and it lands harder every\nmonth: <strong>the primary reader of a lot of your writing is now an LLM, and you are\nprobably still writing for a human.</strong></p>\n<!-- -->\n<p>This is a look at that idea — sometimes called writing an \"LLM wiki,\" or\nknowledge for machines — and what it actually changes about how you produce docs.\nIt's not a call to stop writing for people. It's the recognition that you now\nhave a second, very different reader, and that reader has been quietly ignored.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-two-readers-read-nothing-alike\">The two readers read nothing alike<a href=\"https://blog.catalex.co/write-the-wiki-for-the-llm#the-two-readers-read-nothing-alike\" class=\"hash-link\" aria-label=\"Direct link to The two readers read nothing alike\" title=\"Direct link to The two readers read nothing alike\" translate=\"no\">​</a></h2>\n<p>A human reading your docs skims. They jump to the code block, infer meaning from\na screenshot, tolerate a nav bar and a cookie banner and three paragraphs of\npositioning, and click around until they find the part they need. Human docs are\nbuilt around all of that: visual hierarchy, interactivity, and the reader's\nwillingness to fill gaps.</p>\n<p>An LLM reads none of it that way. It ingests text mostly linearly. It can't click\nyour \"see also.\" It can't see the screenshot that contained the actual answer. It\nchokes on the chrome — the navigation, the boilerplate, the styling markup — and\nspends context on all of it. And critically, it will not tell you it got\nconfused; it will confidently produce an answer from whatever it managed to\nparse, gaps included.</p>\n<p>So the same page that works beautifully for a person can be nearly useless to a\nmodel: the key fact was in an image, the steps were split across five clicked\npages, and half the tokens were spent on a header that repeats on every route.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-writing-for-the-llm-actually-means\">What \"writing for the LLM\" actually means<a href=\"https://blog.catalex.co/write-the-wiki-for-the-llm#what-writing-for-the-llm-actually-means\" class=\"hash-link\" aria-label=\"Direct link to What &quot;writing for the LLM&quot; actually means\" title=\"Direct link to What &quot;writing for the LLM&quot; actually means\" translate=\"no\">​</a></h2>\n<p>Karpathy's framing is useful because it's concrete once you take the reader\nseriously. Writing for the model reader means:</p>\n<ul>\n<li class=\"\"><strong>Plain, linear, self-contained text.</strong> The answer should be <em>in the words</em>,\nnot in a diagram or a video or three hops away. If a model reading top to\nbottom can't extract it, neither can the model your users are asking.</li>\n<li class=\"\"><strong>Structure a machine can parse.</strong> Clear headings, explicit lists, real\nmarkdown — not visual layout that only means something rendered.</li>\n<li class=\"\"><strong>Say the thing, drop the performance.</strong> The positioning fluff, the repeated\nchrome, the SEO padding — all of it is pure cost to a model and adds nothing to\nthe answer. Density of fact per token is the metric.</li>\n<li class=\"\"><strong>Completeness over cleverness.</strong> A human forgives a doc that assumes context.\nA model doesn't have your context; it has what's on the page.</li>\n</ul>\n<p>The mental test is simple: paste the raw text of your page into a model, ask it\nthe question your user would ask, and see if it can answer from that alone. If it\ncan't, your human-facing polish is hiding the substance from your fastest-growing\naudience.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-llm-wiki-made-concrete\">The LLM wiki, made concrete<a href=\"https://blog.catalex.co/write-the-wiki-for-the-llm#the-llm-wiki-made-concrete\" class=\"hash-link\" aria-label=\"Direct link to The LLM wiki, made concrete\" title=\"Direct link to The LLM wiki, made concrete\" translate=\"no\">​</a></h2>\n<p>The clearest instantiation of this idea already exists and is easy to adopt.\n<code>llms.txt</code> — a convention proposed by Jeremy Howard — is a plain-markdown file at\nthe root of your site that gives models a clean, curated map of what you are and\nwhere the real information lives, free of the HTML and navigation they'd\notherwise wade through. It's a wiki entry for your site, written for the machine.</p>\n<p>The broader \"LLM wiki\" idea generalizes that: a body of knowledge maintained\n<em>specifically</em> for model consumption — unambiguous, structured, complete, and\nkept current. Not a copy of your human docs with the styling stripped, but\nknowledge authored with the model as the intended reader. Where Wikipedia\norganized the world's knowledge for people to read, the LLM wiki organizes it for\nmodels to reason over.</p>\n<div class=\"cx-note\"><p><strong>This is the same move as the search era, one layer up.</strong> For two decades, teams\nlearned to write for Google's crawler — that was SEO. The reader was an index.\nNow the reader is a model that will <em>synthesize an answer</em> and cite whoever it\nunderstood best. Writing for the LLM is the AEO successor to writing for search:\nthe audience changed from an index to a reasoner, and the craft changed with it.</p></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-its-worth-doing-now\">Why it's worth doing now<a href=\"https://blog.catalex.co/write-the-wiki-for-the-llm#why-its-worth-doing-now\" class=\"hash-link\" aria-label=\"Direct link to Why it's worth doing now\" title=\"Direct link to Why it's worth doing now\" translate=\"no\">​</a></h2>\n<p>The payoff is direct and it compounds. When a user asks ChatGPT, Claude, or an AI\nsearch engine about your problem space, the answer is assembled from whatever\nsources the model could parse and trust. Documentation written for the model is\ndocumentation the model can actually use — which means you get represented\naccurately in the answer, and cited, instead of paraphrased wrong or skipped for\na competitor whose page was cleaner to read.</p>\n<p>There's an internal payoff too. The same properties that make knowledge good for\nan external model make it good for <em>your own</em> agents. An agent that reads your\ninternal docs to do its job has exactly the LLM reader's constraints — linear,\ncontext-limited, unable to click. Knowledge authored for machines is knowledge\nyour own automation can act on. The wiki you write for the world's models is the\nwiki your own agents read too.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-we-approach-it-at-catalex\">How we approach it at CatalEx<a href=\"https://blog.catalex.co/write-the-wiki-for-the-llm#how-we-approach-it-at-catalex\" class=\"hash-link\" aria-label=\"Direct link to How we approach it at CatalEx\" title=\"Direct link to How we approach it at CatalEx\" translate=\"no\">​</a></h2>\n<p>We try to practice this, including on the blog you're reading. It ships an\n<code>llms.txt</code> that describes what CatalEx is in plain markdown; a <code>robots.txt</code> that\nexplicitly welcomes AI crawlers rather than blocking them; structured data so a\nmodel can attribute a post correctly; and posts written to be self-contained —\nthe argument is in the prose, not locked in an image. The goal is that when a\nmodel reads any of it, it comes away with an accurate, citable understanding, not\na pile of chrome it has to guess through.</p>\n<p>None of this is exotic. It's the old discipline of writing clearly, with one\nupdate to the audience: the most frequent, most literal, least forgiving reader\nof what you publish is now a model. Karpathy's point is really just that — take\nthat reader seriously. Write the wiki for the LLM, and you serve the humans who\nask it questions at the same time.</p>\n<hr>\n<p><em>Written by CatalEx Engineering. We build the AI operating layer for AI-native\ncompanies — one platform to build, deploy, and run AI agents in production. More\nat <a href=\"https://catalex.co/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">catalex.co</a>.</em></p>",
            "url": "https://blog.catalex.co/write-the-wiki-for-the-llm",
            "title": "Write the Wiki for the LLM: Documentation's New Reader",
            "summary": "Andrej Karpathy's point that your docs' primary reader is now a model, not a human — and what an \"LLM wiki\" actually looks like in practice, from llms.txt to knowledge written for machines.",
            "date_modified": "2026-06-26T09:00:00.000Z",
            "author": {
                "name": "CatalEx Engineering",
                "url": "https://catalex.co"
            },
            "tags": [
                "AI Research",
                "Engineering Culture"
            ]
        }
    ]
}