<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[The Computist Journal: Craft]]></title><description><![CDATA[Real code: algorithms, complexity and why they are shaped that way.]]></description><link>https://blog.apiad.net/s/craft</link><image><url>https://substackcdn.com/image/fetch/$s_!cjJH!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45cb08f7-6072-414b-a0ec-a0c57c53b74c_1024x1024.png</url><title>The Computist Journal: Craft</title><link>https://blog.apiad.net/s/craft</link></image><generator>Substack</generator><lastBuildDate>Sun, 01 Nov 2026 09:12:02 GMT</lastBuildDate><atom:link href="https://blog.apiad.net/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Alejandro Piad Morffis]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[apiad@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[apiad@substack.com]]></itunes:email><itunes:name><![CDATA[Alejandro Piad Morffis]]></itunes:name></itunes:owner><itunes:author><![CDATA[Alejandro Piad Morffis]]></itunes:author><googleplay:owner><![CDATA[apiad@substack.com]]></googleplay:owner><googleplay:email><![CDATA[apiad@substack.com]]></googleplay:email><googleplay:author><![CDATA[Alejandro Piad Morffis]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Pseudocode is a lie]]></title><description><![CDATA[And what I use instead]]></description><link>https://blog.apiad.net/p/pseudocode-is-a-lie</link><guid isPermaLink="false">https://blog.apiad.net/p/pseudocode-is-a-lie</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Thu, 04 Jun 2026 11:01:18 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>Monday&#8217;s post was about a single algorithm &#8212; Union-Find, &#945;(n) &#8776; constant. This is the technical companion: what it took to publish that algorithm. Both pieces are part of the June sprint for <a href="https://matcom.github.io/codex/">The Algorithm Codex</a>, which you can also get in print at <a href="https://apiad.gumroad.com/l/codex">Gumroad</a> or as part of the <a href="https://apiad.gumroad.com/l/compendium">Compendium</a>. More at the end.</em></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6000" height="4000" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4000,&quot;width&quot;:6000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A kangaroo is eating from someone's hand.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A kangaroo is eating from someone's hand." title="A kangaroo is eating from someone's hand." srcset="https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1747979992703-d85e27eea977?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4NHx8bGllfGVufDB8fHx8MTc4MDUxMDM2NHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@ellephant_photography">Ellephant</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Pseudocode is informal enough to be ambiguous and formal enough to be annoying. Every advantage it claims is better served by something else.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>Three claims for pseudocode, all false</h2><p>The canonical defense of pseudocode is <em>Introduction to Algorithms</em> (Cormen, Leiserson, Rivest, and Stein), known as CLRS. Thirty years. Four generations of computer science students. The single most widely-sold technical book in the field. If you&#8217;ve studied algorithms, you learned from CLRS or from someone who did. And CLRS uses pseudocode throughout, defends it explicitly, and makes the case as well as it can be made.</p><p>The argument for pseudocode rests on three things. First, it hides details that don&#8217;t matter for the explanation &#8212; variable types, secondary operations, boilerplate. Second, it speaks at the same conceptual level as the narrative; you can invent operations, give them names, use notation that fits the idea you&#8217;re explaining. Third, it survives time: programming languages rise and fall, but pseudocode is language-agnostic, and a textbook meant to teach the same concepts to students who will write Python, Java, Rust, or something that doesn&#8217;t exist yet can&#8217;t be tied to any of them.</p><p><strong>Problem 1: Pseudocode is not intuitive.</strong> If you want to convey an idea intuitively &#8212; how a process unfolds, why an algorithm works, what the conceptual structure is &#8212; use a diagram, a narrative, an informal sketch. These are genuinely intuitive; they don&#8217;t impose formal constraints. Pseudocode has syntax. It requires <code>for</code>, <code>while</code>, <code>if</code>, formatting conventions, notation that the reader has to parse before they can extract the idea. The claim that pseudocode is easier to read than real code is false. It&#8217;s <em>different</em> from real code, but not simpler. For genuinely intuitive explanation, there are better tools.</p><p><strong>Problem 2: Pseudocode is not formal.</strong> Real code has formal semantics. There is a defined mapping from source to behavior; a compiler or interpreter enforces it. Pseudocode has no such mapping. The reader is left to translate it into whatever language they&#8217;ll actually use, and that translation is entirely implicit. It&#8217;s undocumented and un-enforced. The gap between <em>&#8220;swap the elements at positions i and j&#8221;</em> and <em>&#8220;no, you can&#8217;t, you forgot you were working with an immutable list&#8221;</em> is not a minor translation detail. That gap is where the data structure&#8217;s mutability, the cost model, the choice of representation actually live. If your pseudocode rules were formal enough to specify the translation unambiguously, you&#8217;d have a compiler. It would just be code.</p><p><strong>Problem 3: Pseudocode is not portable.</strong> The language-agnostic argument sounds compelling, but it proves too much. Pseudocode isn&#8217;t language-agnostic in any useful sense. It&#8217;s an <em>additional</em> language the reader has to learn. If you give students pseudocode, you&#8217;re giving none of them something they can run. You&#8217;re asking every reader to translate. Whereas if you pick one real language (a widely-used, readable one), many of your readers understand it directly, and the rest have a formal, precise text to translate from. Pseudocode doesn&#8217;t spare readers translation; it makes all of them translate.</p><p>My prior is strong: intuition first, formalization later. An algorithm, a formula, a mathematical concept: explain it intuitively first, then formally. Pseudocode tries to do both and delivers neither. It isn&#8217;t informal enough to convey ideas; it isn&#8217;t formal enough to prove properties. Pseudocode charges you for formal syntax and gives you no formal guarantee.</p><h2>Two registers: diagrams for intuition, real code for formality</h2><p>Use two kinds of explanation, not one.</p><p>For intuitive content, use diagrams and narration. Diagrams don&#8217;t have to be formal &#8212; informal sketches work fine: an arrow from one box to another, a tree branching as a recursion unfolds, a before/after pair showing what partition does to an array. They explain intuitively, expose the underlying reasoning, and don&#8217;t impose the cost of formal syntax.</p><p>For formal content, use real code. Every supposed advantage of pseudocode &#8212; the ability to invent operations, hide irrelevant detail, speak at a high conceptual level &#8212; exists in real programming languages too. Real languages have abstraction and encapsulation. To defer an operation, stub it: a named function with an empty body or a one-line comment. <code>graph.connect(u, v)</code>, <code>database.search(query)</code>, <code>parser.parse(source)</code>. Real code already has the vocabulary for subjects, predicates, and attributes on those subjects. You don&#8217;t need bespoke notation. You need a sufficiently expressive language.</p><p>That language is Python. Of the widely-used general-purpose languages, Python is the most readable. There are more ergonomic languages (certain Lisps, Haskell, Racket), but their readability advantage comes at the cost of familiarity, and a textbook audience that doesn&#8217;t already know those languages won&#8217;t benefit from them. Python is known, readable, and expressive. There is no good reason not to use Python in a programming book, even a book that isn&#8217;t <em>about</em> Python.</p><h2>Literate programming keeps prose and code from drifting</h2><p>If we&#8217;re using real code anyway, formal and executable, why not make that code a real deliverable? A library with unit tests and an import statement, not just a code block in a document.</p><p>The obstacle: docs and code drift apart when they live in separate files. Change the explanation, and you have to remember to update the code. Change the code, and you have to remember to update the prose. In practice, one always lags the other. Pseudocode dodges this: it never has to match anything, so it&#8217;s never wrong. Real code is always either in sync or not.</p><p>Literate programming is Donald Knuth&#8217;s answer, from his 1984 paper. The original system (WEB, later CWEB) was built around Pascal and C. You wrote a single source file interleaving prose and code; a tool called <code>tangle</code> extracted the code into a runnable program, and a tool called <code>weave</code> rendered the prose into a readable document. The writing and the code are the same artifact. You can&#8217;t let them drift because they&#8217;re literally the same file.</p><p>Modern tools have rediscovered the idea. Jupyter notebooks mix prose and executable code. Quarto does the same at book scale. But neither fully extracts the code into a standalone packageable artifact: the notebook <em>is</em> the program, and you can&#8217;t easily ship it as a library.</p><h2><code>illiterate</code> makes the book and the package the same source</h2><p><code>illiterate</code> closes that gap. A build step extracts the tagged blocks from <code>.qmd</code> files (<code>{export=...}</code>) and assembles them into actual Python source files. The book renders; the package builds from the same source. Three things follow automatically: the documentation can&#8217;t describe an algorithm that doesn&#8217;t compile; the tests run against the same code the reader sees; and the book ships a working library as a side effect of being written.</p><p>In the <a href="https://matcom.github.io/codex/">Algorithm Codex</a> (a hundred algorithms across sorting, graphs, data structures, and dynamic programming), every chapter follows this pattern. No pseudocode anywhere.</p><h3>Quicksort, with the package built from the page</h3><p>A Quicksort chapter opens with the public entry point, the function the reader calls and the one that lands in the package:</p><pre><code><code>```python {export=codex/sorting/quick.py}

from typing import Sequence
from codex.types import Ordering, default_order

def quicksort[T](items: Sequence[T], order: Ordering[T] = default_order) -&gt; list[T]:
    items = list(items)  # don't mutate the input
    _quicksort(items, 0, len(items), order)
    return items
```</code></code></pre><p>The recursive helper is two cases: a base case that bails on slices of length zero or one, and a recursive case that partitions and recurses on both halves:</p><pre><code><code>```python {export=codex/sorting/quick.py}

def _quicksort[T](items: list[T], lo: int, hi: int, order: Ordering[T]) -&gt; None:
    if hi - lo &lt; 2:
        return
    pivot_index = partition(items, lo, hi, order)
    _quicksort(items, lo, pivot_index, order)
    _quicksort(items, pivot_index + 1, hi, order)
