<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en_US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://architg.me/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://architg.me/" rel="alternate" type="text/html" hreflang="en_US" /><updated>2026-08-02T09:00:57+00:00</updated><id>https://architg.me/blog/feed.xml</id><title type="html">Terminal Journal</title><subtitle>Thoughts on AI, engineering, pool, and life from Archit Gupta</subtitle><author><name>Archit Gupta</name></author><entry><title type="html">The Lord, the Fleet, and the Rulebook That Binds Them Both</title><link href="https://architg.me/blog/2026/08/lord-fleet-rulebook/" rel="alternate" type="text/html" title="The Lord, the Fleet, and the Rulebook That Binds Them Both" /><published>2026-08-01T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/08/lord-fleet-rulebook</id><content type="html" xml:base="https://architg.me/blog/2026/08/lord-fleet-rulebook/"><![CDATA[<p>Every story about AI agent fleets is secretly a story about governance, and nobody wants to admit it.</p>

<p>The demos show the fun part: ten agents, ten branches, code pouring forth. What they don’t show is agent three killing agent seven’s test suite with an unscoped <code class="language-plaintext highlighter-rouge">pkill</code>, or two agents restoring each other’s files from a shared temp directory, or the night two of my sessions raced to update the same branch and silently disarmed an auto-merge neither knew the other had armed.</p>

<p>I know, because all of that happened to me. I’m the only engineer at my startup, I run up to eight Claude Code sessions in parallel, and for a while my kingdom was pure anarchy with good intentions.</p>

<p>So I did what people have always done when anarchy stops scaling: I wrote law.</p>

<h2 id="what-this-is-in-one-breath">What this is (in one breath)</h2>

<p><strong>Agent fleet governance</strong> is the set of rules, roles, and authorizations that keep many coding agents productive without them stepping on each other—or on production—while still letting <em>you</em> steer mid-flight.</p>

<p>In my setup: one human sovereign (me), one <strong>master session</strong> that governs and never implements, and a <strong>fleet</strong> of child sessions in isolated worktrees. Risky actions require a current <strong>word</strong>—a first-hand, spendable authorization—not a vibe that “someone approved this once.” Claims are not results until verified. Rules are case law: each one points at a named incident.</p>

<p>This is the piece I kept missing in existing tooling. <strong>Loop-style</strong> “set and go” workflows are fantastic when you want to fire a goal and walk away—but they feel thin when you need full control on every decision <em>inside</em> the loop, not just at the start. Traditional <strong>orchestrator</strong> frameworks — the kind I’ve written about in <a href="/blog/2026/01/building-ai-agents/">production agent patterns</a> — are better at structure, yet they often leave you blind and under-steered once work fans out into sub-agents: weak observability into what each child is doing, and little real authority to redirect mid-run. I also got tired of safeguard layers that felt like ceremony instead of actual governance. The court is meant to be the best of both worlds: parallel fleet throughput <em>and</em> steerability—you can inspect, authorize, refuse, and re-brief with full control, not hope the pipeline lands somewhere sane.</p>

<p>It is <strong>not</strong> “prompt the swarm and hope,” and it is <strong>not</strong> a set-and-forget loop with a pretty dashboard. Parallel agents without law invent new failure modes faster. Use a court when you run multi-session fleets with real merge and deploy power and you care about mid-loop control; skip the full constitution for a single agent on a throwaway branch.</p>

<p>Stack: Claude Code sessions, git worktrees, CI as truth, memory files as the archive, me as the only holder of irreversible power.</p>

<h2 id="the-court">The Court</h2>

<p>The system that emerged has a shape old enough to have a vocabulary. At the top there’s me — call it the lord, since we’re committed to the bit. I hold every real power: production credentials, merge authority, the final word on anything irreversible.</p>

<p>Below me sits the <strong>master session</strong> — one Claude Code session that acts as chancellor. It governs and <em>never</em> implements. It writes no code; its entire job is running the machinery of state:</p>

<ul>
  <li><strong>Verifying claims.</strong> Children report; the master checks their reports against git, CI, and live endpoints before believing a word.</li>
  <li><strong>Sequencing.</strong> Merge-queue choreography, deploy windows, who waits for whom.</li>
  <li><strong>Brokering authorization.</strong> Every risky action needs my word; the master routes, batches, and executes those words.</li>
  <li><strong>Keeping the record.</strong> Memory files, incident chronicles, and — when its own context fills up — a succession document for its replacement.</li>
  <li><strong>Watching.</strong> Persistent monitors on CI, deploys, and production readiness, built in the session’s first minutes, because a master that polls on request is a secretary.</li>
</ul>

<p>And below the master, the fleet: child sessions, each in its own git worktree, each with a brief that names its owned files, its evidence bar, and one instruction that turns out to matter more than all the others. (The specialization logic is the same one that shaped <a href="/blog/2026/02/midas/">Midas’s four-agent split</a>: clear jobs, separate context, defined handoffs.) The instruction:</p>

<blockquote>
  <p><em>Verify this brief before acting on it. It is a hypothesis. You are expected to correct me.</em></p>
</blockquote>

<p>They do. Constantly. Last night one child opened its report with “three findings, two of which contradict the brief” — and all three corrections were right. This is not insubordination. This is the system working. A court where nobody may tell the chancellor he’s wrong is a court that ships the chancellor’s mistakes.</p>

<h2 id="the-oaths">The Oaths</h2>

<p>Here’s where it gets genuinely medieval. Authorization in this system isn’t a checkbox — it’s a <strong>word</strong>, in almost the old sense: a thing spoken, binding, with formal conditions on when it holds and when it’s void.</p>

<ul>
  <li>A <strong>first-hand conditional word</strong> — me telling the master <em>“merge these two when both are green, one batch, and watch the deploy”</em> — is banked. When the condition lands, the master executes. I say it once, at 9 p.m., and don’t get pinged at 11.</li>
  <li>A <strong>relayed word is worth nothing.</strong> If the master tells a child “the lord approved this,” the child is <em>required by law</em> to refuse. Authorization lands in the session that holds the button, or it doesn’t exist. One child last night refused to press its own merge on precisely these grounds — and it was right about the rule even though, as it turned out, the button was never its to press.</li>
  <li><strong>Words get spent.</strong> If a PR is evicted from the merge queue — even by pure infrastructure failure, through no fault of its code — its merge word is consumed. Re-entry requires a fresh one. This sounds like bureaucracy until you watch it prevent exactly what it exists to prevent: a stale approval carrying an action into a situation that has changed underneath it.</li>
  <li><strong>Some things never bank.</strong> Database migrations, destructive operations, live calls to external APIs, production flag changes: fresh word, every time, spoken in the executing session. No exceptions, including for me being tired.</li>
</ul>

<p>The payoff is that you get speed <em>and</em> safety from the same mechanism. Seventeen production merges happened last night and I pressed zero buttons — yet not one action ran without a current authorization I had actually given, with semantics both of us understood.</p>

<h2 id="the-law-of-evidence">The Law of Evidence</h2>

<p>The second pillar: <strong>nothing in the realm is a result. Everything is a claim.</strong></p>

<p>Children verify briefs. The master verifies children. Even the second-model adversarial reviewer we use (a rival AI, prompted as a pure skeptic, allowed to fire on exactly three triggers so it never decays into ceremony) produces findings that are — again — claims, verified against the tree before anyone acts on them.</p>

<p>The discipline has teeth because it’s specific:</p>

<ul>
  <li>A test that passes on its first run has proven nothing until it’s been <strong>falsified</strong> — break the thing it guards, watch it go red, restore it.</li>
  <li>A zero must arrive with a <strong>null control</strong> proving the query could have found something. (“No results” from a broken query looks identical to “no results” from a clean system. We learned that one the hard way. Twice.)</li>
  <li>A “no behavior change” claim gets settled by comparing ASTs with docstrings stripped — with a deliberate mutation first, to prove the comparison <em>can</em> fail.</li>
</ul>

<p>Last night this culture caught, among other things: two test suites whose fixtures were configured to <em>disagree with production in exactly the dimension under test</em> — every green proof of a critical code path had been run under the one setting that hid its bug — and a paraphrase of a code review that silently dropped the single clause a coverage gap was hiding behind. The child who caught that last one said it best: <strong>the verbatim beats the relay, and the relay’s gap was found by a test, not an argument.</strong></p>

<h2 id="the-case-law">The Case Law</h2>

<p>Frameworks ship rules and ask you to trust them. My rulebook doesn’t work that way. Nearly every law in it traces to a <strong>named incident with a date</strong>, kept in memory files that read like a court reporter’s archive:</p>

<ul>
  <li><em>Never update a queued PR’s branch</em> — because the merge queue is testing the exact head you’d be rewriting.</li>
  <li><em>Eviction detection must pair timeline events</em> — because a silently evicted PR reads as green, mergeable, and healthy, forever, while quietly not merging.</li>
  <li><em>Read a CI job’s steps, never its conclusion</em> — because a job can “fail” with every test green when the runner dies writing its own diagnostic log.</li>
  <li><em>Exit codes come from files, never wrappers</em> — because a background wrapper once reported success over a suite that had been killed mid-run.</li>
  <li><em>The type-checker is the only frontend gate that reads test files</em> — discovered twice in one hour, by two different agents, from two different failures.</li>
</ul>

<p>This is case law in the honest sense: precedent, with receipts. And it’s why the agents actually follow the rules — every rule can answer <em>“why?”</em> with a story instead of a shrug.</p>

<h2 id="one-night-in-the-realm">One Night in the Realm</h2>

<p>The campaign: get all three of our product’s third-party connectors writing to their upstream platforms in production, live-proven, with readiness reporting that tells the truth.</p>

<p>The first live write through the newest connector — a one-attempt-ever, never-retried production write, by design — <strong>failed</strong>. It left zero log lines. What it left instead was durable state, and durable state is enough: a diagnosis agent traced the death to a one-line defect that every test fixture in the repository had been configured to hide, then — resumed with fresh evidence an hour later — found a <em>second</em>, independent defect meaning the recovery path had never once been able to parse its own input. In any environment. Ever.</p>

<p>Meanwhile, a recovery sweeper that another child had shipped <em>that same evening</em> deployed mid-incident and autonomously rescued the orphaned write on its first production pass. The machine caught its own casualty with a net it had finished weaving twenty minutes earlier.</p>

<p>The infrastructure, refusing to be upstaged, then staged its own siege: GitHub’s hosted runners ran out of disk four separate times, each death evicting an innocent PR from the merge queue and spending one of my words. The fleet’s forensics distinguished infra deaths from real failures by step-level evidence, a stop-rule kicked in at strike four, and a structural fix was chartered before I’d finished being annoyed.</p>

<p>And through all of it, the attended work stayed mine. I repaired a dead production credential by hand, ran the forensic queries against the production database, performed the emergency restart, and made a dozen design rulings the fleet escalated between dinner and midnight.</p>

<p>Then production went dark for half an hour, and the night produced its best scene.</p>

<p>The deploy verifier declared failure. Every signal said <em>rollback</em>. And rollback would have been exactly wrong — because I recognized the failure’s shape from an incident months before any of this tooling existed. <em>“Are we sure this isn’t the embeddings cold start again?”</em> I sent the master to the code to test my hypothesis, and there it was: our API loads a 1.3GB embedding model <em>before binding its port</em>, every deploy restarts that multi-minute warmup, and the night’s merge train had been serially executing almost-warm containers with fresh image repoints. The platform logs showed one container reaching “Site started” — for thirty-five seconds — before the next deploy killed it. No bad code anywhere. Rollback would have reset the clock and extended the outage. The call to wait was mine, and it was right.</p>

<p>That’s the real division of labor: the fleet holds every thread so my attention lands where judgment is scarce. And judgment, it turns out, is the one layer nothing else in the system can supply.</p>

<h2 id="the-rulebook-binds-the-lord">The Rulebook Binds the Lord</h2>

<p>Here is the part that makes the whole thing work, and the reason the title isn’t “Agentic Feudalism.”</p>

<p>A feudal lord’s word is law because of who he is. In my realm, <strong>my word is law because of what the law says about words</strong> — and the same law binds me:</p>

<ul>
  <li>When infrastructure evicted my PRs, <em>my own words were spent</em>, and the system made me speak new ones. It did not once assume “he obviously still wants this.”</li>
  <li>When I asked whether we should run one more adversarial review “for sanity,” my own chancellor told me it would be over-engineering and recommended against it — with reasons, from <em>my</em> doctrine. (I overrode it. That’s allowed. The override was logged.)</li>
  <li>At strike four of the runner disk deaths, the stop-rule fired <em>on me</em>: no more re-presses, even though each one cost me nothing but a word, until the structural fix landed.</li>
  <li>And when a child refused to act on a relayed approval, the resolution wasn’t “the lord insists.” It was a precise clarification of which session held the word — because the child’s refusal was <em>correct under law</em>, and the law wins.</li>
</ul>

<p>A sovereign bound by his own constitution isn’t weaker. He’s <em>auditable</em> — and in a system where the sovereign’s judgment is the scarcest resource, auditability is what lets that judgment run at full speed without anyone (including him) having to wonder, later, what actually happened.</p>

<h2 id="the-price-of-the-crown">The Price of the Crown</h2>

<p>Honesty section. The realm is not free.</p>

<ul>
  <li><strong>Tokens.</strong> A night like this consumes a great deal of model. The evidence discipline roughly doubles the work per change. I think it more than doubles the trustworthiness. That’s a bet, not a theorem.</li>
  <li><strong>The master is a serialization point.</strong> Every question routes through one session. It scales further than you’d expect — governing is cheaper than implementing — but it’s a bottleneck by design.</li>
  <li><strong>It assumes one decisive sovereign.</strong> The word system works because I answer in seconds with one line. A committee would deadlock this constitution by Tuesday.</li>
  <li><strong>The weather is real.</strong> You will spend genuine effort distinguishing your fleet’s failures from your infrastructure’s. Build the forensics before you need them.</li>
</ul>

<h2 id="build-your-own-realm">Build Your Own Realm</h2>

<p>Three things transfer, whatever your setup looks like:</p>

<ol>
  <li><strong>Write the constitution before you need it; let incidents amend it.</strong> The rules that stick are the ones with stories attached. Keep the receipts — dates, run IDs, the exact wrong output.</li>
  <li><strong>Make authorization a first-class object.</strong> Decide what an approval <em>is</em>, who holds it, when it expires, and what spends it. “The human said yes at some point” is not a semantics — it’s a future incident.</li>
  <li><strong>Treat every agent output as a claim.</strong> The single highest-leverage sentence in any brief I write is <em>“verify this brief before acting on it.”</em> It converts executors into skeptics, and skeptics compound.</li>
</ol>

<h2 id="succession">Succession</h2>

<p>The night ended the way the law says it must. The master session — context nearly full, campaign nearly done — refreshed every live fact, settled the record, wrote a succession document with its own verification recipe, briefed its replacement, and asked to be archived. Its final report to me <em>disclaimed a piece of credit I had given it</em>, because the credit belonged to an older record.</p>

<p>I wrote this constitution one incident at a time. The fleet’s job is to hold me to my own rules at a scale I could never hold them alone. That’s the whole trick, and I’ve stopped being surprised by how well it works: the agents aren’t the impressive part.</p>

<p>The law is.</p>

<hr />