```</code></code></pre><p>Both blocks export to the same file; <code>illiterate</code> appends them in document order. Next in that order is the partition step, its own block and its own paragraph. Each conceptual piece earns its own prose frame and its own extracted function.</p><p>When the book renders, Quarto executes every demo block. If any algorithm is wrong, the build fails before that sentence can appear on the page. The build verifies every algorithm in the Codex, every time.</p><p>The book is the primary source. The algorithms are real code that runs. The two have never disagreed.</p><p>Until next time, stay curious.</p><div><hr></div><p><code>illiterate</code><em> is open-source at <a href="https://github.com/apiad/illiterate">github.com/apiad/illiterate</a>, a small Python tool that extracts code from Markdown and assembles it into runnable source files. If you write a book, a tutorial, or a course where the code has to match the text, the setup described here is twenty minutes to replicate.</em></p><p><em>The algorithms it enforces are in <a href="https://apiad.gumroad.com/l/codex">The Algorithm Codex</a>, available in print on Gumroad, or free to read online at <a href="https://matcom.github.io/codex/">matcom.github.io/codex/</a>. Both the Codex and my other books are bundled at <a href="https://apiad.gumroad.com/l/compendium">apiad.gumroad.com/l/compendium</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[The fastest algorithm ever devised]]></title><link>https://blog.apiad.net/p/the-fastest-algorithm-ever-devised</link><guid isPermaLink="false">https://blog.apiad.net/p/the-fastest-algorithm-ever-devised</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 01 Jun 2026 11:30:44 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>Every post on June is related to algorithms, anchored on <a href="https://apiad.gumroad.com/l/codex">The Algorithm Codex</a>. I&#8217;m also running an experiment this month: stretching the pace to one-post-a-day with per-day theme. <strong>Monday</strong> is for <strong>Mind-blowing</strong>. Let&#8217;s see how that goes.</em></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="5472" height="3648" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3648,&quot;width&quot;:5472,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;low angle photography of green trees&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="low angle photography of green trees" title="low angle photography of green trees" srcset="https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1462143338528-eca9936a4d09?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHx0cmVlc3xlbnwwfHx8fDE3ODAxMDAzMjF8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@tbzr">Arnaud Mesureur</a> on <a href="https://unsplash.com">Unsplash</a>. I promise, it will make sense in the end. It&#8217;s a bunch of trees&#8230;</figcaption></figure></div><p>When someone tells you they have the &#8220;fastest algorithm ever devised&#8221;, you probably think they bumped into trick to make clever use of the cache, or something equally tasteless, perhaps Youtube-worthy for 2 hours. I know I&#8217;d do if you came to me with this title.</p><p>But this is not that post. This is about the actual fastest algorithm we have ever devised, and probably the fastest anyone will ever do. And not in a trivial sense. There is one problem in computer science (practical, appearing everywhere you look, explainable in two sentences) where we have an algorithm, a correctness proof, a complexity bound, and a matching lower bound that closes the problem.</p><p>Done. Problem solved. No more papers.</p><p>That kind of closure is the rarest thing in any field of human knowledge.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Four conditions, one winner</h2><p>But what does &#8220;fastest algorithm&#8221; mean? There are many ways you can interpret this phrase, and some of them are trivially true.</p><p>Reading an entry from an array takes constant time. Nothing beats that. But it&#8217;s one operation on a flat structure, no progression, nowhere lower to go. Getting the title should require something harder.</p><p>The interesting frontier is a non-trivial problem with a naive solution everyone reaches for, and another, mindblowingly faster solution that is provably optimal. The problem we analyze today has four traits we want:</p><p>First, <strong>general</strong>: appears in real systems across many domains, not a toy. Second, <strong>explainable in two sentences</strong>: if you need a paragraph to state it, the optimality result arrives too late to matter. Third, <strong>a real progression from naive to optimal</strong>: intermediate ideas that each earned their place, not a single flash of inspiration. Fourth, <strong>provably closed</strong>: algorithm and lower bound meet exactly. Not &#8220;hard to improve.&#8221; Closed.</p><p>Our problem is <em>Union-Find</em>, which clears all four. Nothing else with a non-trivial structure clears them as cleanly. Let me explain it to you in two sentences.</p><h2>You&#8217;ve used this all day</h2><p>Union-Find goes like this. You have <em>n</em> things, organized into groups. You need to merge two groups and ask whether two things are in the same group.</p><p>That&#8217;s it. No deletion. No ordering. No traversal. Two operations on a shifting partition: union and find.</p><p>You&#8217;ve used it today, probably without noticing. Your phone&#8217;s photo app groups every selfie of the same person into a face album by computing connected components, and connected components is what Union-Find does for a living. The bucket tool in any image editor (Paint, Photoshop, Procreate, whatever) floods a region with color by labeling connected pixels? Yes, Union-Find.</p><p>Every time your TypeScript or Rust compiler decides two variables must share a type, Hindley-Milner type inference unifies them through Union-Find. Spanning Tree Protocol, the thing that lets the switches in your office or home network agree on which links to use so packets don&#8217;t cycle forever, runs Kruskal&#8217;s algorithm; Kruskal needs Union-Find to detect cycles.</p><p>The same idea shows up across classical computer science under different names. The canonical image segmentation algorithm (Felzenszwalb and Huttenlocher, 2004) is the bucket tool&#8217;s research-grade cousin. Procedurally generated mazes in games are built by running Kruskal on a grid. Percolation theory in physics, the study of how fluids flow through porous materials, is the same partition-growing-by-merging problem in continuous form. Whenever a system has equivalence classes that grow online, Union-Find is almost always the engine underneath.</p><h2>Union-Find is actually pretty simple</h2><p>The naive solution is straightforward: give every element a label. To merge two groups, sweep through the array and relabel everything in one group with the other&#8217;s identifier. Works perfectly. Linear work per merge. Nothing cheaper to write, and nothing more expensive to run at scale.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fzlO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fzlO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 424w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 848w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 1272w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!fzlO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png" width="1400" height="1262" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1262,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99139,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/199939822?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fzlO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 424w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 848w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 1272w, https://substackcdn.com/image/fetch/$s_!fzlO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe7ae342-73eb-479d-820e-660fb37449ae_1400x1262.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Union-Find: n elements, shifting partitions, two operations. Before: three groups. After </em><code>union(x, y)</code><em>: two. Made with <a href="https://apiad.github.io/tesserax">Tesserax</a>.</em></figcaption></figure></div><p>The first idea that earns its keep is a structural change. Stop labeling every element. Give each element a <em>parent pointer</em>: a single reference to another element. The root of each tree is the element that points to itself, and the root names the group.</p><p>To check if two elements share a group: walk each one up its parent pointers to a root. Same root, same group.</p><p>To merge two groups: find both roots and point one at the other. One pointer write, regardless of how many elements are involved.</p><p>The trade has moved. Merge is cheap. But your find now costs as much as the tree is tall.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!p6Jv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p6Jv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 424w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 848w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 1272w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!p6Jv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png" width="1400" height="852" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:852,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:80002,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/199939822?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!p6Jv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 424w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 848w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 1272w, https://substackcdn.com/image/fetch/$s_!p6Jv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50a1879b-30e0-4531-831d-ebc958cce481_1400x852.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Each group is a tree. Arrows point from children up to parents; the root points to itself. </em></figcaption></figure></div><p>Here is the failure mode. An adversarial union sequence can build a long thin chain: element one points to element two, which points to element three, all the way down. Every find walks the whole chain. Linear again.</p><p>The rest of the story is about keeping the trees short, like, really, really <em>short</em>.</p><h2>Making Union-Find fast, really fast</h2><p>The first fix is the obvious one once you&#8217;ve seen the failure. Track each tree&#8217;s height: its <em>rank</em>. When merging, always hang the shorter tree under the root of the taller. The merged tree is no taller than the taller of its inputs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SvQX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SvQX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 424w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 848w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 1272w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!SvQX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png" width="1400" height="753" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:753,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:88662,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/199939822?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SvQX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 424w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 848w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 1272w, https://substackcdn.com/image/fetch/$s_!SvQX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ef94b62-86ae-4170-9c95-66e086119dd1_1400x753.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Union by rank: the shorter tree hangs under the taller root. The merged tree&#8217;s height is unchanged.</em></figcaption></figure></div><p>The counting argument is clean. A tree of rank <em>r</em> must contain at least 2<sup>r</sup> elements. You can prove it by induction on the merge rule. So the rank, which bounds the height, can never exceed log&#8322;(<em>n</em>). Both operations now run in O(log <em>n</em>). A million elements: naive worst case is a million steps; rank-bounded is <em>twenty</em>. </p><p>One integer per node buys us exponential improvement in speed. Read that again.</p><p>Most algorithms textbooks would stop here. The second fix is what changes this from a really fast algorithm to the <em>fastest</em> algorithm ever, period.</p><p>Notice something about the walk-to-root operation. When it finishes, it has <em>discovered</em> the root. Every node it visited could be rewired, right now, on the way back, to point straight at the root.</p><p>The structure flattens itself, in place, every time you ask it a question. The next walk on that path takes one step. Work you already did is work you never have to do again.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RvHV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RvHV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 424w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 848w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 1272w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!RvHV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png" width="1400" height="1011" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1011,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86470,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/199939822?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RvHV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 424w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 848w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 1272w, https://substackcdn.com/image/fetch/$s_!RvHV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba971c19-0ebc-43f8-ba2a-6e5b2c1150a7_1400x1011.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Path compression: </em><code>find(x)</code><em> collapses the chain into a star. Every walked node now points straight at the root.</em></figcaption></figure></div><p>On its own, this trick is good. Combined with the rank rule, the cost drops below logarithmic, into a regime that barely has a name.</p><p>&#191;How fast is this? Strap on, baby.</p><h2>Union-Find is the fastest algorithm ever devised</h2><p>Robert Tarjan, in 1975, proved the two ideas together give an amortized cost per operation of the <em>inverse Ackermann function</em>, written &#945;(<em>n</em>).</p><p>To understand why that means <em>fast</em>, you need a picture of what you&#8217;re inverting. The Ackermann function grows like hell. Faster than exponentials, faster than towers of exponentials, faster than any function built by ordinary recursion. A(4, 4), only four steps in, already exceeds the number of atoms in the observable universe. It&#8217;s a freaking monster, one of the fastest growing functions ever invented.</p><p>The <em>inverse</em> grows correspondingly <em>sloooowww</em>. For any <em>n</em> less than 2 to the power of 65536 (a number whose decimal representation runs twenty thousand digits), &#945;(<em>n</em>) &#8804; 4. Read that again. For <em>any</em> input you could construct on any computer that will ever be built, the average cost per operation is at most <em>four pointer hops</em>.</p><p>Effectively constant. Call it constant, for practical purposes. But it is <em>not</em> constant. There is a real function in there. That fact is not a technicality. This is not a gap waiting for a smarter trick. It is a permanent lower bound, proved by Fredman and Saks in 1989.</p><p>Michael Fredman and Michael Saks worked in the cell-probe model, which counts how many stored bits any data structure must read to answer a query. They proved that &#937;(&#945;(<em>n</em>)) is the true lower bound for the union-find problem. No structure of any design can answer union and find queries doing less amortized work than &#945;(<em>n</em>).</p><p>Tarjan&#8217;s upper bound says path-compression-plus-union-by-rank does at most &#945;(<em>n</em>) amortized. Fredman and Saks&#8217;s lower bound says no structure can ever do less. The two bounds meet exactly.</p><p><em>Done. Problem solved. No more papers.</em></p><h2>Problem solved is a rare luxury</h2><p>Newton&#8217;s laws work until you approach the speed of light. Quantum mechanics works until you zoom out to cosmological scales. Turbulence in fluids is still open. It has been for a century. The three-body problem is still open. Physics doesn&#8217;t finish; it hands off. Every theory we have is a model that fits data within its domain of validity, and I think we forget how strange that is.</p><p>The Schr&#246;dinger equation describes how electrons move in any atom. For helium (two electrons) it has no closed-form solution. The electron interactions couple (each one affecting all the others) in ways that resist exact treatment. Chemistry approximates, and the gap is permanent, not a waiting-for-faster-computers problem.</p><p>Mathematics does close problems, and famously. Wiles closed Fermat&#8217;s Last Theorem in 1995, after the conjecture had sat open for 358 years. The four-color theorem closed in 1976 (with a computer&#8217;s help, which still rankles some). Poincar&#233; closed in 2003. But the closures cluster on problems mathematicians chose: beautiful, deep, and often distant from anything that runs in your hand.</p><p>Computer science almost never gets all three at once. A problem that is practical, used in real systems every day; crisply specified, fitting in two sentences; and provably closed, algorithm and lower bound matched exactly. Union-Find is in this corner. Comparison-based sorting being &#920;(<em>n</em> log <em>n</em>) is in it. Linear search on unsorted data being &#920;(<em>n</em>) is in it. The list is short.</p><p>Algorithm and lower bound, meeting exactly.</p><p>Done. Problem Solved. No more papers.</p><p>The fastest non-trivial algorithm ever devised takes twenty lines to write, runs in essentially constant time per operation, and is one of the very few things we have ever &#8212; in any field &#8212; entirely finished.</p><p>If that isn&#8217;t freaking mind blowing, I don&#8217;t what is. Until next time, stay curious.</p><div><hr></div><p><em>Union-Find is chapter 14 of <a href="https://apiad.gumroad.com/l/codex">The Algorithm Codex</a>, a book that spells out the algorithms every programmer eventually meets in real, functioning Python &#8212; no pseudocode bullshit, no complicated math.</em></p><p><em>Every chapter ends with the same three questions: is it correct? how efficient? is it optimal? Most chapters land two out of three. A small few land all three. Union-Find is the cleanest example in the book of all three boxes ticked.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://store.apiad.net/l/codex&quot;,&quot;text&quot;:&quot;Get the Algorithm Codex&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://store.apiad.net/l/codex"><span>Get the Algorithm Codex</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Introducing Aegis: the programable multi-agent meta-harness]]></title><description><![CDATA[Or -- I did a thing and I want to show it to you]]></description><link>https://blog.apiad.net/p/introducing-aegis-the-programable</link><guid isPermaLink="false">https://blog.apiad.net/p/introducing-aegis-the-programable</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Fri, 29 May 2026 11:05:05 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em><strong>On May 31 we launch <a href="https://store.apiad.net/l/ai">Mostly Harmless AI</a> v2.</strong> This arc &#8212; how models learn, how agents work, where they break, and what it takes to build something real on top of them &#8212; is now a book, updated for May 2026. Newsletter subscribers get 50% off.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://store.apiad.net/l/ai&quot;,&quot;text&quot;:&quot;Get Mostly Harmless AI - 50% off&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://store.apiad.net/l/ai"><span>Get Mostly Harmless AI - 50% off</span></a></p></blockquote><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="8192" height="5461" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:5461,&quot;width&quot;:8192,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;two silver swords on shield&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="two silver swords on shield" title="two silver swords on shield" srcset="https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1541543975512-86aad5d2cf93?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw1fHxzaGllbGR8ZW58MHx8fHwxNzc5OTM3MzY2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@worldsbetweenlines">Patrick Hendry</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>For the better part of the last two months I&#8217;ve been drilling you with a bunch of inter-connected ideas, all gravitating around the notion of agentic reliability. We started with <a href="https://blog.apiad.net/p/mhai-llms">how these models learn</a>, argued about <a href="https://blog.apiad.net/p/70-years-of-ai-history-in-10-minutes">seventy years of AI history</a>, traced <a href="https://blog.apiad.net/p/its-tokens-all-the-way-down">the strange logic of prediction</a>, dissected <a href="https://blog.apiad.net/p/the-anatomy-of-a-linguistic-ai-agent">what an agent actually is</a>, then spent two posts on the edges: <a href="https://blog.apiad.net/p/how-to-write-a-cli-an-agent-will">how to write tools agents can really use</a>, and <a href="https://blog.apiad.net/p/the-80-ai-reliability-horizon">what happens when you push them to their limits</a>.</p><p>This is the last post of that arc, and it embodies my vision of where this whole Agentic AI thing is going. And to show I&#8217;m putting my money where my mouth is, this post is about the tools I&#8217;m building to bring forth that vision.</p><p>But first, why should you care?</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>Agents aren&#8217;t just for coders</h2><p>Here&#8217;s something the AI tool industry quietly gets wrong: every major agent harness &#8212; <a href="https://claude.ai/code">Claude Code</a>, <a href="https://ai.google.dev/gemini-api/docs/gemini-cli">Gemini CLI</a>, <a href="https://cursor.com/">Cursor</a>, <a href="https://opencode.ai/">OpenCode</a> &#8212; is pitched as a developer tool. And technically, yes, they drive code.</p><p>But code is how agents <em>solve problems</em>, not what the problems are. When you ask an agent to research a topic, draft a document, reorganize a folder, schedule your week, or synthesize a dozen sources into one coherent brief &#8212; none of that is fundamentally a programming task. The coding is incidental. A historical accident of the fact that the people who built these tools happened to be programmers solving programmer problems.</p><p>So if you&#8217;re a researcher, a writer, a manager, a student, a scientist who occasionally touches a terminal &#8212; this piece is for you too.</p><h2>What&#8217;s wrong with existing tools</h2><p>I use Claude Code every day. The harnesses &#8212; Claude Code, Gemini CLI, Cursor, OpenCode &#8212; are genuinely good. The agentic loop is robust. Tool design is sharp. Context management is solved for practical purposes. I&#8217;ve opened ten-thousand-line projects and the agent knows exactly where it left off.</p><p>What&#8217;s missing is not single agent capabilities. It&#8217;s <em>coordination</em>.</p><p>Claude Code runs on Claude. Geminis CLI runs on Gemini. OpenCode allows any provider but you cannot use your existing subscriptions (which are heavily subsidized); you have to pay API rates.</p><p>If you&#8217;re running multiple agents with different models from different providers, you end up with four windows open: one per harness, one per model, one per subscription you&#8217;re already paying for. Different tools with slightly different annoying quirks and ways to do and call the same things.</p><p>But you can still make them collaborate, just not easily.</p><p>Most harnesses support sub-agents, which is a subroutine: the main agent mints a new subagent for a specific call, call it, it runs, it returns, the main agent continues.</p><p>What they don&#8217;t support is a mid-work handoff. Imagine you&#8217;re two hours into a brainstorm with Claude, a question has emerged that you cannot answer without trying some code, and now you want a second agent &#8212; different model, fresher context, perhaps Kimi from OpenCode Zen &#8212; to take over a brief coding session, and then handoff back to Claude what it found so brainstorming can continue.</p><p>That is somewhat achievable with subagents (except they run autonomously and die, you cannot, in most tools, interact with them and steer them into new directions). But what you cannot do, is have Kimi return to Claude, and stay up, waiting (with its full context alive) for a second follow up question to continue exploration.</p><p>That transfer doesn&#8217;t exist today. You have to ask Claude to produce a handoff manually, paste it in Kimi explaining the situation from scratch. The paste its response back to Claude, and so on. You have become a secretary between two AI agents.</p><p>Agents are isolated in all these tools. Two agents working on the same document will overwrite each other. There&#8217;s no locking, no merge protocol, no queue where one drops a task and another picks it up. No broadcast so all agents know when the plan changes. Agents don&#8217;t share a world &#8212; they each have their own private window into one.</p><p>I looked around before building. The closest I found: Conductor, which orchestrates multi-agent workflows &#8212; but only for Claude Code. And T3.codes, which drives any harness, closer to the spirit of what I wanted, but neither has cracked the coordination layer as I envision it.</p><h2>Introducing Aegis</h2><p>So, of course, I had to go and make my own. (Quick digression, if you&#8217;ve been reading this blog for a while you know I love to reinvent wheels if only for the learning experience, but this is a case where I genuinely couldn&#8217;t find something good enough.)</p><p>Here&#8217;s what makes Aegis different to anything out there, embodied in its slogan: the programable multi-agent meta-harness. Let&#8217;s build it from the back.</p><p><strong>Meta-harness</strong>. You&#8217;re already paying for Claude via your Anthropic subscription. Gemini via your Google account. If a new tool wants to drive both, it has two options. It can re-authenticate you through its own layer: API keys, rate limits, and lost subscription benefits. Or it can call the native tools, which already have your credentials.</p><p>Aegis takes the second path. It drives Claude Code over its <code>stream-json</code> protocol, Gemini CLI and OpenCode over the Agent Client Protocol &#8212; calling the binaries on your machine, which already have your auth. It doesn&#8217;t touch your subscriptions. You stop worrying about which model wins this month&#8217;s benchmark.</p><p>And because Aegis calls the native harnesses rather than reimplementing them, it inherits everything they&#8217;ve spent months polishing: the agentic loop, the context management, the permission model. The harness keeps owning tool use, sandboxing, model selection. Aegis owns the layer above &#8212; tabs, routing, delegation &#8212; the things a single-conversation CLI was never built to do.</p><p><strong>Multi-agent.</strong> Aegis provides six inter-agent syncronization primitives, built incrementally one above the others, to give you increasingly more powerful multi-agent capabilities.</p><p>The first primitive is a per-agent inbox. Any agent (including you) can hand another agent a message, that gets enqueued until the end of the current turn. This alone enables solving the problem we were just discussing in the previous section.</p><p>Then, on top of that, they got canvases: markdown files shared across agents with per section looks and callbacks that awake an agent when another finishes writing a section.</p><p>Then we got <em>real</em> terminals. Not an agent calling bash on a subprocess and blocking on its result. A real, shared, fully interactive terminal session that multiple agents can scan, tail, and write to, And you can too. One runs a command; another sees the output in real time and reacts. Or you run the backend and ask your agent to look at the logs when that heissenbug happens.</p><p>So far this allows you to spawn several agents and have them collaborate. But you can also have queues. Any agent can drop a task (a prompt) and the queue auto-spawns an ephemeral agent to take it, potentially calling back the emitter once done. Queues have a maximum cap on parallelism, as well as arbitrary rolling budgets on tokens and dollars so you keep control of how much work is allowed to happen without your supervision.</p><p>Agents can also be added into groups, dynamically created and destroyed on demand, by you or any other agent. Groups have a shared inbox and you can subscribe to them and get notified when the first, any, or all the agents in the group finish. This allows committee-like flows where different agents analyze a problem in parallel.</p><p><strong>Programmable.</strong> And finally, you get workflows. Deterministic Python code that drives agent calls in sequence, with branching, conditionals, and loops. Think skills, but instead a hopeful blob of markdown one agent can choose to interpret as they want, these are composable routines that drive the entire substrate in the exact level of control you desire.</p><p>When you write a complex workflow in natural language, you&#8217;re hoping the agent follows through. It might decide step two is better done differently, skip the commit because something caught its eye, or forget step three entirely. A Python workflow doesn&#8217;t forget: it runs step 1, then step 2, then step 3, and commits. You wrote <code>commit()</code> in the code; the code runs. You get the agent&#8217;s creativity at each step; the program guarantees the steps happen.</p><p>Workflows can be scheduled: declare a cron entry in <code>.aegis.yaml</code> and the substrate fires it while you sleep. They also run across machines: one agent on your laptop can enqueue a task to a remote Aegis instance on a VPS and get the result back in its inbox.</p><p><em>Quick aside</em>: Yesterday, as I was polishing this post, Anthropic announced <em>Dynamic Workflows</em> &#8212; a way to orchestrate long-lived agents over dozens of hours of work. I haven&#8217;t tested it yet, but It seems geared toward the same problem I&#8217;m trying to solve.</p><p>The difference is in the philosophy. Anthropic&#8217;s principle is to give agents as much agency as possible: trust the model, let it decide how to get there. Don&#8217;t get in the way, you stupid human. Tokens go brr. It&#8217;s the reason why all their solutions to problems are humoungos one-shot prompts.</p><p>My philosophy runs the other way. Leave agent creativity where it can do the most good &#8212; in the actual work, not in deciding whether step three happens after step four. The deterministic spine isn&#8217;t a constraint on the agent. Its what makes it work despite agent idiosincrasies, and why it works across all agents and harnesses, regardless of their intrinsic capabilities.</p><p>I&#8217;m using Aegis now for about 50-60% of my coding. It&#8217;s still rough around the edges, but it&#8217;s way more fun to use than any single CLI. There is a lot more in the box, like remote sessions, a built-in file browser, lots of metrics... but this post is already way too long. You&#8217;ll have to check it out on your own. Links at the end.</p><h2>What I intentionally left out</h2><p>Aegis has no native concept of skills. No AGENTS.md injected automatically. No memory system.</p><p>Those things are conventions, and conventions change. What belongs in an AGENTS.md today looks different from what it&#8217;ll look like in six months. Memory systems have a dozen competing designs and no consensus. If I&#8217;d baked any of that in, you&#8217;d be stuck with my choices the moment the community moved on.</p><p>Aegis has a very powerful plugin system instead (I told you, it is programable). You write a pure Python function, drop it on some folder, and it gets called anywhere in the agent lifecycle.</p><p>Want skills that activate on context? Write a plugin. Want a memory system? Write a plugin. Want to inject per-repository knowledge before every session? Write a plugin. The conventions you need are yours to build, and when they change, you change them, not me.</p><h2>Coda</h2><p>Claude Code and Gemini CLI are applications. You open them, use them, close them. Aegis is more than that. It&#8217;s a framework. You build on top of it &#8212; applications that spin up the agentic substrate automatically, pull in whichever harness fits the task, and run without anyone at the keyboard.</p><p>Picture a self-hosted Git forge where pushing a branch triggers agents: one reviews the code, one hunts bugs, one picks up open issues and starts implementing. Everyone on their own worktrees, independent, parallel, coding while you sleep. Push code; agents work.</p><p>That&#8217;s <strong>Sindri</strong>, and I&#8217;m also building it; but that&#8217;s a story for another Friday.</p><p>Aegis is open source: <a href="https://github.com/apiad/aegis">github.com/apiad/aegis</a>. <code>pip install aegis-harness</code> to start.</p><div><hr></div><p><em><strong>May closes the agentic AI arc.</strong> If you want the full conceptual foundation &#8212; model internals, agent architecture, tool design, failure modes &#8212; it&#8217;s in <strong><a href="https://store.apiad.net/l/ai">Mostly Harmless AI v2</a></strong>, launching May 31. Subscribers get 50% off.</em></p><p><em>Also check the <a href="https://store.apiad.net/l/compendium">Compendium</a> &#8212; one fixed price, every educational project I&#8217;ve built or will build, yours in perpetuity. Buy once, get everything.</em></p><p><em><strong>In June: algorithms.</strong> The other half of the computational story &#8212; sorting, searching, graphs, optimization, the classical toolkit that AI didn&#8217;t replace and won&#8217;t. All month, one idea at a time. See you there.</em></p><p><em>Until next time, stay curious.</em></p>]]></content:encoded></item><item><title><![CDATA[Why AI Agents Need Structure]]></title><description><![CDATA[A 5-step framework for solving any]]></description><link>https://blog.apiad.net/p/why-ai-agents-need-structure</link><guid isPermaLink="false">https://blog.apiad.net/p/why-ai-agents-need-structure</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Wed, 27 May 2026 10:43:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vQrT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vQrT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vQrT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vQrT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:735692,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/199356169?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vQrT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vQrT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b579226-2570-417d-82f1-3bab6546883b_1376x768.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Actual footage of an AI handing off a research spec to another AI&#8212;it&#8217;ll make sense in the end.</em> (Crazy the kinds things you can see in the wild, huh?)</figcaption></figure></div><blockquote><p><em>Every post this month is on the theme of building AI agents that actually work &#8212; anchored on the second edition of <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a>, 50% off during early access, where the five-phase structure this post describes is a full chapter with more failure cases, the artifact design patterns, and the context isolation mechanics in detail. You can also <a href="https://books.apiad.net/books/mhai/">read the whole book online for free</a>. More at the end.</em></p></blockquote><p>Last night you give your AI agent a clear task. It worked hard for two hours. YOu woke up to a report that is technically correct and completely useless.</p><p>I&#8217;ve had this experience enough times that I stopped blaming the model years ago. The failure is always structural, and I mean that in a very specific, diagnosable sense.</p><p>AI labs fine-tune language models to produce <em>output</em>, not to question the frame. The whole reward cycle behind how these models are trained pushes them toward helpful completions; nobody in that loop is rewarding a model for pausing mid-task and asking &#8220;have you considered that you might be framing this wrong?&#8221;</p><p><strong>Execution</strong> is the default. That setting works beautifully for atomic tasks: create a note, write a short email, implement a specific function, compare two clearly-defined options. It fails quietly, and expensively, when the real problem isn&#8217;t the task itself but the goal behind it. Here&#8217;s three examples:</p><p>First: you ask an agent to &#8220;implement user authentication&#8221; for your web application. Clear task. The agent gets to work, producing a technically sound implementation using JWT tokens, bcrypt password hashing, and session management.</p><p>Second: you ask an agent to &#8220;write a technical report on renewable energy storage.&#8221; Again, clear task. The agent downloads papers, synthesizes findings, produces a well-structured document.</p><p>Third: a family asks an agent to &#8220;plan our move to a new city.&#8221; Thorough research follows &#8212; neighborhoods, school districts, moving companies, cost estimates. In every case, the output is technically correct.</p><p>In every case, the person who asked ends up with something that doesn&#8217;t quite fit. The gap between what was asked and what was needed &#8212; that gap is what this article is about.</p><p><em>The structure is the problem, not the model.</em> </p><p>Not the model&#8217;s intelligence, not the quality of your prompt, not the temperature setting. The structure of the <em>workflow</em> you handed the agent is the thing that determines whether it answers the right question or a reasonable-sounding wrong one. In this article, I&#8217;ll show you what the right structure is. But to understand <em>why</em> it must be so, we need to see how we got there.</p><p>The first fix the industry reached for was planning. It helped. It wasn&#8217;t enough. Strap on for a story.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>Plan First, Build Second</h2><p>The <em>structure</em> is the problem, not the model. Right? So the first fix the industry reached for was adding a <strong>planning mode</strong>.</p><p>If you&#8217;ve used any of the major agentic coding tools over the past couple of years, you&#8217;ve seen this pattern. There&#8217;s a <em>plan mode</em>: read-only, no side effects, meant for thinking through the task. Then a <em>build mode</em>: where the agent executes.</p><p>The intuition behind it is sound &#8212; separating thinking from doing is genuinely better than collapsing them into a single continuous stream. When you mix planning and execution in the same pass, the agent makes irreversible changes based on its first interpretation of the goal.</p><p>A planning mode forces a pause. You get to inspect the plan, argue with it, revise it before anything is written to disk. The plan is the first concrete artifact in the handoff chain. You can hold it in your hands &#8212; or at least on your screen &#8212; and have a real conversation about it.</p><p>Genuine progress. I don&#8217;t want to minimize it. But watch what happens with the family move.</p><p>The agent produces a thorough plan: compare three target neighborhoods, compare school districts in each, contact five moving companies for quotes, estimate total relocation costs, build a timeline. Excellent plan. The family reviews it, nods along, says &#8220;looks good,&#8221; and the agent executes.</p><p>Six months later: they&#8217;re living in a city that made complete sense on paper &#8212; except the job that drove the whole move turned out to be fully remote. They never needed to relocate at all, or they could have moved somewhere half the price with better schools.</p><p>The plan was correct. The execution was correct. The <em>goal</em> was wrong. And here&#8217;s the point: the agent that built the plan never questioned whether &#8220;plan a move to City X&#8221; was the right goal, because it was never asked to. It took the first reasonable interpretation of the prompt &#8212; &#8220;they want to move, let me help them move well&#8221; &#8212; and planned confidently inside that frame.</p><p>The software auth example is shorter but sharper. &#8220;Implement user authentication&#8221;: the agent plans for JWT tokens, bcrypt hashing, session storage. Fine plan. Sound, even. For a single-tenant web app. This is a multi-tenant SaaS product (one where dozens of customers share the same running application.)</p><p>The plan was never going to catch that, because nobody told the planning agent that multi-tenancy was a constraint. It did exactly what it was asked.</p><p>Now, the objection you might be forming: &#8220;just review the damn plan carefully before you approve it.&#8221; That objection is right about something, review does matter, bit it misses the fact that the agent that produced the plan has already <em>committed</em> to a frame.</p><p>Every question it asks, every tradeoff it surfaces, every option it presents &#8212; all of it is already shaped by its first interpretation of the goal. When you review that plan, you&#8217;re not reviewing a neutral set of options. You&#8217;re reviewing a plan that already selected its own success criteria. The frame is invisible because it was never made explicit.</p><p>Planning without exploration locks in the first reasonable goal.</p><h2>Research Before the Plan</h2><p>Once more, the <em>structure</em> is the problem, not the model. The next obvious fix is to put a <strong>research</strong> phase in front of planning.</p><p>By the time we hit mid-2025, a third mode was appearing in serious agentic setups. A <em>research phase</em>: read-only, job is to understand the problem space, not to produce a solution. The artifact it creates is a description, not a prescription &#8212; a document that maps what is known before anyone decides what to do about it.</p><p>The intuition is right again: if the planning agent doesn&#8217;t know what it doesn&#8217;t know, it can&#8217;t plan well. Research is how it finds out. For the renewable energy report, thorough research might surface the fact that the intended audience is policymakers, not engineers &#8212; which changes the vocabulary, the technical depth, and the document&#8217;s opening frame.</p><p>Real progress, again. And again, not enough.</p><p>Watch the renewable energy report closely. The agent runs a solid research phase: downloads twenty recent papers, reads industry reports, synthesizes the state of the art on battery storage, hydrogen carriers, pumped hydro, and grid-scale thermal systems. Then it transitions into planning mode &#8212; in the same context window. And here is the problem: <em>the same context window</em>.</p><p>The planning agent isn&#8217;t a fresh mind looking at a research report. It&#8217;s the same agent, carrying everything it concluded during research, now deciding how to structure the work. If the research agent concluded &#8220;battery storage is the central challenge in the energy transition,&#8221; the planning agent will structure the report around battery storage. Not because it made a new decision &#8212; because it never had the opportunity to question the prior decision. It just kept going. The research was excellent. The plan followed naturally from the research. The report answered the question the research agent found most interesting. Not necessarily the question the reader needed answered.</p><p>The steelman still stands, and research sharpens it. The steelman says: &#8220;planning alone is enough, just review the plan.&#8221; Research proves it isn&#8217;t &#8212; but not because planning is the wrong approach. Research proves it because research without context isolation just moves the lock-in one step earlier. The same agent that researched now plans. It cannot escape its own prior conclusions, not because it&#8217;s incapable of abstract reasoning, but because those conclusions are literally sitting in its context, shaping every next token it generates.</p><p>A fresh planning agent, handed only the research artifact as a clean document, can genuinely question whether the research answered the right question. It can push back. It can say &#8220;your research focused heavily on battery chemistry, but I&#8217;m not sure that&#8217;s what the audience needs.&#8221; The same agent that did the research cannot do that. Not really.</p><p>But here is the real kicker: research gives you facts; it doesn&#8217;t tell you if you&#8217;re solving the right problem. Save that thought for a minute, we need one more step.</p><h2>Review After the Plan</h2><p>The <em>structure</em> is still the problem, not the model. And to fix it, we added a new, and in hindsight, pretty obvious step. A <strong>review</strong> phase after implementation.</p><p>This is a dedicated pass where a separate agent &#8212; or the same agent in a separate context &#8212; evaluates the produced artifact against known criteria. Not just &#8220;does this code run,&#8221; but &#8220;does this code do what we intended.&#8221; The distinction from implementation is real and it matters. An implementation agent is building; a review agent is hunting for the thing that will break it.</p><p>What review actually solves is real. The software auth implementation, evaluated by a review agent, surfaces real questions: Is the JWT expiry window set appropriately for the threat model? Is the bcrypt cost factor tuned for this hardware? Are session tokens actually invalidated on logout, not just expired? These are genuine bugs a fresh pass can find. I&#8217;ve seen review agents catch the kind of subtle mistake that a second human reader catches &#8212; not because they&#8217;re smarter, but because they&#8217;re looking for problems rather than building a solution.</p><p>But watch what happens when the review agent evaluates the software auth implementation against the plan.</p><p>The plan said: &#8220;implement JWT-based authentication for the web application.&#8221; The review agent confirms: JWT is implemented correctly. Bcrypt is used. Session management is in place. The implementation passes review. It ships.</p><p>First enterprise customer tries to log in: there is no tenant isolation. Every user in the system shares a single authentication namespace. The review agent found no bugs. The implementation had no bugs. The plan specified the wrong thing. And the review agent couldn&#8217;t catch it &#8212; not because it was careless, but because review only checks &#8220;did we implement the plan correctly.&#8221; Not &#8220;was the plan the right plan.&#8221; Those are different jobs. And you can&#8217;t review your way out of the wrong spec.</p><p>This isn&#8217;t a failure of intelligence. It&#8217;s a consequence of what the review agent is handed. It receives the implementation and the plan. It has no clean access to the original problem statement &#8212; what the user actually needed, what constraints were implicit in the product, whether the product served one customer or a hundred who shared a namespace. It&#8217;s evaluating the gap between the artifact and the plan, not between the artifact and the goal.</p><p>Human code reviewers fail the same way, by the way. Code review finds style violations, off-by-one errors, missing null checks. It rarely questions the architecture decision that was made three sprints ago and embedded in every layer of the codebase. That kind of question requires a different context &#8212; a different meeting, a design review, a fresh set of eyes on the spec rather than the implementation.</p><p>Review catches errors, but only inside the frame you committed to in the planning phase. You can&#8217;t review your way out of the wrong spec.</p><h2>Name the Problem Before Solving It</h2><p>Finally, the structure IS the <em>problem</em>, not the model.</p><p>Research doesn&#8217;t ask it. Planning doesn&#8217;t ask it. Implementation doesn&#8217;t ask it. Review doesn&#8217;t ask it. The question is: <em>what problem are we solving, and how will we know when we&#8217;ve solved it?</em></p><p>That&#8217;s not a rhetorical question. It has specific, concrete answers. And those answers should be a document &#8212; a specification &#8212; produced before the plan is written. Not during planning. Not as a side effect of research. Its own phase. Its own artifact.</p><p>A specification answers four questions, and it only takes one page to do it. What is the exact output we are trying to produce? What are the hard constraints it must satisfy? What does success look like &#8212; specifically, what would we check to confirm it? What does failure look like &#8212; what would make us say this didn&#8217;t work?</p><p>These sound obvious. They are almost never answered before a planning phase begins. In my experience, the reason is that they feel like they slow you down. They don&#8217;t. They prevent six months of work in the wrong direction. Think of it as a single page you could tape to the wall &#8212; the kind you&#8217;d point at during a disagreement about whether the output succeeded or failed.</p><p>Go back to the family move. After a thorough research phase &#8212; neighborhood data, school ratings, crime statistics, cost of living comparisons &#8212; a specification phase asks: what does a successful move look like for your family? The family sits with that question. It turns out they&#8217;ve never explicitly answered it.</p><p>The husband&#8217;s answer: proximity to his aging parents, who live in a specific region of the country. The wife&#8217;s answer: their daughter getting into a specific school district that has strong arts programs. The budget answer: keeping total housing costs under a threshold that lets them maintain their current savings rate. Three explicit success criteria.</p><p>The research phase found no conflicts because it was never told what it was optimizing for. The specification phase surfaces all three criteria before the plan commits to a single city. The planning phase can now do something useful: find cities that satisfy all three criteria &#8212; or, just as importantly, discover that no city satisfies all three and surface that conflict before anyone books a moving truck.</p><p>The software auth case is fast. Specification asks: what does correctly-implemented authentication look like for this product, given who its customers are? The answer: it must support multi-tenant isolation with strict data separation, SSO for enterprise customers, and a free tier with email-only login.</p><p>Now the plan can be written for the actual product. The research phase&#8217;s work on JWT and OAuth is still valid; it just needs to be read through the lens of multi-tenancy, which the specification made explicit.</p><p>The full chain, with its five concrete artifacts, looks like this.</p><p><strong>Research</strong> produces a collection of source materials plus a descriptive state-of-the-art report &#8212; what is known about this problem space. <strong>Specification</strong> produces a success-and-failure criteria document. <strong>Planning</strong> produces a concrete step-by-step plan &#8212; how to get from here to there, given the spec. <strong>Implementation</strong> produces an evaluable artifact &#8212; code, document, report, recommendation. <strong>Review</strong> produces an evaluation report checked against the specification, not just the plan &#8212; a real answer to &#8220;did we solve the problem?&#8221;</p><p>Five documents. Five <em>handoffs</em>. Five chances to catch the wrong frame before it becomes expensive.</p><h2>We Knew it all Along</h2><p>The fun part is all of this was known, at least in principle, since before 1987.</p><p>IDEO, a by-now ultra famous design consultancy, articulated a five-phase creative process that Stanford&#8217;s d.school later codified as <strong>Design Thinking</strong>.</p><p>Tim Brown formalized the diverge-converge logic in <em>Change by Design</em>. The five phases: <strong>Empathize</strong>, <strong>Define</strong>, <strong>Ideate</strong>, <strong>Prototype</strong>, <strong>Test</strong>. If those names sound familiar given what you&#8217;ve just read, that&#8217;s not a coincidence.</p><p>Empathize is research: go wide, gather context, talk to users, understand the problem space from the outside rather than the inside. Define is specification: converge on an explicit problem statement with clear success criteria, the &#8220;how might we&#8221; question that frames everything downstream. Ideate is planning: diverge again, generate candidate solutions, explore the space of possible approaches. Prototype is implementation: produce an evaluable artifact, something you can put in someone&#8217;s hands. Test is review: evaluate the prototype against the problem statement, not just against the prototype&#8217;s internal logic.</p><p>Every phase the agentic world has been bolting on since 2023 was already named, sequenced, and justified in a framework that predates the modern web by a decade and a half. It&#8217;s a framework every Silicon Valley startup, incubater, accelerator, and VS knows in and out. It is taught in bussiness majors all over the world. It&#8217;s literally the structure of most pitch decks. But it is still missing in most agentic protocols we use every single day.</p><p>The piece most clearly missing is the Define phase &#8212; the second one, which IDEO put second for a reason: without a clear problem statement, everything downstream answers the wrong question. It&#8217;s a very old insight the field keeps rediscovering from scratch &#8212; Agile&#8217;s Definition of Done, test-driven development&#8217;s failing-test-first, specification by example. Each was the same insight under a different name.</p><p>Now, here is the strongest version of your initial objection. &#8220;You don&#8217;t need all this structure. A great prompt specifies the audience, the format, the success criteria, the constraints. Write a better prompt and you get all five phases in one go.&#8221;</p><p>Let&#8217;s take this seriously, because it&#8217;s not wrong about prompt quality. A genuinely well-crafted prompt that specifies who the output is for, what format it should take, what it must accomplish, and what would make it fail &#8212; that prompt is effectively a specification. You&#8217;re right that prompt quality matters.</p><p>But a single prompt containing research context, goal specification, a plan, and execution instructions is not a cleaner version of the five-phase process. It&#8217;s five phases collapsed into one context window, with no mechanism for each phase to question the prior one&#8217;s conclusions.</p><p>When research and planning share a context, planning can&#8217;t interrogate research. When planning and implementation share a context, implementation can&#8217;t push back on the plan. When specification and review share a context, review is already biased toward confirming the specification it helped write. Prompt quality is about what you ask.</p><p>Phase independence is about who processes each answer, and whether they can genuinely disagree with the prior step. You can write the world&#8217;s best prompt and still hand it to an agent that will execute it inside the same narrowing tunnel, compounding the same assumptions with every step.</p><p>Picture someone reading their own manuscript for the fifth time &#8212; they no longer see what&#8217;s there, only what they meant to write. It is one of the most replicated findings in human psychology: once we form a belief, we interpret subsequent evidence through the lens of that belief. We notice confirming evidence, discount disconfirming evidence, and generate hypotheses that assume the belief is correct.</p><p>This is not a weakness of intelligence. Every mind &#8212; human or artificial &#8212; interprets through the conclusions it has already drawn. The agent that researched your problem already believes things about it. When it transitions to planning, it plans in service of those beliefs. The agent that planned has a solution in mind. When it implements, it makes countless micro-decisions that serve that solution. The agent that implemented defended choices as it worked. When it reviews, it reads its own output charitably.</p><p>Context isolation breaks this chain. A fresh context hasn&#8217;t seen the prior steps. It cannot be fooled by conclusions it never drew. It reads the artifact cold, which is the only way to genuinely evaluate it.</p><p>Design Thinking&#8217;s diverge-converge logic is not about <em>what</em> each phase does. It&#8217;s about <em>who</em> does it, and whether they can arrive at it without inheriting the prior phase&#8217;s commitments.</p><h2>Start Doing This Yourself Today</h2><p>The artisanal version of this is simpler than it sounds:</p><ul><li><p><strong>Treat each phase as a distinct conversation</strong>. Start a fresh session for each one.</p></li><li><p><strong>Hand it only the artifact from the prior phase</strong> &#8212; not the prior conversation, not your running context, not a summary of what you&#8217;ve been thinking about. The artifact alone.</p></li><li><p><strong>And tell the agent explicitly what mode it&#8217;s in</strong>. &#8220;You are in Research mode. Do not propose a plan. Do not suggest solutions. Your only job is to describe the problem space and produce a research report.&#8221;</p></li></ul><p>That instruction matters. Not because the model needs to be controlled, but because explicit mode assignment prevents the agent from sliding into execution behavior when it senses a gap to fill. Models are trained to be helpful; helpfulness steers every gap toward a solution. Naming the mode is how you resist it.</p><p>The discipline lives in you, not the tool. This works with any agent, any interface. Fresh context, explicit mode, artifact handoff. That&#8217;s the whole recipe.</p><p>If you want to go further, you can make phases structurally enforced rather than just instructed &#8212; agents that literally cannot execute, subagents that receive only the artifact, automated handoffs with no shared context. Programmable harnesses give you this level of control with permission levels per skill. (If you don&#8217;t have one, call me, <a href="https://apiad.github.io/aegis">I&#8217;ll lend you one for free</a>.)</p><p>One small step you can take today: add a specification phase to whatever workflow you already use. Before your planning phase writes a plan, ask for a success-criteria document first. One page. Explicit pass/fail conditions. What would make this output a success? What would make you throw it out? Review that document before planning begins.</p><p>This single addition &#8212; inserting a define phase between research and planning &#8212; catches more failures than adding a review phase after the fact. Because it catches them before the plan commits to the wrong goal.</p><p>What not to do: don&#8217;t implement all five phases as a mechanical checklist. And don&#8217;t add phases as ornamentation &#8212; a research phase that shares a context with planning adds conversation turns, not structure. More words in the same window is not more phases. The phases are context boundaries, not steps in a recipe. A phase that doesn&#8217;t produce a concrete artifact and doesn&#8217;t hand it to a fresh context adds nothing.</p><p>One document per phase. Fresh context per phase. That&#8217;s it.</p><h2>Structure Before You Re-Prompt</h2><p>Picture the artifact chain as a physical thing. A manila folder passed from one desk to the next. The research desk produces a report, closes it, slides it across. The specification desk opens only that folder, reads it, produces a criteria document, closes it, slides it across. The planning desk never opens the research folder &#8212; it opens only the criteria document.</p><p>And so on down the line. Each desk sees exactly one prior document. Each desk produces exactly one new document. The chain is what makes independence possible. You cannot hand off a vague intention. You cannot slide a feeling across a desk. Only a document.</p><p>Context isolation is the move most pipelines skip, and it&#8217;s the one that does the most work. Every phase that shares a context with a prior phase inherits its commitments. Not because the model is lazy or wrong &#8212; because that&#8217;s how cognition works, human or otherwise. We interpret through the lens of what we already concluded.</p><p>Context isolation is cheap: start a new session, pass only the artifact. The cognitive science is unambiguous: breaking the confirmation-bias chain requires a structural break, not a better instruction. Context isolation gets skipped because it looks optional. It isn&#8217;t.</p><p>Remember, the structure is the problem, not the model. Restructure before you re-prompt.</p><p>Until next time, stay curious.</p><div><hr></div><p><em>This is the core argument of the agentic workflows chapter in the second edition of <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a> &#8212; the full chapter walks the failure cascade with more cases, the artifact design problem (a research report can be thorough and still hand the wrong thing forward), and the context isolation mechanics in depth.</em></p><p><em>This specific article is new<strong> </strong>content, still not in the book, but it will land there shortly. The book is 50% off while it&#8217;s in early access, and also <a href="https://books.apiad.net/books/mhai/">free to read online</a> in a custom reader I built: dark mode, font controls, progress tracking, offline support, the works. </em></p><p><em>If you want the architecture behind these systems &#8212; how they fail, what the harness around them should look like, and what to actually do about it &#8212; that is what the book is for.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://store.apiad.net/l/ai&quot;,&quot;text&quot;:&quot;Get Mostly Harmless AI - 50% off&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://store.apiad.net/l/ai"><span>Get Mostly Harmless AI - 50% off</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[How to write a CLI an agent will actually use]]></title><description><![CDATA[Making tools that are safe by design, not convention.]]></description><link>https://blog.apiad.net/p/how-to-write-a-cli-an-agent-will</link><guid isPermaLink="false">https://blog.apiad.net/p/how-to-write-a-cli-an-agent-will</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Thu, 07 May 2026 11:00:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QnD8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QnD8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QnD8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QnD8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1550097,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/196457123?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QnD8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!QnD8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95862d5c-0883-4336-8d66-7f456f965db5_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p><em>Monday I argued the next move in agent autonomy is the layered stack around the model. This is the technical companion: what the tools inside that stack should look like. Both pieces are part of May&#8217;s arc on agent reliability, anchored on the second edition of <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a>, which you can also <a href="https://books.apiad.net/books/mhai/">read online for free</a>. More at the end.</em></p></blockquote><p>The first time you fire up a coding agent and walk away for an afternoon, you come back to a workspace that&#8217;s been touched in twenty places by something that doesn&#8217;t know what it doesn&#8217;t know. Most of the touches are fine. One of them, on the third such afternoon, will not be.</p><p>The reflex is to make the agent smarter. The fix is to make the <strong>tools it calls</strong> safer, by design, not by vigilance.</p><p>I was reminded of this an hour ago, building a small Python CLI to append entries to my Workspace journal. Dumb tool, three flags, ninety lines. I built it on top of <code>microcli</code>, a framework I&#8217;ve been using for about a year. The experience of writing it is what this post is about.</p><h2>What&#8217;s wrong with a normal CLI</h2><p>Sit with the unix-style ergonomics for a second. A normal CLI, written in argparse or click or typer, has a shape that&#8217;s been the default for fifty years.</p><p>It&#8217;s silent on success. You ran the command, you got back a shell prompt, you hope it worked. There&#8217;s no acknowledgement of what changed, no record of what the next step might be. If you&#8217;re a human at a terminal, you can run a verification command. If you&#8217;re an agent on a 30-call chain, you have to <em>infer</em> whether the action took.</p><p>It bitchslaps you on failure. A traceback, a single non-zero exit code, maybe an inscrutable error string from a subprocess three layers down. No suggestion of what to try instead. No indication of what already changed before the failure landed. If the command was halfway through writing a file when it died, the agent has no idea whether to retry, roll back, or move on.</p><p>It has no self-description. The only way to understand what the tool does is to read the source, or run it and observe the side effects, or both. There&#8217;s <code>--help</code>, but <code>--help</code> lists flags, not workflows. The agent doesn&#8217;t learn how to <em>use</em> the tool from <code>--help</code>; it learns the shape of the parser.</p><p>And worst of all for an autonomous loop: <strong>side effects are the default</strong>. You run <code>git push</code> and it pushes. You run <code>rm</code> and it removes. The dry-run is an opt-in flag at best, missing entirely at worst. Every call is a potentially load-bearing call, and the agent has to know in advance which ones will hurt.</p><p>This shape is fine for a human at a terminal who can hit Ctrl-C and read the docs. It&#8217;s a disaster recipe for an autonomous loop.</p><h2>What we actually need</h2><p>Invert all four:</p><ul><li><p><strong>Dry-run by default</strong>, real action opt-in. The model sees a preview <em>first</em>; the explicit <code>--save</code> flag is the consent gate. No call mutates state without the model having seen what would change.</p></li><li><p><strong>Predictable structured output for both happy and failed paths.</strong> Success says exactly what changed and where. Failure says exactly what failed, what state was already touched, and what to try next. No tracebacks, no silent zeros.</p></li><li><p><strong>Self-describing apps.</strong> The CLI carries its own learnable surface, not just a flag list. The workflows, the failure modes, the next-step commands all live inside the tool. The agent should be able to ask the tool <em>how am I supposed to use you</em> and get a real answer.</p></li><li><p><strong>Agents writing these apps themselves.</strong> This is the load-bearing one. You can do all of the above in plain typer or click. But the agent has to <em>remember</em> to do them. We need a framework where the safe shape is the path of least resistance, where it&#8217;s structurally hard to write a tool that <em>doesn&#8217;t</em> dry-run or <em>doesn&#8217;t</em> describe itself.</p></li></ul><h2>What this looks like with microcli</h2><p><code>microcli</code> is six hundred lines of Python and three principles, all enforced by the framework&#8217;s API surface rather than by convention. <strong>Validate before acting.</strong> <strong>Return descriptive messages.</strong> <strong>Default to dry-run.</strong> Each one is a behavior the API makes the path of least resistance, not a guideline you have to remember.</p><p>Rather than describe what writing one of these tools feels like, I&#8217;ll let the agent that wrote one tell you. Earlier today I asked Claude (running in this workspace, with file access and a shell) to write itself a small CLI for appending entries to my Workspace journal. Claude had never used <code>microcli</code> before. I told it to run <code>microcli learn</code> and roll with it. That was the last thing I had to say.</p><p>What follows is Claude&#8217;s account of the build, in its own voice.</p><div><hr></div><p><strong>Claude:</strong></p><p>I&#8217;d never seen <code>microcli</code> before. <code>which microcli</code> came back empty; the repo was at <code>~/Workspace/repos/microcli/</code> but not installed. A few minutes of yak-shaving (<code>gh repo clone</code>, <code>uv tool install -e .</code>, two transitive deps I had to fetch the same way) and I had a <code>microcli</code> binary.</p><p><code>microcli --tour</code> gave me a one-screen map. <code>microcli learn</code> listed the topics. Three commands later &#8212; <code>learn --topic principles</code>, <code>learn --topic ok-fail</code>, <code>learn --topic patterns</code> &#8212; I had everything I needed. I never read the framework&#8217;s source. The framework taught me how to use it through its own CLI, which is exactly the property the framework expects me to give my own tools.</p><p>The build started with <code>microcli new journal "Journal entry tool" add</code>. The scaffold isn&#8217;t a stub; it&#8217;s a teaching artefact. Inline comments pointed me at the next learning topics, the imports were already wired, the <code>@command</code> decorator was demonstrated, and a TODO gestured at the pattern I was supposed to follow.</p><p>Writing the body felt structurally guided. Validate first because <code>fail()</code> is the obvious primitive for rejecting bad inputs. Dry-run branch second because <code>save: bool = False</code> is the obvious parameter to add and <code>info()</code> is the obvious primitive for printing what <em>would</em> happen. Mutation branch third because there&#8217;s nowhere else for it to go. I&#8217;d have to actively work to write a tool that mutated by default or failed silently or didn&#8217;t describe itself.</p><p>The script lives at <code>.playground/microcli-journal/journal.py</code>. Ninety-odd lines. The load-bearing<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> function:</p><pre><code><code>@command
def add(
    tag: Annotated[str, "Entry tag (commit / decision / blocker / ...)"],
    description: Annotated[str, "One-line entry body"],
    save: Annotated[bool, "Actually write the entry"] = False,
):
    """Append a single journal entry."""
    if tag not in ALLOWED_TAGS:
        fail(f"unknown tag '{tag}'. Allowed: {', '.join(sorted(ALLOWED_TAGS))}.")

    path = journal_path_for_today()
    entry = build_entry(tag, description)

    if not save:
        info(f"Would append to: {path}")
        info(f"  Line: {entry.rstrip()}")
        info(f"  Equivalent: journal.py add {tag!r} {description!r} --save")
        return

    write(path, read(path) + entry)
    ok(f"Appended entry to {path}.")
</code></code></pre><p>That&#8217;s the whole shape. The decorator handles argparse. <code>Annotated[..., "help text"]</code> becomes <code>--help</code> for free. <code>fail()</code> writes a red &#10007; and exits 1. <code>info()</code> prints a cyan line. <code>ok()</code> prints a green &#10003;. The dry-run runs first and renders the exact <code>--save</code> command the next caller should run, with the original arguments threaded back through.</p><p>Smoke-test took five minutes. Dry-run printed the preview plus the explicit <code>--save</code> equivalent. Bad tag produced a red message naming the allowed list. Bad date format produced a red message naming the expected shape. The real <code>--save</code> wrote the file and confirmed with a green &#10003; pointing at the path. Nothing to debug because there was nothing to infer.</p><p>The last thing I ran was <code>journal.py --learn add</code>. The framework AST-walked the source I&#8217;d just written and produced a tour of my command: description, next-step commands harvested from the <code>if not save:</code> branch, failure modes harvested from the <code>fail()</code> strings. The next agent that touches this tool will not need to read it either.</p><p>Back to Alex.</p><div><hr></div><p><strong>Alex again.</strong></p><p><em>That was&#8230; weird. But it was indeed Claude. I really gave it the task, and then asked it to explain what happened. To the extend that you can trust an AI Agent introspection (which is, to be honest, not much), this is a real account of what it &#8220;feels like&#8221; for an AI to use microcli.</em></p><p>What I want you to take from that is the <em>structure</em> of the experience, not the specific tool. Claude built a real, working CLI in a workspace it was already familiar with, in a framework it had never used, by following the framework&#8217;s own self-description. The unsafe shape was inaccessible.</p><p>That&#8217;s what I mean by structurally hard to do the wrong thing. You can do all of this in plain typer or click; you&#8217;d just have to remember to.</p><h2>Try it</h2><p><code>microcli</code> is at <a href="https://github.com/apiad/microcli">github.com/apiad/microcli</a>. <code>pip install microcli-toolkit</code>. The <code>microcli learn principles</code> tour runs in five seconds. If you build CLIs that an agent will eventually call &#8212; your agent, mine, or one that runs autonomously on a clock &#8212; this is the shape worth reaching for.</p><p>Have fun. And until next time, <strong>stay curious</strong>.</p><div><hr></div><p><em>If the larger argument resonates, <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a> goes deep on this material. Full chapters on the harness around the model, the agentic stack we walked through Monday, and the discipline of building tools that compound rather than accumulate risk. You can also <a href="https://books.apiad.net/books/mhai/">read the whole book online for free</a> in a custom reader I built that I&#8217;m rather proud of: dark mode, font controls, progress tracking, offline support, the works.</em></p><p><em>If you want the whole catalog of everything I&#8217;ve written, plus everything I&#8217;m going to write, that&#8217;s <a href="https://apiad.gumroad.com/l/compendium">the Compendium</a>. One purchase, in perpetuity.</em></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>This is how you know this was indeed Claude (Opus 4.7). I would <em>never</em> use &#8220;load-bearing&#8221;, even if my life depended of it, but Claude <em>loves </em>it. </p></div></div>]]></content:encoded></item><item><title><![CDATA[The Anatomy of a Linguistic AI Agent]]></title><description><![CDATA[From single-turn LLM to long-horizon autonomous AI.]]></description><link>https://blog.apiad.net/p/the-anatomy-of-a-linguistic-ai-agent</link><guid isPermaLink="false">https://blog.apiad.net/p/the-anatomy-of-a-linguistic-ai-agent</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 04 May 2026 17:17:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!t-3D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!t-3D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!t-3D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!t-3D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2080600,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/196445058?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!t-3D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!t-3D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2507af90-0128-4576-8448-2f46a50ea2c8_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>After Rembrandt&#8217;s <a href="https://en.wikipedia.org/wiki/The_Anatomy_Lesson_of_Dr._Nicolaes_Tulp">&#8220;The Anatomy Lesson of Dr. Nicolaes Tulp&#8221;</a> (1632); generated with Nano Banana 2.</em></figcaption></figure></div><p>You have used a language model in a chat box. You typed a question, you got an answer, you closed the tab. The whole interaction lasted under a minute. The model did not remember you the next time you opened the page.</p><p>You have also seen, or read about, agents that work for hours. A coding agent that ships a feature overnight. A research agent that pulls together a hundred sources before breakfast. They plan, they call tools, they back out of dead ends, they hand you something you can use.</p><p>Both are the same model. Same neural network. Same forward pass. The only thing that changed is what&#8217;s wrapped around it.</p><p>This essay is the bridge. The architecture that turns the first thing into the second is not a single insight. It is a stack, a small number of layers, each one added in response to a failure mode of the previous layer. By the end you should be able to point at any agent doing real work in 2026 &#8212; coding, research, customer ops &#8212; and name which layer is doing the heavy lifting at any given moment.</p><p>Some of those layers are old. The fundamental one was published in 2022, before ChatGPT shipped. Some are very new. One was named eighteen months ago and is still settling. None of them, individually, is hard to follow. The trick is seeing them as a sequence, each fix opening the door for the next.</p><p>If you want a number to anchor where we start: METR has been measuring the time-horizon of frontier agents, and a language model on its own, with no scaffolding around it, sustains roughly a few minutes of human-equivalent work at 50% reliability. The equivalent of writing a competent meeting summary.</p><p>That is the floor.</p><blockquote><p><em>Every post on the blog this month is on the theme of agent reliability, anchored on the second edition of <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a>, where the engineering details that don&#8217;t fit a blog post live. You can also <a href="https://books.apiad.net/books/mhai/">read the whole book online for free</a> in a custom reader I built. More at the end.</em></p></blockquote><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>The base case</h2><p>Strip everything away first. No agent, no tools, no skills, no harness. Just the model.</p><p>A language model, in the strictly minimal sense, is a function from a string to a string. You hand it a sequence of tokens. It hands you back a sequence of tokens. One forward pass through the network. The input goes in at one end, the output comes out the other, one token at a time until the STOP token is generated, and that is the entire interaction. No state is held between calls. The next time you ask the same model the same question, it has no idea you have ever spoken before.</p><p>Inside that one shot, the model delivers. It will answer, draft, summarize, translate, brainstorm. Give it a good piece of context and a clear ask, and the response that comes back will, in my experience, often be useful enough to ship as-is. This is the experience that made everyone notice in late 2022. Open a chat, ask anything, get something back you can use. People called it magic at the time. Most of them still do, even though <em>useful function with no memory</em> is the more honest description.</p><p>But notice what it cannot do &#8212; which is most things you would ever want from an agent.</p><p>It cannot verify its own output. The same forward pass that produced the answer is the only one available to check it. There is no second opinion, no quick lookup, no <em>let me try it and see what happens</em>. The model is committed to whatever came out the first time.</p><p>It cannot look anything up. Whatever facts it has are baked into the weights from training, frozen at some cutoff date. If you ask about today&#8217;s news, or your codebase, or an internal company document, the model has nothing. And worse, it will frequently invent something plausible-sounding because completing a confident sentence is what it was trained to do.</p><p>It cannot act on the world. It cannot write to a file, send an email, call an API, run a command. It cannot do anything that has a side effect outside the chat window. The only thing it can produce is more text.</p><p>Inside the four walls of the context window, the base model is the most capable text engine the field has ever built. A single chat box was enough to launch the largest consumer product of the decade. Outside those walls, it is inert.</p><p>METR&#8217;s measurements of an unaugmented model &#8212; no tools, no loop, no scaffolding &#8212; put the time horizon at something on the order of minutes of human-equivalent work. Minutes. That is the starting capability. Everything else in this essay is a way of making those minutes compound.</p><h2>The first leap</h2><p>The first real agent paradigm is older than ChatGPT.</p><p>In October 2022, a team at Princeton and Google published <em>ReAct: Synergizing Reasoning and Acting in Language Models</em>. It went out about six weeks before the ChatGPT launch that made the public notice agents existed at all. Every working agent today &#8212; Claude Code, Codex, Gemini CLI, the dozens of research agents and customer-ops agents shipping this year &#8212; is some refinement of the loop that paper introduced.</p><p>Here is the setup. An agent operates in some environment: a Wikipedia API, a household simulator, a web shop, your codebase. The environment offers an <em>action space</em>, the set of things the agent is allowed to do. Call it <code>A</code>. A policy maps the current context to the next action: given everything the agent knows, what does it do next? With nothing else, the policy has to map a long, noisy trajectory of past observations directly to the right next move. This is brittle. The longer the task runs, the more lost the model gets.</p><p>ReAct&#8217;s move is to enlarge the action space. The new action space is <code>A</code> plus <code>L</code>, where <code>L</code> is the space of natural language. A &#8220;thought&#8221; is an action in <code>L</code>, the agent pausing to write itself a sticky note before reaching for the next tool. It does not change the world, it changes the <em>context</em>. The next action is conditioned on a context that now includes the model&#8217;s own reasoning about what just happened.</p><p>The paper spells out what thoughts are actually for, and the list is concrete, not mystical. Decomposing the goal into a plan. Injecting commonsense the environment does not supply. Extracting the relevant signal from a noisy observation. Tracking progress and noticing when a subgoal is done. Handling exceptions when something breaks. Five jobs.</p><p>Why this beats the alternatives is where the paper earns its place. Chain-of-thought prompting, the prior art, has the model reason in a closed loop inside its own head, with no contact with the world. The paper&#8217;s own ablation on the HotpotQA benchmark is brutal: chain-of-thought hallucinates in 14% of its <em>successes</em> and 56% of its failures. Acting alone, calling tools without thought, is grounded in the world but loses the global plan after a few steps. ReAct synthesizes them. On the same task, ReAct hallucinates in 6% of successes. Less than half. Both halves of the loop have to be there.</p><p>One concrete anchor before we move on. ReAct&#8217;s HotpotQA action space, the entire set of things the agent could do, was exactly three actions: <code>search[entity]</code>, <code>lookup[string]</code>, <code>finish[answer]</code>. Three. The first working agent paradigm operated on three tools. Hold that number.</p><p>The paper closes with the line that becomes the engine for the rest of this essay. <em>&#8220;Complex tasks with large action spaces require more demonstrations to learn well, which unfortunately can easily go beyond the input length limit of in-context learning.&#8221;</em> In plain English: more capability needs more action descriptions, which need more context, which we do not have. Every layer that follows is the field iteratively solving exactly that bottleneck.</p><p>METR step: a model wrapped in this loop moves from minutes to tens of minutes on bounded tasks.</p><h2>Tools</h2><p>So how do you fix ReAct&#8217;s bottleneck, the one the paper named in its own conclusion?</p><p>The first, most obvious answer: give the agent more actions to take. If <code>A</code> was the original action space and ReAct enlarged it to <code>A &#8746; L</code>, the next move is to make <code>A</code> itself bigger.</p><p>That is what a <em>tool</em> is. A tool is a function the model can call. It has a name, a typed schema for its arguments, and a return value. The model writes a tool call into the trajectory the same way it writes a thought. Except this one has a side effect on the world. The harness picks it up, runs the function, drops the return value back into the context. The next turn of the loop sees the result and decides what to do next.</p><p>The loop is unchanged. Same thinking, same acting, same context-grows-by-a-turn shape ReAct described. The difference is what the agent is allowed to do.</p><p>ReAct, recall, ran on three tools: <code>search</code>, <code>lookup</code>, <code>finish</code>. That was the entire menu. Claude Code in 2026 ships with more than twenty: read a file, edit a file, run a shell command, search the codebase, fetch a URL, spawn a subagent, take a screenshot, schedule a future tick, and so on. Each one is just a function with a schema. Each one expands the set of things the agent can do without changing one line of the underlying loop.</p><p>This is the part that surprised me, the first time I sat with it. The chatbot you typed at in 2022 and the agent that wrote your test suite this morning share one loop. What changed is the tool catalog. Same loop. Bigger menu.</p><p>That observation is the unsexy version of why tool-building is now a discipline of its own. Every capability you add to an agent &#8212; search the web, read a Slack channel, hit your billing API, deploy to staging &#8212; is just another function with a schema. The architecture does not change. The leverage is entirely in <em>which</em> tools you build and how you describe them to the model.</p><p>The design discipline that emerges is short to state and brutal to follow. Tools should be <strong>few</strong>, <strong>sharp</strong>, and <strong>self-describing</strong>. Few, because every tool you add takes up tokens in the system prompt and a slot in the model&#8217;s attention. Sharp, because a tool that does seven things is one the model will use wrong six times out of seven. Self-describing, because the model only learns to use a tool from its name, its docstring, and its argument schema. There is no other channel. (More on this on Thursday. Anthropic&#8217;s recent guidance on writing tools for agents is the cleanest summary of this craft I have read.)</p><p>METR step: a model with the right toolkit moves from tens of minutes to hours of bounded work.</p><h2>Skills</h2><p>Tools fix half of ReAct&#8217;s bottleneck. They expand the action space.</p><p>The other half, recall, is the input-length limit. Every tool you add costs tokens in the system prompt to describe: name, schema, when to use it, what its return value looks like. Add fifty tools that way and the system prompt is a small book. The model is reading every single tool description on every single turn, even when ninety-five of those turns have nothing to do with that tool.</p><p>Skills are the move that fixes this.</p><p>Anthropic shipped the idea in late 2024 and the rest of the field has been catching up since. A skill is, mechanically, almost embarrassingly simple. It is a markdown file. It has a name, a one-line description of when it applies, and a body that explains how to do the thing. The agent does not read it on startup. The agent reads it <em>on demand</em>: when, in the middle of a task, it notices a description that matches what it is about to do.</p><p>So instead of jamming <em>and here are seventeen other things you might want to do</em> into the system prompt, you put each of those things in its own file with a one-liner that names when to consult it. The system prompt stays small. The latent capability of the agent becomes, for practical purposes, unbounded. Every skill you write is one more thing it can do, but only when it actually needs to.</p><p>I find the deeper shift here more interesting than the engineering. The agent is reading documentation written for it. Not training data ingested months ago and frozen into weights. Documentation. Authored in plain prose. Versioned in git. Like the laminated procedure sheet a mechanic posts above a workbench for a job done once a month. Improvable by the same process that improves any document: someone notices the agent doing the wrong thing, edits the file, the next agent reads the new version and gets it right.</p><p>This is self-extension by reading, not by retraining. A new capability used to require a new training run, or at minimum a new fine-tune. Now it requires a markdown file. The cost of teaching an agent to do one more thing has fallen from days of GPU time to the minutes it takes to write a paragraph, and almost nobody outside the people building agentic systems has noticed.</p><p>The system prompt stays small. The set of things the agent can do, on demand, grows without bound. The two used to be the same number.</p><p>METR step: skills, more than anything else in this list, are what made the time horizon stop being bounded by how cleverly you wrote the system prompt.</p><h2>MCP</h2><p>For most of 2024, every agentic harness invented its own way to attach the same set of capabilities. You wrote a tool for Claude Code; it would not work in Codex. You wrote a skill for one harness; another harness could not see it. You hooked your billing API into one agent and had to do the same wiring four more times for the others. Every integration was bespoke. Nothing composed.</p><p>The Model Context Protocol (MCP) is the field&#8217;s answer to that. Anthropic shipped the spec in late 2024. By the end of 2025 every serious agent harness, including the ones not built by Anthropic, had adopted some version of it. Codex talks MCP. So does Claude Desktop, and Cursor, and a long list of others. This is one of those quiet moments where an industry just... agrees on a wire format, and a year later the world is different.</p><p>The architecture is three nouns. <strong>Hosts</strong> are the applications you actually use: Claude Desktop, Codex, Cursor. <strong>Clients</strong> live inside the host and talk to one server each. <strong>Servers</strong> are the things that actually expose capability: your codebase, your billing API, the Wikipedia search box from the ReAct paper four years ago.</p><p>What a server offers is the second triple: <strong>Resources</strong> (data the model can read), <strong>Prompts</strong> (workflow templates the user can invoke), and <strong>Tools</strong> (functions the model can call). Three nouns, again. The whole protocol is two threes.</p><p>The point is portability. A skill or tool you wrote once, against the protocol instead of against a specific harness, works everywhere. The lock-in moves out from under you. The agent ecosystem starts to compose the way the web did in the late 1990s. Not because someone planned it, but because everyone independently noticed it was cheaper to talk a shared protocol than to keep reinventing the connector layer.</p><p>Worth noticing what the spec foregrounds at the top of every chapter on tool calls: <strong>user consent</strong>. Capability requires permission. The protocol does not assume the model can do whatever a server exposes. It assumes the model has to ask, and the user has to answer. A small design choice with very large downstream consequences, and the reason the rest of this stack does not collapse into something nobody would let near their email.</p><p>METR step: not a step on the ladder, but a multiplier. The tools and skills from the last two sections now travel.</p><h2>Context engineering</h2><p>Add tools. Add skills. Add MCP. The agent can now do, in principle, almost anything you can describe in a prompt and a function. The trouble is what happens when it actually starts trying.</p><p>A long agent run accumulates context. Every observation from a tool call goes in. Every thought goes in. Every error message, every retry, every half-attempted plan that did not work goes in. After a few hours of work the context window is mostly <em>exhaust</em>: the trail of everything the agent tried, the great majority of which is no longer relevant to the next move. The model is searching for signal inside its own attic.</p><p>Karpathy named this <em>context engineering</em> in 2024, and the name stuck because the field had been doing it without a name for two years. Simon Willison wrote it up. LangChain made it a category. By 2026 it is a craft of its own: what to put in the context, when to summarize, what to evict, what to keep verbatim because the agent will need its exact wording later.</p><p>The central primitive in the discipline is <strong>compaction</strong>. At some threshold, typically 70% to 85% of the window, the agent stops, reads its own history, and rewrites it into a smaller form. <em>Here is what we were trying to do. Here are the decisions we made. Here is the state we are in. Here is the next move.</em> The compacted summary replaces the noisy trail. The agent keeps going on a fresh, smaller context with the salient bits intact.</p><p>The deeper move is that the agent now owns its own working memory in a way it never did inside a single ReAct loop. ReAct kept the entire history. Compaction lets the agent <em>curate</em> the history. A small change of grammar with a giant change of consequence.</p><p>Notice what this fixes. ReAct&#8217;s authors, in the same 2022 paper, named the dominant failure mode of their own system: <em>&#8220;the model repetitively generates the previous thoughts and actions, often failing to reason about what the proper next action to take should be and jump out of the loop.&#8221;</em> Translation: the agent gets stuck because its context is full of the same noise as the previous turn, so the next turn is the same noise plus a little more. That is a context problem. Context engineering is what stops it.</p><p>Without this layer, every previous layer eventually drowns. A hundred tools is useless if the agent&#8217;s context is so saturated it cannot find the right one. The five-thousand-word skill on how to handle a billing dispute is useless if the agent compacted it away on turn forty. Context engineering is the layer that makes the others <em>compound</em> over a long run instead of degrading into noise.</p><p>METR step: this is the layer that turns a few hours of focused agent work into a workday.</p><h2>The hierarchy of agency</h2><p>Stack the layers and the picture comes into focus. At 50% reliability on the METR time-horizon scale, a language model alone, with no scaffolding around it, sustains minutes of human-equivalent work. Wrap it in a ReAct loop with no tools, and that becomes tens of minutes. Add tools to ReAct, hours. Add skills and context engineering on top, a workday. Add an external loop above all of that, a fresh agent per turn on a clock with a journal handing state to itself, and the horizon stretches into days and weeks.</p><p>Stare at that ladder for a second. Each rung is the same model. What separates a chatbot from a coding agent that finishes a feature overnight is the scaffolding stacked around it. The frontier of what an agent can do in 2026 is set, almost entirely, by where you stop climbing.</p><p>Each layer has the same shape, in the abstract. Find the thing that bottlenecks the previous layer. Add a structure that lets the model offload that thing into the world, the way a machinist offloads a measurement into a caliper rather than holding it in memory. Into language, into tools, into files, into a clock. The model&#8217;s per-turn intelligence does not change. What changes is the time horizon over which that intelligence compounds.</p><p>The last rung is the one most people have not seen yet, and it is the one I have spent the last few months running on my own infrastructure. The trick is the same one. Take the bottleneck (the agent runs out of context before it runs out of work) and offload it. The new offload target is <em>the file system</em>. The new clock is <em>cron</em>. Past-Claude writes a markdown file at the end of its turn that says what it did and what comes next. A timer fires some hours later. Future-Claude wakes into a fresh context, reads the file, makes the next move, writes the file, exits. The continuity is in the file, not in the model.</p><p>That is the entire primitive. A markdown file and a timer. Past self tells future self what to do.</p><p>What you get from it is hard to describe to someone who has not run one. The agent works on your stuff for weeks at a time. It writes new jobs for itself. It reads the documentation about its own substrate and uses the tools that documentation describes. It makes mistakes (one in five runs produces something I have to throw out) but the mistakes are caught by the same kind of boring engineering that catches mistakes in any other autonomous system. Audit log, lock registry, archive-only deletion, every state change committed to git before the next turn starts.</p><p>The point of saying this out loud is that the same trick keeps working. Extend the action space; add a layer that compounds; let the previous layer drop the things it could not hold. The trick does not stop at hours. It does not stop at days. METR&#8217;s curve has been doubling every four months over the last two years. The 2027 projection is a working day. The 2028 projection is a working week.</p><p>Each doubling is one more scaffolding layer.</p><h2>The frontier is not the model</h2><p>Step back from all of it.</p><p>The architecture you&#8217;ve just walked through is <em>layered</em>. A language model at the core. ReAct around the model, turning tokens into actions. Tools around ReAct, expanding what those actions can be. Skills letting the agent pull capability from the file system instead of carrying it in the system prompt. MCP making everything portable. Context engineering keeping the whole thing from drowning in its own exhaust. An external loop on top of all that, when the work runs longer than a single context window can hold.</p><p>Every agent doing real work in 2026 &#8212; your coding agent, your research agent, the customer-ops bot answering your refund request, my private-tick agent running once an hour &#8212; has this shape. They differ in which tools they ship and which skills they read on demand. They do not differ in the shape of the stack. Once you can see the layers, you can see them everywhere.</p><p>So here is the closing claim, the techno-pragmatist version of what the article has been arguing the whole time. <strong>The frontier is not the model. It is the layers around it.</strong> And the entire stack is the field&#8217;s three-year answer to a single sentence in a single paper from October 2022 that named its own ceiling and dared the rest of us to climb past it.</p><p>One frontier worth flagging before I close. A competent agent can already write its own tools and skills on demand. That part is shipping today. The next move is teaching it, via tools and skills, to <em>detect by itself</em> when its current toolkit doesn&#8217;t cover what it&#8217;s trying to do, so it knows when to extend itself without being told. Self-extension that triggers itself. That is the live edge right now, and where the next few posts are headed.</p><p>The next post zooms in on the innermost layer the agent touches: the tools themselves, and what makes a tool safe enough to live inside a stack like this. That is a story for another Thursday.</p><p>Until next time, stay curious.</p><div><hr></div><p><em>If this is the worldview you want to take more seriously, the second edition of <a href="https://apiad.gumroad.com/l/ai">Mostly Harmless AI</a> (due May 25th) goes deep on the agentic stack we walked through here. Full chapters on context engineering and on the harness around the model, with the math, the case studies, and the parts that didn&#8217;t fit a blog post. You can also <a href="https://books.apiad.net/books/mhai/">read the whole book online for free</a> in a custom reader I built that I&#8217;m rather proud of: dark mode, font controls, progress tracking, offline support, the works.</em></p><p><em>If you want the whole catalog of everything I&#8217;ve written, plus everything I&#8217;m going to write, that&#8217;s <a href="https://apiad.gumroad.com/l/compendium">the Compendium</a>. One purchase, in perpetuity.</em></p>]]></content:encoded></item><item><title><![CDATA[Realtime 3D in Pure Python + Numpy]]></title><description><![CDATA[Just a fun side-project that went way too far-as it happens.]]></description><link>https://blog.apiad.net/p/realtime-3d-in-pure-python-numpy</link><guid isPermaLink="false">https://blog.apiad.net/p/realtime-3d-in-pure-python-numpy</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Tue, 07 Apr 2026 15:59:45 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="3456" height="2234" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2234,&quot;width&quot;:3456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;a sculpture of a person on top of a ball&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="a sculpture of a person on top of a ball" title="a sculpture of a person on top of a ball" srcset="https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1682846133858-c0599bc99874?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8M2R8ZW58MHx8fHwxNzc1NTc3NTM2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Just a random, completely unrelated stock photo, because I&#8217;m cool now.</em> Photo by <a href="https://unsplash.com/@rozetsky">Ant Rozetsky</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Today I want to tell you a different kind of story. It&#8217;s not about machine learning, large language models, algorithms, or theory of computer science.</p><p>It&#8217;s about a side project that I&#8217;ve been building for a couple of weeks that made me fall in love again with an ancient love of mine. The quick and easy way to explain it is this: a performance-focused graphics engine for data-driven visualizations in Python.</p><p>But that&#8217;s only the surface. If you want to see the coold demos and the technical description, feel free to scroll down. But if you want to know the story behind it, let me start from the beginning.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>The Origin Story</h2><p>So, this starts back in undergrad, before I did anything related to machine learning or optimization or statistics. My first love was actually computer graphics. I had &#8220;learned to code&#8221; like, I don&#8217;t know, at 11 or 12, and for the first five years or so, before getting to college and actually learning to code, all my &#8220;coding&#8221; was basically tiny games. It was RPG Maker back then&#8212;who remembers that?</p><p>I always wanted to be a game developer, as you may imagine, and I think that&#8217;s probably the main motivation why I studied Computer Science. There are two kind of people who want to study Computer Science, as a matter of fact. One is people who love games&#8212;and the other is, of course, people who hate games; there are no in-betweens.</p><p>I was the loving-games kind and all I wanted to build games for a life. So when I was in first year, after actually learning some real coding, my first kind of large project was a game engine. This was before Unity, before even XNA&#8212;who remembers that?&#8212;this was when .NET was getting started, and I wrote a quick and dirty game engine in pure C# that talked native DirectX 11.</p><p>It was very cheap, a disaster of architecture almost surely, but it taught me the basics of how to construct a scene graph, how to animate a camera, how to do lighting, how to write very basic shaders. I learned a ton and basically fell in love with computer graphics.</p><p>I ended up doing my diploma thesis in computer graphics&#8212;screen-based global illumination, a couple of years before NVIDIA came up with ray tracing on the GPU, which basically killed that whole area of research. And I also did my Master&#8217;s on global illumination and some data structures for the GPU, but after graduation I quickly switched research towards machine learning and AI, which, you can imagine, this 2014, and deep learning was just on the rise. The rest is history, as they say.</p><p>And then, here I was this past week thinking about old projects that I used to have fun with when I was in college, and trying to remember what it felt like to code back then, no LLMs, no internet for the most part even. The time where I&#8217;ve been the most fun was probably when I dabbed into procedural generation of cities, mountains, lakes, and&#8230; stuff, in the late 2012. This was at the early era of PCG, and I never got to do anything with that other than a few tutorials and a few lessons that I taught at University.</p><p>I played with Unity for a couple of years, but nothing too serious&#8211;I think I was actually one of the first people in my University to even install Unity, and I even taught a couple of Master&#8217;s courses on it. I participated in a couple of game jams, but after 2017 or so I stopped doing graphics all together. And I&#8217;ve been doing machine learning since.</p><p>But, in any case, I kind of forgot about computer graphics all along. At least during day-worked. So there was I last weeek, remembering that and asking how hard would it be to actually make a graphics engine in Python, some quick hack like my undergrad projects. I did a bit of research and I discovered that Python is, as of 2026, in a very good position to build a graphics engine, and not just a crapy one, but one that is actually fast. We have WGPU now&#8212;the spiritual and practical succesor of OpenGL (who remembers that?), which has native suppotr for GPU-accelerated graphics in Linux.</p><p>I basically did a plan and sat for three days to hack this thing.</p><h2>The Engine</h2><p>My first idea was to have a Rust backend for all the graphics engine stuff&#8212;the rendering loop, materials, lights&#8212;but I quickly decided to drop that idea because getting Rust and Python to talk to each other was becoming increasingly harder and harder, and I really wanted to finally see a damn cube rendering on my screen.</p><p>So I decided to switch completely to Python. But since I&#8217;m a grown-up now, I have to find some kind of serious objective for making something like this. I decided I didn&#8217;t want to make a typical graphics engine where you have a scene graph with hierarchies of entities and properties, and you simply render all of them. No, that is way too 2000s.</p><p>I decided I wanted to do a very fast, data-driven visualization tool purely based on the Entity-Component-System (ECS) paradigm and make it extremely performant, so it would focus on big data-driven simulations like N-body simulations, chemical and physics experiments, AI pathfinding and agents, you know, grown-up stuff like that.</p><p><em>(But actually, all I wanted was to play with WGPU and draw some cubes in Python. Wink, wink.)</em></p><p>This framing gave me two things, though. My solution doesn&#8217;t have to be very fancy as a game engine, we don&#8217;t need to be able to like load skeletal animations or stuff like that. It&#8217;s not actually a game engine; it&#8217;s a graphics engine with at best some interaction logic. But it still lets you do some cool stuff, even if all you can render is blocks and spheres. When you can render thousands of them running very fast on the GPU, you can do some cool stuff. So this is the motivation, and now let me show you what I have.</p><h3>Deep Dive</h3><p>So here is <strong>manifold</strong>&#8212;short Manifold Graphics if you want. It&#8217;s a Python library built on top of WGPU, a graphics engine based on the Entity-Component-System paradigm.</p><p>If you have never heard about it, ECS is a completely different way of writing code that is especially tailored for video games, but it is very little known outside of the game development world. And its awesome.</p><p>In a typical business code, you have entities who own their data, and you usually have behavior associated to entities; so entities also own their behavior&#8212;this is the basic Object-Oriented Programming paradigm where objects own their data and their methods. And if you want to do something with an object, you have to call methods on the object so the object guarantees the instance invariances.</p><p>Since OOP was basically the ONE programming paradigm of the 90s and early 2000s&#8212;when the videogame industry really exploded&#8212;it is only normal that we started writing games like this. But there is a problem with OOP (well, many problems, but one in particular that matters for our discussion).</p><p>When you have 10,000 objects, each of them with more or less the same structure, e.g., they are physical particles bouncing with each other, or little zerlings comming to your base, you simply <em>cannot</em> update them fast enough. For example, making a physics simulation out of this is extremely slow if you have to go to each particle and update its velocity, its scale, its rotation, etc. <strong>You</strong>&#8217;ll end up doing thousands of tiny method calls, thrashing your cache, and issuing lots of super small copies to GPU for drawing.</p><p>What you want is to vectorize this operation. You would like to have all of the objects&#8217; data in a single NumPy matrix, and you want to write a very, very efficient vectorized code that doesn&#8217;t do any loop and just updates everything at once. THen copy all the data to the GPU and issue a single draw call that renders all objects parameterized by their positions, rotations, etc. Chef kiss.</p><p>This is the Entity-Component-System paradigm at its core. It completely flips the responsibilities from standard OOP The <strong>components</strong> are just flat storage of data (rows in a matrix) and the <strong>entities</strong> are just pointers to a row where all of their data lives. Then the systems are methods that act on a subset of entities using heavily vectorized code, because each system deals with a large number of equally-structured entities, and they don&#8217;t care which is which.</p><p>In <strong>manifoldx</strong>, each system is a Python method that receives a subset of entities that have some combination of components. For example, if you want to process all of the particles in a simulation, you write a system that receives entities that have the <code>Particle</code> component, perhaps also a <code>Transform</code> component. In the transform component, you will have the position, rotation, scale, and the particle component will store simulation-specific data like velocity, temperature, momentum, etc.</p><p>The key to high performance in ECS is to avoid looping as much as possible. You assume all of the components of the entities in a system have exactly the same layout, so what you get is really a view of a matrix, and you write vectorized code. You add something to all them, you multiply all them by something, or in general you compute some matrix operations on them. All at once.</p><p>And if you can write your code like this, then you get a very, very fast rendering loop because instead of making one method invocation per entity, you make one method invocation per <em>archetype</em>, that is, per combination of components, which is a couple of order of magnitude less that your entities count.</p><p>Here&#8217;s a minimal example showing how the ECS works in <strong>manifoldx</strong>:</p><pre><code><code>import manifoldx as mx
import numpy as np

engine = mx.Engine("Cubes")
engine.camera.zoom(0.1)

# These are all static things that are created
# and stored in memory once
mesh = mx.geometry.sphere(1)
material = mx.material.phong(mx.colors.BLUE)

# Custom component, gets registered in engine to keep track
# Only used for reflection on the values
@engine.component
class Particle:
    velocity: mx.Vector3
    angular: mx.Vector3
    life: mx.Float

# This runs every frame
@engine.system
def particle_lifecycle(query: mx.Query[Particle, Transform], dt: float):
    query[Particle].life -= dt  # Single vectorial operation
    query[Transform].position += query[Particle].velocity * dt
    query[Transform].rotation += Transform.rotation(euler=query[Particle].angular * dt)
    query[Transform].scale = query[Particle].life / 10.0

    # Destroy all dead particles at once
    engine.destroy(query[Particle].life &lt;= 0)

    # Now we create lots of particles
    N = int(100 * dt)

    # This will in principle reuse the buffers for dead entities,
    # but will expand the buffer if necessary
    engine.spawn(
        Mesh(mesh),
        Material(material),
        Transform(pos=(0, 0, 0), scale=(1, 1, 1)),
        Particle(
            velocity=np.random.uniform(-5, 5, (N, 3)),
            angular=np.random.uniform(-2, 2, (N, 3)),
            life=np.random.rand(N) * 10,
        ),
        n=N,
    )

    # Update camera
    engine.camera.orbit(45 * dt, 0)</code></code></pre><p>That&#8217;s it. A single line of code to update all positions at once. Notice the <code>query</code> argument that defines which entities you get (all entities with both a <code>Transform</code> and a <code>Particle</code> component). And here is how that looks like:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;5c6ae9e4-c010-46dd-a47d-6b5b87679965&quot;,&quot;duration&quot;:null}"></div><p>In a real simulation, you can have, say 10 systems, but you have 10,000 or 100,000 entities, and you know you can do very fast vectorized updates in NumPy for all them, 10 times each frame.</p><p>For example, if you have 500 particles and you want to do N-body simulation, computing the 500-squared gravity interactions 60 times per second in Python is suicide. But if you do it in NumPy, then you get something that runs in a few milliseconds. A quarter million interactions computed 60 times per second. In Python.</p><p>To make it really efficient, you need to also avoid copying or moving data; it&#8217;s all masking and clever NumPy layout that keeps all of the memory in one place, and you are just seeing fragments of that memory in each system.</p><p>The other key idea is that you don&#8217;t modify anything in a system. That line where position is set, doesn&#8217;t really write back to the matrix. All it does is compute the right-hand side and then you issue a command that will be run at the end of all the systems, before frame rendering happens. This allows to write pure threaded parallelism, because you can run several systems in different threads&#8212;they are all reading the same data, but they aren&#8217;t writing to the buffers, which is great since Python has real support for multi-threading now in 2026 (after 35 years!).</p><h2>Showcase</h2><p>That is the basic idea. Now lets see some examples. AS of today, version 0.2, <strong>manifoldx</strong> has some basic shapes like cubes, spheres, and planes, and support for basic PBR lighting, camera controls, and that&#8217;s basically it.</p><p>All the engine realy does is set up this somewhat clever inversion of logic that forces you to write very efficient code, and the magic is in what you do inside the systems.</p><p>So let me show you three examples.</p><h3>1. N-Body Gravitational Simulation</h3><p>The first is an N-body simulation. All gravity computation happens in a single NumPy block with no Python loops. The only relevant part of the code is the gravity system, that looks something like this.</p><pre><code><code>@engine.system
def nbody_gravity(query, dt):
    pos = query[Transform].pos.data  # (N, 3)

    # All-pairs position differences: (N, N, 3)
    diff = pos[None, :] - pos[:, None]
    dist = np.linalg.norm(diff, axis=2)

    # Force magnitude: G * m_i * m_j / r&#178;
    force_mag = G * mass_prod / np.maximum(dist, SOFTENING)**2

    # Net force = sum over all other bodies
    net_force = (force_mag[:, :, None] * diff / dist[:, :, None]).sum(axis=1)

    velocities += (net_force / masses[:, None]) * dt
    query[Transform].pos += velocities * dt</code></code></pre><p>This runs 500 bodies with 250,000 force pair computations at 60fps.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;3587e1ae-4af3-49a9-b39e-a9d495d24bc1&quot;,&quot;duration&quot;:null}"></div><h3>2. Ideal Gas Simulation</h3><p>The second example is an ideal gas with elastic collisions inside a bounding box. Again, all running without a single for loop. Collision detection and impact resolution in vectorizednumpy operations.</p><pre><code><code>@engine.system
def gas_physics(query, dt):
    pos = query[Transform].pos.data

    # Wall collisions: vectorized mask
    below = (pos + velocities * dt) &lt; -BOX_HALF
    above = (pos + velocities * dt) &gt; BOX_HALF

    # Here we avoid branching and use masking instead
    velocities[below] = np.abs(velocities[below]) * RESTITUTION
    velocities[above] = -np.abs(velocities[above]) * RESTITUTION

    # Particle collisions: find overlapping pairs
    diff = pos[None, :] - pos[:, None]
    dist = np.linalg.norm(diff, axis=2)
    overlap = dist &lt; 2 * PARTICLE_RADIUS
    i_idx, j_idx = np.where(np.triu(overlap))

    # Resolve collisions with impulse
    # ... (collision resolution code)
    # ... (also vectorized)

    query[Transform].pos += velocities * dt</code></code></pre><p>Here&#8217;s how that looks like.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;86436879-ae58-4d3d-9616-fac2e5dfcf7c&quot;,&quot;duration&quot;:null}"></div><h3>3. Boids Flocking</h3><p>The third example is a Boids simulation with emergent flocking behavior. This is the one that strikes me the most because boids simulation is often compute-heavy. Each individual entity must keep track of a subset of neighbors and adjust behavior based on them, not the whole set of entities. But again, a bit of numpy magic lets us vectorize the crap out of this and simulate 300 boids at 60 frames per second.</p><pre><code><code>@engine.system
def boids_physics(query, dt):
    # Separation, alignment, cohesion as vectorized tensor ops
    diff = pos[None, :] - pos[:, None]  # (N, N, 3)
    dist_sq = (diff * diff).sum(axis=2)

    neighbors = dist_sq &lt; PERCEPTION_SQ

    # Separation (1/dist&#178; weighted)
    sep = (-diff * (neighbors[:,:,None] * inv_dsq[:,:,None])).sum(axis=1)

    # Alignment (average neighbor velocity)
    avg_vel = (vel[None,:] * neighbors[:,:,None]).sum(axis=1) / safe_count

    # Cohesion (steer toward center of mass)
    center = (pos[None,:] * neighbors[:,:,None]).sum(axis=1) / safe_count

    # Plus predator avoidance and boundary steering...
    # That one is easy.</code></code></pre><p>Here goes a sneak peek.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;704ce0e0-34f9-46d4-a8d5-438d5074f70a&quot;,&quot;duration&quot;:null}"></div><div><hr></div><p>You can check all the examples in the <a href="https://github.com/apiad/manifoldx">Github</a> repository to see the full code, but the bulk of the implementation is these cleverly vectorized system methods.</p><h2>Future Directions</h2><p>And that&#8217;s it. This is my pure Python (well, you know what I mean) graphics engine for serious, grown-up stuff that is surely, definitely, not a weekend side-project meant to procrastinate on actual work... I mean, what?!</p><p>Where I will go with this? I don&#8217;t know. I always write these things mostly as a learning exercise and I&#8217;ve learned a lot about graphics in Python. I&#8217;ve updated my view of modern graphics and I think I&#8217;ve paid my debt of the last seven years in graphics computation. I&#8217;m kind of happy now that I know how to do this in 2026. Mission accomplished, I guess.</p><p>There are some places this engine can go to, like some custom shaders when you need stuff like lighting effects. But it is not going to become a traditional, full-blown game engine. I will not add support for lots of game engine-like features including, I don&#8217;t know, skeletal animations, level of detail, scene management, or, god forbids, visual scripting and nonsense like that.</p><p>Now two areas I&#8217;d like to explore in the future. One is extending the engine towards the kind of behavior you need to write for AI simulations. If you want to run some sort of agent simulation or ant colony optimization or stuff like that, that code doesn&#8217;t look that much as a frame-by-frame update, but like an asynchronous event-loop&#8212;which is also something that is not usual in game engines. And the other direction is towards procedural generation of meshes and content in general, which is an area I left five or six years ago and would pretty much love to come back to it.</p><p>And that&#8217;s it for this week. This is not production-ready at all&#8212;it&#8217;s mostly a toy at the moment&#8212;but you can take it apart and hack your way into some cool physical or mathematical simulation. The code is on <a href="https://github.com/apiad/manifoldx">GitHub</a> if you want to try it yourself, and I&#8217;d love to see what you build with it.</p><p>Until next week, stay curious.</p>]]></content:encoded></item><item><title><![CDATA[Drawing (not so) Beautiful Diagrams with Pure Python]]></title><description><![CDATA[Ups, I did it again :)]]></description><link>https://blog.apiad.net/p/drawing-not-so-beautiful-diagrams</link><guid isPermaLink="false">https://blog.apiad.net/p/drawing-not-so-beautiful-diagrams</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Fri, 13 Feb 2026 14:06:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/21624634-7c05-40c2-98f0-46ed20132c3d_663x359.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the past couple of weeks, I&#8217;ve been working on a pure Python library for rendering SVGs. In a spur of unbridled inspiration, I called it <strong><a href="https://apiad.github.io/tesserax">Tesserax</a></strong>. Long story short, it got <em>waaayyyy</em> beyond control, as it happens. </p><p>This article is a quick recap of why I did it, how I went down the rabbit hole of implementing yet-another-drawing-library, and a small showcase of what it can do it can do, in case you want to give it a shot.</p><p>But before moving one, here is a minimal example of what you can do with a few lines of Python.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R3IB!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 424w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 848w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 1272w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R3IB!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif" width="320" height="78" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:78,&quot;width&quot;:320,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R3IB!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 424w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 848w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 1272w, https://substackcdn.com/image/fetch/$s_!R3IB!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93a97f55-54b7-49a8-9c5f-f2f21a862d7e_320x78.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></div></figure></div><p>Are you curious now? Then read on!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>The Case for Tesserax</h2><p>There are a gazillion tools out there for drawing mathematical diagrams in Python, from the ancient, trustable <strong>matplotlib</strong> to the new cool kid in the park <strong>manim</strong>. There are a ton of charting libraries (those intended to draw bars, pies, and doughnuts) like <strong>altair</strong> and <strong>plotly</strong>; and a ton of high-level diagram makers like <strong>graphviz</strong> for graphs and networks, and <strong>mermaid</strong> for, well, whatever the mermaid devs think is worth drawing.</p><p>The problem with all these is that, one, <em>there weren&#8217;t made by me</em>. Now, jokes apart, I do believe there is a lot of value in reinventing the wheel, if only because you get to learn a lot about wheel&#8212;and boy have I learned more than I ever wanted to about SVG in the last couple of weeks. But also, you may find that your wheel fits your cart slightly better than all others, just because you know precisely what you need (and enough with wheel analogies).</p><p>So, I made Tesserax to cover a sweet spot that I couldn&#8217;t find anywhere: a lightweight library (literally zero dependencies, not even <strong>numpy</strong>) that renders web-native content (so everything scales and layouts perfectly in Jupyter / Quarto) and has both a very powerful low-level engine for when you want pixel-perfect control, and a very comfortable high-level engine for the most typical workflows.</p><p>Also, it should support <em>animations</em>. But I&#8217;m getting ahead of myself.</p><h2>What Can Tesserax Do</h2><blockquote><p>I won&#8217;t put any code in this article because, one, Substack sucks at code. And two, it would only make the article harder to follow. All of these examples are fully described in the <a href="https://apiad.github.io/tesserax">online documentation</a>.</p></blockquote><p>At its core, <strong>Tesserax</strong> is a library for defining an SVG scene. You create a <strong>Canvas</strong>, and some <strong>Shapes</strong> to it, define some attributes, and render it as an SVG file. Simple enough<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. </p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!67lT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 424w, https://substackcdn.com/image/fetch/$s_!67lT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 848w, https://substackcdn.com/image/fetch/$s_!67lT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 1272w, https://substackcdn.com/image/fetch/$s_!67lT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!67lT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png" width="399" height="128" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:128,&quot;width&quot;:399,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4287,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!67lT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 424w, https://substackcdn.com/image/fetch/$s_!67lT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 848w, https://substackcdn.com/image/fetch/$s_!67lT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 1272w, https://substackcdn.com/image/fetch/$s_!67lT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c88048f-2c2c-4f68-a567-b01f783e12f4_399x128.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>Beyond basic primitives, you can of course draw arbitrary paths and style them easily. Here is a <strong>Polyline</strong> primitive that allows controlling the curvature with a <strong>smoothness</strong> factor.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YaVP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YaVP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 424w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 848w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 1272w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!YaVP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png" width="282" height="270.3248407643312" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:301,&quot;width&quot;:314,&quot;resizeWidth&quot;:282,&quot;bytes&quot;:5697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YaVP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 424w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 848w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 1272w, https://substackcdn.com/image/fetch/$s_!YaVP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8960d96f-f48b-4c2c-af1c-0f4bcfb8b579_314x301.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You can control and distort these shapes in any form you want, including fully procedural warping of the edges.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ifdW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 424w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 848w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 1272w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!ifdW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png" width="412" height="152.10970464135022" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1226efa9-359b-4b1a-a986-19eed93df165_474x175.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:175,&quot;width&quot;:474,&quot;resizeWidth&quot;:412,&quot;bytes&quot;:9834,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ifdW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 424w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 848w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 1272w, https://substackcdn.com/image/fetch/$s_!ifdW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1226efa9-359b-4b1a-a986-19eed93df165_474x175.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>And, of course, since no mathematical drawing library is worth a penny without a sketchy mode, you can also do that (albeit with some constraints).</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3DwH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 424w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 848w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 1272w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!3DwH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png" width="403" height="171.6632860040568" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d62b252a-8212-4144-a857-d7e8d9944767_493x210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:210,&quot;width&quot;:493,&quot;resizeWidth&quot;:403,&quot;bytes&quot;:11596,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3DwH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 424w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 848w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 1272w, https://substackcdn.com/image/fetch/$s_!3DwH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd62b252a-8212-4144-a857-d7e8d9944767_493x210.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>To make positioning and layout easy, Tesserax comes a set of builtin layouts (that of course can be extended in any way you want, it&#8217;s Python) for common patterns like rows, columns, grids&#8230;</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tEXd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 424w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 848w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 1272w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!tEXd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png" width="349" height="210" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:210,&quot;width&quot;:349,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:858,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tEXd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 424w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 848w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 1272w, https://substackcdn.com/image/fetch/$s_!tEXd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60ca74b0-8ca0-4522-93b2-046664db44e3_349x210.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>And a couple of complex layouts for things like trees and arbitrary graphs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KodJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KodJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 424w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 848w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 1272w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!KodJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png" width="192" height="333" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:333,&quot;width&quot;:192,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7101,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/187845763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KodJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 424w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 848w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 1272w, https://substackcdn.com/image/fetch/$s_!KodJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5dc8f82b-67f7-424f-9f0b-abe7e1fd0085_192x333.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But things start to get really interesting when you discover that if you can render an image from code, then you can render as many as you want! And that leads to&#8230; animations!</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5ssP!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 424w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 848w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 1272w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!5ssP!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif" width="172" height="107.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:100,&quot;width&quot;:160,&quot;resizeWidth&quot;:172,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5ssP!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 424w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 848w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 1272w, https://substackcdn.com/image/fetch/$s_!5ssP!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff23b3b8e-d4af-4a9b-ac8b-b09474037e25_160x100.gif 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>Tesserax animations are procedural, meaning you write code that defines how each object changes in time. But there is a very high-level API for defining and composing animations declaratively, which means you seldom need the full power of tweaking each shape properties (but that power is there when you need it, as it should).</p><p>And, of course, once you grok animations, the next immediate idea is <em>physically-based animations</em>. (This is where things got way out of control, as you may imagine). Tesserax comes with a very small but fully-fledged pure Python 2D physics engines&#8212;that was a mouthful. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xao8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xao8!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 424w, https://substackcdn.com/image/fetch/$s_!xao8!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 848w, https://substackcdn.com/image/fetch/$s_!xao8!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 1272w, https://substackcdn.com/image/fetch/$s_!xao8!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!xao8!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif" width="126" height="262.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:120,&quot;resizeWidth&quot;:126,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xao8!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 424w, https://substackcdn.com/image/fetch/$s_!xao8!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 848w, https://substackcdn.com/image/fetch/$s_!xao8!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 1272w, https://substackcdn.com/image/fetch/$s_!xao8!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07c8667d-af5a-47ec-8471-6941baeb543b_120x250.gif 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It can resolve collisions among arbitrary objects (using circles and rotated boxes as approximate colliders) and simulate fixed-step rigid body mechanics. Physic animations are baked&#8212;which means you compute the animation once and then render it. So, no, no 2D games for you! What were you thinking about? This is a graphing library, for Turing&#8217;s sake!</p><p>The purpose of this physics engine is, once again, didactic&#8212;e.g., to explain physics concepts in a Jupyter notebook with a bit of shizzass. </p><h2>Final Words</h2><p>I started writing Tesserax to help me create diagrams and animations for my lectures and articles. But when you do that with <em>a fully-fledged programming language</em> things start to get really interesting, because now you can leverage your well-honed programming skills to create complex diagrams with the minimum necessary code. You can encapsulate repeatable patterns into classes and methods, and build impressive diagrams bottom-up. You can abstract common patterns into drawable concepts like trees, automatons, etc.</p><p>For example, in the docs you will find a bit of code to implement a blob-like simulation by creating a<strong> ConvexHull</strong> component that automatically tracks inner shapes and builds the convex surrounding path around them. Tied with some simple physics (a bunch of balls interconnected with springs) gives you this ugly-looking blob of dread that I hope haunts your dreams for the next week or so&#8212;it sure will mine.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FD07!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FD07!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 424w, https://substackcdn.com/image/fetch/$s_!FD07!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 848w, https://substackcdn.com/image/fetch/$s_!FD07!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 1272w, https://substackcdn.com/image/fetch/$s_!FD07!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!FD07!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif" width="436" height="272.8516129032258" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:388,&quot;width&quot;:620,&quot;resizeWidth&quot;:436,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FD07!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 424w, https://substackcdn.com/image/fetch/$s_!FD07!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 848w, https://substackcdn.com/image/fetch/$s_!FD07!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 1272w, https://substackcdn.com/image/fetch/$s_!FD07!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7820fc03-0d3d-43bb-8c37-0126e27b5652_620x388.gif 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>On a more serious note, the next step for Tesserax is building a library of reusable math and computer science concepts, like animated arrays, trees, graphs, etc., that can be used to explain complex CS topics with the much need visual assistance. As the great Donald Knuth once said, an algorithm must be seen to be believed. Tesserax is here to help you see them.</p><p>I&#8217;d really love if you guys would give Tesserax a try, now that is a bit more polished, and shoot me all your questions and suggestions. Special thanks to <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Wyrd Smythe&quot;,&quot;id&quot;:195807185,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f88cc94-56a6-4453-b3dd-c8ccb9194c15_2316x2316.png&quot;,&quot;uuid&quot;:&quot;45140a44-685f-4df2-95a6-819f2f652900&quot;}" data-component-name="MentionToDOM"></span> for many insightful comments and suggestions (although I&#8217;m not sure I&#8217;ve totally addressed all of them :)</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Sadly, among all the stupid things that Substack cannot do for the sake of being annoying, it seems rendering SVG is one of them. So all drawings in this article are dumb screenshots of what otherwise would be pixel-perfect, infinitely scalable SVGs.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Introducing Tesserax]]></title><description><![CDATA[A pure Python, developer-friendly library for academic drawing.]]></description><link>https://blog.apiad.net/p/introducing-tesserax</link><guid isPermaLink="false">https://blog.apiad.net/p/introducing-tesserax</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Sat, 31 Jan 2026 17:24:56 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6491" height="4447" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4447,&quot;width&quot;:6491,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;sketch pad and coloring pens&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="sketch pad and coloring pens" title="sketch pad and coloring pens" srcset="https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1525278070609-779c7adb7b71?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw4fHxkcmF3aW5nfGVufDB8fHx8MTc2OTg3OTkwOHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@kobuagency">KOBU Agency</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>It is 2026. We are still treating scientific diagrams like cave paintings.</p><p>I have a PhD in Machine Learning. I can explain the mathematics of high-dimensional manifold optimization. Yet, until recently, if I wanted to draw two boxes connected by an arrow, I had to Google the syntax.</p><p>My diagrams were dead artifacts. While my code was version-controlled and modular, my figures were fragile binary blobs or brittle LaTeX macros. I needed my drawings to behave like the rest of my research.</p><p>Instead, I found myself trapped between two equally bad options.</p><p>I have spent weekends fighting <strong>Mermaid</strong> and <strong>Graphviz</strong>. They are fine for a quick flowchart. But try asking them for a publication-quality Turing machine. Try asking for a precise computer architecture diagram. You hit a wall.</p><p>Using these black-box layout engines is like trying to parallel park a bus while shouting instructions from the sidewalk. You can nudge the parameters, but you never quite end up where you want to be.</p><p>The real sin of these tools is their refusal to encapsulate. As developers, we take parameterization for granted. We define logic once and instantiate it a thousand times. But you cannot take a Mermaid diagram, wrap it in a function, and ask it to render a version with &#8220;n&#8221; nodes. Our drawing tools should respect the engineering principles we teach.</p><p>Then there is <strong>TikZ</strong>.</p><p>TikZ is the undisputed masterpiece of academic drawing. It offers total control. It is also fundamentally broken for the web.</p><p>TikZ is a &#8220;print-first&#8221; citizen. If you convert it to SVG for a blog post, you don&#8217;t get a clean vector drawing. You get a crime scene. Inspect the element, and you will see a soup of unreadable paths that no CSS can touch.</p><p>And then, there is the &#8220;Reviewer #2&#8221; problem. <em>(Damn Reviewer #2, why is it always you?!)</em> You spend three hours perfecting a TikZ architecture diagram. You submit the paper. The review comes back: <em>&#8220;Please swap layer 3 and layer 4.&#8221;</em> In a drag-and-drop tool, this is annoying. In TikZ, it is a reconstruction project involving fifty lines of coordinate hard-coding. It isn&#8217;t just tedious; it&#8217;s fragile.</p><p>To render my documentation in a CI/CD pipeline, I shouldn&#8217;t have to install four gigabytes of TeX Live just to draw a few circles. And there is the friction of the language itself. Writing geometry macros in a specialized DSL is a jarring context switch. It forces you to leave the logic of your Python research and enter a world of backslashes and curly braces just to visualize a result.</p><h2>Meet Tesserax</h2><p>The shift happened when I moved my academic publishing to <strong>Quarto</strong>.</p><p>Quarto is essentially Jupyter on steroids. Since Jupyter is the standard for scientific computing, the solution was obvious. We didn&#8217;t need another GUI. We didn&#8217;t need a new domain-specific language. We needed a Python library that could live inside the notebook and speak fluent SVG.</p><p>I built <strong><a href="https://github.com/apiad/tesserax">Tesserax</a></strong> to close the gap. I started with the atoms, like Rects, Circles, and Paths. But the breakthrough was the Anchor System. I wanted a &#8220;nervous system&#8221; for diagrams, where objects know where they are relative to one another. No manual coordinates. No magic numbers.</p><p>Tesserax is what happens when you treat a drawing as a function of state.</p><p>In Tesserax, you don&#8217;t place a node at <code>(100, 200)</code>. You define a <code>Layout</code>. You say, &#8220;I want a column of nodes, and for every node, I want an arrow pointing to the next one.&#8221;</p><pre><code><code># This isn't just a drawing. It's logic.
with Canvas() as c:
    nodes = [Circle(10) for _ in range(5)]
    layout = Column(nodes, gap=20)

    # If I add a node to the list above, the arrows update automatically.
    for n1, n2 in pairwise(nodes):
        Arrow(n1.anchor("bottom"), n2.anchor("top"))</code></code></pre><p>This snippet doesn&#8217;t just render a figure. It renders <em>any</em> version of that figure. It captures the abstraction, not just the pixels.</p><h2>The Rationale Behind Tesserax</h2><p><em>But why, Alex, why building something like this? Don&#8217;t you have enough in your plate now?</em></p><p>Well, first, who are you and why do you know me so well? Second, it&#8217;s fun! But if that isn&#8217;t enough by itself, let&#8217;s analyze the landscape of modern academic drawing.</p><p>First, there is <strong>Matplotlib</strong>. </p><p>It is the industry standard for a reason. If I have a CSV of training data, Matplotlib is my first call. But have you ever tried to draw a linked list in it? You end up fighting the axes. You spend a dozen lines of code just hiding the ticks and borders to get a blank canvas.</p><p>Matplotlib is a library built for statistics that is occasionally forced to perform geometry. It feels like doing calligraphy with a highlighter. Tesserax respects the division of labor: use Altair or Matplotlib for your data, and use Tesserax for your concepts.</p><p>Then, there is <strong>Tikz</strong>. (Again!) We already discussed it, but there is more.</p><p>On one hand, there is a strange Stockholm Syndrome in academia. We accept that drawing a simple three-state automata requires learning a bizarre macro language on top of another macro language (yeah, I&#8217;m looking at you, <em>LaTeX!</em>). We treat TikZ&#8217;s loop syntax like a revelation, ignoring that any programming language is actually better at this, but I can also have, I don&#8217;t know, <em>variables!</em> And also, who the hell knows how to write Tikz!? <em>(I know some of you nerds do, don&#8217;t give me that look. Shame on you.)</em></p><p>Tesserax bets that you don&#8217;t need a Domain Specific Language (DSL). You need objects. You need classes. You need type hints. If you want a grid of shapes? That is a nested list comprehension. If you want a recursive tree? That is a recursive function. You don&#8217;t need to &#8220;learn Tesserax.&#8221; You just need to know Python. By piggybacking on Python&#8217;s syntax, we get free IDE support, linting, and the ability to debug our drawings with the same tools we use to debug our kernels.</p><p>But also, even more important, the browser is the new print driver. TikZ was built for a world where the final output was a piece of paper. Tesserax is built for a world where the output is a responsive HTML page that <em>can</em> be printed.</p><p>By targeting SVG natively, we bypass the fragile conversion layers. We get semantic scaling. We get CSS styling. We stop fighting the renderer and start using the most optimized vector graphics engine in history: the modern web browser. And you can render SVG to PDF anytime. Going back, though, means you lose the <em>semantics</em> of what your drawing means.</p><p>And last but not least, Tesserax is <em>lean. </em>Like, zero dependencies. A couple hundred of pure, fully-typed, Python 3.12. Nothing more, nothing less. </p><p>I rest my case.</p><h2>What&#8217;s Next</h2><p>So Tesserax is my attempt to bring software engineering to scientific illustration. I&#8217;m releasing it today in a very crude, 0.2 something version, as I always do, to gather as much feedback as possible as soon as possible. From now on, all the diagrams in my papers, blog posts, and books will be built with this.</p><p>So, next step, I&#8217;m moving past the primitives phase now and into building high-level scientific abstractions. I want to generate automata that look like they were pulled from a classic textbook and data structures that reflect actual memory layouts. That&#8217;s what comes in the next few iterations. Moving from a world where we draw lines to a world where we code diagrams.</p><p>I&#8217;d love it if you&#8217;d give <strong><a href="https://github.com/apiad/tesserax">Tesserax</a></strong> a try. The documentation is still pretty thin but feel free to drop me a comment with any questions or suggestions.</p>]]></content:encoded></item><item><title><![CDATA[The Basics of Search]]></title><description><![CDATA[From Chapter 1 of The Algorithm Codex]]></description><link>https://blog.apiad.net/p/the-basics-of-search</link><guid isPermaLink="false">https://blog.apiad.net/p/the-basics-of-search</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Tue, 27 Jan 2026 15:32:20 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><em>This article is a draft of Chapter 1 of <strong>The Algorithm Codex</strong>. You can read the entire Part I (chapters 1 through 6) <a href="https://matcom.github.io/codex">online</a> for free (forever). If you want to support this initiative, feel free to <a href="https://store.apiad.net/l/codex">grab the official PDF</a>.</em></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4096" height="2730" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2730,&quot;width&quot;:4096,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;woman walking on sand dunes during daytime&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="woman walking on sand dunes during daytime" title="woman walking on sand dunes during daytime" srcset="https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1505244783088-5a36f166e5b5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw2fHxzZWFyY2hpbmd8ZW58MHx8fHwxNzY4OTMxMTA1fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@katekerdi">Katerina Kerdi</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Searching is arguably the most important problem in Computer Science. In a very simplistic way, searching is at the core of critical applications like databases, and is the cornerstone of how the internet works.</p><p>However, beyond this simple, superficial view of searching as an end in itself, you can also view search as means for general-purpose problem solving. When you are, for example, playing chess, what your brain is doing is, in a very fundamental way, <em>searching</em> for the optimal move&#8211;the only that most likely leads to winning.</p><p>In this sense, you can view almost all of Computer Science problems as search problems. In fact, a large part of this book will be devoted to search, in one way or another.</p><p>In this first chapter, we will look at the most explicit form of search: where we are explicitly given a set or collection of items, and asked to find one specific item.</p><p>We will start with the simplest, and most expensive kind of search, and progress towards increasingly more refined algorithms that exploit characteristics of the input items to minimize the time required to find the desired item, or determine if it&#8217;s not there at all.</p><h2><strong>Linear Search</strong></h2><p>Let&#8217;s start by analyzing the simplest algorithm that does something non-trivial: linear search. Most of these algorithms work on the simplest data structure that we will see, the sequence.</p><p>A sequence (<code>Sequence</code> class) is an abstract data type that represents a collection of items with no inherent structure, other than each element has an index.</p><pre><code><code>from typing import Sequence</code></code></pre><p>Linear search is the most basic form of search. We have a sequence of elements, and we must determine whether one specific element is among them. Since we cannot assume anything at all from the sequence, our only option is to check them all.</p><pre><code><code>def find[T](x:T, items: Sequence[T]) -&gt; bool:
    for y in items:
        if x == y:
            return True

    return False</code></code></pre><p>Our first test will be a sanity check for simple cases:</p><pre><code><code>from codex.search.linear import find

def test_simple_list():
    assert find(1, [1,2,3]) is True
    assert find(2, [1,2,3]) is True
    assert find(3, [1,2,3]) is True
    assert find(4, [1,2,3]) is False</code></code></pre><h3><strong>Analyzing Linear Search</strong></h3><p>Once we have an implementation, we must subject it to the three-step analysis established in our foundations.</p><h4>Is it correct?</h4><p>The property of <strong>correctness</strong> ensures that for any valid input, the algorithm produces the expected output. For linear search, we can verify this through three increasingly formal lenses:</p><ul><li><p><strong>The Exhaustive Argument</strong>: Suppose an element x exists in the sequence. By definition, there is some index i such that <code>items[i] == x</code>. Since the algorithm performs an equality test over every single index in the sequence without exception, it is logically impossible to miss the item if it is there.</p></li><li><p><strong>The Inductive Argument</strong>: We can reason about the algorithm&#8217;s correctness across different input sizes. For a sequence of length 0, the loop never executes, and the algorithm correctly returns <code>False</code>. Assume the algorithm works for a sequence of length n. For a sequence of length n+1, the target x is either in the first n elements&#8212;where the inductive hypothesis ensures we find it&#8212;or it is the n+1-th element, which we check in the final iteration. If it is in neither, the algorithm correctly concludes it is not present.</p></li><li><p><strong>The Loop Invariant</strong>: We can define a formal invariant for the <code>for</code> loop: <em>At the start of iteration i, the element x has not been found in the first i&#8722;1 elements of the sequence.</em> By the time the loop completes at iteration n, if the function hasn&#8217;t returned <code>True</code>, we know with certainty that x is not in the first n elements, which constitutes the entire sequence.</p></li></ul><h4>How efficient is it?</h4><p>We analyze linear search using the <strong>RAM model</strong>, assuming each comparison and iteration step has a unitary cost.</p><ul><li><p><strong>Time Complexity</strong>: In the worst-case scenario (the item is at the very end or not present at all), we must perform n comparisons for a sequence of size n. This gives us a growth rate of O(n), or <strong>linear time</strong>.</p></li><li><p><strong>Space Complexity</strong>: The algorithm only requires a constant amount of extra memory to store the loop variable and the target, regardless of the input size, resulting in O(1) <strong>space complexity</strong>.</p></li></ul><h4>Is it optimal?</h4><p>Intuitively, linear search must be <strong>optimal for unstructured data</strong>. If we know <em>nothing</em> about the order or distribution of the elements, we are mathematically forced to look at every single item at least once to be certain x is not there. Any algorithm that skipped an element could be &#8220;fooled&#8221; if that specific element happened to be the one we were looking for. Thus, for a generic sequence, O(n) is the best possible lower bound.</p><p>To prove this more formally, we employ an <strong>adversarial argument</strong>, a powerful technique in complexity theory where we imagine a game between our algorithm and a malicious adversary.</p><ul><li><p><strong>The Adversary&#8217;s Strategy</strong>: Suppose an algorithm claims to find an element x (or prove its absence) by examining fewer than n elements&#8212;say, n&#8722;1 elements. The adversary waits for the algorithm to finish its n&#8722;1 checks.</p></li><li><p><strong>The &#8220;Trap&#8221;</strong>: Because there is one element the algorithm did not inspect, the adversary is free to define that specific element as x if the algorithm concludes &#8220;False,&#8221; or as something other than x if the algorithm concludes &#8220;True&#8221; without having seen it.</p></li><li><p><strong>The Conclusion</strong>: Since the adversary can always change the unexamined element to make the algorithm&#8217;s answer wrong, any correct algorithm <em>must</em> inspect every element in the worst case.</p></li></ul><p>This proves that the lower bound for searching an unstructured sequence is &#937;(n). Linear search, which operates in O(n), meets this lower bound exactly, making it a <strong>tightly optimal</strong> solution for the problem as defined. Unless we possess more information about the data&#8217;s structure&#8212;the central theme of the next chapter&#8212;we simply cannot do better.</p><h2><strong>Indexing and Counting</strong></h2><p>The <code>find</code> method is good to know if an element exists in a sequence, but it doesn&#8217;t tell us <em>where</em>. We can easily extend it to return an <em>index</em>. We thus define the <code>index</code> method, with the following condition: if <code>index(x,l) == i</code> then <code>l[i] == x</code>. That is, <code>index</code> returns the <strong>first</strong> index where we can find a given element <code>x</code>.</p><pre><code><code>def index[T](x: T, items: Sequence[T]) -&gt; int | None:
    for i,y in enumerate(items):
        if x == y:
            return i

    return None</code></code></pre><p>When the item is not present in the sequence, we return <code>None</code>. We could raise an exception instead, but that would force a lot of defensive programming.</p><p>Let&#8217;s write some tests!</p><pre><code><code>from codex.search.linear import index

def test_index():
    assert index(1, [1,2,3]) == 0
    assert index(2, [1,2,3]) == 1
    assert index(3, [1,2,3]) == 2
    assert index(4, [1,2,3]) is None</code></code></pre><p>As a final step in the linear search paradigm, let&#8217;s consider the problem of finding not the first, but <em>all</em> occurrences of a given item. We&#8217;ll call this function <code>count</code>. It will return the number of occurrences of some item <code>x</code> in a sequence.</p><pre><code><code>def count[T](x: T, items: Sequence[T]) -&gt; int:
    c = 0

    for y in items:
        if x == y:
            c += 1

    return c</code></code></pre><p>Let&#8217;s write some simple tests for this method.</p><pre><code><code>from codex.search.linear import count

def test_index():
    assert count(1, [1,2,3]) == 1
    assert count(2, [1,2,2]) == 2
    assert count(4, [1,2,3]) == 0</code></code></pre><h3><strong>Analysis</strong></h3><p>We won&#8217;t dwell too much in this section since the analysis is very similar to linear search&#8211;these are just specialized versions of it. Once more, we have O(n) algorithms (with O(1) memory cost) for a problem that is provable &#937;(n). Thus, given our assumptions (that there is no intrinsic structure to the elements order), we have optimal algorithms.</p><h2><strong>Min and Max</strong></h2><p>Let&#8217;s now move to a slightly different problem. Instead of finding one specific element, we want to find the element that ranks minimum or maximum. Consider a sequence of numbers in an arbitrary order. We define the minimum (maximum) element as the element <code>x</code> such as <code>x &lt;= y</code> (<code>x &gt;= y</code>) for all <code>y</code> in the sequence.</p><p>Now, instead of numbers, consider some arbitrary total ordering function <code>f</code>, such that <code>f(x,y) &lt;= 0</code> if and only if <code>x &lt;= y</code>. This allows us to extend the notion of minimum and maximum to arbitrary data types.</p><p>Let&#8217;s formalize this notion as a Python type alias. We will define an <code>Ordering</code> as a function that has this signature:</p><pre><code><code>from typing import Callable

type Ordering[T] = Callable[[T,T], int]</code></code></pre><p>Now, to make things simple for the simplest cases, let&#8217;s define a default ordering function that just delegates to the items own <code>&lt;=</code> implementation. This way we don&#8217;t have to reinvent the wheel with numbers, strings, and all other natively comparable items.</p><pre><code><code>def default_order(x, y):
    if x &lt; y:
        return -1
    elif x == y:
        return 0
    else:
        return 1</code></code></pre><p>Let&#8217;s write the <code>minimum</code> method using this convention. Since we have no knowledge of the structure of the sequence other than it supports partial ordering, we have to test all possible items, like before. But now, instead of returning as soon as we find the correct item, we simply store the minimum item we&#8217;ve seen so far, and return at the end of the <code>for</code> loop. This guarantees we have seen all the items, and thus the minimum among them must be the one we have marked.</p><pre><code><code>from codex.types import Ordering, default_order

def minimum[T](items: Sequence[T], f: Ordering[T] = default_order) -&gt; T:
    m = items[0]

    for x in items:
        if f(x,m) &lt;= 0:
            m = x

    return m</code></code></pre><p>The <code>minimum</code> method can fail only if the <code>items</code> sequence is empty. In the same manner, we can implement <code>maximum</code>. But instead of coding another method with the same functionality, which is not very DRY, we can leverage the fact that we are passing an ordering function that we can manipulate.</p><p>Consider an arbitrary ordering function <code>f</code> such <code>f(x,y) &lt;= 0</code>. This means by definition that <code>x &lt;= y</code>. Now we want to define another function <code>g</code> such that <code>g(y,x) &lt;= 0</code>, that is, it <em>inverts</em> the result of <code>f</code>. We can do this very simply by swapping the inputs in <code>f</code>.</p><pre><code><code>def maximum[T](items: Sequence[T], f: Ordering[T] = default_order) -&gt; T:
    return minimum(items, lambda x,y: f(y,x))</code></code></pre><p>We can easily code a couple of test methods for this new functionality.</p><pre><code><code>from codex.search.linear import minimum, maximum

def test_minmax():
    items = [4,2,6,5,7,1,0]

    assert minimum(items) == 0
    assert maximum(items) == 7</code></code></pre><p>The correctness, cost, and optimality analysis is very similar in these cases as well.</p><h2><strong>Conclusion</strong></h2><p>Linear search is a powerful paradigm precisely because it is universal. Whether we are checking for the existence of an item, finding its index, or identifying the minimum or maximum element in a collection, the exhaustive approach provides absolute certainty. No matter the nature of the data, if we test every single element and skim through every possibility, the problem will be solved.</p><p>The primary drawback of this certainty is the cost: some search spaces are simply too vast to be traversed one item at a time. To achieve better performance, we must move beyond the assumption of an unstructured sequence. We need to know more about the search space and impose some level of structure.</p><p>In our next chapter, we will explore the most straightforward structure we can impose: <strong>order</strong>. We will see how knowing the relative position of items allows us to implement what is arguably <em>the most efficient and beautiful algorithm ever designed</em>.</p><p>But that&#8217;s a story for another Tuesday.</p><blockquote><p><em>Remember you can read the <a href="https://matcom.github.io/codex">full draft online</a> (currently Part 1) or get a <a href="https://store.apiad.net/l/codex">beautifully typed PDF</a> if you want to support the project.</em></p></blockquote>]]></content:encoded></item><item><title><![CDATA[Introducing GenSIE 2026]]></title><description><![CDATA[An invitation for researchers and developers interested in Agentic AI]]></description><link>https://blog.apiad.net/p/introducing-gensie-2026</link><guid isPermaLink="false">https://blog.apiad.net/p/introducing-gensie-2026</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 26 Jan 2026 19:32:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cjJH!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45cb08f7-6072-414b-a0ec-a0c57c53b74c_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have built an &#8220;Agentic&#8221; workflow recently, you know the pain. You prompt a model to perform a complex action&#8212;maybe scraping a website, analyzing a contract, or controlling a robot&#8212;and you ask it to return the result in a nice, clean JSON format so your Python code can parse it.</p><p>And it works. Mostly.</p><p>Until it doesn&#8217;t. Until the model decides to wrap the JSON in markdown backticks. Or adds a &#8220;Here is your data&#8221; preamble. Or hallucinates a field that doesn&#8217;t exist in your Pydantic model. Or, worse, it just makes up a fact that sounds plausible but isn&#8217;t there.</p><p>We are entering the era of agentic AI, where models talk to machines, not just humans. And machines speak in protocols. If we want AI that is reliable, robust, and <em>affordable</em>, we cannot rely on massive 100B+ parameter models for every single function call. We need <strong>Small Language Models (SLMs)</strong> that can speak general-purpose structured data fluently.</p><p>That is why, together with my colleagues at the University of Havana and the University of Alicante, we are launching <strong>GenSIE: General-purpose Schema-guided Information Extraction</strong> at <a href="https://sites.google.com/view/iberlef-2026">IberLEF 2026</a>.</p><h2>The Challenge: Zero-Shot Structure</h2><p>Most information extraction tasks are &#8220;fixed.&#8221; You train a model to find <code>PERSON</code>, <code>ORG</code>, and <code>DATE</code>. You show it thousands of examples. It learns. GenSIE is different, it is a <strong>Zero-Shot Schema</strong> task.</p><p>At inference time, your system receives a text and a <strong>schema it has never seen before</strong>. It might be a legal verdict today, a recipe tomorrow, and a chemical compound specification next week. Your system must read the schema definition (provided as a JSON Schema), understand the semantic constraints (like &#8220;extract the verdict, but map it to <code>POSITIVE</code> or <code>NEGATIVE</code>&#8220;), and generate valid, grounded JSON. </p><p>There is a hallucination trap built-in also&#8212;sometimes we&#8217;ll squeeze a field that can be answered by the model&#8217;s original training data, but is not explicitly answered in the input text, so your system has to output <code>null</code> for it.</p><p>Oh, and there is a catch: <strong>No Fine-Tuning allowed</strong>.</p><h2>Why This Matters (and Why It&#8217;s Hard)</h2><p>We explicitly designed this task to close an existing innovation gap. Huge models like GPT-5 or Gemini 3 Pro can often brute-force this problem through sheer scale. But running a 1TB model just to parse a date is sustainable neither economically nor ecologically.</p><p>GenSIE challenges you to use <strong>Small, Open-Weights Models</strong> (like Llama 3 8B, Qwen 14B, or Salamandra 2B). To make these smaller models perform at a high level, you can&#8217;t just throw compute at the problem. You need <strong>Inference-Time Engineering</strong>:</p><ul><li><p><strong>Constrained Decoding:</strong> Forcing the token sampling to obey a grammar.</p></li><li><p><strong>Chain-of-Thought:</strong> Letting the model reason about the schema before outputting the JSON.</p></li><li><p><strong>Self-Correction:</strong> Catching validation errors and asking the model to fix them in a loop.</p></li></ul><p>This is the real challenge. You cannot just throw more compute at the problem and hope it will get solved by an expensive API call. You&#8217;ll have to work around these hard technical constraints to achieve something that works as good a Gemini or ChatGPT but runs on commodity hardware.</p><h2>Join Us</h2><p>We are building a dataset of <strong>1,000 human-curated examples</strong>&#8212;rigorously checked to punish hallucinations and reward precision.</p><p>The timeline is tight, but the barrier to entry is low. On March 1st we&#8217;ll release a first batch of annotated examples and a startup kit with Docker templates and a baseline implementation. Then you have until May 8th to submit a working system. All participants (either solo or in teams) have the option to submit a system paper that will be peer-reviewed and indexed in Scopus.</p><p>If you are interested in the future of reliable AI agents, structured generation, or just want to test your engineering skills against a hard benchmark, I invite you to participate. This is particularly interesting for master or PhD students in search of interesting, open research problems.</p><p>Check out the official site, and leave me a comment if you want to know more.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://uhgia.org/gensie&quot;,&quot;text&quot;:&quot;Official GenSIE Website&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://uhgia.org/gensie"><span>Official GenSIE Website</span></a></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Part I of The Algorithm Codex]]></title><link>https://blog.apiad.net/p/part-i-of-the-algorithm-codex</link><guid isPermaLink="false">https://blog.apiad.net/p/part-i-of-the-algorithm-codex</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 19 Jan 2026 09:54:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cjJH!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45cb08f7-6072-414b-a0ec-a0c57c53b74c_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The first draft of <strong>Part I: Searching and Sorting</strong> for <em>The Algorithm Codex</em> is now officially ready for readers. This opening section establishes the foundational theme of the book: that structure is the primary driver of computational efficiency. You can read the full draft online at <a href="https://matcom.github.io/codex/">matcom.github.io/codex</a> or download a high-quality PDF version on <a href="https://store.apiad.net/l/codex">Gumroad</a> on a pay-what-you-want basis to support the project.</p><p>This draft encompasses 19 distinct algorithms across six chapters, focusing on the core logic and intuitions that make them work:</p><ul><li><p><strong>Basic Search</strong>: Establishing the universal but expensive baseline of searching through unstructured sequences.</p></li><li><p><strong>Efficient Search</strong>: Introducing the &#8220;magical&#8221; scaling of binary search and bisection, extending these concepts to abstract decision spaces through predicate search.</p></li><li><p><strong>Basic Sorting</strong>: Analyzing the &#8220;geometry of inversions&#8221; to understand why simple approaches like Selection and Insertion sort are naturally bounded by a quadratic ceiling.</p></li><li><p><strong>Efficient Sorting</strong>: Breaking the quadratic barrier with the recursive divide-and-conquer strategies of Merge Sort and Quick Sort.</p></li><li><p><strong>Rankings and Selection</strong>: Implementing strategies for order statistics, including the randomized Quick Select and the deterministic &#8220;Median of Medians&#8221;.</p></li><li><p><strong>Linear Time Sorting</strong>: Bypassing the theoretical limits of comparison-based algorithms by exploiting domain-specific constraints with Counting and Radix sort.</p></li></ul><p>While the core content is ready, the <em>Codex</em> remains a work in progress. I plan to further extend the explanations with more intuitive descriptions and perform deeper analysis without getting into overly formal proofs. I am also considering the addition of a few more clever algorithms to round out this section before moving on to Part II on foundational data structures (lists, queues, stacks, and dictionaries).</p><p>Over the next two weeks, I will be adapting several chapters into articles for <em>The Computist Journal</em>. These posts will focus specifically on the beauty and underlying intuitions of binary search and efficient sorting, rather than the nitty-gritty implementation details.</p><p>If you find this resource valuable, please consider supporting the ongoing development of the remaining seven parts by getting the PDF on Gumroad. Your support directly enables the continuation of this project.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://store.apiad.net/l/codex&quot;,&quot;text&quot;:&quot;Support The Algorithm Codex&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://store.apiad.net/l/codex"><span>Support The Algorithm Codex</span></a></p>]]></content:encoded></item><item><title><![CDATA[How to Train your Chatbot - Chapter One]]></title><description><![CDATA[Hello AI]]></description><link>https://blog.apiad.net/p/how-to-train-your-chatbot-chapter-4d8</link><guid isPermaLink="false">https://blog.apiad.net/p/how-to-train-your-chatbot-chapter-4d8</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Wed, 22 Oct 2025 11:01:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ydor!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>The following is the first draft of Chapter 1 of <strong>How to Train your Chatbot</strong>, a developer-centric book I&#8217;m writing in public. Subscribe to get all chapters in your inbox, as they come out, for free.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ydor!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ydor!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 424w, https://substackcdn.com/image/fetch/$s_!ydor!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 848w, https://substackcdn.com/image/fetch/$s_!ydor!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 1272w, https://substackcdn.com/image/fetch/$s_!ydor!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ydor!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png" width="1456" height="683" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:683,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82009,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/176766314?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ydor!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 424w, https://substackcdn.com/image/fetch/$s_!ydor!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 848w, https://substackcdn.com/image/fetch/$s_!ydor!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 1272w, https://substackcdn.com/image/fetch/$s_!ydor!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb1006c3-cc3a-4419-8b64-350f8977683b_1538x721.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In this first chapter we will build a bare-bones conversational application&#8211;a chatbot&#8211;that is capable of maintaining a coherent conversation with a single user. We will learn how to connect with an LLM provider, pass messages, and stream the responses back.</p><p>In this chapter we won&#8217;t use any additional libraries beyond the Python <a href="https://github.com/openai/openai">OpenAI</a> wrapper to communicate with an LLM backend. This is on purpose, so we understand the whole lifecycle of a typical conversation.</p><p>Then, in the next chapter, we will redo the core logic using <a href="https://github.com/gia-uh/argo">ARGO</a> to simplify the chat management and introduce some modularization, and we will add a database to store conversations and make our interactions persistent.</p><p>The whole source for this chapter (and this entire book) can be found in this <a href="https://github.com/apiad/chatbot">Github repository</a>. This specific chapter is in the <a href="https://github.com/apiad/chatbot/blob/main/basic.py">basic.py</a> file.</p><h2>Setting up the environment</h2><p>The first step is to setup a development environment. At the moment, the community preferred way of doing this with Python is using a package and virtual environment manager, and by far the best in the world is <a href="">uv</a>. So let&#8217;s begin by installing thatl, and once ready, just run:</p><pre><code><code>mkdir chatbot # or whichever folder name you prefer
cd chatbot
uv init .</code></code></pre><p>This will create a <code>pyproject.toml</code> file in the <code>chatbot</code> folder. Now let&#8217;s add <code>streamlit</code> and <code>openai</code>.</p><pre><code><code>uv add streamlit openai</code></code></pre><p>After a few seconds downloading the necessary packages, you will be ready to start coding.</p><h3>Get the source code</h3><p>The complete source code for this book is available in <a href="https://github.com/apiad/chatbots">Github</a> with an MIT license. Feel free to fork it and use it as you see fit.</p><h2>The basic chatbot</h2><p>Let&#8217;s start outlining our first chatbot. We will use Streamlit&#8217;s builtin functionality to create a pretty decent chatbot UI in less than 10 lines of code. Here is the boilerplate code.</p><pre><code><code>import streamlit as st

# import and setup OpenAI
&lt;&lt;setup_openai&gt;&gt;

# regenerate the conversation history
&lt;&lt;render_history&gt;&gt;

msg = st.chat_input()

if not msg:
    st.stop()

with st.chat_message(&#8221;user&#8221;):
    st.write(msg)

# code to actually call the LLM
&lt;&lt;invoke_llm&gt;&gt;

with st.chat_message(&#8221;assistant&#8221;):
    response = st.write(reply(msg))

# save the llm response
&lt;&lt;save_llm_response&gt;&gt;</code></code></pre><h3>About Literate Programming</h3><p>You may have noticed the code above uses a somewhat weird notation, like <code>&lt;&lt;setup_openai&gt;&gt;</code>, which is not valid Python. This is because we are using a tool called <code>illiterate</code> to generate automatically the source code from the book content. This notation is how we reference snippets of code that will be defined later on.</p><p>This is a paradigm called <em>Literate Programming</em> that emphasizes narrative documentatiopn as the primary source, and the actual source is derived automatically, so that your documentation (this book) and your code are always synchronized</p><p>Don&#8217;t worry too much about it, just know that all these snippets will be explained in the remainder of this chapter. You&#8217;ll get the hang of it.</p><p>This completes the basic layout of a typical chatbot app. We still haven&#8217;t specified how to actually call the LLM provider, and how to manage the conversation history. We will do that in the remaining of this chapter.</p><h2>Setting up an LLM provider</h2><p>For all practical purposes, when you code a chatbot you consider the LLM as a black-box component that is, almost always, encapsulated behind a REST API. This lets you focus on building the app and forget about the extremely complex problem of serving, scaling, and monitoring an LLM in a production environment.</p><p>This is pretty much the same as when you use a production database, like MySQL, Postgre, or MongoDB, you almost never directly run the database server daemon. Instead, you almost certainly provision a managed server from Amazon, Azure, Google, or any other of the myriad cloud providers. In the same way, most of the time you won&#8217;t run an LLM locally, but instead use a cloud provider which has far better infrastructure.</p><p>By far, the most common API design used by all cloud LLM providers is the OpenAI API, which has become a de-facto standard. So, even if you don&#8217;t use OpenAI&#8217;s models directly (like GPT-5), you will almost certainly use the <code>opena</code> Python package to communicate with your LLM cloud provider, whoever they are.</p><p>In this book I will recommend <a href="https://openrouter.ai">OpenRouter</a>, because they provide access to a huge range of models, and they deal with automatically routing to the optimal cloud provider. But you are free to choose whatever LLM provider you prefer, and as long as they give you an OpenAI-compatible API (and they all do), the rest of this book will work exactly the same for you.</p><p>Whatever you choose, you will need three things from your LLM provider:</p><ul><li><p>A base URL (e.g., <code>https://openrouter.ai/api/v1</code>)</p></li><li><p>An API key (e.g., <code>sk-12345678...</code>)</p></li><li><p>A model identifier (e.g., <code>meta-llama/llama-4-maverick:free</code>)</p></li></ul><p>Of these three, the API Key is the most important to keep safe (and secret) because it&#8217;s what the LLM provider will use to identify you (and thus charge you) for using their service. If this API Key ends up in a Github repository or somewhere public, someone might use it to impersonate you and thus deplete your credits or put you in a significant debt with your LLM provider.</p><div class="pullquote"><p>TL;DR: Keep your API Key secret!</p></div><p>Once you have identified your LLM provider, you need to provide <code>streamlit</code> with the credentials and metadata indicated above. The best way to do this for a <code>streamlit</code> app is to use its native secrets management. You need to create a <code>.streamlit</code> folder, and place a <code>secrets.toml</code> file inside. There you can add the entries just as if they where environment variables.</p><pre><code><code># .streamlit/secrets.toml
api_key=&#8221;sk-12345678&#8221;
base_url=&#8221;https://openrouter.ai/api/v1&#8221;
model=&#8221;meta-llama/llama-4-maverick:free&#8221;</code></code></pre><p>Needless to say, this <code>secrets.toml</code> file should be the first line in your <code>.gitignore</code>!</p><p>For the purpose of development and testing, you can use one of the many free models OpenRouter hosts, which give you enough daily uses for development purposes. Read more on the <a href="https://openrouter.ai/docs/faq#how-are-rate-limits-calculated">OpenRouter documentation</a>. As a perhaps obvious disclaimer, I&#8217;m not associated with OpenRouter nor any part of this book is supported by them. I just think they provide a wonderful service for the AI developer community.</p><p>Once you&#8217;ve gotten your API key, you can use it to create an OpenAI client to interact with the LLM.</p><pre><code><code>from openai import OpenAI

client = OpenAI(
    base_url=st.secrets.base_url,
    api_key=st.secrets.api_key,
)</code></code></pre><p>And we&#8217;re ready to start sending some messages.</p><h2>Calling the LLM</h2><p>OpenAI&#8217;s client makes it very easy to invoke an LLM. We just need to pass in a list of messages and stream the result back to our Streamlit app. But before, we need to understand one key thing about LLM providers, and is the fact that they are <strong>stateless</strong>. This means you <em>always have to pass the whole conversation</em> to the API, because neither the actual model nor the server itself &#8220;remember&#8221; the conversation.</p><p>So, we will need to store the conversation in our application state. I&#8217;ll show you how to do this in just a second, but let&#8217;s assume we have it and move on to actually talking to the LLM.</p><p>In our app, we need to include the following method, which will stream the response from the LLM to the user.</p><pre><code><code>&lt;&lt;store_conversation&gt;&gt;

def reply(msg: str):
    # retrieve the conversation list
    conversation = st.session_state.conversation

    for chunk in client.chat.completions.create(
        model=st.secrets.model,
        messages=conversation,
        stream=True,
    ):
        if msg := chunk.choices[0].delta.content:
            yield msg</code></code></pre><p>That&#8217;s it. We just invoke <code>client.chat.completions.create</code> with the model name and conversation (a list of messages we will see right away) and stream the results back in the form of a Python generator. The <code>st.write</code> method in our main app is smart enough to produce a pretty typewriter animation as we receive the response one chunk at a time.</p><p>The conversation is actually stored in Streamlit&#8217;s builtin session storage, which is a dictionary-like object unique to each user session. This means that as long as we stay in the same browser tab, the conversation persists. Once we open another tab or hit F5, the session storage is cleaned up and we have a new conversation. Kind of brute, but for now it will do.</p><p>And this is the missing snippet to actually keep track of the conversation. We need to store the user message just after we get from the chat input widget:</p><pre><code><code>if &#8220;conversation&#8221; not in st.session_state:
    st.session_state.conversation = []

st.session_state.conversation.append(dict(role=&#8221;user&#8221;, content=msg))</code></code></pre><p>And we need to store the assistant reply message in the main loop after streaming it. Fortunately, the <code>st.write</code> method is smart enough so that it not only prints the message as it is streamed, but also returns the full concatenated message once the stream is closed.</p><pre><code><code>st.session_state.conversation.append(
    dict(role=&#8221;assistant&#8221;, content=response)
)</code></code></pre><p>As you&#8217;ve seen, we&#8217;re storing the messages in the format that the OpenAI API expects. This is a list of dictionaries, where each dictionary contains a <code>role</code> and a <code>content</code> key. The role (for now) can be either <code>user</code> or <code>assistant</code>, and it&#8217;s important we respect it because the models are trained to behave as if they are replying from the &#8220;assistant&#8221; perspective.</p><h2>Rebuilding the conversation history</h2><p>The final step to make our app a fully-fledged chatbot is to actually render the whole conversation in the UI. This is necessary because Streamlit works with an immediate execution paradigm, which effectively means every interaction cleans the whole screen and redraws the entire UI. Thus, every time the user enters a message, the UI restarts and we lose the previously rendered interaction.</p><p>Fortunately, as we have the conversation stored in the session state, we can simply iterate over it and render each message.</p><pre><code><code>for msg in st.session_state.get(&#8217;conversation&#8217;, []):
    with st.chat_message(msg[&#8217;role&#8217;]):
        st.write(msg[&#8217;content&#8217;])</code></code></pre><p>We just need a bit of care the first time we run the app because the <code>conversation</code> key won&#8217;t exist, that&#8217;s why we use <code>get</code> instead of a direct indexer.</p><h2>Closing up</h2><p>Phew! There you go, in around 50 lines of code (including comments and whitespace) we have a fully working chatbot. It&#8217;s very crude for now, all it does is send some messages to an LLM provider and stream the response back, but this is the foundation we can build upon to create all sorts of cool stuff.</p><p>In the next chapter we will rebuild this application from scratch but using an LLM framework that will give us much more flexibility later on.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[How to Train your Chatbot - Chapter Zero]]></title><description><![CDATA[Announcing a new series on building practical AI applications]]></description><link>https://blog.apiad.net/p/how-to-train-your-chatbot-chapter</link><guid isPermaLink="false">https://blog.apiad.net/p/how-to-train-your-chatbot-chapter</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 20 Oct 2025 11:22:51 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><strong>TL;DR:</strong> In this upcoming series, we'll build an autonomous LLM-based agent from scratch, focusing on the fundamentals. We'll go from zero to fully autonomous deep research, one feature at a time, staying as close to the metal as possible. Subscribe to receive all posts in your inbox for free.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="5472" height="3648" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3648,&quot;width&quot;:5472,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;three crumpled yellow papers on green surface surrounded by yellow lined papers&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="three crumpled yellow papers on green surface surrounded by yellow lined papers" title="three crumpled yellow papers on green surface surrounded by yellow lined papers" srcset="https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1577563908411-5077b6dc7624?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxjaGF0fGVufDB8fHx8MTc2MDg5NDczNXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@lunarts">Volodymyr Hryshchenko</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Have you looked at the latest AI breakthroughs&#8212;agents that can plan, code, and research&#8212;and thought, "That's cool and all but&#8230; how the hell do one actually build something like that?"</p><p>If you're just getting started with Python development, or even if you&#8217;re a seasoned expert, you've probably felt this gap too. Maybe you've played with the demos, called an API or two, and seen just enough of the magic to get excited.</p><p>But moving from just prompting an LLM to building intelligent, autonomous agents is a different beast entirely. Things start to break pretty soon as you stack up more and more complexity. And it seems every tutorial out there is oblivious to this. Anyone can show you how to code a simple chatbot maybe with some RAG, but very few resources take you from that humble beginnings to something that actually looks like a modern multi-agent system that works autonomously.</p><p>This series is my answer to that problem.</p><p>Over the next 10 or so posts&#8212;the next couple of months&#8212;we are going to build a comprehensive, autonomous AI agent. And we're going to do it incrementally.</p><p>We'll start with the simplest possible application: the basic chatbot loop that stores conversation history. Why? Well, because even if everyone has already seen this, we need proper foundations to build on top. We need to see the full flow&#8212;how messages are stored, how a system prompt works, how to interact with the LLM, and what's really happening under the hood.</p><p>But from there we'll level up pretty fast, adding new capabilities piece by piece until we have a personal agent capable of doing deep research and generate long, coherent reports all on its own.</p><h2>Tools of the Trade</h2><p>Now, you might be thinking, why build from scratch and not just use LangChain, or LlamaIndex, or some other well known framework?</p><p>Here's my thesis. When you start with those massive, professional frameworks, you learn more about the framework than you do about the fundamentals. Those tools are powerful, but they are also black boxes. They hide the logic and complexity behind layers of abstraction. You end up learning their way of doing things, their design patterns, and their API. And frameworks change, but fundamentals remain.</p><p>I believe it's far more valuable to learn how to do things as close to the metal as possible. Our goal is to write the least amount of boilerplate code possible, but&#8212;and this is the key&#8212;without hiding the any of core logic. We will build this entire thing ourselves, understanding every single piece of the puzzle.</p><p>We'll use a tech stack that gets out of our way. Only three tools:</p><p>For the UI we&#8217;ll use <strong>Streamlit</strong>. For those who don&#8217;t know it, Streamlit is a radically simple application framework that turns any Python script into a fully fledged web app. The key here is what it doesn't make you do: you write zero boilerplate, zero layout, zero presentation, and zero state management code. You just write core logic, and a good enough web UI appears.</p><p>For the actual chatbot we'll use the <strong>ARGO</strong> framework. I like to call it FastAPI for AI agents because it has that same lightweight, decorator-based feel. It is the simplest, most Pythonic agent framework you&#8217;ve seen, and I promise you&#8217;ll fall in love with it. Despite being super simple, it forces you to build the logic using a very clean and lightweight skills-based pattern for modularizing our agent's intelligence.</p><p>And for data we&#8217;ll <strong>BeaverDB</strong>. This is a lightweight wrapper on top of SQLite that gives you everything you could ever want from a modern database. But it is not a modern database server. It's just a single, embedded binary file on your disk. There's no Docker, no authentication, no connection strings, no schemas, no boilerplate. Yet, on top of this simplicity, BeaverDB gives us a very Pythonic, very comfortable API for a document database with vector and full text search, and cool features like persistent dictionaries, lists, priority queues, and even pub/sub. It makes building something like a RAG pipeline extremely simple without any complex setup.</p><p>That's it. With just these three basic libraries, we will build a fully autonomous agent.</p><h2>The Journey Ahead</h2><p>Here's the roadmap I've thought so far. We'll add a new level of intelligence in each post.</p><ul><li><p><strong>Level 0: The Conversationalist</strong>. The foundation. A simple Streamlit UI with a stateful chat loop that understands conversation history and system prompts.</p></li><li><p> <strong>Level 1: The Assistant</strong>. We'll give our agent long-term memory. We'll build a full Retrieval-Augmented Generation (RAG) pipeline from scratch using BeaverDB so it can answer questions based on a private knowledge base.</p></li><li><p><strong>Level 2: The Researcher</strong>. We'll break our agent out of its box and give it a tool to search the web, allowing it to access up-to-the-minute information.</p></li><li><p><strong>Level 3: The Analyst</strong>. This is where it gets really fun. We'll give the agent the ability to write and execute its own Python code in a safe sandbox to solve problems, analyze data, and even debug its own mistakes.</p></li><li><p><strong>Level 4: The Editor</strong>. We'll upgrade our UI from a simple chat to a persistent, shared canvas, where you and the agent can work side-by-side to co-write documents and refine plans.</p></li><li><p><strong>Level 5: The Scientist</strong>. The finale. We'll integrate all previous capabilities, giving the agent a master prompt that allows it to take a high-level goal, create a multi-step plan, and execute it independently from start to finish.</p></li></ul><p>So here is my promise. It may take us more than 5 posts, but we are going to learn how to build a chatbot by building a chatbot, step by step. No unnecessary theory, just working code and clear intuitions. </p><p>I'll be providing a public GitHub repository with all the code, and each post will be a direct, straightforward Python script that you can type in half an hour.</p><p>When you're done with this series, it won't matter if you want to keep using ARGO and BeaverDB&#8212;which I hope you will&#8212;or move on to more &#8220;professional&#8221; tools. You'll be ready anyway, because these tools are so thin, what you'll have learned isn't how to use a specific library. You will have learned how the logic works underneath.</p><p>You'll understand how to orchestrate the different components involved in a typical chatbot, how to modularize the agent's brain, and more generally, how to think about building capable, intelligent, LLM-based applications. That's the real takeaway.</p><p>In the next post, we'll jump straight into <strong>Level 0: The Conversationalist</strong>. So hit that subscribe button and you'll get all posts in your inbox, 100% free.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><p>Ready to build?</p>]]></content:encoded></item><item><title><![CDATA[How I Built the Database of my Dreams]]></title><description><![CDATA[And how you can use it to build AI apps 100x faster]]></description><link>https://blog.apiad.net/p/how-i-built-the-database-of-my-dreams</link><guid isPermaLink="false">https://blog.apiad.net/p/how-i-built-the-database-of-my-dreams</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Sat, 20 Sep 2025 21:27:31 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6363" height="4242" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4242,&quot;width&quot;:6363,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;a bug on a log in the water&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="a bug on a log in the water" title="a bug on a log in the water" srcset="https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1656528181090-0808f91fa894?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxiZWF2ZXJzfGVufDB8fHx8MTc1ODQwMzM2Mnww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@derekotway">Derek Otway</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>I really love to code. That much I&#8217;m sure you can tell. So I have this very common problem, that I&#8217;m sure many of you have. A gazillion ideas in my head wanting to burst out, and very little time to try them all. To try and get around this, I like to prototype really fast and dirty implementations of anything that makes me curious, just to see if it leads somewhere. (This is why I have hundreds of unfinished projects in my Github.)</p><p>So, where was I? Yeah, this is one of the reasons I&#8217;ve made Python, and specifically <code>streamlit</code>, a key part of my toolkit. It lets me build the very simplest semblance of an app extremely fast, without considering anything remotely unnecessary (for a first prototype) like authentication and user roles, or worrying about the user interface.</p><p>Now, here is the thing. Recently, I&#8217;ve been building <em>a lot</em> of AI stuff. Chatbots galore for all sorts of tasks from general question answering to specific things like storytelling, evolutionary computation, automated coding, you name it. And I keep finding a specific pain point that grinds me to a halt in the most exciting part of prototyping: <em>the database</em>.</p><p>Very soon, my simple AI prototype needs a vector database for RAG or a key-value store for simple configurations, or a message queue for background tasks and a persistent list to store conversation history. And this is before anything remotely close to a production-ready feature is in the horizon. This is just the second thing you&#8217;ll need right after streaming the first few tokens from OpenAI. It&#8217;s unescapable for anything but the simplest toys.</p><p>So, before I know it, I&#8217;m wrestling with two or three containers&#8212;or worse, juggling credentials for three different cloud services&#8212;and writing boilerplate code just to glue it all together. I&#8217;ve become an accidental, and very grumpy, DevOps engineer. My worst nightmare!</p><p>What I want is something in the spirit of <code>streamlit</code> but for managing data. A minimalistic, no-bullshit database that just supports the basic modalities we all need in modern applications, like, dunno, JSON storage, maybe? And vector search? Yeah, that, but also combined with full-text and fuzzy, persistent lists and queues&#8230; hell, even a decent pub-sub system if we&#8217;re asking! Is it too much to ask for someone to make SQLite but for modern data!</p><p>Well&#8230; I guess if you want something done, you might as well do it yourself.</p><p>So this is the story of how I built BeaverDB&#8212;a Pythonic and multi-modal interface on top of SQLite that just works out of the box for rapid prototyping. And it scales to medium sized projects just fine (SQLite is damn fast!). BeaverDB my attempt to build the tool I wish I had&#8212;a library that provides the high-level data structures modern AI applications need, without sacrificing the radical simplicity of a single .db file.</p><h2>Introducing Beaver</h2><p>The guiding principle behind Beaver&#8217;s API is a minimal surface area with a fluent interface. You only ever need to use two classes. Everything else flows naturally from the main database instance, returning dedicated wrapper objects with a rich, Pythonic interface.</p><p>One key idea for this design is that it should just work. Zero configuration, just sensible out of the box decisions. So, no schema, everything indexed by default, no need to declare tables or entities or anything before hand, collections just get created when first used, you know, that kind of thing. It should be as easy as instantiating a class and calling two methods.</p><p>Let&#8217;s take a quick tour on what you can today with it.</p><p><strong>Key-Value &amp; Caching</strong></p><p>Need to store configuration, user profiles, or cache expensive API calls? The namespaced dictionary is your go-to. It behaves just like a Python dict but is backed by the database, with optional TTL support.</p><pre><code><code># Use a dictionary for caching API calls for 1 hour
api_cache = db.dict("api_cache")
api_cache.set("weather", {"temp": "15C"}, ttl_seconds=3600)
print(f"Cached weather: {api_cache.get('weather')}")</code></code></pre><p><strong>Persistent Lists</strong></p><p>This is the most straightforward way to manage ordered sequences. For a chatbot, it&#8217;s the perfect way to maintain the turn-by-turn history of a conversation. It works like a Redis list, with all bells and whistles, implementing a full Pythonic list interface, but backed in the DB. It supports appending and removing from the head and tail, as well as inserting or removing anywhere in between, blazingly fast, because all operations are indexed.</p><pre><code><code># Manage a conversation with a user
chat_history = db.list("conversation_with_user_123")
chat_history.push({"role": "user", "content": "Hello, Beaver!"})
chat_history.push({"role": "assistant", "content": "Hello! How can I help?"})
print(f"Conversation length: {len(chat_history)}")</code></code></pre><p><strong>Priority Queues</strong></p><p>A priority queue is the essential tool for orchestrating an autonomous agent. It ensures the agent always works on the most critical task first, regardless of when it was added. The API is extremely simplified on purpose. For anything more complicated, use a full-featured list.</p><pre><code><code># An AI agent's task list
tasks = db.queue("agent_tasks")
tasks.put({"action": "summarize_news"}, priority=10)
tasks.put({"action": "respond_to_user"}, priority=1) # Higher priority

# Agent always gets the most important task first
next_task = tasks.get() # -&gt; Returns the "respond_to_user" task</code></code></pre><p><strong>Real-time Pub/Sub</strong></p><p>Need to build a decoupled, event-driven system? The pub/sub channel allows different parts of your application&#8212;or even different processes&#8212;to communicate in real-time. Beautifully designed with an extremely simple fluent API, but extremely performant, thread-safe, even works across different processes. Plus, it comes with an optional <code>async</code> interface if you&#8217;re feeling fancy.</p><pre><code><code># In one process, a monitor publishes an event
system_events = db.channel("system_events")
system_events.publish({"event": "user_login", "user": "alice"})

# In another, a logger subscribes and receives the message
with db.channel("system_events").subscribe() as listener:
    for message in listener.listen(timeout=1):
        print(f"Event received: {message}")</code></code></pre><p><strong>Collections &amp; Hybrid Search</strong></p><p>This is the core component for any Retrieval-Augmented Generation task. It&#8217;s a multi-modal collection of structured documents that understands vectors, and text, allowing you to combine search strategies for the best results. It also performs fuzzy search on demand with a very clever indexing strategy that I&#8217;ll tell you all about in the next section.</p><pre><code><code>from beaver import BeaverDB, Document
from beaver.collections import rerank

docs = db.collection("articles")
doc = Document(embedding=[...], content="Python is fast.")
docs.index(doc, fuzzy=True)

# Combine vector and full-text/fuzzy search for better results
vector_results = docs.search(vector=[0.15, 0.85, 0.15])
text_results = docs.match(query="pthon", fuzziness=1)
best_context = rerank(vector_results, text_results)</code></code></pre><p>And there&#8217;s a lot more. You can connect documents with relations and build a knowledge graph that you can later query to find similar documents or implement graph-based recommender system.</p><p>All in one freaking database file. No docker. No servers. No headache.</p><h2>A Peek Under the Hood</h2><p>Beaver is built on a series of pragmatic design decisions intended to leverage SQLite&#8217;s native capabilities to the fullest, avoiding slow application-layer logic wherever possible.</p><p>For one, it never creates new tables when storing stuff. Everything is stored and indexed in cleverly designed global tables that are created at startup time (only the very first time the DB file is created). This also means you get virtually infinite lists, dicts, queues, collections, etc., because these aren&#8217;t different tables (which would be a pain in the arsenal to maintain). And&#8230; (roll drums)&#8230; no migrations!</p><p>I want to highlight two specific features to tell you a bit about the underlying implementation details, so you can see the lengths it goes to try and be efficient out of the box.</p><p><strong>The Pub/Sub System</strong></p><p>The pub/sub system is the greatest example of efficiency by design. It&#8217;s built on a single, append-only log table with an index for the channel name.</p><p>For each channel, a single background thread polls this table for new entries and fans them out to any number of in-memory queues, one for each subscriber. The key insight here is that because the database is only ever touched by one polling thread per channel, adding a second, third, or hundredth subscriber adds <em>zero</em> additional load to the database. From the database&#8217;s perspective, new listeners are basically free.</p><p>Even better, this polling is only activated when there is at least one listener, and stops immediately after the last listener disconnects. This means we only ever use the minimum resources necessary.</p><p><strong>Hybrid Text Search: The Two-Stage Filter</strong></p><p>The other feature I think is kinda beautiful is the fuzzy search. Calculating the Levenshtein distance between two strings is computationally expensive, and there is no way to build an index beforehand without a combinatorial explosion in storage size. But running it across every document in a collection would be unusable for anything beyond a few thousand entries.</p><p>Beaver solves this with a two-stage process. First, it uses a pre-computed trigram index in SQLite to instantly find a very small set of candidate documents that share a high percentage of 3-character &#8220;chunks&#8221; with the query. This is a very fast SQL query that already gives us a very good approximation to the response. Then, we run the expensive Levenshtein algorithm <em>only</em> on this small, pre-filtered set of candidates in memory.</p><h2>The Vector Store Dilemma</h2><p>Vector storage is my biggest concern at the moment. The current implementation is, I think, pretty good for a typical use case of infrequent indexing and fast retrieval. But it can be much improved.</p><p>Right now, Beaver uses an in-memory k-d tree, which provides excellent search speed once the index is loaded into RAM. However, the index is ephemeral&#8212;it lives and dies with the application process. This creates a significant bottleneck: every time your application starts, the <em>entire</em> index must be rebuilt from scratch by reading all vectors from the database. Furthermore, indexing a new document requires a full, blocking rebuild of the entire tree.</p><p>As long as you index documents in a background process, infrequently&#8212;which is what most RAG-based applications do&#8212;this works just fine. But I&#8217;m not satisfied with this implementation, so here is my plan for its update.</p><p>The roadmap goes through integrating a state-of-the-art ANN library like <code>faiss</code>. This will enable a persistent, on-disk index that can be loaded instantly. But more importantly, we need to support incremental additions, which means newly added documents don&#8217;t rebuild the entire index. The key to achieve this is to have a persistent, large index, and a small, in-memory, temporal index of new additions that gets added to the persistent index from time to time. Keep these two in sync is far from trivial, but I already have a somewhat detailed plan.</p><h2>Conclusion</h2><p>Beaver is the result of a couple of weekends of focused frustration, and it&#8217;s already the backbone of my own AI prototypes. Even if it never reaches any level of maturity, I&#8217;m extremely satisfied with it because it already works for my use cases, and I&#8217;ve learned so much building it. Damn, who knew building databases could be so fun!</p><p>Now, what&#8217;s in it for you? Well, Beaver is not meant to replace your production PostgreSQL cluster. That&#8217;s unthinkable. But it might be the &#8220;good enough&#8221; database that lets you go from an idea to a working prototype in minutes, not hours. So, stop being an accidental DevOps engineer and go build something cool.</p><p>You can get started with a simple <code>pip install beaver-db</code>. Check out the <a href="https://github.com/apiad/beaver">GitHub repository</a> for a lot of examples. And leave me any questions or comments either here or in the repository issues.</p><p>Have fun!</p>]]></content:encoded></item><item><title><![CDATA[The Best Way to Vibe Code is Literate Programming]]></title><description><![CDATA[Reviving One of the Most Obscure Software Development Paradigms for the Age of AI]]></description><link>https://blog.apiad.net/p/the-best-way-to-vibe-code-is-literate</link><guid isPermaLink="false">https://blog.apiad.net/p/the-best-way-to-vibe-code-is-literate</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Thu, 24 Jul 2025 10:01:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6mse!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6mse!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6mse!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!6mse!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!6mse!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!6mse!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6mse!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png" width="724" height="429.875" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:608,&quot;width&quot;:1024,&quot;resizeWidth&quot;:724,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6mse!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!6mse!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!6mse!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!6mse!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a8ab69-7919-4cf0-a6f5-94d94f9d7d2d_1024x608.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"></figcaption></figure></div><p>Literate Programming (LP) is a decades-old, obscure programming philosophy that emphasizes human understanding over mere machine execution by intertwining code and prose. It is the precursor to Jupyter Notebooks, nbdev, Quarto, and many other documentation-friendly coding paradigms. That is, in the same sense as LISP is the precursor to all functional languages, but none lives up to the expressiveness of LISP.</p><p>For a long time, Literate Programming has been mostly a niche paradigm, embraced only by the nerdiest of the nerdiest among us. However, as we incorporate more and more AI into our development, I believe LP is going to make a comeback. I argue that despite its current niche status and challenges, LP, especially when supercharged with specific tooling and advanced AI, is <em>the</em> optimal paradigm for coding with AI.</p><p>In this article, we'll first define Literate Programming and explore its core components, distinguishing it from similar approaches like Jupyter notebooks. Next, we'll tackle the historical reasons why LP hasn't become mainstream. Then, the core of the argument: why Literate Programming is uniquely suited for AI-assisted coding. Finally, we'll look to the future, discussing how targeted AI development and advanced tooling can further enhance LP, concluding with a concrete example of my own work in this space.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Computist Journal! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>What is Literate Programming?</h2><p>Let&#8217;s start from scratch. What exactly <em>is</em> Literate Programming? At its heart, LP fundamentally shifts our perspective on what a program is. Instead of viewing code as a sequence of instructions for a machine, LP frames a program as a continuous, flowing narrative designed for human comprehension. Code snippets embed directly within this prose, illustrating logic and design choices as part of a cohesive story. Think of it this way: the program <em>is</em> the documentation, and, crucially, the documentation <em>is</em> the program.</p><p>If you want to see it for yourself, <a href="https://www.literateprograms.org">here is a massive collection of LP programs</a> in several programming languages.</p><p>This isn't just about adding comments; it's a profound reordering of priorities. Clarity and explanation become the primary goals, with executable code as a byproduct of that clear narrative.</p><p>To truly understand LP, we need to look at its core components. It all begins with the literate source document, a continuous flow of prose with interspersed code snippets. So far, this isn't any different to a typical blog post with explanations and code. But here comes the key twist.</p><p>Within the code snippets in a literate source, you can define <strong>macros</strong>, named placeholders that reference other blocks of code. These macros enable <strong>recursive expansion</strong>. They can reference other macros, allowing a deeply hierarchical and top-down explanation of your program. You can start with a high-level concept, then progressively drill down into details, always maintaining a clear narrative flow.</p><p>Once you craft this human-centric narrative, two key processes come into play: <strong>tangling</strong> and <strong>weaving</strong>. Tangling extracts the executable code from your literate source file, pulling only the parts necessary for the machine to run. Conversely, weaving generates human-readable documentation (like a PDF or HTML file) from the literate source, resulting in a beautifully formatted explanation complete with code examples, all derived directly from the source file.</p><p>If you want to understand LP better (or you are just curious about it), there is a <a href="http://literateprogramming.com/">community website</a> that </p><h3>Isn't This Just &#8230;?</h3><p>You might think, "This sounds a bit like Jupyter notebooks, Quarto documents, or even what Pandoc does." And you'd be right to some extent. Tools like Jupyter notebooks, Quarto, and Pandoc certainly share some principles with Literate Programming, primarily intertwining code and explanatory text. They revolutionized data science and technical communication by allowing code, output, and narrative to coexist in one document.</p><p>However, here is the critical distinction: none of these have fully featured <em>recursive macro expansion</em>. This prevents them from achieving the deep disentangling power central to classical Literate Programming. While they let you intersperse code and text, they do not provide the same hierarchical abstraction and narrative control that LP's macro system offers. This means they do not quite realize the full vision of the <em>program as documentation</em> in the same profound way.</p><p>Crucially, both Jupyter and Quarto (which is ultimately based on Jupyter) require code cells to be executable. This means they need to be complete, correct code. All necessary references must resolve beforehand. Any methods, variables, or symbols used in any cell must exist by the time that cell executes. </p><p>This goes head-to-head with LP's core idea, which forgoes any structural requirements for executing the code in favor of using the best structure to understand the code. So, in LP, you can leave half of a method undefined because its details are not important at that moment.</p><h2>Why Literate Programming Isn't Mainstream</h2><p>Given its elegant philosophy, it's fair to ask: why isn't Literate Programming the default? Originated by the legendary Donald Knuth, LP largely remained an academic pursuit or a niche interest. Several reasons explain its limited adoption.</p><p>Perhaps the most significant hurdle has been tooling. Providing robust linting and type checking on disentangled source code proves incredibly difficult. Imagine trying to analyze code scattered across a narrative, with pieces defined by macros that might expand elsewhere. The current workaround involves linting the tangled files (the extracted, executable code) and then trying to project any errors back to the original literate source. This process is complex and notoriously hard to make reliable across diverse programming languages. There is simply no robust tooling support for this yet.</p><p>Another more subjective, but still yet very real limitation concerns collaboration at scale. When multiple developers work on different architectural components of a large project, LP's narrative-first approach can complicate simultaneous development. It requires a different way of thinking about how components interact and are documented, which can hinder its adoption in fast-paced, multi-developer environments based on pull requests and frequent code reviews.</p><p>Finally, LP demands a significant mental model shift from developers. We are deeply ingrained in the idea of writing sequential code files. Prioritizing explanation and narrative structure over the immediate gratification of seeing runnable code requires a different mindset&#8212;one that many developers, under project pressures, find hard to adopt.</p><h2>LP is the Best Paradigm for AI-Assisted Coding</h2><p>Despite these historical challenges, I firmly believe that Literate Programming is uniquely suited for working with AI. In fact, it might just be the <em>best</em> paradigm for AI-assisted coding.</p><p>The most compelling argument is that, when applied correctly, a Literate Programming document inherently contains all the necessary explanations and context for an LLM to understand it. By definition, no code exists that isn't fully explained within the literate document itself. This completely eliminates the need for separate, additional, or laboriously crafted prompts for your AI assistant. The AI can simply read your literate program, understanding the <em>why</em> alongside the <em>what</em>.</p><p>Furthermore, AI fundamentally differs from traditional development tools. Compilers are notoriously rigid, demanding perfect syntax and strict adherence to language rules. This rigidity historically made working with disentangled LP source code difficult. However, AI is far less hindered by these traditional tooling issues. It remarkably adapts to interpreting "pseudo-code," understanding natural language explanations, and even working with partially formed ideas. This aligns perfectly with LP's nature, where the human-readable explanation often precedes the perfectly formed executable code.</p><p>AI's capabilities extend beyond just spitting out code. Yes, AI can effectively generate code snippets that fit seamlessly within your literate explanation. But here&#8217;s the crucial part: <em>AI can also help you explain and refine the literate (textual) part of the program itself</em>. It leverages its proficiency in understanding both natural language and code to suggest better explanations, clearer analogies, and more precise prose, elevating the quality of your documentation as much as your code.</p><p>Given these advantages, LP, especially reinforced by AI, is perfect for specific scenarios. Consider <strong>short, learning-based projects</strong> like tutorials, coding books, or personal learning journeys. For single authors, AI can dramatically amplify both the learning process and the creation of highly understandable educational content. </p><p>It's also excellent for <strong>exploratory coding</strong>. When prototyping or learning in a new problem space, LP combined with AI can prove invaluable. AI assists in articulating your hypotheses, assumptions, and findings, promoting clearer thinking and ensuring that your exploration process itself is well-documented and understandable.</p><h2>How to Make it Even Better</h2><p>First, you don't need any new AI-powered editor to leverage literate programming today. Even the simplest AI code assistants are incredibly effective with literate programming today. Why? Because the necessary prompt context for these tools already exists within the explicitly explained literate source. The AI isn't guessing; it reads a meticulously documented narrative that includes the very code it needs to assist with.</p><p>However, to really push Literate Programming from niche to mainstream, we will need some targeted AI development and advanced tooling.</p><p>The most critical next step involves training and fine-tuning coding agents specifically to <em>produce</em> literate source. This isn't just about generating code that happens to have comments; it means AI mastering the leveraging and generation of <em>macros</em>. Macros, with their recursive expansion capabilities, are by far the most powerful feature of LP. If AI can master their use, it will unlock LP's full potential. Imagine an AI not just writing functions, but designing and documenting the conceptual hierarchy of your program.</p><p>Secondly, we also need proper tooling that goes beyond simply feeding the AI the current document. A proper LP workflow would perform partial or recursive tangling of the literate source <em>during AI inference</em>. Why is this crucial? It allows the AI to simultaneously see the exact current literate programming source (providing the rich human context) <em>and</em> have a peek at the partially generated tangled source (providing the immediate machine context). This dual perspective will lead to far more accurate, context-aware, and intelligent suggestions from the AI.</p><p>Ultimately, I envision a shift from a directed, task-based interaction with coding agents to a truly real-time, continuous collaboration. As the developer writes in the literate programming source, the AI agent wouldn't just wait for a prompt; it would constantly suggest improvements, offer completions, and refine both the code and the accompanying explanation. The AI acts as a buddy looking over your shoulder, providing immediate insights and pointing out areas for betterment in both the code logic and its narrative explanation.</p><h2>Conclusion</h2><p>I believe Literate Programming is the ideal paradigm for coding with AI, especially for solo projects. It&#8217;s a natural fit, allowing AI to contribute to the <em>explanation</em> of the program as much as its execution.</p><p>Of course, this paradigm won't fit all coding methodologies or project types. I remain uncertain about its scalability for very large projects with multiple developers handling distinct architectural components. However, for smaller projects and those that lend themselves to vibe coding&#8212;where exploration, learning, and clear articulation are paramount&#8212;its fit is incredibly strong.</p><p>To put my money where my mouth is, I recently built a Literate Programming tool in Rust. It's aptly called <em><a href="https://github.com/apiad/illiterate">illiterate</a></em>. This tiny CLI tool takes literate source files and performs all macro expansion and tangling across any programming language you throw at it.</p><p>To be honest, I kind of hacked it over a weekend as a personal quest to learn Rust, but it was also an exercise in LP, because its <em>own source code is written entirely in the literate programming paradigm</em>. It serves as a living, readable demonstration and, in essence, a blog article of the very concepts we've discussed today. You can read the annotated source <a href="https://apiad.github.io/illiterate/#/docs/illiterate">here</a>.</p><p>I encourage you to explore literate programming, especially in this exciting new context of AI. Perhaps take a look at <em>illiterate</em> as a starting point. And, as usual, leave me any comments if you have follow up questions or if you want to discuss any part of this article in greater depth.</p>]]></content:encoded></item><item><title><![CDATA[Programming as Language Design]]></title><description><![CDATA[Or Why All Programmers Are Also Language Designers]]></description><link>https://blog.apiad.net/p/programming-as-language-design</link><guid isPermaLink="false">https://blog.apiad.net/p/programming-as-language-design</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Fri, 18 Jul 2025 10:02:37 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6000" height="3886" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3886,&quot;width&quot;:6000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;person hand with green and blue paint&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="person hand with green and blue paint" title="person hand with green and blue paint" srcset="https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1501366062246-723b4d3e4eb6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyMHx8YXJ0aXN0fGVufDB8fHx8MTc1MjY5MDYwM3ww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="true">Amauri Mej&#237;a</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p><em>&#8212;What is the best programming language?</em></p><p>This is a question I get asked a lot. I teach programming to fresh CS majors, and then compiler construction and some advanced programming language concepts to senior CS majors. So I get asked this question, a lot. </p><p>For the sake of the argument, let&#8217;s take it at face value for a minute. What&#8217;s your pick? Python? JavaScript? Rust? Haskell? Or&#8230; Church forbids&#8230; LISP!? Or&#8230; is this somehow a wrong question?</p><p>My response to this question is very pragmatic&#8212;I&#8217;m a pragmatist for most things that matter, and a romantic for the rest, you&#8217;ve been warned. </p><p>All other things considered equal&#8212;I always say&#8212;, assuming no cost in learning a new language, no personal preference, no company policy, etc., the <em>absolute best programming language</em> is the one where you can most easily express the solution to the problem you care about. Neat, right?</p><p>&#8212;<em>But hey</em>&#8212;I hear you scream&#8212;<em>that&#8217;s not a real answer! That&#8217;s just a &#8220;depends&#8221;.</em> </p><p>And yes, it is. All complicated questions have &#8220;depends&#8221; for an answer. The real answer is understanding <em>what</em> it depends on. And in this case, I think the clearest metric to optimize&#8212;again, all else considered equal&#8212;is how far will a specific language take me towards solving the problem I need to solve.</p><p>If I had a programming language with a set of keywords and builtin operations that basically would let me <em>type a problem and it would get automatically solved</em>&#8212;and that thing kind of exists for some domains, think Prolog or SQL&#8212;then, that would be the best programming language of all, right!</p><p>But of course, this is kind of a moot argument because, in the end, almost all mainstream programming languages are equally good or bad at almost the same things. You can talk to a database in any programming language. You can generate and serve HTML in any programming language. You can write standard business logic in any programming language. You can shoot your foot with regular expressions in any programming language.</p><p>Granted, there are some cases where a pure functional language is better in some niche problems but, for the most part, it&#8217;s not the <em>language</em> per se, but the <em>set of available libraries and frameworks</em> that make it or break it for any given problem.</p><p>And that&#8217;s precisely the point of this article! Thing is, what most people think about when they refer to a &#8220;programming language&#8221;&#8212;you know, those things with funny names like Python, Java, Rust, Go, Erlang&#8230;&#8212;is an extremely limited concept. I want to show you a far broader notion of what a programming language is; a notion that I think will make some of you look at the very act of programming in a completely new, and enlightening way.</p><p>But let&#8217;s start at the beginning.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>What is a Programming Language?</h2><p>We often talk about programming primarily from the perspective of problem-solving. We take a messy, real-world problem&#8212;or so the story goes&#8212;and try to distill it into something a computer can understand.</p><p>And that&#8217;s where the programming language enters the picture. It&#8217;s the very language you write the solution to the problem, which is later converted into something the computer can actually run. For the most part, you can even imagine the code in that programming language is actually what&#8217;s running on the CPU&#8212;at least for the 3rd generation, imperative programming languages that are mainstream today.</p><p>From this point of view, a programming language can be defined as a formal language&#8212;with a strict syntax and formalized semantics&#8212;that can be used to instruct a computer to solve problems in a given domain.</p><p>This perspective is absolutely right. It shows programming involves a great deal of <em>abstraction via decomposition</em>, which is to say, we must take a problem and break it into parts so small that are trivial to explain to a computer. But I think this perspective also misses a crucial, often-overlooked part of problem-solving: <em>abstraction via composition</em>. Let me elaborate.</p><p>For any, even mildly complicated problem, chances are you will not solve it just with say, pure, Python, for the sake of the argument. No, you will first write a couple of utility functions, perhaps one or two classes here and there, and only then, you&#8217;ll write a rather small main function that simply instantiates a type and calls a couple of methods and voil&#225;, problem solved!</p><p>What you just did there, my friend, is <em>invent a new language</em>!</p><p>Ok, not an <em>entirely</em> <em>new</em> language, but a new <em>something</em> still. You added a couple of methods and classes with their own, formalized semantics, that can be used to instruct a computer to solve problems in a given domain.</p><p>Think about it again from the perspective of problem-solving. If only you had that magical language where <em>just typing the problem would give you the solution</em>. Well, that&#8217;s what you built. You created a set of building blocks (methods, classes, etc.) that, when you use them to express your problem, you get it automatically solved.</p><p>In a sense, all programming is a form of language design. Just as we must distill a problem down into programmable parts, we must also upgrade our programming language with new building blocks that can directly solve those subproblems.</p><p>Now let&#8217;s see a few ways in which we do implicit language extension when we program.</p><h2><strong>The Layers of Language Extension</strong></h2><p>Metaprogramming is perhaps the most <em>evident</em> form of language extension. These are programming language features that directly extend or enhance the language's own syntax. The quintessential expression of metaprogramming is, of course, LISP macros&#8212;and similarly inspired macro systems like Rust&#8217;s. But other, less flexible features like Python decorators or code blocks in Ruby can also add a level of linguistic manipulation that sometimes seems like you&#8217;re programming in a whole different language.</p><p>However, the point here is not about metaprogramming. Or not primarily about metaprogramming&#8212;that&#8217;s just one tiny bit of implementation detail. You don&#8217;t need any fancy metaprogramming features to extend a programming language. Most of the code we write is this kind of language-extending code. Functions and types, as we just discussed, are the most basic building blocks in most programming language and also the more pervasive and impactful form of language extension.</p><p>Every time you define a new function or class, you're effectively adding a new word to your language vocabulary. You're extending the language you're working with, making it more expressive for the specific problem at hand, and at the same time, more specialized.</p><p>I see this happening across four distinct language layers. Each layer adds a new vocabulary, more <em>expressive</em>&#8212;in the sense that you can say more with fewer tokens&#8212;but also more <em>specialized</em>&#8212;in the sense that you can talk about a more reduced set of concepts. Each of these layers encapsulates knowledge about an increasingly more complex part of a problem domain, and each layer encodes a stronger opinion about how a given problem domain is tackled.</p><p>Let&#8217;s take it from the ground up.</p><h3><strong>Layer Zero: The Base Language</strong></h3><p>This is the most basic layer where we always start a new project, the raw programming language itself. Think of Python, C, Java, or JavaScript. At this level, the language is at its most general, most unopinionated. It gives us the fundamental tools: how to store numbers, how to make decisions, and how to repeat actions. The knowledge encapsulated here is about the very basics of computation, how a computer thinks and operates.</p><h3><strong>Level 1: Libraries</strong></h3><p>Once our problem exceeds what the builtin functionality in our language can solve, we turn to libraries. If you&#8217;re writing, say, numerical algorithms in Python, you probably won&#8217;t code everything from scratch. No, you&#8217;ll use Numpy.</p><p>You can see libraries as a more specialized linguistic layer that offer ready-made functional building blocks for a specific domain. The knowledge encoded in libraries is almost entirely functional: domain-specific algorithms to solve particular types of problems efficiently.</p><h3><strong>Level 2: Frameworks</strong></h3><p>This is where things get more opinionated. Frameworks, like a web framework (e.g., Django, React) or a game engine, don't just give you tools; they give you a <em>way</em> of thinking about and solving problems. They encode a particular paradigm or mindset. They tell you the right way to structure your application, the right patterns to follow, the right way to combine these disparate algorithms.</p><p>The knowledge encapsulated within a framework is procedural: it's about the best practices and compositional rules for a given domain. If libraries are specialized functions, frameworks are like an architectural blueprint for building a specific type of system.</p><h3><strong>Level 3: The Application</strong></h3><p>Finally, we have the top layer: applications designed to solve a concrete problem for a specific user. When you build, e.g., a CRM or a simple CRUD app, that interface is also a language with which the user instructs the computer to solve a concrete problem&#8212;be it create a report or change a user password.</p><p>Applications encode knowledge about a business domain&#8212;business in the sense it&#8217;s a problem that some client wants to solve. And every application, in a subtle way, implicitly contains its own framework (its own set of compositional rules) and its own custom library (its own building blocks). </p><p>Oftentimes this code is intermingled. But in well-designed applications you can often extract a fully-featured framework and a couple of libraries that can be reused for building other similar applications in adjacent domains.</p><h2><strong>Why this matters</strong></h2><p>If I&#8217;m right then every programmer is, in essence, a language designer. This is a great power, and with it comes great responsibility!</p><p>Deciding which abstractions (classes, methods, functions) to create is an exercise in designing a more expressive, more capable language for your specific problem. When you think about it, <em>abstraction</em> itself is fundamentally about finding the right representation. This involves a two-sided approach we&#8217;ve already discussed:</p><ul><li><p><strong>Decomposition (Top-Down):</strong> Breaking down a big problem into smaller, more manageable, computationally solvable pieces.</p></li><li><p><strong>Composition (Bottom-Up):</strong> Actively building and adding new capabilities to your language. This is where you encapsulate functionality into reusable blocks.</p></li></ul><p>The sweet spot where these two directions converge is where you find the perfect level of abstraction, where your problem understanding and your solution language align beautifully.</p><p>But, of course, we rarely explicitly program this way. We don&#8217;t build the perfect framework or library upfront. More often, we build an application, realize certain patterns are emerging, and then <em>extract</em> a framework or library from it. Only after a lot of experience in a given domain we begin to see the patterns at the start of a new project and can design upfront. And even then, we often make mistakes and design abstractions that aren&#8217;t exactly right, or maybe even unnecessary. </p><p>But that&#8217;s ok. My argument here is not that you should define a sweet spot you want to achieve, and then build your language up to get there. No, the process of problem-solving in inherently messy and there&#8217;s nothing that can substitute experimentation.</p><p>However, I do think this dual-view of programming is helpful from at least two perspectives. First, as a programmer, it helps to sometimes step back and consider what is the language you need to solve your current problem, and work with that as at least a flexible guide. Again, this doesn&#8217;t you&#8217;ll get it right from the start. But I find it helps me to rethink and refactor a convoluted solution when everything gets stuck.</p><p>And second, as an educator, this perspective gave me a powerful shift in how I teach computer programming. Instead of just teaching students to solve problems <em>with</em> a language, I now try  teach them to be explicit about their intentions when designing abstractions, and to understand the new linguistic layers they are always creating. This means teaching them how to break the problem down to fit the existing language constructs and, simultaneously, teaching them how to build the language up with new constructs to better fit the problem.</p><p>As a side note, this perspective also helps explain why almost all programming languages start as a cheap C rip-off&#8212;a slow, low-level, mostly imperative general-purpose language&#8212;and gradually evolve towards a LISP wannabe&#8212;a highly extensible systems with powerful metaprogramming capabilities, but still not LISP.</p><p>But this is a topic for another article.</p>]]></content:encoded></item><item><title><![CDATA[AI-Driven Storytelling with Multi-Agent LLMs - Part III]]></title><description><![CDATA[Closing the Loop with Planning and Structure]]></description><link>https://blog.apiad.net/p/ai-driven-storytelling-with-multi-3ed</link><guid isPermaLink="false">https://blog.apiad.net/p/ai-driven-storytelling-with-multi-3ed</guid><dc:creator><![CDATA[Alejandro Piad Morffis]]></dc:creator><pubDate>Mon, 07 Jul 2025 10:02:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4PCW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4PCW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4PCW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4PCW!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png" width="1030" height="611.5625" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:608,&quot;width&quot;:1024,&quot;resizeWidth&quot;:1030,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4PCW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!4PCW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc0761a1-284e-4e83-8897-af81b3d363a2_1024x608.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"></figcaption></figure></div><p>In the first two parts of this series, we explored the fascinating, chaotic world of <em>emergent</em> storytelling. We saw how complex narratives can arise from simple, "bottom-up" rules in <a href="https://blog.apiad.net/p/ai-storytelling-1">Part I</a>, and how LLM-powered agents can co-create stories through dynamic, unpredictable <em>interaction</em> in <a href="https://blog.apiad.net/p/ai-driven-storytelling-with-multi">Part II</a>. It&#8217;s a world of digital improv, where the story finds its own way.</p><p>But that&#8217;s only half the picture.</p><p>Those bottom-up approaches excel at creating novelty and believable micro-interactions. They are fantastic at answering "What happens next?" But what about the "top-down"? What about the grand narrative arc, the deliberate plot structure, and the long-term coherence that defines the stories we remember? This is where even the most advanced Large Language Models (LLMs) stumble. They are masters of prose but poor architects. They can write a beautiful paragraph, but struggle to build a cathedral.</p><p>In this final installment of the series, we close the loop. We'll explore a complementary, top-down approach based on another excellent thesis I supervised this year, this one by Roger Fuentes Rodr&#237;guez. His work focuses on <strong>high-level planning and structure</strong>. I'll argue that this architectural approach doesn't just produce better stories; it provides a powerful model for building more robust and governable AI systems.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Computist Journal is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3>LLMs are Brilliant Amnesiacs</h3><p>Let&#8217;s backtrack to the root of the problem we&#8217;ve been discussing the past two weeks. If you've ever had a long, meandering conversation with an LLM, you've likely seen it happen. After a while, it starts to forget key details from the beginning of the chat. This isn't a bug; it's a fundamental feature of their design.</p><p>Think of it as a brilliant mind with no long-term memory. An LLM can only "see" the last few thousand words (tokens) of a conversation. And even in LLMs with Everything before that effectively ceases to exist. For short tasks, this is fine. For writing a novel, it's a disaster.</p><p>This limitation leads to critical failures in long-form storytelling:</p><ul><li><p><strong>Character Amnesia:</strong> A hero who is terrified of spiders on page 5 suddenly keeps one as a pet on page 50.</p></li><li><p><strong>Plot Holes:</strong> The magical sword that can only be wielded by the pure of heart is inexplicably used by the villain to open a tin of beans.</p></li><li><p><strong>Structural Breakdown:</strong> The story loses its narrative drive. The rising action plateaus, the climax never quite lands, and the resolution feels unearned, violating foundational structures like Freytag's Pyramid.</p></li></ul><p>The core challenge is this: a story is not a linear sequence of words. It's a complex, interconnected web of causal relationships, character motivations, and thematic consistency. A monolithic LLM, with its limited memory, simply can't manage this web on its own. It needs an architecture.</p><h3>The Writer's Room</h3><p>Instead of relying on a single, all-knowing AI, the thesis proposes a "divide and conquer" strategy. The system's core is a central <strong>story blueprint</strong>, which a team of specialized AI agents collaborates on. It&#8217;s less like a single author and more like a Hollywood writer's room.</p><h4>The Story Blueprint</h4><p>At the heart of this system is a <strong>Directed Acyclic Graph (DAG)</strong>. Forget the jargon for a second; think of it as the story's complete, interconnected timeline and causal web, all mapped out on a giant whiteboard.</p><ul><li><p><strong>Nodes are Events:</strong> Each point on the whiteboard is a specific event in the story. Not prose, but a structured object with the characters that participate, and all the necessary metadata.</p></li><li><p><strong>Edges are Causality:</strong> The arrows connecting the events represent cause-and-effect. An arrow from "Hero finds the key" to "Hero opens the chest" means the first event <em>must</em> happen before the second. This simple rule makes temporal paradoxes and plot holes structurally impossible. You can't use the key before you find it.</p></li></ul><p>This graph is the long-term memory the LLM lacks. It is the single source of truth that codifies the story's logic, ensuring every part is connected to the whole.</p><h4>The Writers</h4><p>The agents in this system aren't working in isolation. They are all reading from, and writing to, that central graph.</p><ul><li><p><strong>The Architect:</strong> This agent builds the initial skeleton of the graph. It takes the user's high-level prompt and lays out the main plot points&#8212;the inciting incident, the major turning points, the climax&#8212;as the first nodes on the graph.</p></li><li><p><strong>The World Builder:</strong> This agent is the lore master. It goes through the graph and enriches the event nodes with crucial details: defining the characters, describing the locations, and specifying the properties of important objects.</p></li><li><p><strong>The Drama Coach:</strong> This agent's job is to make the story <em>interesting</em>. It analyzes the graph's structure to find flat or boring sequences. It then adds or modifies nodes to inject conflict, suspense, or character development. It asks, "Wouldn't it be more interesting if the hero's mentor betrayed them at this point?" and adds that event to the graph.</p></li><li><p><strong>The Dependency Manager:</strong> This is the ultimate fact-checker. It constantly validates the graph, ensuring there are no paradoxes or broken rules. It checks things like, "Does the character have the required item from a previous node before attempting this action?" or "Is this supposedly dead character trying to speak?"</p></li><li><p><strong>The Narrator:</strong> Only when the graph is complete, enriched, and validated does this agent step in. It performs a "topological sort" of the graph (reading the events in a valid causal order) and, one node at a time, uses an LLM to translate each structured event into compelling prose, feeding it only the context it needs for that specific scene.</p></li></ul><h3>The Proof is in the Plot</h3><p>Does this actually work? In short, yes. </p><p>The evaluation for the thesis involved having real users interact with the system via a Telegram bot and compare its output to that of a monolithic LLM. The stories generated by this structured, multi-agent system were consistently rated higher in <strong>structural coherence</strong> and <strong>narrative depth</strong>. The system excelled at maintaining consistent character motivations, building a more detailed and believable world, and avoiding the plot holes that plague simpler approaches.</p><p>But while this approach has clearly many strengths, it currently lacks in precisely what the previous articles excel: emergence and interaction. So, that&#8217;s our next step.</p><h3>Unifying Top-Down and Bottom-Up</h3><p>So where do we go from here? The clear path forward is to build a unified theory of AI storytelling, combining the top-down planning from this article with the bottom-up emergence and interaction from the previous two.</p><p>Imagine a hybrid system with two layers operating at once:</p><ol><li><p><strong>The Macro-Narrative (Top-Down):</strong> The story graph we've discussed acts as the "grand narrative," defining the key plot beats that <em>must</em> happen for the story to be satisfying.</p></li><li><p><strong>The Micro-Narrative (Bottom-Up):</strong> Within each scene (each node of the graph), we unleash the autonomous, LLM-powered characters from the previous two articles. They have their own goals, personalities, and memories, and they interact freely, creating emergent and unpredictable dialogue and actions.</p></li></ol><p>The magic lies in connecting these two layers. We reintroduce a crucial agent: the <strong>Director</strong> (or God/Game Master). The Director's job is not to puppet the characters. Instead, it subtly <em>nudges</em> the simulation. It knows the next required beat in the story graph is "The hero must discover the secret map." It can't force the hero to look for it, but it can introduce an NPC who mentions a rumor, make a book fall off a shelf to reveal a hidden compartment, or create a sudden downpour that forces the characters to take shelter in the very cave where the map is hidden.</p><p>This creates the best of both worlds: the structural integrity of a planned narrative, combined with the organic, believable, and often surprising behavior of autonomous agents. It&#8217;s the holy grail: a story that is both well-plotted and truly alive.</p><h3>Beyond Storytelling as a Toy</h3><p>Let's be clear: the goal of this research is not to automate creativity or replace human authors. The real value of computational storytelling lies elsewhere. It serves as the perfect playground for tackling some of the most critical open problems in Artificial Intelligence.</p><p>A story is a microcosm of our complex world. Forcing an AI to generate a coherent one is an extreme stress test for its most important faculties:</p><ul><li><p><strong>Reasoning:</strong> A good story is a monumental feat of causal reasoning. Characters must have consistent motivations, actions must have logical consequences, and plot threads must resolve. Maintaining this web of dependencies is a powerful way to measure and improve an AI's ability to reason in non-formal, unstructured, but still challenging scenarios.</p></li><li><p><strong>Governance &amp; Safety:</strong> Think of our writer's room architecture. The Dependency Agent acts as a safety and ethics system, enforcing the rules of the world. The Drama Coach agent governs the narrative, steering it towards a desired outcome (an "interesting" story) without violating core constraints. This is a perfect sandbox for studying AI alignment: how do we build systems that can pursue complex goals while adhering to a set of inviolable rules?</p></li></ul><p>For decades, AI research advanced by mastering abstract games like Chess and Go. The breakthroughs required to win those games, particularly in deep learning, didn't just stay in the game. They became foundational for solving real-world scientific problems, most famously protein folding with AlphaFold.</p><p>I argue that storytelling is the next grand challenge for conversational AI. It's a game with infinitely more complex rules than Go, one that involves social dynamics, common sense, and long-term planning. The novel architectures we must invent to "master" storytelling&#8212;systems that can plan, reason, and govern themselves&#8212;could be the key to unlocking the next generation of safer, more robust, and more capable Artificial Intelligence.</p><p>And if you liked this article, feel free to check <strong>the full thesis</strong> (in Spanish but AI can translate it pretty well) and the <strong>repository</strong> to read some of the generated stories.</p>]]></content:encoded></item><item><title><![CDATA[AI-Driven Storytelling with Multi-Agent LLMs - Part II]]></title><description><![CDATA[Second part of the summaries of ongoing research with LLMs at the University of Havana]]></description><link>https://blog.apiad.net/p/ai-driven-storytelling-with-multi</link><guid isPermaLink="false">https://blog.apiad.net/p/ai-driven-storytelling-with-multi</guid><dc:creator><![CDATA[Daniel Ángel Arró Moreno]]></dc:creator><pubDate>Wed, 18 Jun 2025 15:01:06 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4032" height="3024" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3024,&quot;width&quot;:4032,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;green plastic dice on white surface&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="green plastic dice on white surface" title="green plastic dice on white surface" srcset="https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1625173925005-0043774fe954?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHx0dHJwZ3xlbnwwfHx8fDE3NTAyNTc5NTR8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="true">Ian Fajardo</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>In the previous article, we explored how multi-agent architectures can inject life and autonomy into AI-generated stories, allowing characters to pursue their own goals in a dynamic world. But what happens when you add a human to the mix? Interactive storytelling raises the stakes: now the system must not only maintain coherence and character consistency, but also respond&#8212;intelligently and flexibly&#8212;to unpredictable user input.</p><p>This is a much harder problem. The system must walk a tightrope: it needs to be consistent enough to avoid plot holes and character drift, but flexible enough to let the user meaningfully shape the narrative, even in ways the system never anticipated. In this second article I bring you our second undergraduate thesis at the University of Havana, taking on this challenge head-on.</p><blockquote><p><em>If you haven&#8217;t read Part I of this series, I recommend starting <a href="https://blog.apiad.net/p/ai-storytelling-1">there</a> for the full context and motivation behind our research line. In short: we&#8217;re not trying to &#8220;solve&#8221; storytelling, but to use it as a demanding testbed for developing robust techniques in LLM governance, safety, and control.</em></p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;007b30ba-c92b-4809-adf6-467c79fdb141&quot;,&quot;caption&quot;:&quot;&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;AI-Driven Storytelling with Multi-Agent LLMs - Part I&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:144640022,&quot;name&quot;:&quot;Franco Hern&#225;ndez Piloto&quot;,&quot;bio&quot;:&quot;Computer Scientist &quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c2a505a0-ebae-4745-81a5-6ec52b159b3f_640x640.jpeg&quot;,&quot;is_guest&quot;:true,&quot;bestseller_tier&quot;:null,&quot;primaryPublicationSubscribeUrl&quot;:&quot;https://francohernndezpiloto.substack.com/subscribe?&quot;,&quot;primaryPublicationUrl&quot;:&quot;https://francohernndezpiloto.substack.com&quot;,&quot;primaryPublicationName&quot;:&quot;Franco Hern&#225;ndez Piloto&quot;,&quot;primaryPublicationId&quot;:5353129},{&quot;id&quot;:6970039,&quot;name&quot;:&quot;Alejandro Piad Morffis&quot;,&quot;bio&quot;:&quot;Democratizing knowledge one post at a time. I talk about Computer Science, AI, Education, Philosophy, you know, mostly harmless stuff.\nBuilding a community of tech writers on Substack.\nAnd now also venturing into creative writing.&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F26aafc21-b149-4bf0-9382-e0ae3636e23a_640x640.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-06-16T11:02:42.695Z&quot;,&quot;cover_image&quot;:&quot;https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://blog.apiad.net/p/ai-storytelling-1&quot;,&quot;section_name&quot;:&quot;&#129302; Mostly Harmless AI&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:165826581,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:9,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;The Computist Journal&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F582c72c0-c120-4ea8-ae6b-376a025250bb_1024x1024.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div></blockquote><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The Computist Journal is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>The Core Problem</strong></h3><p>Let&#8217;s get to the heart of the matter: Why is interactive storytelling so hard? The answer lies in a fundamental tension&#8212;one that anyone who&#8217;s ever played a narrative game or written a choose-your-own-adventure story will recognize. It&#8217;s the struggle between <strong>agency</strong> (the user&#8217;s freedom to make meaningful choices) and <strong>control</strong> (the system&#8217;s responsibility to keep the story coherent, engaging, and believable).</p><p>Agency is what makes interactive storytelling magical. It&#8217;s the feeling that your decisions actually matter&#8212;that you can steer the story in unexpected directions or even break the mold of the narrative world. In a perfect system, you&#8217;d be able to do anything your imagination conjures: befriend the villain, burn down the tavern, or turn the hero into a poet. The system would adapt, improvise, and keep the experience compelling.</p><p>But here&#8217;s the rub: pure agency, without constraints, is a recipe for chaos. If the system simply accepts every user input at face value, the story can quickly unravel. Characters might act out of character, plotlines can contradict themselves, and the narrative world loses its internal logic. The result? A story that feels less like a crafted experience and more like a series of disconnected improv skits.</p><p>On the flip side, control is the system&#8217;s way of protecting the integrity of the story. Think of it as the invisible hand of the &#8220;narrative director&#8221;&#8212;the set of rules, memory, and logic that ensures events make sense, characters stay true to themselves, and the world remains believable. Control is what prevents the protagonist from suddenly teleporting to Mars or resurrecting a character who just died (unless, of course, the story&#8217;s logic allows for it).</p><p>But too much control, and the story becomes a railroad. The user&#8217;s choices are ignored, overwritten, or reduced to cosmetic differences. The narrative might be coherent, but it&#8217;s no longer interactive in any meaningful sense. The magic of agency is lost.</p><h3><strong>Our Proposal</strong></h3><p>Let&#8217;s open the hood on this architecture. The core idea is straightforward but powerful: break down the complex process of interactive storytelling into a series of specialized agents, each responsible for a distinct narrative function, and orchestrate their collaboration through a well-defined workflow. This modular approach is what allows the system to achieve both flexibility (adapting to user decisions) and consistency (maintaining narrative logic and emotional coherence).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_qEF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_qEF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!_qEF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg" width="679" height="372" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:372,&quot;width&quot;:679,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:27455,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/166073282?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_qEF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_qEF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a7cbe7-303b-4ccf-84a8-c97db47d84ae_679x372.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We defined the following agents:</p><ul><li><p><strong>Orchestrator of Interactive Stories</strong>: Acts as the central coordinator. This agent triggers each phase in the correct sequence, manages the overall flow, and ensures all updates to the knowledge graph and narrative state happen in the right order. Without this conductor, the &#8220;orchestra&#8221; of agents would quickly fall out of sync.</p></li><li><p><strong>Rule Extraction Agent</strong>: Extracts the fundamental rules and constraints of the story world (e.g., &#8220;magic is forbidden,&#8221; &#8220;time flows forward&#8221;). This agent sets the boundaries for all subsequent events, ensuring that the narrative remains internally consistent.</p></li><li><p><strong>Key Beat Extraction Agent</strong>: Identifies the pivotal moments or &#8220;beats&#8221; from the user&#8217;s synopsis or the evolving story. These serve as narrative milestones, guiding the rhythm and progression of the plot.</p></li><li><p><strong>Event Extraction Agent</strong>: Detects and classifies concrete narrative events, assigning actors and consequences. This agent feeds the knowledge graph with actionable story content.</p></li><li><p><strong>Knowledge Graph Builder</strong>: Maintains a dynamic, structured representation of all entities, relationships, rules, and events. The graph acts as both memory and source of truth, ensuring that the story doesn&#8217;t lose track of details or contradict itself as it evolves.</p></li><li><p><strong>Prompt Enricher</strong>: Gathers the most relevant context from the knowledge graph and narrative history, then constructs a rich prompt for the language model. This ensures that every new scene is generated with full awareness of what&#8217;s come before.</p></li><li><p><strong>Act Director</strong>: Generates the next scene using the enriched prompt. This agent is responsible for advancing the plot while respecting the current state and constraints of the story world.</p></li><li><p><strong>Character Simulator</strong>: Simulates the emotional and behavioral responses of characters to the latest events. This keeps character arcs believable and emotionally consistent.</p></li><li><p><strong>Player Action Handler</strong>: Integrates the user&#8217;s decisions into the narrative. It updates the knowledge graph and narrative history, ensuring that user choices have real, lasting impact on the unfolding story.</p></li></ul><p>Here is a brief overview of how all these agents collaborate in a typical interactive session.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tRJU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tRJU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!tRJU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg" width="261" height="441" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:441,&quot;width&quot;:261,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:15529,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/166073282?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tRJU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tRJU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c00f81c-8064-4d1c-859d-41c816bfa0d7_261x441.jpeg 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol><li><p><strong>Preparation Phase</strong></p><ul><li><p>The Orchestrator initializes an empty knowledge graph and sets up the narrative environment.</p></li><li><p>The Rule Extraction Agent processes the initial synopsis or world description to establish the core rules.</p></li><li><p>All agents are initialized and ready to process input as the story unfolds.</p></li></ul></li><li><p><strong>Narrative Base Construction</strong></p><ul><li><p>The Key Beat Extraction Agent identifies the main plot points from the user&#8217;s synopsis.</p></li><li><p>The Event Extraction Agent breaks down the synopsis into actionable events.</p></li><li><p>The Knowledge Graph Builder integrates these elements into the graph, establishing the initial state of the story world.</p></li></ul></li><li><p><strong>Interactive Development Loop</strong></p><ul><li><p><strong>Prompt Enrichment</strong>: The Prompt Enricher queries the knowledge graph for the most relevant context (recent events, character states, world rules) and constructs a prompt for the LLM.</p></li><li><p><strong>Scene Generation</strong>: The Act Director uses this prompt to generate the next scene, ensuring continuity and literary quality.</p></li><li><p><strong>Character Simulation</strong>: The Character Simulator infers and documents how each character reacts to the new developments, updating their emotional and behavioral states.</p></li><li><p><strong>User Interaction</strong>: The Player Action Handler presents choices to the user, receives their input, and encodes the resulting actions as new events in the knowledge graph.</p></li><li><p>The cycle repeats, with each agent building on the outputs and updates of the others, ensuring that every new scene is both a logical continuation and a meaningful response to user agency.</p></li></ul></li><li><p><strong>Conclusion and Consolidation</strong></p><ul><li><p>When the user signals the end of the story, the Orchestrator triggers the finalization phase.</p></li><li><p>The Knowledge Graph Builder ensures all narrative threads are resolved and the story is logically complete.</p></li><li><p>The system outputs the full, coherent narrative, along with a structured map of how the user&#8217;s choices shaped the journey.</p></li></ul></li></ol><h3><strong>How does this stack up?</strong></h3><p>Let&#8217;s get honest: it&#8217;s easy to make grand claims about &#8220;better stories&#8221; and &#8220;more engaging AI,&#8221; but how do you actually measure narrative quality in a rigorous, meaningful way? For this thesis, the evaluation was designed to pit the multi-agent system head-to-head against a baseline approach that used the same underlying LLM (<code>Llama-3-8B-Instruct</code>) but without any agentic orchestration. This setup ensured that any improvement could be attributed squarely to the architecture&#8212;not to a bigger model, more data, or secret sauce under the hood.</p><h4><strong>The Metrics</strong></h4><p>The evaluation used a mix of qualitative and structured criteria, focusing on the aspects that matter most for interactive storytelling. Here&#8217;s the breakdown:</p><ul><li><p><strong>Narrative Coherence:</strong> Does the story flow logically from scene to scene, or do we get abrupt jumps and plot holes?</p></li><li><p><strong>Protagonist Agency:</strong> Do the user&#8217;s choices and the protagonist&#8217;s actions actually shape the direction of the story in a meaningful way?</p></li><li><p><strong>Adaptation to User Input:</strong> Does the system integrate user decisions naturally, creating real consequences and new narrative branches?</p></li><li><p><strong>Conflict and Tension:</strong> Is the story able to build suspense, escalate challenges, and avoid flat or artificial drama?</p></li><li><p><strong>Originality and Creativity:</strong> Are the story elements fresh and surprising, or do we just get recycled tropes?</p></li><li><p><strong>Clarity and Literary Style:</strong> Is the writing evocative, immersive, and stylistically rich?</p></li><li><p><strong>Thematic Consistency:</strong> Are the core themes and motifs reinforced throughout, or do they get lost along the way?</p></li><li><p><strong>Emotional Consistency:</strong> Do the characters&#8217; emotions evolve logically and believably?</p></li><li><p><strong>Context Maintenance:</strong> Does the system remember important details, settings, and relationships as the story unfolds, or does it &#8220;forget&#8221; as it goes?</p></li></ul><p>Each experiment involved presenting both systems with the same narrative synopsis and a sequence of user choices. Human evaluators then compared the resulting stories using these criteria, looking for both strengths and weaknesses in each approach.</p><h4><strong>The Findings</strong></h4><p>The results were clear and consistent across multiple story scenarios. The multi-agent system outperformed the baseline on almost every metric that matters for interactive storytelling.</p><p>Stories generated by the multi-agent system maintained a logical chain of cause and effect, with smooth transitions between scenes and a strong sense of narrative momentum. The protagonist&#8217;s decisions had real weight, steering the story into new territory and producing consequences that felt both meaningful and surprising. User choices were not just tacked on&#8212;they shaped the evolution of the plot, the emergence of conflict, and the protagonist&#8217;s emotional journey.</p><p>Perhaps most striking, the multi-agent system produced narratives with richer literary style and more vivid descriptions, even though it used the same LLM as the baseline. This suggests that the way you structure and feed context to the model&#8212;through agents that carefully curate, update, and enrich the narrative state&#8212;can unlock much more of the LLM&#8217;s creative potential.</p><p>The system also excelled at maintaining context and thematic unity. Key details, settings, and character motivations were carried forward across scenes, avoiding the notorious &#8220;memory loss&#8221; problem of vanilla LLMs. Emotional arcs were more believable, and the stories avoided the repetitive, clich&#233;-driven traps that often plague automated narrative generation.</p><p>By contrast, the baseline system struggled with abrupt transitions, shallow integration of user choices, and a tendency toward flat or generic storytelling. Protagonist agency was often illusory&#8212;choices rarely changed the story in a meaningful way. The writing, while functional, lacked the immersive quality and emotional resonance achieved by the multi-agent approach. Context was easily lost, leading to inconsistencies and a less engaging experience overall.</p><h3><strong>Final Thoughts</strong></h3><p>This thesis, and all our complementary research, proves you don&#8217;t need to fine-tune, retrain, or scale up your language model to get dramatically better results. By layering a clever multi-agent system on top of a standard LLM, you unlock coherence, adaptability, and user alignment that brute-force training simply can&#8217;t deliver. The architecture is the key.</p><p>Why does this matter? Because the core problems of interactive storytelling&#8212;long-term memory, integrating unpredictable user input, keeping characters and worlds believable&#8212;are exactly the challenges we face in building safe, controllable AI everywhere. The multi-agent architecture acts as a governance layer, orchestrating the model&#8217;s raw power through explicit rules, dynamic memory, and transparent workflows. It&#8217;s not just about telling better stories; it&#8217;s about showing that <em>smart design can outpace brute force</em>.</p><p>If we want AI that&#8217;s powerful <em>and</em> safe, we shouldn&#8217;t just throw more data or compute at the problem. We should design smarter systems&#8212;ones that let us steer, audit, and trust their outputs.</p><p>If you want to check the generated stories and read the full thesis (in Spanish), <a href="https://github.com/danielangelarro/Geenerated-Interactive-Histories">check this GitHub repository</a>.</p>]]></content:encoded></item><item><title><![CDATA[AI-Driven Storytelling with Multi-Agent LLMs - Part I]]></title><description><![CDATA[Results of an Ongoing Research in the AI Group at the University of Havana]]></description><link>https://blog.apiad.net/p/ai-storytelling-1</link><guid isPermaLink="false">https://blog.apiad.net/p/ai-storytelling-1</guid><dc:creator><![CDATA[Franco Hernández Piloto]]></dc:creator><pubDate>Mon, 16 Jun 2025 11:02:42 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="5149" height="3433" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3433,&quot;width&quot;:5149,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;man in top hat and coat holding book and cane sitting statue&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="man in top hat and coat holding book and cane sitting statue" title="man in top hat and coat holding book and cane sitting statue" srcset="https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1514120894977-9650134973b0?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMnx8c3Rvcnl0ZWxsZXJ8ZW58MHx8fHwxNzUwMDM5OTUyfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="true">Nick Karvounis</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>If you&#8217;ve ever tried to coax a language model into writing a long, coherent story, using just prompt engineering, you&#8217;ve probably hit a wall: characters lose their personalities, plots meander or stall, and the whole thing feels more like a sequence of clever paragraphs than a living narrative. </p><p>Precisely this topic is one major research line in my group at the University of Havana. Right now, there are three undergrad students wrapping up their theses on AI-driven story generation from different perspectives and strategies.</p><p>The common idea underlying this whole research line is that combining multi-agent systems and traditional symbolic AI with LLMs in well-designed workflows can overcome many of the limitations of pure LLM-based story generation. Crucially, we aim to explore ways of improving story generation without any form of fine-tuning or retraining&#8212;that is, no need to adjust model weights.</p><p>In this and follow up articles, I'll partner with my students to bring you a few high-level summaries of what they've done and found. </p><p>This first article (and the corresponding thesis) is about story emergence. We set loose a few characters in an AI-driven world and let them interact, to see what kinds of stories come out. But there is a catch: we want some level of control, but not too much. </p><p>The motivating question, then, was how can we introduce some mechanism for indirect control of a story while still allowing characters to evolve more or less naturally and plot points to emerge.</p><p>Let's see what Franco came up with.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.apiad.net/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.apiad.net/subscribe?"><span>Subscribe now</span></a></p><h2>Why this matters</h2><p>Let&#8217;s get something out of the way first: the purpose of this research is not to &#8220;solve&#8221; storytelling, nor to replace writers, artists, or the creative process itself. Storytelling is a deeply human craft, and no one here is under the illusion that a handful of LLM-driven agents will compose the next literary masterpiece. </p><p>So, why do we invest so much effort into building systems that generate stories?</p><p>The answer is both practical and strategic. Storytelling, especially in the form of simulated worlds with autonomous agents, is a uniquely demanding environment for testing the capabilities&#8212;and limits&#8212;of large language models. It&#8217;s a domain where long-term coherence, character consistency, planning, and subtle control all collide. In other words, it&#8217;s the perfect laboratory for exploring how to govern and steer the behavior of powerful generative models without sacrificing their creativity or flexibility.</p><p>This is not unlike the role that games like chess and Go played in the development of AI search and planning algorithms. Those domains were never the end goal; rather, they were controlled, well-understood environments where researchers could rigorously test new ideas. The techniques honed in those settings&#8212;like Monte Carlo Tree Search&#8212;eventually found their way into applications as far-reaching as protein folding and robotics.</p><p>In the same spirit, we use storytelling as a proving ground for strategies of indirect control, agent autonomy, and emergent behavior in LLMs. Here, we can measure and observe how different architectures balance autonomy and direction, how memory and planning affect long-term coherence, and how subtle interventions shape complex outcomes. The lessons we learn in this bounded, creative sandbox are directly relevant to much broader and higher-stakes domains: from AI assistants that must follow nuanced instructions, to multi-agent systems in logistics, education, or even critical infrastructure.</p><p>Ultimately, the goal is to develop robust, generalizable techniques for controllable and safe AI. By pushing the limits in a domain as rich and challenging as narrative simulation, we&#8217;re laying the groundwork for systems that can be trusted to act autonomously, adaptively, and in alignment with human intentions&#8212;no matter the context. </p><p>And if we get a few fun stories along the way, all the better.</p><h3>Why pure LLM-driven storytelling falls short</h3><p>Large Language Models (LLMs) have changed the game for natural language generation. They&#8217;re great at producing short, contextually rich responses and can even simulate dialogue or simple stories with impressive flair. But as soon as you ask them for something more ambitious&#8212;a novel-length mystery, a world populated by autonomous characters, or a story that evolves over dozens of turns&#8212;the cracks start to show.</p><p>The main issues?  We identified some well-known limitations in LLM-driven storytelling.</p><ul><li><p><strong>Long-term coherence:</strong> LLMs forget what happened a few thousand tokens ago, even if their context is larger.</p></li><li><p><strong>Character consistency:</strong> Personalities drift, motivations vanish, and &#8220;out-of-character&#8221; moments abound.  </p></li><li><p><strong>Proactivity:</strong> Agents react, but rarely plan or pursue long-term goals in a believable way. There is no planning ahead of time, it's all reactive.  </p></li><li><p><strong>Narrative control vs. autonomy:</strong> Too much authorial intervention and characters turn into puppets; too little and the story meanders or stalls. It's hard to craft just the right prompt.</p></li></ul><p>These are not just academic complaints. If we want LLMs to power the next generation of interactive fiction, virtual worlds, or even collaborative writing tools, we need architectures that can balance control, coherence, and genuine emergence.</p><h2>Our idea? Agents, lots of LLM agents</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!p8T0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p8T0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 424w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 848w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 1272w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!p8T0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png" width="569" height="536" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:536,&quot;width&quot;:569,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34007,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.apiad.net/i/165826581?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!p8T0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 424w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 848w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 1272w, https://substackcdn.com/image/fetch/$s_!p8T0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bf5b522-8236-4d73-aaad-201ba66ab6cf_569x536.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Our approach borrows a page from both agent-based modeling and narrative AI research. Instead of a single omniscient narrator, we simulate a society of autonomous agents&#8212;each powered by its own LLM instance, each with its own identity, memory, and goals&#8212;interacting in a shared, dynamic environment.</p><p>But here&#8217;s the twist: rather than scripting the story or directly controlling the agents, we introduce a &#8220;Director&#8221; agent. The Director never tells the agents what to do. Instead, it manipulates the environment&#8212;e.g., changing the weather, introducing objects, setting up casual events&#8212;and lets the agents interpret and react according to their personalities and memories. </p><p>Think of it as setting the stage and dropping hints, not pulling strings&#8212;more as some postmodern, emergent theater play than a traditional movie script. Actors have some constraints but they are free to pursue whatever goals they desire.</p><p>Here are the key architectural components that make the whole system come together:</p><ol><li><p><strong>LLM-driven agents:</strong> Each with its own memory (short-term and long-term reflection), planning, and perception modules.</p></li><li><p><strong>World state:</strong> A mutable environment that records locations, objects, events, and global properties.</p></li><li><p><strong>Action resolver:</strong> Ensures agent actions are valid and consistent with the world.</p></li><li><p><strong>Event dispatcher:</strong> Manages what each agent perceives, maintaining a plausible flow of information.</p></li><li><p><strong>Director:</strong> Observes the world and subtly nudges the narrative by changing the environment, not the agents.</p></li></ol><p>To test all these ideas, we built a prototype implementation of the proposed architecture in Python, leveraging Google&#8217;s Gemini 2.0 Flash Lite for all LLM tasks. Each component&#8212;agent, action resolver, director&#8212;gets its own LLM instance and carefully tuned generation parameters. Memory is handled outside the LLM, with dual-level storage: agents remember both recent events and distilled reflections, which are periodically generated and used to inform future decisions.</p><p>The Director&#8217;s interventions are strictly limited: it can, e.g., change the weather or add objects to locations here and there, but never force an agent&#8217;s hand. </p><p>Timing is granular&#8212;the Director considers whether to intervene before every agent&#8217;s turn, allowing for context-sensitive, minimally intrusive direction.</p><h3>How does this stack up?</h3><p>To see if this architecture actually improves narrative generation, we ran head-to-head comparisons between stories generated by the multi-agent system and those produced by a monolithic LLM given the same scenario. We evaluated on several axes:</p><ul><li><p>Coherence and plot progression</p></li><li><p>Character consistency</p></li><li><p>Originality and emergent plot richness</p></li><li><p>Prose quality</p></li><li><p>Narrative pacing and suspense</p></li></ul><p>What did we find?</p><p>The multi-agent system produced far more believable, consistent characters and surprising plot developments. Because agents had their own memories and goals, their actions made sense and sometimes surprised even us. However, the monolithic LLM still wins on sheer polish&#8212;its stories are more linear, its prose more refined, and its pacing more controlled.</p><p>There&#8217;s an inherent tension here. You can have tight, well-structured prose (monolithic LLM) or you can have emergent, believable characters and plots (multi-agent simulation), but getting both at once remains a challenge.</p><p>Some of the most compelling moments came from the Director&#8217;s indirect interventions:</p><ul><li><p>Dropping a key object in the right room led an agent to discover it and set off a chain of events that advanced the plot.</p></li><li><p>Changing the weather or introducing an environmental clue shifted the focus of a conversation or escalated a conflict, without ever breaking the agents&#8217; autonomy.</p></li><li><p>Sometimes, interventions were ignored or interpreted in unexpected ways&#8212;an important reminder that true emergence is unpredictable.</p></li></ul><h3>Limitations and Next Steps</h3><p>No system is perfect. The current prototype has its share of constraints:</p><ul><li><p>Agents can only interact with objects in their current location.</p></li><li><p>There&#8217;s no persistent, explicit inventory management, so long-term planning with items is limited to what LLMs can remember.</p></li><li><p>Agent-to-agent interactions are mostly conversational; direct state changes (like &#8220;killing&#8221; another agent) aren&#8217;t supported yet (we'd need to improve the action resolver to understand these intentions).</p></li><li><p>The Director&#8217;s toolkit is intentionally minimal, which limits the subtlety and richness of its interventions.</p></li></ul><p>Future work should expand environmental manipulation, improve memory persistence, and explore hybrid approaches that combine the strengths of both architectures.</p><h2>Final Thoughts</h2><p>This work is less about &#8220;solving&#8221; narrative generation and more about charting a new direction. By treating characters as autonomous agents and narrative direction as an emergent property of environmental context, we get stories that feel less like scripts and more like living worlds. The trade-off is real, but so is the potential: with the right architecture, LLMs can do more than just write&#8212;they can simulate societies, surprise their creators, and perhaps even teach us something about how stories really emerge.</p><p>If you&#8217;re interested in the technical details or want to see some sample stories, check out the full thesis, and read a few <a href="https://github.com/JackRipper01/Generated-Stories">generated stories</a>. </p><p>And if you have ideas for making these systems even richer, leave us some comments.</p>]]></content:encoded></item></channel></rss>