<p><em>The realm runs on Claude Code sessions coordinated through cross-session messaging, git worktrees, a merge queue, and a growing archive of memory files. The master-session doctrine, the word system, and the case law are homegrown. Happy to talk about it — reach me through this site.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="ai" /><category term="AI Agents" /><category term="Multi-Agent" /><category term="Claude Code" /><category term="Governance" /><category term="Systems Design" /><summary type="html"><![CDATA[In one July 2026 session, my Claude Code fleet shipped seventeen production PRs under a homegrown constitution: oaths, case law, and a bound sovereign.]]></summary></entry><entry><title type="html">Why I Built a Priority Queue for Ollama</title><link href="https://architg.me/blog/2026/03/ollama-queue/" rel="alternate" type="text/html" title="Why I Built a Priority Queue for Ollama" /><published>2026-03-02T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/03/ollama-queue</id><content type="html" xml:base="https://architg.me/blog/2026/03/ollama-queue/"><![CDATA[<p>Sixteen AI agents. One GPU. Eighty-four cron jobs. All funneling through a single Ollama instance on a home server.</p>

<p>The GPU (a Tesla P40 with 24GB of VRAM) generates at 63 tokens per second and embeds text in 34 milliseconds. Plenty fast.</p>

<p>The problem wasn’t compute. It was the line.</p>

<h2 id="what-this-is-in-one-breath">What this is (in one breath)</h2>

<p>An <strong>inference priority queue</strong> is a thin proxy in front of a model server that reorders work by urgency so short, critical jobs are not starved by long ones.</p>

<p>Here: sixteen agents and dozens of crons share one Ollama instance on one GPU. Generation takes tens of seconds; embeddings take tens of milliseconds. Ollama’s default queue is pure FIFO, so embeddings die waiting behind LLM jobs. A ~620-line Node.js proxy on port 11440 classifies requests into priority tiers and feeds Ollama in that order.</p>

<p>It is <strong>not</strong> a multi-GPU cluster, a custom inference engine, or <code class="language-plaintext highlighter-rouge">OLLAMA_NUM_PARALLEL</code> theater (that made throughput worse on this card). Build a priority line when many clients share one serial model server and latency classes differ by orders of magnitude; skip it if you’re the only caller and load is light.</p>

<p>Stack: Node.js stdlib proxy, Ollama, Qwen 3.5 for generation, nomic-embed-text for embeddings, Tesla P40.</p>

<h2 id="one-card-sixteen-agents">One Card, Sixteen Agents</h2>

<p>I run a 16-agent system on my home server: 15 specialized workers plus a coordinator, handling everything from news intelligence to <a href="/blog/2026/02/midas/">commodity trading</a> to pool coaching. The hardware is an Intel i7-8700K with 32GB RAM and a used Tesla P40. No cloud GPU. One card doing everything.</p>

<p>The inference stack is Ollama serving two models: Qwen 3.5 30B-A3B (a mixture-of-experts LLM) for generation and nomic-embed-text for embeddings. Two models, sixteen agents, one dispatch queue.</p>

<h2 id="the-fifo-problem">The FIFO Problem</h2>

<p>Ollama serializes all requests. One at a time. Pure FIFO: first in, first out. If you’re the only user, this is fine.</p>

<p>With sixteen agents and 84 cron jobs, it’s a disaster.</p>

<p>LLM generation requests take 40–90 seconds each. Embedding requests take ~34 milliseconds. When 2–3 LLM jobs stack in Ollama’s internal queue (which happens routinely), a 34ms embedding call gets stuck behind all of them. Waiting 80 to 270 seconds. Then it times out.</p>

<p>This wasn’t a theoretical edge case. Embedding timeouts were breaking my Mem0 persistent memory system, my personal knowledge base search, the pool coaching RAG pipeline, and <a href="/blog/2026/02/midas/">Midas’s</a> commodity signal ingestion. Four critical systems, all failing because a 34ms operation couldn’t cut the line ahead of 90-second LLM jobs.</p>

<p>I tested <code class="language-plaintext highlighter-rouge">OLLAMA_NUM_PARALLEL=2</code>. Made things worse. On a memory-bandwidth-bound GPU, splitting parallel requests dropped single-request throughput from 63 to 56 tok/s — an 11% haircut on the very generation speed I was trying to protect, with no help for the embedding starvation. Ollama’s scheduler (<code class="language-plaintext highlighter-rouge">server/sched.go</code>) is pure FIFO with no priority system. So I built one.</p>

<h2 id="620-lines-of-stdlib-nodejs">620 Lines of Stdlib Node.js</h2>

<p>The solution is a proxy sitting between all clients and Ollama. Port 11440 in front of Ollama’s 11434. Three priority tiers:</p>

<table>
  <thead>
    <tr>
      <th>Priority</th>
      <th>How It’s Classified</th>
      <th>Typical Latency</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>CRITICAL</strong></td>
      <td><code class="language-plaintext highlighter-rouge">/api/embed</code> and <code class="language-plaintext highlighter-rouge">/api/embeddings</code>, auto-detected by URL path</td>
      <td>~34ms</td>
    </tr>
    <tr>
      <td><strong>HIGH</strong></td>
      <td><code class="language-plaintext highlighter-rouge">X-Priority: high</code> header</td>
      <td>40–90s</td>
    </tr>
    <tr>
      <td><strong>NORMAL</strong></td>
      <td>Everything else (default)</td>
      <td>40–90s</td>
    </tr>
  </tbody>
</table>

<p>The key design choice: embeddings are auto-classified as CRITICAL by their URL path alone. No client changes needed for the most important case. The OpenClaw gateway sends LLM requests without any priority header. Cron jobs are the bulk of traffic and should always yield to embeddings.</p>

<p>Passthrough endpoints bypass the queue entirely. Health checks, model listing, model info, model pulls. None of these need GPU resources, so they’re forwarded directly.</p>

<p>620 lines. Zero npm dependencies, just Node.js stdlib. No Redis. No external state. The queue is three arrays. The cache is a Map. If the proxy restarts, state resets, and that’s fine because the queue drains in seconds and the cache rebuilds organically.</p>

<p>This is <a href="/blog/2026/01/boring-technology/">boring technology</a> doing exactly what it’s supposed to do.</p>

<h2 id="the-concurrent-embed-discovery">The Concurrent Embed Discovery</h2>

<p>After deploying v1, production logs revealed something I didn’t expect about Ollama’s internals.</p>

<p>Despite <code class="language-plaintext highlighter-rouge">PARALLEL=1</code>, Ollama can handle cross-model concurrent requests. I tested this empirically: nomic-embed-text (137M parameters) completes in ~35ms <em>even while Qwen 3.5 30B is actively generating</em>. Only 1.8% overhead on LLM speed. The embed model is small enough that it doesn’t meaningfully compete for memory bandwidth.</p>

<p>This led to a concurrent embed slot, a second dispatch path in the proxy. When a CRITICAL embed arrives and the primary slot has an active LLM request, the embed fires immediately on a separate slot. 42ms during active generation, instead of waiting in line.</p>

<p>One constraint: embed-during-embed still serializes. Same model, same VRAM, no benefit from concurrency there.</p>

<p>Here’s the production reality check: the concurrent slot only fires ~1.3% of the time. In practice, embeds arrive in bursts <em>after</em> an LLM job completes (the agent processes its response, then fires embeds), not <em>during</em> generation. The feature is architecturally correct and valuable when it triggers, but most embeds benefit from the priority queue alone.</p>

<p>This is why I trust production logs over intuition. The optimization I was most excited about barely fires.</p>

<h2 id="what-twelve-hours-of-logs-taught-me">What Twelve Hours of Logs Taught Me</h2>

<p>I pulled 12 hours of production data: 103 queued requests, 454 LLM chats, 1,664 embeds. Six improvements fell out of that analysis. Three are worth highlighting.</p>

<p><strong>The NaN Port Bug.</strong> During a systemd restart cascade, the <code class="language-plaintext highlighter-rouge">OLLAMA_QUEUE_TARGET</code> environment variable briefly went undefined. <code class="language-plaintext highlighter-rouge">parseInt(undefined)</code> returns <code class="language-plaintext highlighter-rouge">NaN</code> in JavaScript. The proxy tried to forward requests to <code class="language-plaintext highlighter-rouge">127.0.0.1:NaN</code>. Every request failed for 41 seconds.</p>

<p>Fix: <code class="language-plaintext highlighter-rouge">parseInt(env) || 11434</code>. One line. Prevents an entire class of restart-cascade failures. Defensive defaults aren’t glamorous, but they’re the difference between 41 seconds of downtime and zero.</p>

<p><strong>The Embed Cache.</strong> Embeddings are deterministic: same model, same input, same vector, every time. Yet the system was recomputing identical embeddings on every call. The Mem0 plugin especially hit this with warmup calls and repeated searches.</p>

<p>In-memory LRU cache. Key is <code class="language-plaintext highlighter-rouge">model + "\0" + input_text</code>. Five-minute TTL, 1000 entries, ~2MB of memory. Cache hits intercept <em>before</em> queue entry. Zero queue wait, zero GPU time. 6ms cached vs ~35ms uncached. The boring optimization that probably saves more aggregate time than the concurrent embed slot I was so excited about.</p>

<p><strong>The Silent Timeout Mismatch.</strong> Two shell scripts wrapping RAG search pipelines had hard <code class="language-plaintext highlighter-rouge">timeout 30</code> limits. But embed timeouts had been bumped to 90 seconds to accommodate queue waits. The shell script would kill the Python process at 30 seconds while the embed was still legitimately waiting in queue. No error message. The search just returned nothing.</p>

<p>This one is a reminder that system boundaries are where bugs hide. Every timeout in a pipeline needs to respect the timeouts downstream of it.</p>

<h2 id="what-i-didnt-build">What I Didn’t Build</h2>

<p>Things I explicitly researched and rejected:</p>

<ul>
  <li><strong>Request batching.</strong> Ollama Issue #6262 shows embedding quality degrades at batch sizes above 16. Not worth the trade-off.</li>
  <li><strong>Circuit breakers.</strong> Already have a watchdog agent (Bones) monitoring Ollama health via Telegram. Adding circuit breaker logic would duplicate responsibility.</li>
  <li><strong>Model-aware scheduling.</strong> Only two models. Not enough diversity to justify a fair-share scheduler like ollamaMQ’s round-robin.</li>
  <li><strong>Redis or external state.</strong> In-process state is fine. The queue drains in seconds. The cache rebuilds organically. Adding a dependency would be complexity without value.</li>
</ul>

<p>Knowing what <em>not</em> to build is half the work. I researched four existing Ollama proxies (ollamaMQ, Olla, LLMProxy, and Ollama’s own parallel mode) before writing a line of code. None fit. They’re built for multi-GPU routing, multi-model fairness, or cloud-style health checking. My problem was simpler: priority scheduling on a single GPU with two models.</p>

<h2 id="the-accidental-stress-test">The Accidental Stress Test</h2>

<p>I found out the queue was resilient by accident.</p>

<p>While fixing the fan controller on the P40’s aftermarket cooling shroud, I ran a 10-minute thermal validation: 80 heavy prefill requests hammering Ollama directly at 100% GPU utilization, 215W sustained. The GPU held at 73°C (23 degrees below throttle). Good.</p>

<p>What I didn’t think about was the cron jobs. They were still firing on schedule, routed through the queue proxy, while I monopolized the GPU through the back door.</p>

<p>The queue just absorbed it. Jobs waited their turn. The longest wait was 6.8 seconds. The moment the stress test finished, the queued jobs drained immediately. Back to normal.</p>

<p>No crashes. No connection errors. No lost work. Ten minutes of total GPU monopolization, and the agents didn’t notice.</p>

<p>You build safety nets hoping they’re unnecessary. Then one day you accidentally trip off a ledge and find out they work.</p>

<h2 id="the-numbers">The Numbers</h2>

<p><strong>Before the proxy:</strong> Embeddings timing out at 80–270 seconds. Memory, search, RAG, and trading systems failing intermittently.</p>

<p><strong>After:</strong> Embeddings completing in under 50ms regardless of LLM queue depth. Cached embeds in 6ms. Zero embedding timeouts.</p>

<p><strong>The proxy:</strong> 620 lines. Zero dependencies. Running as a systemd service since deployment with zero crashes. 21 tests covering the NaN port defense, cache behavior, EWMA latency tracking, passthrough routing, and concurrent embed dispatch.</p>

<hr />

<p>The GPU was never the bottleneck. Scheduling was. A 620-line proxy with no dependencies solved a problem that was breaking four critical systems, and the most impactful optimizations weren’t the clever ones. They were the boring ones I found by reading logs.</p>

<p>Build it. Ship it. Read the logs. Fix what the logs tell you to fix. That’s the process.</p>

<p><em>Self-hosting LLMs for multi-agent systems, or hitting similar scaling walls with Ollama? I’d love to compare notes. Reach out at <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="engineering" /><category term="Infrastructure" /><category term="Ollama" /><category term="Self-Hosted AI" /><category term="Node.js" /><category term="Systems Design" /><summary type="html"><![CDATA[Sixteen AI agents sharing one GPU. The GPU was fast enough. Ollama's FIFO queue wasn't. Here's how 620 lines of Node.js fixed it.]]></summary></entry><entry><title type="html">Why Four AI Agents Beat One Smart One</title><link href="https://architg.me/blog/2026/02/midas/" rel="alternate" type="text/html" title="Why Four AI Agents Beat One Smart One" /><published>2026-02-25T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/02/midas</id><content type="html" xml:base="https://architg.me/blog/2026/02/midas/"><![CDATA[<p>When I set out to build Midas — a system that monitors commodity markets and translates price movements into equity trading signals — the obvious approach was one powerful AI agent. Give it all the data, all the tools, and let it figure things out.</p>

<p>I chose four specialized agents instead. Not because it was more elegant. Because one agent trying to do everything is a recipe for mediocre everything. That multi-agent bias also shows up in <a href="/robynn.html">production work at Robynn AI</a> and in the <a href="/blog/2026/03/ollama-queue/">self-hosted inference queue</a> that keeps a larger agent fleet fed.</p>

<p>Midas started as two things colliding: a genuine interest in how commodity markets drive equity prices, and a need to stress-test multi-agent architecture on a problem complex enough to break it. Markets qualified on both counts.</p>

<h2 id="what-multi-agent-means-here">What multi-agent means here</h2>

<p>A <strong>multi-agent system</strong> (in this post) is several specialized agents with clear jobs, separate context, and a defined handoff—not one mega-prompt wearing many hats.</p>

<p>Midas uses four: intelligence (breadth on commodities), analysis (depth on companies), decision (portfolio and signals), and a router that directs ad-hoc queries to the right specialist. Each agent gets the context and tools for <em>its</em> kind of thinking so it doesn’t drag trading mindset into fundamental research or vice versa.</p>

<p>It is <strong>not</strong> “more agents = smarter.” Extra agents without hard boundaries just multiply coordination bugs. Split when the work needs different failure modes and review standards; keep one agent when the task is a single linear tool loop (<a href="/blog/2026/01/building-ai-agents/">production agent patterns</a>).</p>

<h2 id="one-agent-to-rule-them-all-and-why-i-didnt">One Agent to Rule Them All (And Why I Didn’t)</h2>

<p>The pitch for a single agent is appealing. One model, one context, one place to debug. It sees all the data, makes all the connections, produces all the output.</p>

<p>Here’s the problem: the real world doesn’t fit in a context window.</p>

<p>A market intelligence system needs to do fundamentally different types of thinking. Tracking commodity prices and detecting macro shifts is pattern recognition — fast, broad, signal-heavy. Evaluating a company’s management quality and capital discipline is deep analysis — slow, nuanced, judgment-heavy. Deciding whether to buy or sell based on conflicting signals is decision-making — probabilistic, risk-aware, portfolio-conscious.</p>

<p>One agent can’t be great at all three. Not because the models aren’t capable — they are — but because the <em>context and mindset</em> for each task are different. An agent deep in the weeds of technical analysis carries that framing into fundamental evaluation. An agent making trading decisions gets anchored by the analysis it just performed.</p>

<p>This is the same insight that makes microservices work in traditional engineering. Not that a monolith <em>can’t</em> work — it can — but as complexity grows, the cognitive overhead of “one thing that does everything” eventually collapses under its own weight.</p>

<p>I’d seen this pattern before building multi-agent systems professionally. Specialized agents consistently outperform general-purpose ones for complex domains. So when I designed Midas, I split it from the start.</p>

<h2 id="the-four-agent-pipeline">The Four-Agent Pipeline</h2>

<p>Midas runs four agents, each with a clearly defined mandate:</p>

<p><strong>The Intelligence Agent</strong> monitors commodity markets — tracking prices across dozens of categories, detecting macro shifts, extracting events from market news. Its job is breadth: scan the landscape and flag what’s moving and why.</p>

<p><strong>The Analysis Agent</strong> takes those signals and goes deep on individual companies. It evaluates fundamentals, assesses management quality, and scores competitive moats. Where the first agent asks “what’s happening in copper?”, this one asks “how does this copper movement affect specific mining companies, and are they well-run enough to capitalize on it?”</p>

<p><strong>The Decision Agent</strong> synthesizes everything into actionable signals. It maintains a composite scoring system, manages the portfolio, and executes trades. It’s the only agent that touches positions.</p>

<p><strong>The Router</strong> handles ad-hoc queries — when I want to investigate a specific ticker or market condition, it figures out which agent should handle the request.</p>

<p>Each agent operates in an isolated workspace with its own data and memory. The intelligence agent doesn’t know what trades the decision agent is making. The analysis agent doesn’t know the portfolio composition. This isolation is by design.</p>

<p>The whole system runs daily on autopilot across hundreds of equities linked to dozens of commodity categories. Total operating cost, as of February 2026? Less than a Netflix subscription.</p>

<h2 id="structured-handoffs-over-shared-memory">Structured Handoffs Over Shared Memory</h2>

<p>The agents don’t talk to each other in real-time. There’s no shared memory, no agent-to-agent chat, no centralized state.</p>

<p>Instead, they communicate through structured handoff files. The intelligence agent writes a priority list: here are the commodities with significant movement, here are the tickers most affected, here’s what changed and why. The analysis agent reads that file, does its deep-dives, and writes its own output — convictions, red flags, scoring updates. The decision agent reads both and makes trading decisions.</p>

<p>This is more work to set up than shared memory. But it’s dramatically more resilient.</p>

<p>When agents share state in real-time, a hallucination in one agent can cascade. Bad data gets picked up immediately, amplified through the pipeline, and acted on before anyone can intervene. I’ve seen this in production multi-agent systems — one agent confidently produces garbage, and downstream agents trust it because it came from inside the system.</p>

<p>Structured handoffs create natural circuit breakers. Each agent reads, interprets, and <em>validates</em> what the previous agent wrote. If the intelligence agent flags something unusual, the analysis agent can recognize it as noise. If the analysis agent overstates a conviction, the decision agent’s scoring system dampens it.</p>

<p>Think of it like a well-run engineering team. The best teams don’t communicate through constant meetings — they communicate through clear interfaces. Each team produces documented output. The next team consumes it, applies their own judgment, and produces their own output. Trust the interface, not the implementation.</p>

<h2 id="the-surprise-stewardship-beats-technicals">The Surprise: Stewardship Beats Technicals</h2>

<p>Here’s the finding I didn’t expect.</p>

<p>When I built the scoring engine, I assumed technical analysis would dominate. Price momentum, trend strength, volume patterns — the signals every trading system leans on heavily.</p>

<p>Across Midas’s full ticker universe — hundreds of equities linked to dozens of commodity categories — management quality (what I call “stewardship”) turned out to be the strongest predictor of medium-term signal performance. By a wide margin.</p>

<p>Companies with disciplined capital allocation, strong insider alignment, and proven management teams consistently outperform regardless of short-term technicals. A company with great management in a bad technical setup outperforms a poorly managed company with perfect chart patterns.</p>

<p>This makes intuitive sense in retrospect. Technicals tell you what the market is doing <em>right now</em>. Stewardship tells you what the company will do over the <em>next year</em>. For medium-term signals, management quality is the more durable edge.</p>

<p>I wouldn’t have discovered this without the four-agent architecture. The analysis agent — focused solely on company evaluation without being distracted by price charts — surfaces stewardship signals that a single agent would bury under technical noise. Specialization didn’t just make the system more reliable. It made it smarter.</p>

<hr />

<p>Multi-agent systems aren’t always the right call. For straightforward tasks, <a href="/blog/2026/01/building-ai-agents/">one agent with clear instructions</a> is still the best approach. But when your domain requires fundamentally different types of thinking — and markets definitely qualify — specialization wins.</p>

<p>The same principle applies to infrastructure choices. <a href="/blog/2026/01/boring-technology/">Boring, proven tools</a> for the foundation. Cutting-edge architecture where it creates real value. Know where to spend your complexity budget.</p>

<p><em>Building multi-agent systems or thinking through similar architecture decisions? I’d love to compare notes. Reach out at <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="ai" /><category term="AI Agents" /><category term="Architecture" /><category term="Systems Design" /><category term="Multi-Agent" /><summary type="html"><![CDATA[When I designed Midas, the obvious approach was one powerful AI agent. I chose four specialized ones instead. Here's what that architecture taught me about building systems that actually work.]]></summary></entry><entry><title type="html">The Craft of Showing Up Every Day</title><link href="https://architg.me/blog/2026/02/showing-up/" rel="alternate" type="text/html" title="The Craft of Showing Up Every Day" /><published>2026-02-03T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/02/showing-up</id><content type="html" xml:base="https://architg.me/blog/2026/02/showing-up/"><![CDATA[<p>I’ve been playing pool 3-4 times a week for three years. I’ve been writing code almost every day for twelve. Neither happened because of motivation. The same idea shows up in <a href="/blog/2026/01/position-play/">position play</a>: setup for the next shot beats one heroic make.</p>

<p>Both happened because I made showing up non-negotiable.</p>

<h2 id="the-myth-of-motivation">The Myth of Motivation</h2>

<p>Motivation is unreliable. It comes and goes like weather. Some days you wake up excited to work on your craft. Most days you don’t. If you only practice when you feel like it, you’ll practice when you’re already feeling good—and those aren’t the sessions that build skill.</p>

<p>The days you don’t want to show up are the days that count the most.</p>

<p>Anyone can practice when they’re inspired. The question is: can you practice when you’re tired, distracted, stressed, or bored? That’s where consistency beats talent. That’s where showing up becomes a superpower.</p>

<p>Waiting to “feel like it” is a trap. The feeling follows the action, not the other way around. I’ve never regretted a practice session, but I’ve regretted plenty of sessions I skipped while waiting for motivation to strike.</p>

<h2 id="what-pool-taught-me-about-consistency">What Pool Taught Me About Consistency</h2>

<p>When I started playing seriously, I was mediocre. Missed easy shots. Choked under pressure. Lost to players I should have beaten.</p>

<p>No single practice session fixed that. Hundreds of them did.</p>

<p>The improvement is invisible day-to-day. You don’t wake up one morning suddenly better. Instead, you look back after six months and realize you’re playing a different game. Shots that used to scare you are routine now. Position play that felt impossible is automatic.</p>

<p>The same players who beat me when I started—I beat them now. Not because I’m more talented. Because I kept showing up and they didn’t.</p>

<p>The uncomfortable truth about consistency is that it’s not about being the best on any given day. It’s about being good enough, often enough, for long enough that the compound interest takes over.</p>

<h2 id="the-compound-effect">The Compound Effect</h2>

<p>You’ve probably heard the math: 1% better every day compounds to roughly 37x better in a year, because 1.01^365 ≈ 37.8. It’s not just a motivational cliché—the arithmetic is real.</p>

<p>But here’s what the math doesn’t tell you: it doesn’t <em>feel</em> like 1%. It feels like nothing. On any given day, you’re indistinguishable from yesterday. The improvement is so small it’s invisible.</p>

<p>You have to trust the process before you see results. That’s the hard part. You’re putting in work based on faith that the curve will eventually bend.</p>

<p>This is where most people quit. Right before the inflection point. They’ve been showing up for months, they don’t feel any different, and they conclude it’s not working. They stop. Three months later, if they’d continued, they would have looked back and seen exponential growth.</p>

<p>The people who break through aren’t more talented. They just didn’t quit at month three.</p>

<h2 id="how-i-make-showing-up-easy">How I Make Showing Up Easy</h2>

<p>Consistency isn’t about willpower. It’s about removing friction. Here’s what works for me:</p>

<p><strong>Lower the bar.</strong> My rule is “just 30 minutes.” That’s it. Some days I only do 30 minutes. Most days, once I start, I go longer. But the commitment is 30 minutes, not two hours. The bar is low enough that I can always clear it.</p>

<p><strong>Remove decisions.</strong> Same time, same place. I’m at the pool hall Tuesday, Thursday, Sunday. It’s not a question I ask myself—it’s just what happens on those days. The decision was made once; now I just execute.</p>

<p><strong>Track it.</strong> I keep a simple checkbox calendar. Did I practice today? Check. The streak becomes its own motivation. After 30 consecutive days, you don’t want to break the chain. The tracking creates accountability, even if it’s just to yourself.</p>

<p><strong>Accept bad days.</strong> Showing up tired counts. Showing up distracted counts. Showing up and playing terribly counts. Not every session will be your best. The goal isn’t quality—it’s presence. Bad practice is still practice.</p>

<h2 id="applying-this-to-work">Applying This to Work</h2>

<p>The same principles apply to engineering.</p>

<p>I write code every day, even if it’s just a small PR or a bug fix. Some days I’m shipping features and solving hard problems. Most days I’m doing maintenance: reviews, documentation, fixing small issues. Both are practice.</p>

<p>The engineers I respect most aren’t the ones with occasional bursts of genius. They’re the ones who ship consistently. They’re in the codebase every day, making small improvements, fixing bugs nobody asked them to fix, leaving things better than they found them.</p>

<p>Over time, those small daily contributions compound into deep expertise and massive impact. The engineer who ships one small improvement every day will <a href="/blog/2026/01/boring-technology/">outperform the engineer who ships one brilliant feature every quarter</a>.</p>

<p>Consistency beats intensity. Always.</p>

<h2 id="the-identity-shift">The Identity Shift</h2>

<p>The biggest change wasn’t tactical—it was internal.</p>

<p>I used to say “I play pool.” Now I say “I’m a pool player.”</p>

<p>The difference is subtle but massive. One is an activity; the other is who you are. When pool is something you <em>do</em>, skipping feels like a reasonable choice. When pool is something you <em>are</em>, skipping feels like a betrayal of identity.</p>

<p>This is the real goal: not to build habits, but to build identity. Not “I’m trying to practice consistently,” but “I’m the kind of person who shows up.”</p>

<p>Once that identity is in place, consistency becomes automatic. You don’t have to convince yourself to practice. You just do what people like you do.</p>

<hr />

<p>Excellence isn’t an act. It’s a habit. And habits are just showing up, over and over, until the showing up becomes who you are.</p>

<p>You don’t rise to the level of your goals. You fall to the level of your systems. Build systems that make showing up easy, and the results will follow.</p>

<p>Start small. Show up tomorrow. Then the day after. String enough days together and you’ll look back in a year wondering how you got so far.</p>

<p><em>What’s your showing-up practice? What systems keep you consistent? I’d love to hear what works for you. <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="thoughts" /><category term="Habits" /><category term="Discipline" /><category term="Pool" /><category term="Career" /><summary type="html"><![CDATA[I've been playing pool 3-4 times a week for three years. I've been writing code almost every day for twelve. Neither happened because of motivation.]]></summary></entry><entry><title type="html">On Being a Generalist in a Specialist’s World</title><link href="https://architg.me/blog/2026/01/generalist/" rel="alternate" type="text/html" title="On Being a Generalist in a Specialist’s World" /><published>2026-01-29T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/01/generalist</id><content type="html" xml:base="https://architg.me/blog/2026/01/generalist/"><![CDATA[<p>I’ve written production code in Python, JavaScript, Go, and C++. I’ve done frontend, backend, infra, and now AI—including <a href="/robynn.html">production agent systems at Robynn AI</a>. Every few years, someone asks: “But what’s your <em>thing</em>?”</p>

<p>I never have a good answer.</p>

<p>For a long time, this felt like a weakness. Like I was somehow failing at the career game by not picking a lane. But I’ve come to see it differently.</p>

<h2 id="what-a-generalist-engineer-is">What a generalist engineer is</h2>

<p>A <strong>generalist engineer</strong> works effectively across domains—frontend, backend, infrastructure, AI—instead of going deep on one stack. The trade: less encyclopedic depth in any single technology, more skill at seeing how systems connect. Generalists win where problems cross boundaries: early-stage startups, platform work, and new domains where nobody has ten years of experience yet.</p>

<h2 id="the-pressure-to-specialize">The Pressure to Specialize</h2>

<p>Career advice is clear: be the X person. The React expert. The Kubernetes guru. The machine learning specialist. Pick something, go deep, become the person everyone thinks of when that topic comes up.</p>

<p>LinkedIn rewards it. Your title becomes your identity. “Senior Backend Engineer” tells people exactly what you do. “Senior Software Engineer” tells them nothing.</p>

<p>Job descriptions want “5+ years in [specific thing].” Never mind that a lot of these things haven’t even been around for 5 years. The industry rewards depth—or at least the appearance of it.</p>

<p>I’ve never had 5+ years in one thing. Not because I’m flaky, but because I keep getting pulled into new problems. Every time I get comfortable, something interesting appears in a different domain.</p>

<h2 id="how-i-became-a-generalist">How I Became a Generalist</h2>

<p>Here’s my actual career trajectory, in order:</p>

<p><strong>Cisco (2017)</strong>: Security, cloud infrastructure, Python.</p>

<p><strong>PayPal (2018)</strong>: Payments, distributed systems, scale. Now I’m thinking about transactions, consistency, and what happens when millions of people want their money at the same time.</p>

<p><strong>Riverbed (2019)</strong>: Systems programming, C++, network protocols. Completely different domain, different language, different problems—deep in the weeds of packet inspection and performance optimization.</p>

<p><strong>Robynn AI (2025)</strong>: LLMs, agent architectures, product. Suddenly I’m building AI systems, thinking about prompt engineering, orchestrating autonomous agents.</p>

<p>Each jump felt like starting over. Each time, I wondered if I was making a mistake by not staying to become the expert.</p>

<p>In retrospect, nothing was wasted. The systems thinking from Riverbed helps me <a href="/blog/2026/01/building-ai-agents/">debug AI agent behavior</a>. The security mindset from Cisco makes me paranoid about prompt injection. The scale lessons from PayPal informed how we architected infrastructure at Robynn.</p>

<h2 id="what-generalists-are-actually-good-at">What Generalists Are Actually Good At</h2>

<p>Generalists have real advantages that specialists don’t:</p>

<p><strong>Translation.</strong> I can talk to the ML team AND the infra team because I’ve been both. I speak both languages. When those teams are talking past each other—which happens constantly—I can bridge the gap.</p>

<p><strong>Problem decomposition.</strong> When you’ve seen problems in multiple domains, you start to recognize patterns. You know which parts are actually hard and which parts just seem hard because you haven’t seen them before. You can break complex problems into subproblems more effectively.</p>

<p><strong>Rapid prototyping.</strong> I can build a full-stack proof of concept without waiting on anyone. Need a quick ML model, an API, a frontend, and a deployment pipeline? I can do all of that—maybe not beautifully, but fast enough to learn if the idea is worth pursuing.</p>

<p><strong>Debugging.</strong> The hardest bugs live at the intersection of systems. When the frontend, backend, and database are all behaving correctly in isolation but something’s broken in production, you need someone who understands all three. Generalists find those bugs.</p>

<h2 id="the-costs-lets-be-honest">The Costs (Let’s Be Honest)</h2>

<p>I won’t pretend there’s no downside.</p>

<p>I’ll never be the person who wrote the book on [X]. When you read about the world’s leading experts on some technology, I won’t be on that list. That door is closed.</p>

<p>In deep technical interviews, specialists will always have deeper domain knowledge. They’ll know the niche edge cases, the obscure configuration options, the historical context for why things work the way they do. If the interview is “how well do you know [specific technology],” I’ll lose.</p>

<p>And imposter syndrome hits different when everyone around you seems to have “a thing.” At conferences, people introduce themselves by their specialty. “I work on compilers.” “I’m a security researcher.” “I focus on distributed consensus.” What do I say? “I… work on things”?</p>

<h2 id="where-generalists-thrive">Where Generalists Thrive</h2>

<p>That said, there are environments where generalists have a massive advantage:</p>

<p><strong>Early-stage startups.</strong> When you’re 10 people, you can’t afford specialists. You need people who can do whatever needs doing this week. Someone who can write the backend, set up the CI pipeline, debug the mobile app, and talk to customers. That’s the generalist’s home turf.</p>

<p><strong>Technical leadership.</strong> To lead an engineering organization, you need to understand all the pieces. The CTO who only understands backend will make bad decisions about frontend architecture. The VP who only knows infrastructure will underestimate product complexity. Breadth is a prerequisite for leadership.</p>

<p><strong>New domains.</strong> In AI, crypto, or any emerging field—no one has 10 years of experience because the field didn’t exist 10 years ago. Generalists have a pattern for learning new domains quickly. That’s valuable when the domain itself is new.</p>

<p><strong>Platform and infrastructure.</strong> When everything connects to everything, you need someone who understands everything. Platform engineers live at the intersection of frontend, backend, databases, DevOps, and security. Specialists struggle here.</p>

<h2 id="why-a-generalist-is-really-a-systems-thinker">Why a Generalist Is Really a Systems Thinker</h2>

<p>I used to think I was a “jack of all trades, master of none.” The full saying is actually “jack of all trades, master of none, but oftentimes better than a master of one.” That’s closer to how I think about it now.</p>

<p>But even that framing is wrong. I’m not a generalist because I failed to specialize. I’m a <em>systems thinker</em>. My specialty is understanding how pieces fit together.</p>

<p>The T-shape model helped me make peace with my career: broad knowledge across many areas, with depth in a few. For me, those depth areas are distributed systems, AI agents, and web design. Not because I chose them strategically, but because I kept returning to them as I solved real problems.</p>

<p><a href="/blog/2026/02/showing-up/">Depth comes naturally when you’re solving real problems</a>. You don’t have to force it. The generalist’s breadth gives you context; the depth emerges from repeated application.</p>

<hr />

<p>If you’re a generalist feeling like a fraud, consider this: someone needs to build the bridges between specialties. Someone needs to see the whole system. Someone needs to translate between the experts who can’t understand each other.</p>

<p>That’s not a consolation prize. That’s a skill. And in a world that’s increasingly complex and interconnected, it might be the most valuable skill of all.</p>

<p><em>What’s your experience with the generalist/specialist tradeoff? I’d love to hear from others who’ve grappled with this. <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="thoughts" /><category term="Career" /><category term="Learning" /><category term="Identity" /><summary type="html"><![CDATA[Generalist engineers win where systems intersect: early startups, platform work, new domains. Breadth isn't a failure to specialize—it's a skill.]]></summary></entry><entry><title type="html">Why Position Play Matters More Than Shot-Making</title><link href="https://architg.me/blog/2026/01/position-play/" rel="alternate" type="text/html" title="Why Position Play Matters More Than Shot-Making" /><published>2026-01-23T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/01/position-play</id><content type="html" xml:base="https://architg.me/blog/2026/01/position-play/"><![CDATA[<p>I can make the same shots as players ranked above me. So why do they win?</p>

<p>Because after they pocket the ball, they’re lined up perfectly for the next one. After I pocket it, I’m praying.</p>

<p>This realization changed how I think about pool—and honestly, how I think about a lot of things (including <a href="/blog/2026/01/building-ai-agents/">shipping agent systems</a> where the next state matters more than a flashy demo).</p>

<h2 id="the-shot-making-trap">The Shot-Making Trap</h2>

<p>When you start playing pool, the feedback loop is obvious: pocket balls, feel good. Miss balls, feel bad. So you practice pocketing balls. Cuts, banks, combinations. The satisfying <em>thunk</em> of a ball dropping into a pocket.</p>

<p>I spent my first two years doing exactly this. I got pretty good at making difficult shots. I could impress people at the bar with a long cut or a tricky bank.</p>

<p>But I couldn’t run more than three balls consistently.</p>

<p>The problem wasn’t my stroke. The problem was where I left the cue ball. I’d make a beautiful shot and end up with no angle on the next one, or worse, completely out of position. Then I’d have to make <em>another</em> difficult shot, and another, until eventually I missed.</p>

<p>The bottleneck was never shot-making. It was position play.</p>

<h2 id="what-position-play-actually-is">What Position Play Actually Is</h2>

<p>Position play isn’t just “get close to the next ball.” That’s the beginner’s understanding.</p>

<p>Real position play is controlling the cue ball to land in a <em>zone</em> that gives you an angle. And angle is everything. Angle means control—you can use spin, speed, and natural roll to get to your next position.</p>

<p>Straight-in shots are actually harder to play position from, not easier. When the cue ball, object ball, and pocket are in a line, your only options are follow and draw. You’ve eliminated most of your position routes.</p>

<p>The best players know where they want the cue ball before they even look at the object ball. They’re not thinking “can I make this?”—they’re thinking “where do I need to be, and what shot gives me that?”</p>

<p>The shot itself is almost an afterthought.</p>

<h2 id="the-math-of-running-out">The Math of Running Out</h2>

<p>Here’s some simple math that changed my perspective:</p>

<p>Let’s say you’re 90% on each individual shot. Sounds pretty good, right? But if you need 8 shots to run out a rack, your probability of success is:</p>

<p><strong>0.9^8 = 43%</strong></p>

<p>You’ll lose the majority of games even with 90% shot-making.</p>

<p>Now let’s say good position play makes each shot 95% instead of 90%—because you’re never awkward, never stretched out, never dealing with tough angles.</p>

<p><strong>0.95^8 = 66%</strong></p>

<p>That’s the difference between losing and winning the race. A 5% improvement on each shot compounds into 23 percentage points on winning the rack.</p>

<p>Position play isn’t just important. Mathematically, it’s the whole game.</p>

<h2 id="how-i-practice-position">How I Practice Position</h2>

<p>Here are the drills that actually improved my game:</p>

<p><strong>The Line Drill</strong></p>

<p>Place all the solids (or stripes) in a straight line down the center of the table. You must pocket them in order: 1, 2, 3, and so on.</p>

<p>This forces you to plan three balls ahead. You can’t just make the 1—you have to leave yourself an angle on the 2 that lets you get to the 3. It’s brutal at first, but it trains your brain to think in sequences.</p>

<p><strong>The Box Drill</strong></p>

<p>Pick a shot. Before you shoot, draw a box on the table with chalk where you want the cue ball to land. Now execute. Did you land in the box?</p>

<p>This builds precision. Most players have a vague sense of “I want to be over there.” The box drill forces specificity. You start to understand exactly how much speed and spin produce exactly what result.</p>

<p><strong>The Ghost Player</strong></p>

<p>This one transformed my practice sessions. Play a full rack against an imaginary perfect player—the ghost. Take ball in hand after the break. If you run out, you win the rack. If you miss or lose position, the ghost runs out and wins.</p>

<p>Keep count of how close you got. Did you pocket 5 balls before losing? 7? Track it over time.</p>

<p>You can also start easier: 3 balls spread on the table. Run them out. Then 4. Then 5. Gradually work up to a full rack. The ghost drill builds both position play and pressure tolerance—because every shot matters.</p>

<h2 id="reading-the-table">Reading the Table</h2>

<p>Before I shoot anything now, I ask myself: “What’s my route to the 8?”</p>

<p>Not just the next ball—the whole sequence. I identify problem balls early: clusters that need to be broken, balls near rails that will be trouble, potential snookers. Then I work backward from the 8-ball to figure out what sequence gives me the best chance.</p>

<p>Sometimes the right shot isn’t the obvious one. The easy pocket might leave me no angle on the next ball. The harder shot might set up a breakout I need. Position play means thinking about consequences, not just immediate results.</p>

<h2 id="the-mental-shift">The Mental Shift</h2>

<p>The transformation in my game came when I stopped asking “can I make this?” and started asking “where do I need to be next?”</p>

<p>The shot you’re on is already in the past. Your job is to set up the future.</p>

<p>This is why professionals make it look easy. They’re playing a different game than beginners. While we’re celebrating that we made a tough shot, they’re upset that they ended up 6 inches from perfect position. Their standards are different because they’re optimizing for different things.</p>

<p>Watch any pro match and pay attention to the cue ball, not the object ball. That’s where the real skill is.</p>

<hr />

<p>Shot-making impresses spectators. Position play wins matches. I’d rather look boring and cash the check.</p>

<p>The same principle applies beyond pool. In engineering, in career decisions, in life—the flashy move isn’t always the right move. <a href="/blog/2026/02/showing-up/">Sometimes the right move is the one that sets up your next three moves</a>.</p>

<p><em>If you’re in Boulder, Colorado and want to shoot some pool, hit me up. Always looking for practice partners who take the game seriously. <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>. I also stream matches on <a href="https://www.youtube.com/@poolhall_cuey">YouTube</a> if you want to watch how these drills hold up under pressure.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="pool" /><category term="Strategy" /><category term="Practice" /><category term="Competition" /><summary type="html"><![CDATA[I can make the same shots as players ranked above me. So why do they win? Because after they pocket the ball, they're lined up perfectly for the next one.]]></summary></entry><entry><title type="html">The Underrated Power of Boring Technology</title><link href="https://architg.me/blog/2026/01/boring-technology/" rel="alternate" type="text/html" title="The Underrated Power of Boring Technology" /><published>2026-01-18T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/01/boring-technology</id><content type="html" xml:base="https://architg.me/blog/2026/01/boring-technology/"><![CDATA[<p>At <a href="/robynn.html">Robynn AI</a>, we built cutting-edge AI agents with LangGraph for orchestration, Claude’s SDK for reasoning, OpenClaw for tool integration, and a dozen other libraries that didn’t exist two years ago. The same “boring core” idea shows up in how I <a href="/blog/2026/03/ollama-queue/">queue self-hosted inference</a>.</p>

<p>Our database? PostgreSQL. Our cache? Redis. Our message queue? RabbitMQ.</p>

<p>This isn’t a contradiction. It’s a strategy.</p>

<h2 id="what-boring-technology-means-here">What “boring technology” means here</h2>

<p><strong>Boring technology</strong> is infrastructure that’s old enough that its failure modes are documented, its operators are available, and its upgrades are boring—so you can spend novelty on the product instead of the plumbing.</p>

<p>At Robynn, that meant LangGraph, modern model SDKs, and agent tooling on the edge; <strong>PostgreSQL, Redis, RabbitMQ</strong> (and friends) underneath. The rule is a <strong>complexity budget</strong>: every team can absorb only so much newness before debugging becomes folklore. Spend that budget where you compete; refuse it where the industry already solved the problem.</p>

<p>It is <strong>not</strong> “never use new tools” or “always pick the oldest library.” It’s refusing to invent a database when you’re trying to invent an agent. Use boring cores when data, queues, and caches must just work; use sharp tools when the product <em>is</em> the new thing (<a href="/blog/2026/01/building-ai-agents/">production agents</a>, <a href="/blog/2026/02/midas/">multi-agent systems</a>).</p>

<h2 id="the-complexity-budget">The Complexity Budget</h2>

<p>Every engineering team has a limited complexity budget. You can only absorb so much novelty before your system becomes unmaintainable, your debugging becomes guesswork, and your on-call rotations become nightmares.</p>

<p>The question isn’t “should we use new technology?” It’s “where should we spend our complexity budget?”</p>

<p>At Robynn, we spent it on AI: LangGraph to <a href="/blog/2026/01/building-ai-agents/">orchestrate multi-step agent workflows</a>, Claude’s SDK with structured outputs for reliable tool calls, and experiments in prompting, memory architectures, and agent failure handling.</p>

<p>That’s where competitive advantage lived. That’s where we <em>should</em> have been on the cutting edge.</p>

<p>But storing data? Caching responses? Queuing background jobs? These are solved problems. We don’t need innovation here—we need reliability.</p>

<h2 id="the-temptation-of-the-new-a-confession">The Temptation of the New (A Confession)</h2>

<p>I learned this the hard way. Early in my career at Riverbed, I pushed for MongoDB when PostgreSQL would’ve been perfectly fine. The allure was irresistible: “web scale,” document flexibility, the thrill of being on the cutting edge.</p>

<p>What actually happened? Some time later, we needed transactions. We needed relational queries. We needed the things that boring databases had solved decades ago.</p>

<p>The migration cost more than the original implementation.</p>

<p>The irony? If I’d saved that complexity budget, we could have used it on features that actually mattered to users.</p>

<h2 id="what-boring-actually-means">What “Boring” Actually Means</h2>

<p>When I say “boring technology,” I don’t mean outdated. I mean <em>battle-tested</em>.</p>

<p>PostgreSQL, first released in 1989, has 35+ years of edge cases discovered and fixed. When you Google an error at 2 AM, there’s a Stack Overflow answer from 2014 that still works. The documentation is comprehensive. The failure modes are well-understood.</p>

<p>Boring tech hasn’t failed—it’s <em>graduated</em>. It survived the hype cycle and emerged as infrastructure you can trust.</p>

<p>This is especially important when your application layer is doing something novel. When your AI agent does something unexpected (and they will), you need to debug the agent—not wonder if the issue is in your experimental database.</p>

<p>Boring infrastructure gives you stable ground to stand on while you experiment above it.</p>

<h2 id="the-hidden-costs-of-shiny-infrastructure">The Hidden Costs of Shiny Infrastructure</h2>

<p>New infrastructure technology comes with costs that don’t show up in the README:</p>

<p><strong>Documentation gaps.</strong> The docs assume you already understand concepts they haven’t explained. You’ll spend hours piecing together blog posts, GitHub issues, and Discord messages.</p>

<p><strong>Hiring difficulty.</strong> Finding engineers who know CockroachDB is harder than finding engineers who know PostgreSQL. The ones who do know it? They’re expensive.</p>

<p><strong>Operational unknowns.</strong> What happens at 3 AM when it breaks? With PostgreSQL, I can find a DBA who’s seen my exact error a hundred times. With the new hotness, I’m filing GitHub issues and hoping.</p>

<p><strong>Migration tax.</strong> Technologies get abandoned. Companies pivot. When the shiny thing gets deprecated, you’re stuck with a migration you didn’t budget for.</p>

<p>When you’re already dealing with the operational unknowns of AI agents—which are substantial—you don’t want to add infrastructure unknowns on top.</p>

<h2 id="where-we-do-use-cutting-edge-tech">Where We Do Use Cutting-Edge Tech</h2>

<p>Let me be clear: we weren’t Luddites at Robynn. Here’s the stack we actually ran:</p>

<p><strong>Cutting-edge (where it matters):</strong></p>
<ul>
  <li><strong>LangGraph</strong> for agent orchestration—because multi-step AI workflows are genuinely new territory</li>
  <li><strong>Claude SDK</strong> with structured outputs—because reliable tool calling requires the latest capabilities</li>
  <li><strong>OpenClaw</strong> for tool integration—because the AI tool ecosystem is evolving rapidly</li>
  <li><strong>Vector databases</strong> for semantic search—because embedding-based retrieval is a new requirement</li>
  <li><strong>Custom prompt engineering patterns</strong>—because we’re discovering best practices in real-time</li>
</ul>

<p><strong>Boring (where it doesn’t):</strong></p>
<ul>
  <li><strong>PostgreSQL</strong> for relational data—because ACID transactions are a solved problem</li>
  <li><strong>Redis</strong> for caching and simple queues—because it’s fast and predictable</li>
  <li><strong>S3</strong> for blob storage—because it just works</li>
  <li><strong>Linux, Nginx, Python</strong> for compute—because debugging is easy</li>
</ul>

<p>The key insight: we’re cutting-edge in our <strong>product</strong> (AI agents) and boring in our <strong>infrastructure</strong> (data storage, caching, deployment).</p>

<h2 id="my-heuristic-where-does-innovation-create-value">My Heuristic: “Where Does Innovation Create Value?”</h2>

<p>Before adopting any new technology, I ask two questions:</p>

<ol>
  <li>
    <p><strong>Does this directly improve what customers pay us for?</strong> If we use a better orchestration framework, our agents work better. That’s value. If we use a fancier database, our data is stored… the same. That’s not value.</p>
  </li>
  <li>
    <p><strong>Would I bet my 3 AM sleep on this?</strong> New AI tools? The team expects some rough edges—it’s the nature of the space. New infrastructure? Nobody wants to debug a mysterious database failure while also debugging agent behavior.</p>
  </li>
</ol>

<p>For AI/ML tooling, the answer is often “yes, the innovation is worth it.” For infrastructure, the answer is almost always “no, use the boring thing.”</p>

<h2 id="the-boring-stack-that-enables-innovation">The Boring Stack That Enables Innovation</h2>

<p>Here’s the pattern I recommend:</p>

<p><strong>Infrastructure layer (boring):</strong></p>
<ul>
  <li>PostgreSQL or MySQL for relational data</li>
  <li>Redis for caching</li>
  <li>S3 for files</li>
  <li>Standard cloud compute (EC2, Cloud Run, etc.)</li>
</ul>

<p><strong>Application layer (as innovative as you need):</strong></p>
<ul>
  <li>Whatever frameworks make sense for your problem</li>
  <li>Cutting-edge AI/ML libraries if that’s your domain</li>
  <li>New tools that directly improve your product</li>
</ul>

<p>This layering matters. When something breaks, you want to know immediately whether it’s an infrastructure issue (unlikely with boring tech) or an application issue (likely with cutting-edge stuff). Clear separation makes debugging tractable.</p>

<h2 id="the-meta-lesson">The Meta-Lesson</h2>

<p>The engineers I respect most aren’t the ones chasing every new framework. They’re the ones who <a href="/blog/2026/01/generalist/">understand where to innovate</a>.</p>

<p>They use boring databases and exciting product architectures. They use reliable infrastructure and experimental features. They save their complexity budget for problems that actually differentiate their product.</p>

<p>At Robynn, we pushed on what AI agents can do—new orchestration patterns, memory systems, and ways of handling uncertainty.</p>

<p>We can do that <em>because</em> we’re not also experimenting with our database. Our boring infrastructure gives us the stability to be bold where it counts.</p>

<hr />

<p>Innovation should happen in your product, not your infrastructure. Save your complexity budget for problems your customers actually pay you to solve.</p>

<p>The best engineers aren’t the ones using the newest everything. They’re the ones who know which battles to pick.</p>

<p><em>If you’re building AI agents and thinking through these tradeoffs, I’d love to chat. Reach out at <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="engineering" /><category term="Architecture" /><category term="PostgreSQL" /><category term="AI" /><category term="LangGraph" /><category term="Career Advice" /><summary type="html"><![CDATA[At Robynn AI we built with LangGraph, Claude SDK, and bleeding-edge AI tools. Our database? PostgreSQL. Our cache? Redis. Here's why that combination works.]]></summary></entry><entry><title type="html">Building AI Agents: Lessons from the Trenches</title><link href="https://architg.me/blog/2026/01/building-ai-agents/" rel="alternate" type="text/html" title="Building AI Agents: Lessons from the Trenches" /><published>2026-01-11T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>https://architg.me/blog/2026/01/building-ai-agents</id><content type="html" xml:base="https://architg.me/blog/2026/01/building-ai-agents/"><![CDATA[<p>Building AI agents that work in demos is easy. Building agents that work reliably in production? That’s where things get interesting.</p>

<p>Since 2025, I’ve built agents for everything from <a href="/robynn.html">production workflows at Robynn AI</a> to helping local businesses automate operational tasks. Here’s what I’ve learned. For a deeper cut on multi-agent architecture, see <a href="/blog/2026/02/midas/">Why Four AI Agents Beat One Smart One</a> and <a href="/blog/2026/03/ollama-queue/">the Ollama priority queue</a>.</p>

<h2 id="what-a-production-agent-is">What a production agent is</h2>

<p>A <strong>production AI agent</strong> is a loop that calls tools, keeps state, and finishes real work under failure—not a chat demo that looks smart once.</p>

<p>In practice: pick the simplest architecture that fits (tool-calling loop → graph/workflow → multi-agent), constrain outputs so you can trust them, log and measure what broke, and only spend complexity where the product differentiates (<a href="/blog/2026/01/boring-technology/">boring infrastructure, sharp AI</a>).</p>

<p>It is <strong>not</strong> “give the model every tool and a vague goal.” That ships demos. Production agents have narrow jobs, explicit success criteria, and a plan for timeouts, bad tool results, and human override.</p>

<p>Stack I keep reaching for: Python, structured outputs, LangGraph when the control flow actually branches, boring datastores underneath.</p>

<h2 id="the-agent-architecture-spectrum">The Agent Architecture Spectrum</h2>

<p>There’s a spectrum of agent architectures, and choosing the right one depends entirely on your use case:</p>

<h3 id="simple-tool-calling-agents">Simple Tool-Calling Agents</h3>

<p>For straightforward tasks with clear inputs and outputs, you don’t need complex orchestration. A simple loop works:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">simple_agent</span><span class="p">(</span><span class="n">task</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tools</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Tool</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="n">messages</span> <span class="o">=</span> <span class="p">[{</span><span class="s">"role"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="n">task</span><span class="p">}]</span>

    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">response</span> <span class="o">=</span> <span class="n">llm</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="n">tools</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="n">is_complete</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">content</span>

        <span class="c1"># Execute tool calls
</span>        <span class="k">for</span> <span class="n">tool_call</span> <span class="ow">in</span> <span class="n">response</span><span class="p">.</span><span class="n">tool_calls</span><span class="p">:</span>
            <span class="n">result</span> <span class="o">=</span> <span class="n">execute_tool</span><span class="p">(</span><span class="n">tool_call</span><span class="p">)</span>
            <span class="n">messages</span><span class="p">.</span><span class="n">append</span><span class="p">({</span><span class="s">"role"</span><span class="p">:</span> <span class="s">"tool"</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="n">result</span><span class="p">})</span>
</code></pre></div></div>

<p>This pattern handles 80% of use cases. Don’t over-engineer it. (This is part of <a href="/blog/2026/01/boring-technology/">my philosophy on boring technology</a>—save complexity for where it matters.)</p>

<h3 id="state-machine-agents">State Machine Agents</h3>

<p>When you need deterministic control flow with AI-powered decisions at each step, state machines shine:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langgraph.graph</span> <span class="kn">import</span> <span class="n">StateGraph</span>

<span class="n">workflow</span> <span class="o">=</span> <span class="n">StateGraph</span><span class="p">(</span><span class="n">AgentState</span><span class="p">)</span>

<span class="c1"># Define nodes
</span><span class="n">workflow</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="s">"analyze"</span><span class="p">,</span> <span class="n">analyze_task</span><span class="p">)</span>
<span class="n">workflow</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="s">"plan"</span><span class="p">,</span> <span class="n">create_plan</span><span class="p">)</span>
<span class="n">workflow</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="s">"execute"</span><span class="p">,</span> <span class="n">execute_plan</span><span class="p">)</span>
<span class="n">workflow</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="s">"validate"</span><span class="p">,</span> <span class="n">validate_results</span><span class="p">)</span>

<span class="c1"># Define edges
</span><span class="n">workflow</span><span class="p">.</span><span class="n">add_edge</span><span class="p">(</span><span class="s">"analyze"</span><span class="p">,</span> <span class="s">"plan"</span><span class="p">)</span>
<span class="n">workflow</span><span class="p">.</span><span class="n">add_conditional_edges</span><span class="p">(</span>
    <span class="s">"plan"</span><span class="p">,</span>
    <span class="n">should_execute</span><span class="p">,</span>
    <span class="p">{</span><span class="s">"execute"</span><span class="p">:</span> <span class="s">"execute"</span><span class="p">,</span> <span class="s">"revise"</span><span class="p">:</span> <span class="s">"plan"</span><span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="multi-agent-systems">Multi-Agent Systems</h3>

<p>For complex domains, <a href="/blog/2026/02/midas/">multiple specialized agents often outperform one generalist</a>:</p>

<ul>
  <li><strong>Coordinator</strong>: Routes tasks to specialists</li>
  <li><strong>Researcher</strong>: Gathers and synthesizes information</li>
  <li><strong>Executor</strong>: Takes actions</li>
  <li><strong>Validator</strong>: Checks results</li>
</ul>

<h2 id="the-patterns-that-actually-work">The Patterns That Actually Work</h2>

<h3 id="1-structured-outputs-are-non-negotiable">1. Structured Outputs Are Non-Negotiable</h3>

<p>Free-form text output from LLMs is unreliable. Always constrain the output:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>

<span class="k">class</span> <span class="nc">ActionPlan</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">reasoning</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">actions</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Action</span><span class="p">]</span>
    <span class="n">confidence</span><span class="p">:</span> <span class="nb">float</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">llm</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
    <span class="n">prompt</span><span class="p">,</span>
    <span class="n">response_format</span><span class="o">=</span><span class="n">ActionPlan</span>
<span class="p">)</span>
</code></pre></div></div>

<p>In my experience, this alone eliminates half the production bugs I’d otherwise be chasing.</p>

<h3 id="2-retry-with-exponential-backoff">2. Retry with Exponential Backoff</h3>

<p>LLM APIs fail. Rate limits hit. Network hiccups happen. Build resilience in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">retry</span><span class="p">(</span>
    <span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="mi">3</span><span class="p">),</span>
    <span class="n">wait</span><span class="o">=</span><span class="n">wait_exponential</span><span class="p">(</span><span class="n">multiplier</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="nb">max</span><span class="o">=</span><span class="mi">10</span><span class="p">),</span>
    <span class="n">retry</span><span class="o">=</span><span class="n">retry_if_exception_type</span><span class="p">(</span><span class="n">RateLimitError</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">call_llm</span><span class="p">(</span><span class="n">prompt</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">llm</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="3-observability-from-day-one">3. Observability from Day One</h3>

<p>You can’t debug what you can’t see. Log everything:</p>

<ul>
  <li>Input prompts (anonymized)</li>
  <li>Tool calls and results</li>
  <li>Token usage</li>
  <li>Latency breakdowns</li>
  <li>Error rates by category</li>
</ul>

<p>I use a simple pattern:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">trace</span><span class="p">(</span><span class="s">"agent_step"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">process_step</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="n">AgentState</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">AgentState</span><span class="p">:</span>
    <span class="k">with</span> <span class="n">span</span><span class="p">(</span><span class="s">"llm_call"</span><span class="p">):</span>
        <span class="n">response</span> <span class="o">=</span> <span class="n">llm</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">state</span><span class="p">.</span><span class="n">messages</span><span class="p">)</span>

    <span class="n">log_metrics</span><span class="p">({</span>
        <span class="s">"tokens_used"</span><span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="n">usage</span><span class="p">.</span><span class="n">total_tokens</span><span class="p">,</span>
        <span class="s">"latency_ms"</span><span class="p">:</span> <span class="n">response</span><span class="p">.</span><span class="n">latency</span><span class="p">,</span>
        <span class="s">"tool_calls"</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">tool_calls</span><span class="p">)</span>
    <span class="p">})</span>

    <span class="k">return</span> <span class="n">state</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="4-human-in-the-loop-escape-hatches">4. Human-in-the-Loop Escape Hatches</h3>

<p>No agent should run completely autonomously in production. Always provide:</p>

<ul>
  <li>Confidence thresholds that trigger human review</li>
  <li>Easy ways to pause and inspect state</li>
  <li>Clear audit trails</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">action</span><span class="p">.</span><span class="n">confidence</span> <span class="o">&lt;</span> <span class="mf">0.8</span><span class="p">:</span>
    <span class="k">await</span> <span class="n">notify_human</span><span class="p">(</span>
        <span class="n">action</span><span class="o">=</span><span class="n">action</span><span class="p">,</span>
        <span class="n">context</span><span class="o">=</span><span class="n">state</span><span class="p">,</span>
        <span class="n">timeout</span><span class="o">=</span><span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div>

<h2 id="common-pitfalls">Common Pitfalls</h2>

<h3 id="the-just-add-more-context-trap">The “Just Add More Context” Trap</h3>

<p>When an agent fails, the temptation is to add more instructions to the prompt. This usually makes things worse. Instead:</p>

<ol>
  <li>Analyze <em>why</em> it failed</li>
  <li>Constrain the output format</li>
  <li>Add specific examples (few-shot)</li>
  <li>Consider splitting into sub-tasks</li>
</ol>

<h3 id="ignoring-edge-cases">Ignoring Edge Cases</h3>

<p>Agents in production see inputs you never imagined. That “simple” task of parsing customer emails? Wait until you see:</p>

<ul>
  <li>Emails in multiple languages</li>
  <li>Forwarded threads with nested quotes</li>
  <li>Attachments described but not attached</li>
  <li>Sarcasm and ambiguity</li>
</ul>

<p>Build defensive parsing and graceful degradation.</p>

<h3 id="over-relying-on-the-llm">Over-Relying on the LLM</h3>

<p>Not everything needs AI. A well-crafted regex or a simple rule often beats an LLM call for:</p>

<ul>
  <li>Data validation</li>
  <li>Format checking</li>
  <li>Simple transformations</li>
  <li>Deterministic routing</li>
</ul>

<p>Save the LLM for actual reasoning tasks. Use <a href="/blog/2026/01/boring-technology/">boring, proven tools</a> for everything else.</p>

<h2 id="what-im-excited-about">What I’m Excited About</h2>

<p>The field is moving fast. A few things I’m watching:</p>

<p><strong>Tool Libraries</strong>: Standardized tool definitions (like MCP) make agents more portable and composable.</p>

<p><strong>Smaller, Faster Models</strong>: Haiku-class small models make agent loops economically viable for more use cases.</p>

<p><strong>Memory Systems</strong>: Long-term memory and retrieval are becoming table stakes for production agents.</p>

<p><strong>Evaluation Frameworks</strong>: We desperately need better ways to test agent behavior at scale.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Building production AI agents is part software engineering, part prompt engineering, and part systems design. It’s one of those domains where <a href="/blog/2026/01/generalist/">being a generalist</a> actually helps—you need to understand the full stack. The agents that work best are:</p>

<ul>
  <li>Simple where possible</li>
  <li>Observable always</li>
  <li>Constrained in their outputs</li>
  <li>Resilient to failures</li>
  <li>Honest about their limitations</li>
</ul>

<p>Start simple, measure everything, and iterate based on real-world feedback. <a href="/blog/2026/02/showing-up/">Showing up consistently</a> and making incremental improvements is the path to agents that actually work.</p>

<hr />

<p><em>Have questions about building AI agents? Drop me a line at <a href="mailto:architgupta941@gmail.com">architgupta941@gmail.com</a> or find me on <a href="https://x.com/0xArchitG">X</a>.</em></p>]]></content><author><name>Archit Gupta</name></author><category term="ai" /><category term="AI Agents" /><category term="LangGraph" /><category term="Python" /><category term="Automation" /><summary type="html"><![CDATA[After building AI agents professionally at Robynn AI and for local businesses, here are the patterns and pitfalls I've discovered for creating agents that actually work in production.]]></summary></entry></feed>